ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java+OpenCV几何图形识别:颜色与形状双判定实战

Java+OpenCV几何图形识别:颜色与形状双判定实战

简介:这份PDF面向具备一定Java基础、希望入门计算机视觉的开发者与学习者,聚焦用Java调用OpenCV视觉库完成几何图像的颜色与形状识别,解决彩色几何图形识别中颜色分离难、形状判别准确率不高的问题。资源共1个PDF文件,压缩包约894KB,内容为完整的技术论文,涵盖HSV颜色空间转换、按H/S/V取值范围分割图像、滤波去噪、灰度化、二值化、轮廓提取与干扰轮廓剔除、轮廓形状拟合及凸点距离判断等关键环节,可识别三角形、圆、矩形、正方形、多边形等多种几何图形,并延伸至交通信号灯等实际应用场景。已有169人学习,适合作为Java+OpenCV图像处理入门与算法思路参考。

1. 用 Java 做几何图形识别:一份能跑通的 OpenCV 落地笔记

手上有一批几何图形图片,要同时判断颜色和形状,第一反应往往是上深度学习。但真到工程里,样本是规则几何体、颜色种类固定、又不想背模型和标注成本时,传统视觉方案反而更省事。这份资源就是围绕 Java + OpenCV 走通一条完整链路:BGR 转 HSV、按 H/S/V 区间做颜色分离、滤波灰度二值化、轮廓提取、去干扰轮廓、多边形拟合,最后用凸点个数和相邻凸点距离判形状。它适合做交通灯、信号灯、色块分拣这类颜色与形状双判定的场景,也适合想用 Java 而不是 Python 落地视觉的开发者。下面按我拆包复现的顺序讲,参数和坑都摊开。

2. 环境搭起来:OpenCV Java 库导入与第一个 Mat

2.1 为什么选 Java 调 OpenCV 而不是纯手写

很多人一看到图像处理就默认 Python,但这份资源的价值恰恰在于它证明了 Java 侧也能把 OpenCV 的 native 能力用起来。OpenCV 的 Java 绑定本质是 JNI 封装,核心运算仍然跑在 C++ 层,Java 只负责组织调用和业务逻辑。对已经有一套 Java 后端、想把识别能力嵌进现有服务的团队来说,少引入一个 Python 运行时、少做一次跨进程通信,维护成本是实打实降下来的。

选型上还有一层考虑:颜色识别用 HSV 而不是 RGB。RGB 三个分量都耦合了亮度信息,同一个红色在强光和阴影下 R/G/B 数值能差出一大截,阈值根本没法固定。HSV 把色相 H 单独拎出来,S 是饱和度、V 是明度,颜色判断主要看 H,光照变化主要影响 V,两者解耦之后阈值区间稳定得多。这也是后面所有颜色分割能成立的前提。

2.2 导入 OpenCV Java 库的实操步骤

资源里用的是 OpenCV 3.0.0 的 Java 包,流程放到现在依然通用。核心是把解压目录里的build/java下的 jar 和对应平台的 native 库挂进项目。以 Eclipse 为例,新建 Java 项目后按下面几步走:

# 假设 OpenCV 解压到 D:\opencv3.0.0 # 1. 找到 Java 库文件 D:\opencv3.0.0\build\java\opencv-300.jar # 2. 找到对应平台的 native 库(32 位系统选 x86,64 位选 x64) D:\opencv3.0.0\build\java\x86\opencv_java300.dll

在 Eclipse 里右键项目 → Build Path → Configure Build Path → Libraries → Add External JARs,选中opencv-300.jar。然后关键一步是让 JVM 能找到 native 库,在 Run Configurations 的 VM arguments 里加:

-Djava.library.path=D:\opencv3.0.0\build\java\x86

这里有个血泪经验:jar 和 dll 的位数必须和 JVM 一致。32 位 JVM 配 x86 的 dll,64 位 JVM 配 x64 的 dll,混搭的典型现象是编译通过、一运行就抛UnsatisfiedLinkError,报错信息还只告诉你找不到方法,不告诉你位数不匹配。我第一次就栽在这,排查了半天才发现 JDK 是 64 位而 dll 拿的是 x86。

2.3 加载库与读取图像的最小验证

环境配好后先别急着写识别逻辑,用一段最小代码验证 OpenCV 是否真的加载成功:

import org.opencv.core.Core; import org.opencv.core.Mat; import org.opencv.imgcodecs.Imgcodecs; public class LoadCheck { public static void main(String[] args) { // 必须最先加载 native 库,否则后续所有调用都会失败 System.loadLibrary(Core.NATIVE_LIBRARY_NAME); // 以灰度方式读入,仅用于验证链路 Mat img = Imgcodecs.imread("test.png"); if (img.empty()) { System.out.println("图像读取失败,检查路径"); return; } System.out.println("宽=" + img.cols() + " 高=" + img.rows() + " 通道=" + img.channels()); } }

System.loadLibrary(Core.NATIVE_LIBRARY_NAME)这行必须放在所有 OpenCV 调用之前,NATIVE_LIBRARY_NAME在 3.x 里解析出来就是opencv_java300,它会去java.library.path指定的目录找对应 dll。Imgcodecs.imread读进来的 Mat 默认是三通道 BGR,这点要记住,后面转 HSV 时顺序不能错。如果这步打印出的宽高和图片实际不符,或者直接抛异常,先回头查 library path 和位数,别往下写。

3. 颜色分离:HSV 区间怎么定、阈值怎么卡

3.1 从 BGR 到 HSV 的转换链路

OpenCV 读图默认是 BGR 顺序,而资源里处理的原始图多是 RGB 语义。转换要分两步走,先 RGB 转 BGR,再 BGR 转 HSV:

import org.opencv.core.Mat; import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; // src 为原始读入图像 Mat resized = new Mat(); // 先缩小,降低后续逐像素遍历的开销 Imgproc.resize(src, resized, new Size(src.cols() / 2, src.rows() / 2)); Mat hsv = new Mat(); // 若源图确为 RGB 语义,先转 BGR;若 imread 直接读入则已是 BGR,可跳过这步 Imgproc.cvtColor(resized, hsv, Imgproc.COLOR_RGB2BGR); Imgproc.cvtColor(resized, hsv, Imgproc.COLOR_BGR2HSV);

Imgproc.resize的第三个参数是目标尺寸,这里直接砍半。为什么要缩?因为后面要遍历像素统计各颜色点数,像素总量直接决定耗时,缩一半面积就降到四分之一,对识别精度影响很小。cvtColor的第三个参数是转换码,COLOR_BGR2HSV是固定常量,别手写数字。注意资源里那段 RGB2BGR 再 BGR2HSV 的写法,如果图本来就是imread读进来的 BGR,第一步其实是多余的,甚至会引入色偏,实际用的时候先确认源图色彩空间。

3.2 七种颜色的 H/S/V 区间表

HSV 里 H 的取值范围在 OpenCV 中是 0~180(不是常见的 0~360,因为 8 位存储除以了 2),S 和 V 都是 0~255。资源给了绿色区间的示例,其余颜色按同一思路定。下面这张表是我按常见几何色块调出来的参考区间,实际项目要拿样本图微调:

颜色H 最小值H 最大值S 最小S 最大V 最小V 最大
红0104325546255
红(高区)1561804325546255
橙11254325546255
黄26344325546255
绿35774325546255
青78994325546255
蓝1001244325546255
紫1251554325546255

红色要拆成两段是因为 H 在 0 和 180 附近是首尾相接的,单段区间会漏掉偏紫的红。S 下限设 43、V 下限设 46 是为了滤掉灰白和过暗的像素,这两个值来自 OpenCV 社区常用的经验下限,不是拍脑袋。调区间时优先动 H 的上下界,S/V 保持宽一点,否则容易把正常色块也切掉。

3.3 逐像素统计与 inRange 分割

定好区间后,先统计每种颜色的像素点数,用阈值判断图里到底有没有这个颜色,避免对不存在的颜色做无谓的后续处理:

import org.opencv.core.Scalar; import org.opencv.core.Core; // 统计绿色像素点数量 int greenCount = 0; for (int i = 0; i < hsv.rows(); i++) { for (int j = 0; j < hsv.cols(); j++) { double[] px = hsv.get(i, j); int h = (int) px[0], s = (int) px[1], v = (int) px[2]; if (h >= 35 && h <= 77 && s >= 43 && v >= 46) { greenCount++; } } } // 阈值 200,低于此值认为图中无该颜色,防止反光噪点误判 if (greenCount > 200) { Mat mask = new Mat(); Mat dst = new Mat(); Core.inRange(hsv, new Scalar(35, 43, 46), new Scalar(77, 255, 255), mask); Core.bitwise_and(resized, resized, dst, mask); }

hsv.get(i, j)返回的是double[],三个元素依次是 H、S、V,注意这里 i 是行、j 是列,别写反。阈值 200 是资源里给的,作用是过滤环境反光和零散噪点,太小会把噪点当目标,太大可能漏掉小图形,按你的图分辨率调。Core.inRange把区间内的像素置 255、区间外置 0,得到一张二值 mask;Core.bitwise_and再用 mask 和原图做与运算,把目标颜色区域抠出来。这两步是颜色分离的核心,mask 质量直接决定后面轮廓干不干净。

4. 形状识别:从轮廓到凸点判形

4.1 三通道转换与滤波去噪

bitwise_and出来的dst是单通道图,直接做灰度化会报错,得先转回三通道:

import org.opencv.core.CvType; import org.opencv.core.Size; Mat colorImg = new Mat(); // 单通道转三通道,否则后续 cvtColor 灰度化会抛异常 dst.convertTo(colorImg, CvType.CV_8UC3); Mat blurred = new Mat(); // 高斯核 5x5,sigma 自动推导 Imgproc.GaussianBlur(colorImg, blurred, new Size(5, 5), 0);

convertTo的第二个参数CV_8UC3表示 8 位无符号三通道,这是灰度化函数能接受的前提。高斯核用 5×5 是平衡去噪和保留边缘的常用值,核越大越糊,边缘一糊轮廓拟合就飘。sigma 传 0 让 OpenCV 按核大小自动算,一般不用手动指定。

4.2 灰度化、二值化与形态学清理

接着灰度化、二值化,再用腐蚀膨胀把零散像素和断口处理掉:

Mat gray = new Mat(); Imgproc.cvtColor(blurred, gray, Imgproc.COLOR_BGR2GRAY); Mat binary = new Mat(); // 阈值 100,最大值 255,THRESH_BINARY 表示超过阈值置白 Imgproc.threshold(gray, binary, 100, 255, Imgproc.THRESH_BINARY); Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3)); // 先腐蚀去掉不连通的零散点 Imgproc.erode(binary, binary, kernel); // 再膨胀把断开的轮廓接上 Imgproc.dilate(binary, binary, kernel);

阈值 100 是资源里的取值,实际要看你的图对比度,背景偏亮就往上调。腐蚀在前、膨胀在后,顺序不能反:先腐蚀把噪点吃掉,再膨胀补回主体轮廓,反过来的话噪点会被先放大再腐蚀,清理不干净。3×3 的矩形核适合规则几何体,核再大容易把细边磨没。

4.3 findContours 与面积过滤

二值图准备好后提取轮廓,并按面积剔除干扰:

import org.opencv.core.MatOfPoint; import java.util.ArrayList; import java.util.List; List<MatOfPoint> contours = new ArrayList<>(); Mat hierarchy = new Mat(); // RETR_EXTERNAL 只取最外层轮廓,CHAIN_APPROX_SIMPLE 压缩直线段 Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE); // 倒序遍历,边删边遍历时正序会漏元素 for (int j = contours.size() - 1; j >= 0; j--) { double area = Imgproc.contourArea(contours.get(j)); if (area <= 10 || area >= 80000) { contours.remove(j); } }

RETR_EXTERNAL只保留最外层,几何图形识别一般不需要嵌套轮廓。面积上下限 10 和 80000 是资源给的,下限滤掉噪点小轮廓,上限滤掉整张图边框这类超大轮廓。这里有个容易翻车的点:资源原文是正序遍历加remove,边遍历边删会导致索引错位、漏删元素,我改成倒序才稳。这个细节不写出来,照着抄的人大概率会踩。

4.4 approxPolyDP 拟合与凸点判形规则

最后对每个轮廓做多边形拟合,用凸点个数和相邻凸点距离判形状:

import org.opencv.core.MatOfPoint2f; import org.opencv.core.Point; import org.opencv.imgproc.Imgproc; for (MatOfPoint c : contours) { MatOfPoint2f curve = new MatOfPoint2f(c.toArray()); MatOfPoint2f approx = new MatOfPoint2f(); // 周长作为 epsilon 基准,0.02 是常用逼近精度 double peri = Imgproc.arcLength(curve, true); Imgproc.approxPolyDP(curve, approx, 0.02 * peri, true); Point[] pts = approx.toArray(); int vertexCount = pts.length; // 相邻凸点距离,用于区分正方形和矩形 double d1 = dist(pts[0], pts[1]); double d2 = dist(pts[1], pts[2]); // 判形逻辑见下方说明 }

approxPolyDP的第三个参数 epsilon 决定拟合精度,取周长的 2% 是通用经验值,太大顶点会被合并、三角形可能拟合成一条线,太小则保留过多锯齿点、正方形拟合成多边形。true表示闭合轮廓。判形规则按顶点数走:3 个点是三角形;4 个点且相邻边近似相等是正方形,否则是矩形;5 个点是五边形;顶点数更多且相邻凸点距离波动小、轮廓接近圆的是圆形。圆形没有严格顶点,拟合后顶点数会偏多,所以圆要结合面积和周长比(圆度)辅助判断,单靠顶点数容易和正多边形混淆。

5. 避坑与排查:五个真实翻车点

5.1 现象:运行即抛 UnsatisfiedLinkError

原因基本是 native 库没加载上,要么java.library.path没配、要么 jar 和 dll 位数不一致、要么System.loadLibrary没放在最前面。解决顺序是先确认 JVM 位数(java -version看是否 64 位),再确认 dll 目录,最后检查加载语句位置。这三步走完九成能解决。

5.2 现象:颜色分割出来一片空白或整片全白

多半是 HSV 区间定错,或者转换链路多转了一次。如果源图是imread读的 BGR,又做了一次 RGB2BGR,色相会整体偏移,红色可能跑到蓝色区间去。排查方法是把某个已知颜色像素的 H/S/V 打印出来,对照区间表看落在哪,再决定是改区间还是改转换链路。

5.3 现象:轮廓提取数量对不上,目标图形被漏掉

常见原因是二值化阈值不合适,或者腐蚀核太大把细轮廓磨断了。先看二值图长什么样,如果目标区域本身就不完整,问题在阈值;如果完整但轮廓断开,把腐蚀核从 3×3 降到 2×2 或改用开运算。另外面积过滤的上下限也要按图分辨率调,图小的时候 80000 的上限可能把正常目标也滤掉。

5.4 现象:正方形被判成矩形,或圆被判成多边形

正方形判成矩形是相邻凸点距离比较的容差太严,实际拍摄有透视变形,边长不会完全相等,容差放到 10%~15% 比较稳。圆判成多边形是approxPolyDP的 epsilon 太小,顶点保留太多,把 epsilon 从 0.02 提到 0.03~0.04,或者改用圆度(4π×面积/周长²)辅助判定,圆度接近 1 就是圆。

5.5 现象:同一张图多次运行结果不一致

如果代码里用了多线程或并行流处理轮廓,Mat不是线程安全的,共享 Mat 会出随机结果。另外contours.remove在正序遍历时删元素也会导致结果飘。解决是把轮廓处理改成单线程顺序执行,删除操作一律倒序,Mat 用完及时release()。

6. 进阶:把识别率从能跑到稳的几个手法

资源里提到识别准确率 99% 以上,但那是特定样本下的数字,换一批图大概率掉。想把稳定性拉起来,我一般会在这几个地方加码。第一是颜色区间自适应:不要写死 H 范围,先对样本图做一次直方图统计,取目标色像素 H 值的 5% 和 95% 分位作为区间边界,比固定表抗光照差异。第二是形状判定加一层圆度兜底,顶点数判形对规则多边形有效,但透视变形下正方形和菱形容易混,圆度加边长比双条件能压住误判。

第三是预处理阶段加一步自适应阈值,Imgproc.adaptiveThreshold在光照不均的图上比全局阈值稳得多,代价是参数多一个 blockSize 要调,一般取奇数、从 11 起步试。第四是把整个链路包成一个可配置的 pipeline,颜色区间、面积上下限、epsilon 系数都抽成配置项,换场景只改配置不改代码,这在多品类分拣场景里能省大量重复劳动。

验证方法上,别只看单张图跑通就收工。准备一组覆盖七种颜色、五种形状、不同光照的样本,跑一遍统计混淆矩阵,重点看哪些颜色对容易串(红和橙、蓝和紫是高频混淆对),哪些形状容易混(正方形和矩形、圆和正多边形)。混淆对出来了,针对性收窄对应颜色的 H 区间或加形状二次校验,比盲目调参高效。

有个习惯我后来一直保持:每次改完阈值或区间,先把中间产物(HSV 图、mask、二值图、拟合后的轮廓图)全部落盘存一份,出问题时直接看是哪一步开始不对,比在代码里打断点快得多。这套流程看着笨,但省下的排查时间远超存图那点开销。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进