需求

先来看这样一个场景,拿一个网站举例

这里有一个常见的网站 banner 图容器,大小为为 1910*560 ,看起来背景图完美的充满了宽度,但是图片原始大小时,却是:

它的宽度只有 1440 ,且 background-size 设置的是 contain ,即等比例缩放,那么可以断定它两边的蓝色是依靠背景色填充的。

那么问题来了,这是一个 轮播 banner ,如果希望添加一张不是蓝色的图片呢?难道要给每张图片提前标注好背景颜色吗?这显然是非常死板的做法。

所以需要从图片中提取到图片的主题色,当然这对于 js 来说,也不是什么难事,市面上已经有众多的开源库供我们使用。

探索

首先在网络上找到了以下几个库:

  • color-thief 这是一款基于 JavaScript 和 Canvas 的工具,能够从图像中提取主要颜色或代表性的调色板
  • vibrant.js 该插件是 Android 支持库中 Palette 类的 JavaScript 版本,可以从图像中提取突出的颜色
  • rgbaster.js 这是一段小型脚本,可以获取图片的主色、次色等信息,方便实现一些精彩的 Web 交互效果

我取最轻量化的 rgbaster.js (此库非常搞笑,用 TS 编写,npm 包却没有指定 types ) 来测试后发现,它给我在一个渐变色图片中,返回了七万多个色值,当然,它准确的提取出了面积最大的色值,但是这个色值不是图片边缘的颜色,导致设置为背景色后,并不能完美的融合。

另外的插件各位可以参考这几篇文章:

可以发现,这些插件主要功能就是取色,并没有考虑实际的应用场景,对于一个图片颜色分析工具来说,他们做的很到位,但是在大多数场景中,他们往往是不适用的。

在文章 2 中,作者对比了三款插件对于图片容器背景色的应用,看起来还是 rgbaster 效果好一点,但是我们刚刚也拿他试了,它并不能适用于颜色复杂度高的、渐变色的图片。

思考

既然又又又没有人做这件事,正所谓我不入地狱谁入地狱,我手写一个

整理一下需求,我发现我希望得到的是:

  1. 图片的主题色(面积占比最大)
  2. 次主题色(面积占比第二大)
  3. 合适的背景色(即图片边缘颜色,渐变时,需要边缘颜色来设置背景色)

这样一来,就已经可以覆盖大部分需求了,1+2 可以生成相关的 主题 TAG 、主题背景,3 可以使留白的图片容器完美融合。

开搞

本小节内容非常硬核,如果不想深究原理可以直接跳过,文章末尾有用法和效果图

思路

首先需要避免上面提到的插件的缺点,即对渐变图片要做好处理,不能取出成千上万的颜色,体验太差且实用性不强,对于渐变色还有一点,即在渐变路径上,每一点的颜色都是不一样的,所以需要将他们以一个阈值分类,挑选出一众相近色,并计算出一个平均色,这样就不会导致主题色太精准进而没有代表性。

对于背景色,需要按情况分析,如果只是希望做一个协调的页面,那么大可以直接使用主题色做渐变过渡或蒙层,也就是类似于这种效果

但是如果希望背景与图片完美衔接,让人看不出图片边界的感觉,就需要单独对边缘颜色取色了。

最后一个问题,如果图片分辨率过大,在遍历像素点时会非常消耗性能,所以需要降低采样率,虽然会导致一些精度上的丢失,但是调整为一个合适的值后应该基本可用。

剩余的细节问题,我会在下面的代码中解释

使用 JaveScript 编码

接下来我将详细描述 autohue.js 的实现过程,由于本人对色彩科学不甚了解,如有解释不到位或错误,还请指出。

首先编写一个入口主函数,我目前考虑到的参数应该有:

export default async function colorPicker(imageSource: HTMLImageElement | string, options?: autoColorPickerOptions)
type thresholdObj = { primary?: number; left?: number; right?: number; top?: number; bottom?: number }
interface autoColorPickerOptions {
/**
* - 降采样后的最大尺寸(默认 100px )
* - 降采样后的图片尺寸不会超过该值,可根据需求调整
* - 降采样后的图片尺寸越小,处理速度越快,但可能会影响颜色提取的准确性
**/
maxSize?: number
/**
* - Lab 距离阈值(默认 10 )
* - 低于此值的颜色归为同一簇,建议 8~12
* - 值越大,颜色越容易被合并,提取的颜色越少
* - 值越小,颜色越容易被区分,提取的颜色越多
**/
threshold?: number | thresholdObj
}

概念解释 Lab ,全称:CIE L*a*bCIE L*a*b*CIE XYZ色彩模式的改进型。它的“L”(明亮度),“a”(绿色到红色)和“b”(蓝色到黄色)代表许多的值。与 XYZ 比较,CIE L*a*b*的色彩更适合于人眼感觉的色彩,正所谓感知均匀

然后需要实现一个正常的 loadImg 方法,使用 canvas 异步加载图片

function loadImage(imageSource: HTMLImageElement | string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
let img: HTMLImageElement
if (typeof imageSource === 'string') {
img = new Image()
img.crossOrigin = 'Anonymous'
img.src = imageSource
} else {
img = imageSource
}
if (img.complete) {
resolve(img)
} else {
img.onload = () => resolve(img)
img.onerror = (err) => reject(err)
}
})
}

这样我们就获取到了图片对象。

然后为了图片过大,我们需要进行降采样处理

// 利用 Canvas 对图片进行降采样,返回 ImageData 对象
function getImageDataFromImage(img: HTMLImageElement, maxSize: number = 100): ImageData {
const canvas = document.createElement('canvas')
let width = img.naturalWidth
let height = img.naturalHeight
if (width > maxSize || height > maxSize) {
const scale = Math.min(maxSize / width, maxSize / height)
width = Math.floor(width * scale)
height = Math.floor(height * scale)
}
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
if (!ctx) {
throw new Error('无法获取 Canvas 上下文')
}
ctx.drawImage(img, 0, 0, width, height)
return ctx.getImageData(0, 0, width, height)
}

概念解释,降采样:降采样( Downsampling )是指在图像处理中,通过减少数据的采样率或分辨率来降低数据量的过程。具体来说,就是在保持原始信息大致特征的情况下,减少数据的复杂度和存储需求。这里简单理解为将图片强制压缩为 100*100 以内,也是 canvas 压缩图片的常见做法。

得到图像信息后,就可以对图片进行像素遍历处理了,正如思考中提到的,我们需要对相近色提取并取平均色,并最终获取到主题色、次主题色。

那么问题来了,什么才算相近色,对于这个问题,在 常规的 rgb 中直接计算是不行的,因为它涉及到一个感知均匀的问题

概念解释,感知均匀:XYZ 系统和在它的色度图上表示的两种颜色之间的距离与颜色观察者感知的变化不一致,这个问题叫做感知均匀性(perceptual uniformity)问题,也就是颜色之间数字上的差别与视觉感知不一致。由于我们需要在颜色簇中计算出平均色,那么对于人眼来说哪些颜色是相近的?此时,我们需要把 sRGB 转化为 Lab 色彩空间(感知均匀的),再计算其欧氏距离,在某一阈值内的颜色,即可认为是相近色。

所以我们首先需要将 rgb 转化为 Lab 色彩空间

// 将 sRGB 转换为 Lab 色彩空间
function rgbToLab(r: number, g: number, b: number): [number, number, number] {
let R = r / 255,
G = g / 255,
B = b / 255
R = R > 0.04045 ? Math.pow((R + 0.055) / 1.055, 2.4) : R / 12.92
G = G > 0.04045 ? Math.pow((G + 0.055) / 1.055, 2.4) : G / 12.92
B = B > 0.04045 ? Math.pow((B + 0.055) / 1.055, 2.4) : B / 12.92 let X = R * 0.4124 + G * 0.3576 + B * 0.1805
let Y = R * 0.2126 + G * 0.7152 + B * 0.0722
let Z = R * 0.0193 + G * 0.1192 + B * 0.9505 X = X / 0.95047
Y = Y / 1.0
Z = Z / 1.08883 const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)
const fx = f(X)
const fy = f(Y)
const fz = f(Z)
const L = 116 * fy - 16
const a = 500 * (fx - fy)
const bVal = 200 * (fy - fz)
return [L, a, bVal]
}

这个函数使用了看起来很复杂的算法,不必深究,这是它的大概解释:

  1. 获取到 rgb 参数

  2. 转化为线性 rgb (移除 gamma 矫正),常量 0.04045 是 sRGB (标准 TGB )颜色空间中的一个阈值,用于区分非线性和线性的 sRGB 值,具体来说,当 sRGB 颜色分量大于 0.04045 时,需要通过 gamma 校正(即采用 ((R + 0.055) / 1.055) ^ 2.4)来得到线性 RGB ;如果小于等于 0.04045 ,则直接进行线性转换(即 R / 12.92

  3. 线性 RGB 到 XYZ 空间的转换,转换公式如下:

  • X = R * 0.4124 + G * 0.3576 + B * 0.1805
  • Y = R * 0.2126 + G * 0.7152 + B * 0.0722
  • Z = R * 0.0193 + G * 0.1192 + B * 0.9505
  1. 归一化 XYZ 值,为了参考白点( D65 ),标准白点的 XYZ 值是 (0.95047, 1.0, 1.08883)。所以需要通过除以这些常数来进行归一化

  2. XYZ 到 Lab 的转换,公式函数:const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)

  3. 计算 L, a, b 分量

L:亮度分量(表示颜色的明暗程度)

  • L = 116 * fy - 16

a:绿色到红色的色差分量

  • a = 500 * (fx - fy)

b:蓝色到黄色的色差分量

  • b = 200 * (fy - fz)

接下来实现聚类算法

/**
* 对满足条件的像素进行聚类
* @param imageData 图片像素数据
* @param condition 判断像素是否属于指定区域的条件函数(参数 x, y )
* @param threshold Lab 距离阈值,低于此值的颜色归为同一簇,建议 8~12
*/
function clusterPixelsByCondition(imageData: ImageData, condition: (x: number, y: number) => boolean, threshold: number = 10): Cluster[] {
const clusters: Cluster[] = []
const data = imageData.data
const width = imageData.width
const height = imageData.height
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (!condition(x, y)) continue
const index = (y * width + x) * 4
if (data[index + 3] === 0) continue // 忽略透明像素
const r = data[index]
const g = data[index + 1]
const b = data[index + 2]
const lab = rgbToLab(r, g, b)
let added = false
for (const cluster of clusters) {
const d = labDistance(lab, cluster.averageLab)
if (d < threshold) {
cluster.count++
cluster.sumRgb[0] += r
cluster.sumRgb[1] += g
cluster.sumRgb[2] += b
cluster.sumLab[0] += lab[0]
cluster.sumLab[1] += lab[1]
cluster.sumLab[2] += lab[2]
cluster.averageRgb = [cluster.sumRgb[0] / cluster.count, cluster.sumRgb[1] / cluster.count, cluster.sumRgb[2] / cluster.count]
cluster.averageLab = [cluster.sumLab[0] / cluster.count, cluster.sumLab[1] / cluster.count, cluster.sumLab[2] / cluster.count]
added = true
break
}
}
if (!added) {
clusters.push({
count: 1,
sumRgb: [r, g, b],
sumLab: [lab[0], lab[1], lab[2]],
averageRgb: [r, g, b],
averageLab: [lab[0], lab[1], lab[2]]
})
}
}
}
return clusters
}

函数内部有一个 labDistance 的调用,labDistance 是计算 Lab 颜色空间中的欧氏距离的

// 计算 Lab 空间的欧氏距离
function labDistance(lab1: [number, number, number], lab2: [number, number, number]): number {
const dL = lab1[0] - lab2[0]
const da = lab1[1] - lab2[1]
const db = lab1[2] - lab2[2]
return Math.sqrt(dL * dL + da * da + db * db)
}

概念解释,欧氏距离:Euclidean Distance ,是一种在多维空间中测量两个点之间“直线”距离的方法。这种距离的计算基于欧几里得几何中两点之间的距离公式,通过计算两点在各个维度上的差的平方和,然后取平方根得到。欧氏距离是指 n 维空间中两个点之间的真实距离,或者向量的自然长度(即该点到原点的距离)。

总的来说,这个函数采用了类似 K-means 的聚类方式,将小于用户传入阈值的颜色归为一簇,并取平均色(使用 Lab 值)。

概念解释,聚类算法:Clustering Algorithm 是一种无监督学习方法,其目的是将数据集中的元素分成不同的组(簇),使得同一组内的元素相似度较高,而不同组之间的元素相似度较低。这里是将相近色归为一簇。

概念解释,颜色簇:簇是聚类算法中一个常见的概念,可以大致理解为 "一类"

得到了颜色簇集合后,就可以按照 count 大小来判断哪个是主题色了

// 对全图所有像素进行聚类
let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary)
clusters.sort((a, b) => b.count - a.count)
const primaryCluster = clusters[0]
const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0]
const primaryColor = rgbToHex(primaryCluster.averageRgb)
const secondaryColor = rgbToHex(secondaryCluster.averageRgb)

现在我们已经获取到了主题色、次主题色

接下来,我们继续计算边缘颜色

按照同样的方法,只是把阈值设小一点,我这里直接设置为 1 ( threshold.top 等都是 1 )

// 分别对上、右、下、左边缘进行聚类
const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top)
topClusters.sort((a, b) => b.count - a.count)
const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom)
bottomClusters.sort((a, b) => b.count - a.count)
const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left)
leftClusters.sort((a, b) => b.count - a.count)
const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right)
rightClusters.sort((a, b) => b.count - a.count)
const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor

这样我们就获取到了上下左右四条边的颜色

这样大致的工作就完成了,最后我们将需要的属性导出给用户,我们的主函数最终长这样:

/**
* 主函数:根据图片自动提取颜色
* @param imageSource 图片 URL 或 HTMLImageElement
* @returns 返回包含主要颜色、次要颜色和背景色对象(上、右、下、左)的结果
*/
export default async function colorPicker(imageSource: HTMLImageElement | string, options?: autoColorPickerOptions): Promise<AutoHueResult> {
const { maxSize, threshold } = __handleAutoHueOptions(options)
const img = await loadImage(imageSource)
// 降采样(最大尺寸 100px ,可根据需求调整)
const imageData = getImageDataFromImage(img, maxSize) // 对全图所有像素进行聚类
let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary)
clusters.sort((a, b) => b.count - a.count)
const primaryCluster = clusters[0]
const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0]
const primaryColor = rgbToHex(primaryCluster.averageRgb)
const secondaryColor = rgbToHex(secondaryCluster.averageRgb) // 定义边缘宽度(单位像素)
const margin = 10
const width = imageData.width
const height = imageData.height // 分别对上、右、下、左边缘进行聚类
const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top)
topClusters.sort((a, b) => b.count - a.count)
const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom)
bottomClusters.sort((a, b) => b.count - a.count)
const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left)
leftClusters.sort((a, b) => b.count - a.count)
const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right)
rightClusters.sort((a, b) => b.count - a.count)
const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor return {
primaryColor,
secondaryColor,
backgroundColor: {
top: topColor,
right: rightColor,
bottom: bottomColor,
left: leftColor
}
}
}

还记得本小节一开始提到的参数吗,你可以自定义 maxSize (压缩大小,用于降采样)、threshold (阈值,用于设置簇大小)

为了用户友好,我还编写了 threshold 参数的可选类型:number | thresholdObj

type thresholdObj = { primary?: number; left?: number; right?: number; top?: number; bottom?: number }

可以单独设置主阈值、上下左右四边阈值,以适应更个性化的情况。

autohue.js 诞生了

名字的由来:秉承一贯命名习惯,auto 家族成员又多一个,与颜色有关的单词有好多个,我取了最短最好记的一个 hue(色相),也比较契合插件用途。

此插件已在 github 开源:GitHub autohue.js

npm 主页:NPM autohue.js

在线体验:autohue.js 官方首页

安装与使用

pnpm i autohue.js
import autohue from 'autohue.js'

autohue(url, {
threshold: {
primary: 10,
left: 1,
bottom: 12
},
maxSize: 50
})
.then((result) => {
// 使用 console.log 打印出色块元素 s
console.log(`%c${result.primaryColor}`, 'color: #fff; background: ' + result.primaryColor, 'main')
console.log(`%c${result.secondaryColor}`, 'color: #fff; background: ' + result.secondaryColor, 'sub')
console.log(`%c${result.backgroundColor.left}`, 'color: #fff; background: ' + result.backgroundColor.left, 'bg-left')
console.log(`%c${result.backgroundColor.right}`, 'color: #fff; background: ' + result.backgroundColor.right, 'bg-right')
console.log(`%clinear-gradient to right`, 'color: #fff; background: linear-gradient(to right, ' + result.backgroundColor.left + ', ' + result.backgroundColor.right + ')', 'bg')
bg.value = `linear-gradient(to right, ${result.backgroundColor.left}, ${result.backgroundColor.right})`
})
.catch((err) => console.error(err))

最终效果

复杂边缘效果

纵向渐变效果(这里使用的是 left 和 right 边的值,可能使用 top 和 bottom 效果更佳)

纯色效果(因为单独对边缘采样,所以无论图片内容多复杂,纯色基本看不出边界)

突变边缘效果(此时用 css 做渐变蒙层应该效果会更好)

横向渐变效果(使用的是 left 和 right 的色值),基本看不出边界

参考资料

番外

Auto 家族的其他成员

autohue.js:让你的图片和背景融为一体,绝了!的更多相关文章

  1. DD_belatedPNG.js解决透明PNG图片背景灰色问题

    <!--[]> <script type="text/javascript" src="http://www.phpddt.com/usr/themes ...

  2. 延迟加载外部js文件,延迟加载图片(jquery.lazyload.js和echo,js)

    js里一说到延迟加载,大都离不开两种情形,即外部Js文件的延迟加载,以及网页图片的延迟加载: 1.首先简单说一下js文件的3种延迟加载方式: (1)<script type="text ...

  3. ImageLightbox.js – 响应式的图片 Lightbox 插件

    ImageLightbox.js 是一款很简洁的用于显示图片灯箱效果(Lightbox)的插件,没有字幕,导航按钮或默认背景.如果默认功能不够用的话,你可以很容易地自定义 JavaScript 函数扩 ...

  4. ie6,7下js动态加载图片不显示错误

    ie6,7下js动态加载图片不显示错误 先描述一下出现这种匪夷所思bug的背景: 我在页面加载的时候加载一堆小缩略图,<a href="javascript:void(0);" ...

  5. 原生js和jquery实现图片轮播特效

    本文给大家分享的是使用原生JS和JQ两种方法分别实现相同的图片轮播特效,十分的实用,也非常方便大家对比学习原生js和jQuery,有需要的小伙伴可以参考下. 1)首先是页面的结构部分对于我这种左右切换 ...

  6. js/jquery中实现图片轮播

    一,jquery方法 <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type&qu ...

  7. 原生js和jquery实现图片轮播特效(转)

    本文给大家分享的是使用原生JS和JQ两种方法分别实现相同的图片轮播特效,十分的实用,也非常方便大家对比学习原生js和jQuery,有需要的小伙伴可以参考下. 1)首先是页面的结构部分对于我这种左右切换 ...

  8. js实现移动端图片预览:手势缩放, 手势拖动,双击放大...

    .katex { display: block; text-align: center; white-space: nowrap; } .katex-display > .katex > ...

  9. js实现本地的图片压缩上传预览

    js在设计时考虑到安全的原因是不允许读写本地文件的,随着html5的出现提供了fileReader AP从而可以I实现本地图片的读取预览功能, 另外在移动端有的限制图片大小的需求,主要是考虑图片过大会 ...

  10. vue打包后js和css、图片不显示,引用的字体找不到问题

    vue打包后js和css.图片不显示,引用的字体找不到问题:图片一般都是背景图片. 一.vue打包出现js和css不显示问题: 1.不使用mode:'history' 2.使用mode:'histor ...

随机推荐

  1. HarmonyOS Next 集成支付宝SDK后无法在模拟器上安装调试的问题

    之前使用模拟器调试都正常,在集成支付宝SDK后,同事说在模拟器上无法安装调试,因为真机资源不够,模拟器不能用实在耽误事,所以就花了点时间研究一下. 报错原因 官方文档的解释 根据文档的说明,应该是cp ...

  2. uni-app 使用笔记

    1.前言 也不知道是我水平菜还是文档太烂,这个框架使用的过程中踩了无数的坑,屡次想砸键盘,最后贫穷让我平复了心情.为了纪念这段操蛋的日子,我决定把这些坑都记录下来. 2.数据请求 在实际的项目中,数据 ...

  3. PDFSharp 1.5 更新

    PDFsharp 1.50 Preview Information - PDFsharp & MigraDoc PDFShapr 1.50 修复与改进 支持 Object Streams - ...

  4. 使用terraform管理Proxmox VE资源

    terraform-proxmox 使用terraform管理proxmox资源 Using terraform to manage proxmox resources env: Proxmox VE ...

  5. 解决SSH免密登录配置成功后不生效问题

    今天配置SSH免密登录时,使用 ssh-keygen 命令成功生成了公钥和私钥,并且也执行了 ssh-copy-id 机器地址 将公钥添加到了服务器的authorized_keys文件中.紧接着用 s ...

  6. .NET周刊【12月第3期 2024-12-15】

    国内文章 重磅推出 Sdcb Chats:一个全新的开源大语言模型前端 https://www.cnblogs.com/sdcb/p/18597030/sdcb-chats-intro Sdcb Ch ...

  7. Qt/C++音视频开发59-使用mdk-sdk组件/原qtav作者力作/性能凶残/超级跨平台

    一.前言 最近一个月一直在研究mdk-sdk音视频组件,这个组件是原qtav作者的最新力作,提供了各种各样的示例demo,不仅限于支持C++,其他各种比如java/flutter/web/androi ...

  8. Qt音视频开发23-视频绘制QPainter方式(占用CPU)

    一.前言 采集到的图片,用painter绘制是最基础的方式,初学者可能第一次尝试显示图片用的qlabel的setpixmap,用下来会发现卡成屎,第二次尝试用样式表设置背景图,依然卡成屎,最终选用pa ...

  9. Qt通用方法及类库3

    函数名 //设置全局样式 static void setStyle(QUIWidget::Style style); static void setStyle(const QString &q ...

  10. Qt音视频开发29-Onvif云台控制

    一.前言 云台控制也是onvif功能中最常用的,最常用的功能排第一的是拿到视频流地址,排第二的就是云台控制了,云台控制的含义就是对带云台的摄像机进行上下左右的移动,一般云台摄像机都是带有一个小电机,一 ...