OpenCV实战:多方向箭头识别与模板匹配优化

📅 发布时间:2026/10/21 3:10:04
OpenCV实战:多方向箭头识别与模板匹配优化
“OpenCV 项目实战多方向箭头识别与模板匹配优化”这个标题里的每一个词都是我踩过坑之后才真正理解的。接到需求时我原以为多方向箭头识别只是模板匹配的入门级应用做起来才发现光是把“多方向”三个字落地就有数不清的细节要处理模板旋转后黑边怎么消除、角度步长选多少、匹配值阈值设多高、误检怎么去掉、速度怎么提上去。这篇文章就把整个项目的完整思路、代码实现、参数调优和排坑过程写透适合正在做工业视觉识别、地图方向标识、自动驾驶辅助或者机器人导航方向检测的朋友参考。哪怕你刚接触OpenCV跟着这篇文章走一遍也能对模板匹配的实战用法有系统性的理解。1. 项目背景与需求拆解1.1 这个项目到底要做什么项目本身不复杂给一张带箭头的图像自动识别出箭头指向的方向。听起来像是很小的问题实际落地场景却不少。比如地图块上的方向指示、工业传送带上的产品方向标识、室内机器人读取地面导向箭头或者驾驶辅助系统中对路面导向箭头的检测本质上都是同一类问题。我那次的场景是这样摄像头拍下流水线上的工件工件上印刷着指向不同方向的箭头系统要根据箭头方向控制机器人调整加工角度。方向种类不多大概就是上、下、左、右加上四个斜角方向一共八个方向但难在箭头的形态不固定——有的粗、有的细有的带装饰性描边有的只是纯色块。一开始我也想过用深度学习方案训练一个目标检测模型来识别箭头方向。但实际确定方案时发现几个硬性约束一是样本量不够箭头样式五花八门收集到足够的标注数据要花很长时间二是部署环境算力有限工控机上跑深度学习模型虽然也不是不行但复杂度高三是项目周期紧需要快速交付可用的原型。综合考虑后我选择了OpenCV里最经典的模板匹配作为基础方案再针对多方向、多尺度、复杂背景几个痛点做优化。1.2 为什么模板匹配反而是最优解很多人一听到图像识别就想到深度学习实际上模板匹配在特定场景下依然有不可替代的价值。模板匹配的本质是拿一张已知的模板图在待搜索的图像上逐像素滑动计算每个位置的相似度相似度最高的位置就是目标所在。这个过程不需要训练不需要标注大量的数据只要有一张清晰的模板图就能跑起来。在我那个场景里模板匹配的优势尤其明显工件上的箭头虽然方向不同但形态基本一致都是从同一个模板旋转得到的。这意味着我可以只提取一个基准模板通过旋转生成各种方向的模板库然后逐一匹配。整个过程完全可控每个环节都能解释出了问题也容易排查。而深度学习模型虽然泛化能力强但更像是黑盒一旦识别错了很难定位到底是因为训练数据不足还是模型结构问题还是阈值设置问题。模板匹配还有一个容易被忽视的好处它对图像质量的容忍度比想象中高。归一化相关系数匹配对光照变化有一定的鲁棒性因为它在计算时会对图像子区域做均值归一化处理。当然前提是要正确选择匹配方法这一点后面细说。1.3 真实场景中的痛点清单真正动手实现之前我先梳理了一下这个项目可能遇到的难点这些都是做视觉识别的通用痛点多方向覆盖问题箭头方向是任意的模板匹配是刚性匹配模板向右、目标向左匹配值就会很低所以必须构建覆盖所有方向的模板库。尺度不匹配问题摄像头的高度、工件的远近会导致箭头在图像里的尺寸发生变化固定尺寸的模板无法覆盖。背景干扰问题工件表面不是纯色的有纹理、反光、点状杂质很容易造成误检。运算实时性问题八个方向加上多尺度就是几十上百次模板匹配不做优化的话耗时不可控。阈值设定问题匹配值设高了漏检设低了误检怎么取一个平衡点这些问题单独看都不难但组合在一起就需要一套完整的方案设计。我采用的思路是基准模板提取 旋转模板库生成 金字塔多尺度匹配 非极大值抑制去重 多级阈值筛选。下面把每一步的细节和原理详细拆开讲。2. 模板匹配原理与多方向模板库构建2.1 模板匹配的几种模式与选型逻辑OpenCV的模板匹配主要有六种方法常用的是这几个TM_SQDIFF平方差、TM_CCORR_NORMED归一化相关性、TM_CCOEFF_NORMED归一化相关系数。第一个是值越小越匹配后两个是值越大越匹配。实际项目里我强烈建议直接选择TM_CCOEFF_NORMED。原因是它在计算时对模板和图像子区域都做了减均值的操作等于先归一化再求相关性。这意味着即使图像的亮度整体偏高或者偏低比如工件表面左侧有强反光、右侧有阴影匹配结果也不会有大的波动。相比之下TM_SQDIFF对亮度偏移非常敏感同样的箭头放在不同亮度背景下匹配值会差异巨大。我自己做过对比实验同一张箭头图用TM_CCOEFF_NORMED方法在正常光照和加了一层层阴影的情况下匹配值从0.92下降到0.85而用TM_SQDIFF得分变化幅度明显更大阈值很难设置。所以对于工业场景这种光照环境不太稳定的情况TM_CCOEFF_NORMED是天然的首选。这里也顺便提一句OpenCV匹配结果的处理方式matchTemplate函数会返回一个浮点型的相似度矩阵尺寸等于搜索图高度 - 模板高度 1乘以搜索图宽度 - 模板宽度 1。矩阵里每个点的值代表模板左上角放在该位置时的相似度。我们要做的就是从矩阵中找到峰值点这个峰值点的位置就是目标匹配位置。2.2 多方向模板库的设计思路多方向识别的核心问题是模板匹配是刚性匹配模板是什么角度就只能匹配什么角度的目标。所以最简单的思路就是把基准模板旋转出各个角度形成模板库然后逐一遍历匹配。这里的关键参数是旋转步长。步长太大会导致某些中间角度没被覆盖漏掉目标步长太小模板数量暴增匹配速度下降。我当时的做法是先测试一下基准模板本身对于角度偏移的容忍度。方法是拿模板旋转5度、10度、15度分别与原模板做匹配看匹配值下降到什么程度。实测下来10度以内的角度偏移对匹配值影响不大从0.95掉到0.88左右超过15度后开始明显下降20度以上几乎就不稳定了。所以步长选10度到15度比较合理我最终选了10度一共生成36个方向的模板。这个思路和相机标定里的角度分辨率概念很像你需要的角度分辨率越高模板数量就越多消耗的计算资源也就越大。如果你只需识别八个方向可以先用45度粗扫定位大致方向区间再用小步长细化省下不少时间。生成旋转模板时有一个坑必须注意直接使用cv2.getRotationMatrix2D cv2.warpAffine旋转图像输出图像的大小如果不调整边缘会被裁剪掉。比如一个向右的箭头旋转45度后箭头的头部和尾部可能超出原图边界。如果这里处理不好后面匹配的全是残缺的箭头模板效果自然差。解决办法是计算旋转后的新画布尺寸并把旋转中心平移补偿到位。代码逻辑是这样的旋转矩阵M的第0行第2列和第1行第2列代表平移量要让旋转后的图像居中在新画布上就要根据旋转角度计算新画布的宽高然后调整平移量。import cv2 import numpy as np import math def generate_rotated_templates(base_template, step_angle10): templates [] h, w base_template.shape[:2] center (w // 2, h // 2) for angle in range(0, 360, step_angle): M cv2.getRotationMatrix2D(center, angle, 1.0) # 计算旋转后的新画布尺寸关键步骤防止箭头被裁剪 rad math.radians(angle) cos_a abs(math.cos(rad)) sin_a abs(math.sin(rad)) new_w int(h * sin_a w * cos_a) new_h int(h * cos_a w * sin_a) # 补偿平移量使旋转后的图像居中 M[0, 2] (new_w / 2) - center[0] M[1, 2] (new_h / 2) - center[1] rotated cv2.warpAffine( base_template, M, (new_w, new_h), flagscv2.INTER_CUBIC, borderModecv2.BORDER_REPLICATE ) templates.append((angle, rotated)) return templates这里有一点要补充borderMode我选的是BORDER_REPLICATE也就是复制边缘像素填充旋转后产生的空白区域。默认的BORDER_CONSTANT会填充纯黑或纯白在模板匹配时这部分空白会被当成有效内容参与计算严重影响匹配值。BORDER_REPLICATE虽然也只是近似但填充的内容与模板边缘相近对匹配值的干扰小很多。2.3 金字塔多尺度匹配模板与目标的尺度不一致是模板匹配的另一大痛点。我的工件距离摄像头并不是完全固定的偶尔会有几毫米的波动这反映到图像上就是箭头大小差几个像素到十几个像素。如果模板尺寸和目标尺寸差得太大匹配值会大幅下降。最简单的办法是直接生成多个尺寸的模板。但这样模板数量就是方向数乘以尺度数膨胀得非常快匹配耗时也会成倍增长。我当时的做法是用图像金字塔的思路在搜索图上做缩放而不是在模板上做。具体逻辑是把待搜索图像分别缩放到0.8倍和1.2倍得到三张不同尺度的搜索图原图对应1.0倍然后拿同一套36个方向的旋转模板分别在这三张图上做匹配综合三次匹配的结果。这样做的好处是模板库的规模不用翻倍坏处是匹配次数变成了原来的三倍。不过后面我会讲到怎么配合金字塔先粗后精的策略把这部分耗时降下去。def search_multi_scale(img, templates, scales(0.8, 1.0, 1.2), threshold0.75): results [] for scale in scales: if scale ! 1.0: resized cv2.resize(img, None, fxscale, fyscale, interpolationcv2.INTER_LINEAR) else: resized img.copy() gray cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) for angle, tmpl in templates: tmpl_gray cv2.cvtColor(tmpl, cv2.COLOR_BGR2GRAY) result cv2.matchTemplate(gray, tmpl_gray, cv2.TM_CCOEFF_NORMED) # 找出所有高于阈值的局部峰值 loc np.where(result threshold) for pt in zip(*loc[::-1]): x_orig int(pt[0] / scale) y_orig int(pt[1] / scale) val result[pt[1], pt[0]] results.append((val, angle, x_orig, y_orig)) return results这里需要注意的是坐标换算在缩小的图像上找到的匹配坐标要还原到原图坐标系需要除以对应的缩放系数。如果漏掉这一步画出来标记框的位置就会偏差看起来像是匹配到了实际上框的位置是错的。3. 核心代码实现与关键参数调优3.1 模板预处理与提取模板的质量决定了整个识别系统的上限。我提取基准模板时没有直接用原图截取而是做了一步预处理转灰度、高斯模糊、自适应阈值分割。这么做是为了去掉模板里的背景纹理和噪点让模板内容更干净只有箭头本身。其中一个关键细节是模板尺寸的选择。如果模板取得太大包含太多背景信息匹配时反而容易被干扰如果取得太小比如箭头只占了模板的一半旋转后箭头的特征不够明显匹配值也不稳定。我最后用的模板尺寸是64x64像素箭头主体约占中间60%的区域四周留出约20%的余量。这个余量是为了给箭头轮廓的形态变化留空间同时不至于引入太多无关信息。箭头提取用自适应阈值要注意参数。我用的blockSize是11C值为2。blockSize代表计算局部阈值时参考的邻域大小C是最终阈值减去的常量。这两个参数决定了分割的细腻程度。blockSize太小背景噪声容易变成黑点太大箭头区域和背景的亮度差异被平滑掉分割不干净。C值也是类似的作用太大导致箭头内部出现空洞太小则背景残留多。def extract_base_template(img, roi): x, y, w, h roi region img[y:yh, x:xw] gray cv2.cvtColor(region, cv2.COLOR_BGR2GRAY) blur cv2.GaussianBlur(gray, (5, 5), 0) thresh cv2.adaptiveThreshold( blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2 ) # 确保模板主体的箭头区域为白色背景为黑色 # 如果反了做一次取反 if np.mean(thresh) 127: thresh 255 - thresh return thresh3.2 主匹配流程与阈值策略主匹配流程的逻辑其实不复杂遍历所有方向的模板用matchTemplate算出相似度矩阵找到高于阈值的点记录下来然后统一做非极大值抑制NMS去掉互相重叠的重复结果。阈值怎么设这里面有很大学问。我一开始把阈值设成0.8发现漏检率很高因为光照稍微差一点的话匹配值可能只有0.7。后来把阈值降到0.6误检又开始变多背景上的纹理偶尔也会达到0.6以上。最终我采用的是双阈值策略先用一个较低的阈值0.6做初筛把所有候选框全部找出来然后用非极大值抑制和角度一致性检查做复筛最后再要求最高匹配值必须大于0.8才会输出结果。这个策略的本质是初筛阶段求全复筛阶段求精。低阈值负责保证召回率后续的NMS和角度检查负责保证准确率。实际效果比我以前用单一阈值好了很多误检率和漏检率都能控制在可接受范围内。阈值的选择也跟匹配方法相关。使用TM_CCOEFF_NORMED时匹配值的范围在-1到1之间一般的有效匹配都在0.6以上。如果换了TM_SQDIFF阈值的含义完全不同不能混用。def non_max_suppression(results, overlap_threshold0.3): if not results: return [] final [] results_sorted sorted(results, keylambda r: -r[0]) for val, angle, x, y in results_sorted: keep True for fval, fangle, fx, fy in final: # 计算两个矩形框的重叠面积比例 w 48 h 48 x_overlap max(0, min(x w, fx w) - max(x, fx)) y_overlap max(0, min(y h, fy h) - max(y, fy)) inter_area x_overlap * y_overlap union_area w * h * 2 - inter_area iou inter_area / union_area if iou overlap_threshold: keep False break if keep: final.append((val, angle, x, y)) return final这段代码里有个细节容易忽略判断两个候选框是否重叠时用的模板宽度w和高度h指的是候选框在搜索图上的尺寸而不是模板在原点处的尺寸。因为我们在不同尺度下匹配候选框的实际大小会不同计算IoU时要基于各自的实际尺寸。我上面代码里简化成了固定48x48实际上应该根据每个结果的尺度信息动态获取矩形尺寸。如果你的场景尺度变化较大这里要改一下否则重叠抑制可能失效。3.3 角度一致性检查光靠位置重叠的NMS还不够因为同一个箭头在相邻角度方向上可能都有超过阈值的匹配值。比如一个指向正右方的箭头在0度和-10度方向上都可能匹配成功位置基本重合但如果NMS只看位置重叠会把两个结果都保留下来只是IoU很高应该会被抑制。但如果匹配位置略有偏差两个结果并不重叠就会出现两个输出结果指向同一个箭头。我加了一步角度一致性检查对于位置接近的两个候选框保留匹配值更高的那个同时把另一个角度信息合并进来。这样输出结果仍然是唯一的并且角度信息是匹配值更高那个方向。本质上这可以看成NMS的加强版把角度维度也纳入抑制范围。还有一个容易踩的坑在匹配过程中旋转模板的尺寸不一。一个45度的箭头必然比0度的箭头占用更大的画布尺寸因为斜着放需要更大的矩形才能容纳。这意味着同一个箭头在不同角度模板下匹配到的位置微有差异画出来的检测框大小也不同。NMS计算IoU时如果直接用两个不同尺寸的框做比较重叠比例会被拉低导致抑制失败。所以我在保存结果时还做了一个位置修正统一以基准模板的中心点为锚点重新计算候选框位置保证不同角度下的框对齐。3.4 背景干扰的进一步处理工业场景里最常见的干扰源是工件表面的金属纹理、喷码字符、不规则形状的污渍。这些干扰在灰度图下往往具有一定的边缘特征和箭头有相似之处。匹配值虽然不高但阈值低时容易混进来。处理思路有两个方向。一是从模板侧出发使用带掩码的模板匹配。如果箭头是纯色块形状可以同时生成一个掩码模板背景区域标记为0箭头区域标记为255调用cv2.matchTemplate(img, tmpl, method, mask)时OpenCV只计算掩码区域的匹配度。这样模板周边的填充区域不会被计入匹配分数干扰会明显减少。二是从图像侧出发对搜索图做边缘提取或者梯度增强。把彩色图转成梯度幅值图再进行模板匹配。因为箭头通常具有规则的轮廓边缘而纹理干扰的梯度方向是混乱的梯度域匹配能极大减弱纹理影响。我实测下来用Sobel算子求梯度幅值后匹配误检数量能减少一半以上。代价是预处理时间稍微增加了一些但总体仍然可控。4. 性能优化实战记录4.1 速度瓶颈定位初次实现跑通后我发现单张图像识别耗时非常夸张36个方向的模板每个方向都做一次全图匹配加上多尺度一共是108次matchTemplate调用。在我的工控机上一张640x480的图像108次匹配加起来将近2.3秒。这个速度用来离线分析可以但流水线上完全不可接受。要先定位瓶颈在哪里。matchTemplate的时间复杂度大致是O(NMT)其中N和M是搜索图的宽高T是模板的像素数。108次全图匹配每一步都需要遍历整张图像的每个位置乘以模板的每个像素。显然最直接的加速手段就是减少匹配次数、减少搜索图大小、减少模板大小。4.2 三步提速法我最终落实的提速方案分三步走每一步都能省下可观的时间第一搜索图降采样。将原图从640x480缩到320x240匹配次数虽然不变但每次匹配的计算量降到了原来的四分之一。代价是定位精度从像素级变成2像素级对于我们的应用来说2像素的精度完全足够。第二全局快速预筛。在降采样后的图上先用90度步长的粗模板库只有4个模板快速扫一遍找到目标可能存在的大致区域。然后只在这个候选区域里用10度步长的完整模板库做精细匹配。候选区域的尺寸可以设定为全图的四分之一这样精细匹配阶段的计算量又减掉了四分之三。第三模板尺寸裁剪。我把模板从64x64缩小到48x48模板像素面积减少了约44%。因为是旋转生成的模板斜角模板的画布尺寸会比基准模板更大裁剪统一的画布大小后匹配计算量可以进一步控制。三步走完单张图像的识别耗时从2.3秒降到了约120毫秒完全满足我们每秒处理8帧的需求。这个优化过程充分说明了一个道理性能优化不要四处乱试先定位瓶颈再针对性地减少计算量效果立竿见影。4.3 金字塔匹配的另一种用法前面讲到用图像缩放来覆盖尺度变化但它是直接放大搜索图多做了两次全图匹配其实还是有优化空间的。更聪明的做法是反过来的金字塔流程先在缩小的图像上用较低的阈值做一次粗匹配粗匹配后得到候选位置然后再在原图上用高阈值精匹配验证这些候选位置。具体来说我把搜索图缩小到原来的1/4用所有36个方向模板匹配一遍这一步虽然也做了36次但每次匹配只有原来的十六分之一计算量总耗时大约是全图精匹配的2%。如果粗匹配阶段发现某个位置匹配值大于0.5就记录下它对应的模板角度然后在原图里只对这个角度附近的模板做精匹配角度搜索范围缩窄到±20度只匹配5到6个角度模板。同时精匹配的区域也缩小到候选位置附近的一小块而不是全图搜索。这个两级匹配策略最核心的收益在于把原来必须做的108次全图匹配降到了36次小图低分辨率匹配加6次局部精匹配。整个流程的总计算量反而比简单的三步提速法更小。我实现后耗时进一步降到了70到80毫秒左右。这里我想提醒一下金字塔匹配虽然快但对小目标不友好。如果箭头在缩小的图像里已经小于模板的一半粗匹配阶段就会因为特征损失而漏掉目标。遇到这种情况缩小的倍数要适当放小比如缩到1/2而不是1/4或者第一层就用原图的1/2尺度做粗匹配。5. 常见问题排查与避坑指南5.1 匹配值高但位置是错的这是我最常被问到的问题之一模板匹配跑到一个完全不是箭头的地方匹配值却高达0.9。原因往往出在模板本身的特征区分度不够。比如模板是大面积纯色块加小箭头匹配时纯色区域也参与了打分背景里只要有类似亮度的区域就能拿到高分。解决思路有两个一是重新设计模板把模板裁小只保留箭头的轮廓部分避免无关背景混入二是改用边缘图匹配在梯度域做匹配而不是在灰度域。我在项目中用边缘图的方案比较成功因为箭头轮廓清晰纹理区域在梯度图上的响应虽然存在但方向不一致匹配值自然偏低。5.2 光照变化导致匹配值不稳定如果工件表面反光严重有的区域过亮有的区域过暗TM_CCOEFF_NORMED的鲁棒性也会被逼近极限。我实测发现强反光区域的匹配值可能比正常区域低0.15到0.2这个差异足以让一些在阈值边缘的目标被判漏检。针对这个场景我建议做两步预处理一是对搜索图做高斯模糊让局部的反光斑块不那么锐利二是把图像归一化到统一的均值和方差例如用cv2.normalize将图像像素值拉伸到0到255的完整范围这样能部分消除亮度不一致的影响。如果还是不稳定就要考虑换用更复杂的特征描述比如方向梯度直方图HOG特征加支持向量机SVM这是模板匹配到一个分水岭时会自然想到的升级路径。5.3 旋转模板数量到底该选多少模板数量是个博弈参数。数量少了角度覆盖不全和目标角度不一致时匹配值下滑严重数量多了匹配速度变慢。我的建议是先测试、后定性不要拍脑袋。具体做法是准备一张与基准模板方向完全一致的测试图然后把测试图旋转5度、10度、15度、20度分别记录匹配值的变化曲线。找到匹配值下降到临界阈值比如0.7的临界角度取这个角度的1/2到1/3作为模板步长即可。比如我实测中5度偏移时匹配值约0.9310度约0.8515度约0.7620度约0.65。如果我希望即使偏移15度也能稳定匹配那么步长应取7到8度左右。这个测试很简单花十分钟就能得到结论比任何经验法则都靠谱。5.4 同一方向出现两个结果这个问题大多数时候是NMS实现有漏洞主要是IoU计算错误或者阈值设置不当。最容易犯的错误是拿不同尺度下匹配到的框直接做IoU计算忘记把框坐标和尺寸还原到同一坐标系。不同尺度下的框尺寸不同即使真重叠IoU也可能被算得很低导致抑制失败。解决办法是把候选框坐标统一到原图坐标系下计算并且使用基于中心点的距离做抑制。对于模板匹配场景同一个目标产生的匹配结果中心点基本一致用中心距离加角度相似度做判断比纯IoU更简单有效。距离阈值可以取模板宽度的1/3角度阈值取15度左右。这个方法在编码上更简单也避免了很多IoU计算的细节坑。5.5 模板匹配真的够用吗做完这个项目后我也复盘过模板匹配方案的边界。如果你的场景中箭头形态完全统一方向有限背景干净或者可控模板匹配是效率极高、稳定可靠的选择。但如果箭头有变形、遮挡、重叠或者背景非常复杂且不断变化模板匹配的缺陷会逐渐放大这时候就该认真考虑基于特征点的匹配或者深度学习方案了。我个人的判断标准是如果模板匹配在真实测试集上的准确率达不到98%以上且无法通过预处理改善就不要硬撑尽早换方案。模板匹配的优势在于快速验证和资源占用低不适合在复杂场景里勉强使用。6. 经验沉淀与后续可扩展的方向从零开始实现一个多方向箭头识别项目最大的收获并不是那几十行OpenCV代码而是一套问题拆解和性能调优的方法论。任何视觉识别需求到手先分清楚是分类问题、检测问题还是定位问题再结合样本量、实时性要求、部署环境去选方案。像我这次的情况样本少、方向明确、背景可控模板匹配就是那个时间成本和效果平衡最好的选择。几个技术细节我再用个人经验强调一遍第一模板匹配前把图像和模板都处理成同样的规格包括灰度化、模糊、阈值分割这比在匹配之后费劲调阈值有效得多。第二旋转模板库生成时一定要处理画布尺寸问题直接用默认参数旋转出带黑边的模板后面所有匹配结果都会受影响。第三NMS不要只做一次至少把位置重叠抑制和角度抑制串在一起处理输出结果才干净。第四性能优化要有数据支撑每次改动都测一下耗时和准确率否则很容易陷入盲目调参数的状态。这个项目后续还有不少可以扩展的地方。比如把多方向识别的输出结果接入机械臂控制逻辑实现真正的自动化分拣或者把模板匹配换成基于边缘点的形状匹配进一步提高对遮挡和噪声的鲁棒性也可以用同样的旋转模板库思路去识别其他类型的对称或非对称图形标记。最后再分享一个小技巧如果在匹配结果里有多个疑似目标把每次匹配的峰值图和匹配值打印出来用一个简单的可视化脚本把所有结果画到原图上一目了然。调试视觉识别项目时任何文字日志都不如一张画满检测框的图直观。