Description
去年开年后,两度准备换工作,到最后入职新公司,前前后后快 5 个月了。等入职后发现新环境的比以前强不少,一上来的要学习的内容多,除了忙还是忙的,有点瞎忙乎的感觉,兜兜转转的博客也就拉下了很多。想想博客还是要继续写的,哪怕是一点点积累。最近在家办公三周了,楼下还在广播核酸检查的通知,周末也无法外出,想想还是学点东西好了。
偶然间接到一个图像相关的任务,虽然其他同学已经做的差不多了,挂着 AI 的名头,算法主导识别的过程,甚至后面还用到了 op 67E6 encv,这一切都很陌生,一开始还担心自己搞不懂的,只是接手后,发现蛮有趣的。
knn 介绍
想一下图像要如何根据算法分类呢?同一个物体可以受到很多因素印象,比如光照、视角、大小的影响,甚至是一个算法判断出了向左边转头马,那如何识别向右边转头的马呢?图和图之间如何区分?图像分类的过程,有点像让机器去学习如何分类,提供大量的数据,去学习分类,数据本身是有标注分类的,然后根据学习到的特征来对输入的进行分类,几年前的识别精度就已经高于肉眼了。图像识别涉及到的神经网络学习,是很大的范畴,幸运的是用的 knn 还不是深度学习,连神经网络的范畴都没有接触到,只是很适合用在我们这个任务场景。
knn 全称为 k-Nearest Neighbor。先介绍 Nearest Neighbor 分类器,分类的过程不是无中生有,不像排序算法那样直接,分类会根据提供的训练集,也就是分好类的图像,来判断输入图像的分类,而 Nearest Neighbor 算法将会拿着输入的图片和训练集中每一张图片去比较,然后将它认为最相似的训练集标签给到测试图片
上面的例子使用 32 * 32 的图像,左边是训练集,右边第一列是输入,而第一列后面是算法找到的最接近第一列的 10 个图像。虽然有点模糊,但是还是可以看到,命中率不是 100% 的,比如青蛙里面甚至出现了战机,这是因为飞机的背景是白色的,和输入的青蛙高度相似,最后导致误判。这里面是如何比较输入图像和哪个形似的呢?
这里用的是 L1 曼哈顿距离,用 python 来表示的话则是: distances = np.sum(np.abs(self.Xtr - X[i,:]), axis = 1)
, 当然还有另外一个 L2 欧式距离,复杂一点可以自己去了解。
前面是 Nearest Neighbor 分类器,那 knn 又是如何处理的?Nearest Neighbor 分类器会去计算每个训练图和测试图之间的 L1 距离,然后取最小的一个,但是最小距离就代表着该图片的分类吗?一种比较好的方法是通过 k 个最临近的来判断。比如下面图
图像的突出的点,代表训练集的分类,其他像素点的颜色代表其归属的分类。当是 k = 1
也就是普通的 Nearest Neighbor 分类时。可以看到正中间存在一个黄色点,所有其周围都是黄色区域,但是很明显这是异常的,除了该黄色点,周围突出点都是绿色的,分类器将该黄色点周围区域分类为黄色是错误的。可以看到当 k = 3
的时候,中间区域都是绿色的。那是不是 k 值越大越好呢?通过大量测试可以发现 k 在 7 的时候可以达到比较好的效果,当然成功率也不过 32%。
成功率这么低为什么还要用呢?因为这里的业务场景比较单一,不同图像之间具有明显的差异,距离不一样,其直观上也会明显不一样,所以可以通过 knn 来区分。
knn python 实现
这里先给出 python 的方式,直观易懂, predict
部分就是输出
class KNearestNeighbor(object):
def __init__(self):
pass
def train(self, X, y):
self.X_train = X
self.y_train = y
def predict(self, X, k=1):
y_pred = np.zeros(X.shape[0])
for i in xrange(X.shape[0]):
closest_y = []
distances = np.sum(np.abs(self.X_train - X[i,:]), axis = 1)
closest_y = self.y_train[np.argsort(distances)[:k]]
y_pred[i] = np.argmax(np.bincount(closest_y))
return y_pred
输入的 X
是多个图像,每次都会计算单个输入图像和所有测试图像的 distances
,对所有的 distances
排序,前 k 个作为最邻近的测试图像,然后取里面出现次数最多的分类作为最后结果。和 js 不同,python 里面可以用 numpy 库直接对多维数组进行计算,不用一层一层的遍历,操作相当丝滑。
knn tfjs 实现
那对于前端er要如何使用呢?搜索 npm 可以看到一个 @tensorflow-models/knn/knn-classifier
库,里面直接使用了 K-Nearest Neighbors 算法,这个感情好,直接使用就好了。使用方法如下:
// 创建分类器并加载神经网络
const classifier = knnClassifier.create();
const mobilenet = await mobilenetModule.load();
// 添加 img0/img1 训练图,同时标注img0/img1的分类 0/1
const img0 = tf.browser.fromPixels(document.getElementById('class0'));
const logits0 = mobilenet.infer(img0, 'conv_preds');
classifier.addExample(logits0, 0);
const img1 = tf.browser.fromPixels(document.getElementById('class1'));
const logits1 = mobilenet.infer(img1, 'conv_preds');
classifier.addExample(logits1, 1);
// 通过输入的图 x 来预测其是分类 0 还是 1
const x = tf.browser.fromPixels(document.getElementById('test'));
const xlogits = mobilenet.infer(x, 'conv_preds');
console.log('分类:', classifier.predictClass(xlogits));
实例里面喂了两张图片 img0/img1
,分别对应不同的类 0/1
,最后通过调用 classifier.predictClass
就可以实现图像分类,是不是很简单?只是为什么用到 tf
?明明是一个和神经网络不沾边的算法,后面的计算规则是什么样的?
这里先介绍一下 mobilenet
的用法。
mobilenet
mobilenet 是一个轻量的深度可分离卷积神经网络,开箱即用的预训练模型,用于移动或者嵌入式的应用场景,避免模型过于庞大导致的高内存和响应过长问题,可以用于 ImageNet 的图像分类,其目前有超过 1 千万张图片,是一个大型视觉数据库。
本文用到的是 mobilenet V1,卷积的过程可以分为深度卷积(depthwise convolution)和 1 * 1 卷积(pointwise convolution),可以有效的降低卷积运算的参数量和运算量。简单看下深度可分离卷积和标准卷积的不同。
图 a 是标准卷积核 DK * DK * M * N,其中 M 为输入通道,图 b 是深度卷积核, M 个 DK * DK,图 c N 个是 1 * 1 * M 卷积核。
这里可以看一个直观点的卷积神经网络的操作:
上面是一张图片经过反复的卷积、激活函数、池化最后输出到全连接层的过程,能有效的对车分类。在 mobilenet V1 里面也有类似的操作,只是这里稍微不同,是先经过深度卷积再到 1 * 1 卷积:
左边是标准过程,右边是深度可分离卷积过程。
同时整个网络包含结构如下:
MobileNet 网络结构里面第一层是标准的 3 * 3 卷积核,接下来是 13 个上图中的深度可分离卷积,最后通过 MEAN 平均池化来操作,输出 1 * 1024。
上面的网络计算核心位置在:
// graph_executor.ts GraphExecutor.execute
execute(inputs: NamedTensorMap, outputs?: string[]): Tensor[] {
// 省略其余代码
for (let i = 0; i < orderedNodes.length; i++) {
const node = orderedNodes[i];
if (!tensorsMap[node.name]) {
const tensors =
executeOp(node, tensorsMap, context, this._resourceManager) as Tensor[];
tensorsMap[node.name] = tensors;
// 省略其余代码
}
}
// 省略其余代码
}
通过 orderedNodes
里面的结构顺序来,先是完成网络结构里面的第一层标准卷积,然后是其余的 13 层。orderedNodes
的单个节点结构如下:
export declare interface Node {
signatureKey?: string;
name: string;
op: string;
category: Category;
inputNames: string[];
inputs: Node[];
inputParams: {[key: string]: InputParamValue};
attrParams: {[key: string]: ParamValue};
children: Node[];
rawAttrs?: {[k: string]: tensorflow.IAttrValue};
defaultOutput?: number;
outputs?: string[];
}
节点里面定义了要进行的操作,以及输入节点是哪些,辅助参数等,通过遍历 orderedNodes
,一步步的完成计算图。
回到前面用 knn 识别图像的例子里面,mobilenet.infer
再处理图像数据之前,需要加载模型,会发起请求 https://tfhub.dev/google/imagenet/mobilenet_v1_100_224/classification/1/model.json?tfjs-format=file
获取对应的模型数据,也就是卷积神经网络里面用到的
A3E2
数,比如卷积核等。加载模式后,mobilenet.infer
将数据从 0 到 255 标准化为 0 到 1 的 tensors 对象,并 resizeBilinear
转换为 244 * 244 * 3 的大小,再执行 GraphModel.execute
,通过深度可分离卷积神经网络输出图像 1 * 1024。
knn/knn-classifier
mobilenet 里面生成的图像特征,会先通过 knn/knn-classifier
,进行分类。输入的训练集的时候,会有图像的特征以及其对应 lable
,同一个 lable
的数据会归类到一块。
图像的特征输入时,会进行范数计算,方式为 tf.div(vec, vec.norm())
,范数 计算可以方便我们操作训练集数据和输入数据,举例子:
// 输入训练集
const train = [a, b, c];
const trainNorm = Math.sqrt(a * a + b * b + c * c);
const trainUnitNormal = [a / trainNorm, b / trainNorm, c / trainNorm];
// 输入矩阵
const input = [x, y, z];
const inputNorm = Math.sqrt(x * x + y * y + z * z);
const inputUnitNormal = [x / trainNorm, y / trainNorm, z / trainNorm];
这里的范数指的是两个矩阵之间的距离,具体的就是欧式距离。可知如果 input 和 train 完全相等,则 trainUnitNormal * inputUnitNormal === 1
,knn/knn-classifier
将训练集和输入矩阵转换后的 unitNormal
相乘,并从大到小排序。可知如果欧式距离为 1 的会排在最前面,也就是认为最相似度的图像。
计算了输入和训练集中间的距离,正常是采用为 1 的那个分类就好了,为了提高准确性,会对前 k 个进行判断。calculateTopClass
对前 k 个数据,通过偏移和序号来计算分类,最后返回权重最高的一个分类。
for (let i = 0; i < topKIndices.length; i++) {
const index = topKIndices[i];
for (const label in this.classDatasetMatrices) {
if (index < classOffsets[label]) {
votesPerClass[label]++;
break;
}
}
}
// Compute confidences.
let topConfidence = 0;
for (const label in this.classDatasetMatrices) {
const probability = votesPerClass[label] / kVal;
if (probability > topConfidence) {
topConfidence = probability;
topLabel = label;
}
confidences[label] = probability;
}
除了 label
外还会返回对应的 classId
, 每个输入的训练集都会有对应的递增的 classId
,外部用 label
可以了。
knn opencv 实现
上面提到了 knn 的 python 实现,和 tfjs 的实现,最后还有 opencv 的,当然对于前端,opencv 也提供了 webassembly 版本,只是没有在 opencvjs 里面找到 knn 相关函数,只有 python 版本的,由于具体实现逻辑也不清楚就不在这里介绍了。
或者用 opencvjs 实现上面的 python 操作,本质也是添加训练图像,然后对输入图像对比,计算距离,取距离最小的前 k 个图像,并获取其分类。只是意义不太大,本质还是 knn 算法。
knn 小结
从业务的角度出发,knn 可以很好地实现图像分类,但是如果目标图像有些许变化,可能会导致分类异常,这个需要手动调整训练集。如果搭建一个神经网络去分类,又复杂化了,由于能力有限,后续就没有继续研究了。
tfjs 提供的 MobileNet,其训练数据来源 ImageNet,如果我们的输入图像是阿猫阿狗这些来源于生活的图像,输入图像的些许变化通过 MobileNet 能够被有效分析的。只是这里输入图像特定的前端界面,不在 MobileNet 范畴里面。所以目前还用不到 tfjs 里面神经网络的能力,相当于是大刀小用了,不过后续也可以在上面扩展下,增加卷积,倒是一个比较好的方向。
简单的通过 python 的方式,或者用 js 的方式也能实现 knn 算法,就是没有直接用 tfjs 的 knn/knn-classifier
方便。
knn 这里也遗留了不少问题,除了提到的目标对象稍微变化会导致分类异常,无法通用化,还有以下问题:
- 缺少无监督学习,每次 UI 界面有调整,都要手动更新训练集
- 模型结果持久化,由于采用 tf,启动时候的模型训练会消耗非常多时间,导致初始化过慢
除了 knn 算法能对图像分类外,还有两个比较简单的朴素贝叶斯算法、svm 分类,这两个也没有涉及卷积神经网络,就不多介绍了。
opencv 对比
knn 可以比较粗暴的对图像进行分类,然后在业务里面还需要对图像进一步识别,需要获取相似度以及异常区域标记。
相似度计算
相似度计算有不少方法,一开始我们用的是 compareHist
通过直方图的方式计算图像灰度值分布的相似情况,如果元素出现错位等情况或者纹理结构相似但是亮度不一致的情况,会导致计算的相似度有明显偏差。最后用的是 ssim
结构相似性来处理,会从亮度、对比度、结构等三个方面处理,具体的计算方式可以看看 getStructureSimilarity,这里可以修改高斯模糊的大小来调整分块的影响。
具体计算过程可以参考上面的公式,其中 C1
和 C2
是常数。opencv4nodejs 里面的计算也和上图原理类似。
分割白色背景的相似度计算
前面的 ssim
相似度计算已经可以很好的满足我们的使用要求了,只是由于场景存在大面积的白色区域,用户关注界面往往是其中有内容的部分,比如浏览器里面的谷歌搜索的结果页,就会有大量空白区域,不是用户关注的。下面个例子:
同样的内容,但是周围空白区域大小不一样,文字处在的位置也不一样,其相似度会低到 0.899。相似度不为 1 本身,是符合预期的,只是出于业务需要,要去除掉空白的区域,只校验有内容的部分。尤其是前端响应式的适配,在大屏下可能会留下大面积的空白。出于业务考虑这里提出了白色背景分割相似度算法:
第一步是将背景去除,保留分割的图像。业务场景不需要针对每个内容区域都进行分割,比如上图的 LowCodeEngine
不用细分到每个字母,整体内容就好了,对这样的业务特征,检测水平两边内容的起始点和结束点,以及垂直方向的开始点和结束点,就是该内容的区域了。
参考Python + opencv 实现图片文字的分割的方式,采用方式是扫描每一行,从左到右扫描,记录该行的信息,但是不用细化到每个文字,记录到内容区域就好了。方式如下:
function calcContentHorizonRange(img, line) {
let contentStart = 0;
let contentEnd = 0;
img.scan(0, line, img.bitmap.width, 1, (...[x, , idx]) => {
// ...起始点和结束点计算
});
return {
lineHasContent: contentStart !== 0 || contentEnd !== 0,
contentStart: contentEnd === 0 ? 0 : contentStart,
contentEnd,
};
}
// 通过垂直方向,从上到下,再对每行进行从左到右的扫描
while (i < imgHeight) {
const { lineHasContent, contentStart, contentEnd } = calcContentHorizonRange(img, i);
// 边缘检查
// 通过 lineHasContent 判断垂直方向连续内容区域,从而获得垂直方向的连续有内容的集合
}
calcContentHorizonRange
可以获取到每行的内容起始点和结束点的信息,如果连续有内容,则为一个有内容的垂直区域。再对该垂直区域的水平方向的起始和末尾做判断,遍历每行,分别取最小 contentStart 和最大值 contentEnd,作为该区域的水平方向起始点和结束点。最后得到就是去除背景的区域。
第二步相似度计算,相似度计算还是采用 ssim
的方式,只是这里由于分割成一块块区域了,需要一一去对比,只是由于分割后的图像不一定大小相等,同时也可能存在区域缺失的问题、边界问题,这几个细节需要专门处理的。最后整体的相似度计算是采用:
for (let i = 0; i < compareList.length; i++) {
const area = compareList[i].width * compareList[i].height;
sumArea += area;
sum += compareList[i].similarity * area;
}
由于是长方型的相似度计算,所以长宽相乘就是面积,面积代表相似度的权重,最后累加得到整体的相似度。可以计算到上面两张图的相似度最后会为 1。
异常图计算
异常标红的绘制过程,先成灰度图,absdiff
对比异常部分,再用 findContours
方式,对比新图像和原图像的异常,然后用红边绘制轮廓,需要避免绘制原图像的与新图像的异常。还是用分割白色背景的相似度计算里面的图做对比,其异常标红图如下所示:
异常轮廓其实也反应了图像的相似度问题,如果不存在异常的轮廓,那是不是说明了原图和新图是一致的呢?
增量式算法
用 knn 可以很好的将图像分类,然后对每个类里面的图像通过 opencv 对比得到相似度和异常情况,只是分类和下一个分类之间的关系呢?分类和下一个分类之间也是存在不同的,而且这种不同有的是允许的,而有的则是异常的。通过分析场景变化,初步认为分类和下一个分类之间只存在增量的变化,其他变化可以认为是都是异常,同时如果模块和模块之间是相同的,并且位置发生变化也是异常。
图像分割
模块如何划分呢?这里有几个图像分割方法,漫水算法,分水岭算法。其中漫水算法用的是 opencv 自带的 floodFill
方法,每次通过给定种子点来注水,注水意思是查找和种子点联通的颜色相近的点,就像 ps 里面的魔术棒,适合用于单个区域选择,这里不合适用在模块划分里面。分水岭算法则会从全局出发,多个局部最低点开始注水,而不用自己选择种子点,随着水位的升高,相邻区域最终会相遇,从而出现分水岭,也就是我们的图像的分割边界了,这里介绍一下分水岭算法的一个实现
分水岭算法
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY, 0);
cv.threshold(gray, gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU);
// 获取背景
let M = cv.Mat.ones(3, 3, cv.CV_8U);
// cv.erode(gray, gray, M);
cv.dilate(gray, opening, M);
cv.dilate(opening, bg, M, new cv.Point(-1, -1), 2);
// 距离变换
cv.distanceTransform(opening, distTrans, cv.DIST_L2, 3);
cv.normalize(distTrans, distTrans, 1, 0, cv.NORM_INF);
// 获取前景
cv.threshold(distTrans, fg, 0.1 * 1, 255, cv.THRESH_BINARY);
fg.convertTo(fg, cv.CV_8U, 1, 0);
cv.subtract(bg, fg, unknown);
show(unknown, 'unknown.png');
cv.connectedComponents(fg, markers);
for (let i = 0; i < markers.rows; i++) {
for (let j = 0; j < markers.cols; j++) {
markers.intPtr(i, j)[0] = markers.ucharPtr(i, j)[0] + 1;
if (unknown.ucharPtr(i, j)[0] == 255) {
markers.intPtr(i, j)[0] = 0;
}
}
}
cv.cvtColor(src, src, cv.COLOR_RGBA2RGB, 0);
cv.watershed(src, markers);
图像的某个区域如果肯定是前景对象或则背景,则标记为某个值,而不确定的部分则标记为0,然后通过原生方法 watershed
(分水岭算法)得到边界,最后其边界的值会为 -1。
背景可以操作形态学腐蚀与膨胀获取,而前景对象则是通过距离变换、threshold
处理得到。而不确定的区域则通过两者相减得到。watershed
传入的第二个参数,掩膜 markers
的不确定区域需要为 0,但是经过 connectedComponents
处理后标记的背景为 0 了,需要 + 1
来提升标记值。操作结果如下图:
最后输出图通过红色标记区域,可以看到对上面部分的文字能很好的识别出来,而下面的徽章部分也会被标记出来,但是徽章里面的文字,由于前景对象和背景对象获取的差异,使得 marker
里面不存在徽章的文字差异,最后导致文字无法被识别出来。可以看到徽章里面仅有个别圈被标识出来,无法在我们的业务场景里面使用。
轮廓检测算法
轮廓检测采用灰度图、阈值和轮廓的方式来处理,比较简单的就能获取到图像的轮廓,代码实现:
cv.cvtColor(src, gray, cv.COLOR_BGR2GRAY, 0);
cv.threshold(gray, gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU);
cv.findContours(gray, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
处理方式和上面的异常图计算方式是类似的,效果如下:
和分水岭算法的问题类似,上面文字部分可以很好的识别出来,但是徽章里面的文字无法检查到。findContours
方法里面第四个传参 cv.RETR_EXTERNAL
指的轮廓的查找模式,其中:
- cv.RETR_EXTERNAL: 只提取最外面的轮廓;
- cv.RETR_LIST:表示提取所有轮廓并将其放入列表;
- cv.RETR_CCOMP:表示提取所有轮廓并将组织成一个两层结构;
- cv.RETR_TREE:表示提取所有轮廓并组织成轮廓嵌套的完整层级结构;
这里如果采用 RETR_CCOMP
模式则可以很好的获取到所有的轮廓了,如下图:
只是这里回到原本的意图,按模块分割图像,cv.RETR_CCOMP
直接分到了最小的单元了,比如 code
单词里面的字母 o
和 d
都被分成两块了,当然分水岭算法也有这个问题。模块的划分是为了后续的相似度对比检查,细分到最小单元容易导致相似度检查异常,而按照模块来对比则会好很多,比如字母 o
和 d
中间的圈是很相似的,但是字母 o
和 d
本身是不相似性,只是采用 cv.RETR_EXTERNAL
又会出现下面徽章部分遗漏的问题。那如何处理好呢?
cv.RETR_TREE
模式提取轮廓的同时会组织轮廓的嵌套关系。对于模块的概念,如何划分呢?初步认为超过一定面积大小的区域则认为也是子轮廓。最后判断方式为外轮廓和超过一定区域的子轮廓。
轮廓里面有如下概念
每个轮廓有他自己的关于层级的信息,谁是他的孩子,谁是他的父亲等。OpenCV 用一个包含四个值得数组来表示:[Next, Previous, First_Child, Parent]`
可以看到 Next
和 Previous
就是兄弟轮廓,而后面的则是子轮廓和父轮廓对应的序列。轮廓关系有个经典图:
这里面 2a 和 2 不属于同级关系,而是父子关系,2a 是 2 的子层级,而 3 又是 2a 的子层级,4 和 5 是兄弟关系。每个轮廓都有其对应的父子,兄弟关系,如果没有就是 -1。
function workInLayer(index) {
if (index === undefined || index === -1) return;
const hier = hierarchy.intPtr(0, index);
if (hier[2] !== undefined && hier[2] !== -1) {
// 存在 grandChild
workChildLayer(hier[2]);
}
if (hier[0] !== undefined) {
workInLayer(hier[0]);
}
}
function workChildLayer(index) {
// 判断面积大小,超过一定面积的子轮廓,才继续迭代
collectPartitionRectList(index, 30);
const hier = hierarchy.intPtr(0, index);
// 处理兄弟节点
workChildLayer(hier[0]);
// 处理该节点的子节点
}
for (let i = 0; i < contours.size(); ++i) {
const hier = hierarchy.intPtr(0, i);
// 只处理最外层 EXTERNAL
if (hier[3] !== -1) {
continue;
}
collectPartitionRectList(i);
// 处理子轮廓,会调用 collectPartitionRectList
workInLayer(hier[2]);
}
hierarchy.intPtr
的方式可以拿到每个轮廓对应的层级关系,在通过关系的索引分析得到我们需要的模块,从而实现区域的划分。
可以看出轮廓检测算法比分水岭算法更加的灵活,方便我们对复杂情况的处理,在加上其他的区域大小的判断,能较好的解决模块划分问题。
模块相似度对比
模块区分还需要对模块的相似对对比,这里会先将模块按照位置排序,采用一一对比的方式,如果相似度大于一定值,则认为是同一个模块。模块之间出现位置偏移,或者新增元素都会被认为是异常。位置偏移判断比较简单,通过位置判断就可以了,只是需要注意的是模块位置可能出现的复杂变化,比如原本是 abc
,现在变成 adcb
,这里 b
和 c
都存在着变化,问题点是如何对比这种错乱的循序关系。
最理想的是一个个对比,从第一个开始到最后一个,取最相似和最靠进当前位置的一个,只是这样时间复杂度太高接近 O(2),而且每次相似的计算非常耗时,会导致整个程序运行非常慢。观察分类和下一个分类之间的的图像可以发现,模块基本顺序是不会变化的,只是会出现个别模块的新增加,以及少部分的消失。于是可以按顺序对比,如果下一个分类出现不相同的模块则跳过,继续对比下一个分类的下个模块,同时也要发现 abc
到 adcb
这样的变化。
可想而知如果下个分类里面没有上一个分类的某个模块那会一直对比直到最后一个元素,只是如果考虑我们的场景,要对比模块是不是发现了移动和变化,所以如果超过一个距离有或者间隔太多模块了,则可以认为是两个毫不相关的模块了,也就是不用继续对比下去了,从而提前结束该模块对比。
opencv 小结
关于 opencv 的还有很多事情可以做的,比如动态区域识别,如果输入的图像是,动态的,比如前端常见的 loading 动画,就会出现识别问题;相似度对比,分模块的方式还可以继续摸索寻找最合适业务的一条路,并且 opencv 还可以和 tfjs 结合到一起,或者完全可以用 mobilenet 分割图像的方式,只是初入,后面的方向还有待探索。
总结
春节前一两周接手任务,到 3 月中开始写博客,边写还边查缺补漏的,之间差不多学了三个月,越学习越是觉得还有很多地方待研究的,看了 cs231n计算机视觉课程,当然只看前面 19 个课程,不理解的部分还反复看了三遍,学了好多东西,只是继续投入代价又非常高,而且缺少方向性的建议,就工作外的扩展。虽然不想浅尝辄止,不过后续项目也交付到其他团队,继续学习投入代价也高,所以探索之路就到这里了。
回顾下,这次应该是从 react 源码学习之后久违长时间的研究另外一个方向,并且有不少收获,之前也长时间学习了 webgl,数据可视化,只是应用都非常少,基本只是在 demo 阶段。AI 方向学习要比前端门槛高不少,学习的过程中也一直子反思,深度学习要如何和前端结合上呢?除了对比页面差异,还有哪些呢?还是要落到图像、语音、视频识别检测上,毕竟图像不能回传到后端再返回处理结果,只是在我们团队好像也没有其他智能化可以挖掘的领域,前阵子比较火的 imgcook 去年声音也越来越少,前端智能化还是在探索的领域。
参考