简介:这份PDF面向Java开发者与计算机视觉初学者,聚焦如何用Java调用OpenCV视觉库完成几何图像的颜色与形状识别,解决Java生态下彩色几何图形识别资料偏少、识别率不高的问题。资源包共1个文件,为894KB的PDF文档,内容围绕HSV颜色空间展开,涵盖图像颜色分离、滤波、灰度化、二值化、轮廓提取、干扰轮廓去除、轮廓形状拟合及凸点距离判断等完整流程,可识别三角形、圆、矩形、正方形、多边形等图形,并延伸至交通信号灯等实际场景。目前已有169人学习下载。读者可从中获得一套可复用的Java+OpenCV识别思路与算法实现细节,理解HSV相较RGB在颜色识别上的优势,掌握从颜色分割到形状判别的关键步骤,适合作为课程设计、毕业设计或视觉入门项目的参考材料。
1. 几何图像颜色与形状识别:Java OpenCV 到底能落地到什么程度
车间质检工位上摆着一台普通 USB 摄像头,传送带送过来一堆塑料齿轮,操作员需要在一秒内判断齿轮的颜色是否合格、齿形是否缺角。这种场景用 Python + OpenCV 写个 demo 只要半小时,但产线上的工控机跑的是 Java 服务,MES 系统也是 Spring Boot 那一套,运维不想再维护一套 Python 环境。于是问题变成:Java 能不能扛住 OpenCV 的图像处理活,颜色识别和形状识别的精度够不够用。
这个方向解决的就是这件事——用 Java 调用 OpenCV 的原生库,在 JVM 里完成图像的色彩空间转换、颜色阈值分割、轮廓提取和多边形逼近,最终输出颜色分类和形状分类结果。适合有 Java 基础、需要把视觉能力嵌入现有 Java 后端或桌面应用的开发者。它不适合追求极致帧率的深度学习场景,但对规则明确的几何图像——色块、零件轮廓、卡片形状、交通标志——完全够用。下面把我实际落地时踩过的路拆开讲。
2. 环境搭建与 JavaCV 选型:为什么不用纯 JNI 手写
2.1 Java 调 OpenCV 的三条路,选错返工一整天
Java 本身没有图像处理的底层能力,BufferedImage做像素级操作性能很差。要让 Java 用上 OpenCV,常见做法有三条:
第一条是官方 OpenCV 的 Java binding。下载 OpenCV 安装包后,opencv-4xx.jar加上对应平台的libopencv_java4xx.so(Linux)或.dll(Windows),通过System.loadLibrary(Core.NATIVE_LIBRARY_NAME)加载。这条路最“正统”,但跨平台部署时动态库路径经常翻车,Windows 上还要处理java.library.path。
第二条是 JavaCV。JavaCV 是 Bytedeco 维护的一套 JavaCPP 预设包,把 OpenCV、FFmpeg 等原生库打包成 Maven 依赖,通过 classifier 自动拉取对应平台的二进制。你只需要在pom.xml里声明依赖,不用手动配.so或.dll。对于需要快速在 Windows 开发、Linux 部署的团队,这条路省事得多。
第三条是自己写 JNI 封装。除非你有极特殊的性能需求,否则不建议——维护成本高,OpenCV 版本升级时接口全要重写。
我一般选 JavaCV,原因是依赖管理干净,CI/CD 里不用额外装 OpenCV 系统包。代价是包体积偏大,一个javacv-platform依赖会拉下所有平台的二进制,几百 MB。生产环境可以用 classifier 只保留目标平台。
2.2 Maven 依赖与最小可运行代码
下面是一个只保留 Linux x86_64 平台的依赖配置,避免把 Windows 和 macOS 的库也打进去:
<dependencies> <!-- JavaCV 核心,排除全平台二进制 --> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv</artifactId> <version>1.5.10</version> </dependency> <!-- 只拉取 Linux x86_64 的 OpenCV 原生库 --> <dependency> <groupId>org.bytedeco</groupId> <artifactId>opencv</artifactId> <version>4.9.0-1.5.10</version> <classifier>linux-x86_64</classifier> </dependency> <!-- JavaCPP 运行时 --> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacpp</artifactId> <version>1.5.10</version> </dependency> </dependencies>逻辑说明:javacv是聚合包,opencv带 classifier 的依赖才是真正包含libopencv_java原生库的包。版本号里4.9.0-1.5.10表示 OpenCV 4.9.0 对应 JavaCPP 1.5.10 的封装。如果你在 Windows 上开发,把 classifier 换成windows-x86_64,或者干脆用javacv-platform一次性全平台。
参数说明:classifier决定了拉取哪个平台的二进制,写错会在运行时抛UnsatisfiedLinkError。version里 OpenCV 和 JavaCPP 的版本必须匹配,不能随意混搭。
最小验证代码——读一张图,打印尺寸和通道数:
import org.bytedeco.opencv.opencv_core.Mat; import org.bytedeco.opencv.global.opencv_imgcodecs; public class EnvCheck { public static void main(String[] args) { // 用 imread 读取图片,第二个参数 IMREAD_COLOR 强制三通道 Mat img = opencv_imgcodecs.imread("test.jpg", opencv_imgcodecs.IMREAD_COLOR); if (img.empty()) { System.out.println("图片读取失败,检查路径"); return; } // rows=高, cols=宽, channels=通道数 System.out.println("高:" + img.rows() + " 宽:" + img.cols() + " 通道:" + img.channels()); } }逻辑说明:imread返回的Mat是 OpenCV 的内存矩阵,empty()判断是否读取成功。rows()是图像高度,cols()是宽度,channels()对彩色图是 3。这一步跑通说明原生库加载正常,后面所有处理都建立在这个基础上。
参数说明:IMREAD_COLOR会强制转成 BGR 三通道,即使原图是灰度或带 alpha 通道。如果你要保留 alpha,用IMREAD_UNCHANGED。路径用相对路径时,工作目录是项目根目录,不是src。
提示:如果
imread返回空但路径没错,先确认文件确实存在且不是中文路径。OpenCV 在部分平台上对非 ASCII 路径支持不好,这是血泪经验。
3. 颜色识别:从 BGR 到 HSV 的阈值分割与掩膜提取
3.1 为什么颜色识别必须先转 HSV
直接用 BGR 三个通道做阈值,光照一变阈值就废。HSV 把色相(H)、饱和度(S)、明度(V)分开,色相描述“是什么颜色”,明度描述“有多亮”。调整光照主要影响 V,色相基本稳定。所以颜色识别的标准流程是:BGR 转 HSV,对 H 通道设范围,生成掩膜,再用掩膜提取原图区域。
OpenCV 里 H 的范围是 0 到 179(不是 360),S 和 V 是 0 到 255。红色比较特殊,它跨在 H 的两端(0 附近和 179 附近),需要两个区间合并。
3.2 颜色阈值分割的完整代码
import org.bytedeco.opencv.opencv_core.*; import org.bytedeco.opencv.global.opencv_imgproc; import org.bytedeco.opencv.global.opencv_core; import static org.bytedeco.opencv.global.opencv_imgproc.*; public class ColorDetect { public static Mat detectRed(Mat bgr) { Mat hsv = new Mat(); // BGR 转 HSV,颜色空间转换是后续阈值的前提 opencv_imgproc.cvtColor(bgr, hsv, opencv_imgproc.COLOR_BGR2HSV); // 红色跨 H 两端,分两段 Mat mask1 = new Mat(); Mat mask2 = new Mat(); // 低端:H 0-10 opencv_core.inRange(hsv, new Scalar(0, 100, 100, 0), new Scalar(10, 255, 255, 0), mask1); // 高端:H 170-179 opencv_core.inRange(hsv, new Scalar(170, 100, 100, 0), new Scalar(179, 255, 255, 0), mask2); // 两段掩膜合并 Mat mask = new Mat(); opencv_core.bitwise_or(mask1, mask2, mask); // 形态学开运算去噪:先腐蚀后膨胀,去掉小噪点 Mat kernel = opencv_imgproc.getStructuringElement( opencv_imgproc.MORPH_RECT, new Size(5, 5)); opencv_imgproc.morphologyEx(mask, mask, opencv_imgproc.MORPH_OPEN, kernel); return mask; } }逻辑说明:cvtColor把 BGR 转成 HSV。inRange对每个像素判断是否落在[lower, upper]区间内,落在区间内输出 255,否则 0,结果是一张单通道二值图。红色因为 H 跨两端,用两次inRange再bitwise_or合并。morphologyEx的开运算先腐蚀掉小噪点再膨胀恢复主体,能有效去掉阈值分割产生的零星白点。
参数说明:Scalar的四个参数是 H、S、V、alpha,第四个不用填 0。S 下限设 100 是为了排除灰白色(饱和度低)的干扰,V 下限设 100 排除过暗区域。这两个值要根据实际光照调,光照强时 V 下限可以提到 150。Size(5,5)的核越大去噪越强,但也会吃掉小目标,一般 3 到 7 之间。
3.3 颜色分类的判定逻辑
拿到掩膜后,统计掩膜中非零像素占比,超过阈值就判定为该颜色:
public static String classifyColor(Mat bgr) { Mat mask = detectRed(bgr); // countNonZero 统计掩膜中白色像素数 int redPixels = opencv_core.countNonZero(mask); double ratio = (double) redPixels / (bgr.rows() * bgr.cols()); // 占比超过 15% 判定为红色 if (ratio > 0.15) return "RED"; return "UNKNOWN"; }逻辑说明:countNonZero返回掩膜中值不为 0 的像素个数,除以总像素数得到占比。占比阈值根据目标在画面中的大小定,如果目标占画面一半,阈值可以设 0.3;如果目标很小,设 0.05。
参数说明:多颜色分类时,对每种颜色各生成一个掩膜,取占比最大的作为结果。注意不同颜色的掩膜可能重叠(比如橙色同时落在红和黄的范围),需要按优先级或互斥区间处理。
4. 形状识别:轮廓提取、多边形逼近与形状判定
4.1 轮廓提取的前置处理链
形状识别的前提是二值图。流程是:灰度化 → 二值化(或 Canny 边缘检测)→ 找轮廓 → 对每个轮廓做多边形逼近 → 根据逼近后的顶点数判断形状。
灰度化用cvtColor转COLOR_BGR2GRAY。二值化用threshold或自适应阈值adaptiveThreshold。如果背景光照不均,自适应阈值比全局阈值稳。Canny 适合边缘清晰的场景,但会产生断裂边缘,后续要做闭运算连接。
4.2 从轮廓到形状判定的完整代码
import org.bytedeco.opencv.opencv_core.*; import org.bytedeco.opencv.global.opencv_imgproc; import org.bytedeco.opencv.global.opencv_core; import org.bytedeco.javacpp.PointerPointer; import org.bytedeco.javacpp.IntPointer; public class ShapeDetect { public static String detectShape(Mat bgr) { Mat gray = new Mat(); opencv_imgproc.cvtColor(bgr, gray, opencv_imgproc.COLOR_BGR2GRAY); // 高斯模糊降噪,减少轮廓碎片 opencv_imgproc.GaussianBlur(gray, gray, new Size(5, 5), 0); // 自适应阈值二值化,适合光照不均 Mat binary = new Mat(); opencv_imgproc.adaptiveThreshold(gray, binary, 255, opencv_imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, opencv_imgproc.THRESH_BINARY_INV, 11, 2); // 找轮廓 MatVector contours = new MatVector(); Mat hierarchy = new Mat(); opencv_imgproc.findContours(binary, contours, hierarchy, opencv_imgproc.RETR_EXTERNAL, opencv_imgproc.CHAIN_APPROX_SIMPLE); String result = "UNKNOWN"; double maxArea = 0; for (int i = 0; i < contours.size(); i++) { Mat contour = contours.get(i); double area = opencv_imgproc.contourArea(contour); // 过滤太小的轮廓,避免噪点干扰 if (area < 500) continue; // 多边形逼近,epsilon 是逼近精度 Mat approx = new Mat(); double epsilon = 0.02 * opencv_imgproc.arcLength(contour, true); opencv_imgproc.approxPolyDP(contour, approx, epsilon, true); int vertexCount = (int) approx.total(); String shape = classifyByVertex(vertexCount); if (area > maxArea) { maxArea = area; result = shape; } } return result; } private static String classifyByVertex(int n) { switch (n) { case 3: return "TRIANGLE"; case 4: return "RECTANGLE"; case 5: return "PENTAGON"; case 6: return "HEXAGON"; default: // 顶点多且接近圆,用圆形度判断 return n > 8 ? "CIRCLE" : "POLYGON"; } } }逻辑说明:GaussianBlur平滑图像,减少噪点导致的假轮廓。adaptiveThreshold对每个像素根据邻域计算阈值,比全局阈值更适合光照不均。findContours的RETR_EXTERNAL只取最外层轮廓,CHAIN_APPROX_SIMPLE压缩水平垂直斜向的冗余点。approxPolyDP用 Douglas-Peucker 算法把轮廓逼近成多边形,epsilon控制逼近精度,越大顶点越少。arcLength算轮廓周长,epsilon取周长的 2% 是常用经验值。
参数说明:adaptiveThreshold的blockSize是邻域大小,必须是奇数,11 适合中等分辨率图像。C是从均值减去的常数,2 是常用值,越大二值化越激进。contourArea的过滤阈值 500 要根据图像分辨率调,1080p 图像可以设 1000 以上。epsilon的系数 0.02 是经验值,形状复杂时降到 0.01,形状简单时提到 0.04。
4.3 圆形判定为什么要单独处理
多边形逼近对圆形不友好,因为圆的轮廓点太多,approxPolyDP会输出很多顶点,单纯按顶点数判断会误判。圆形的判定用圆形度:4π × 面积 / 周长²,完美圆形为 1,越接近 1 越圆。
double area = opencv_imgproc.contourArea(contour); double perimeter = opencv_imgproc.arcLength(contour, true); double circularity = 4 * Math.PI * area / (perimeter * perimeter); // 圆形度大于 0.85 判定为圆 if (circularity > 0.85) return "CIRCLE";逻辑说明:圆形度对噪声敏感,周长稍微偏大就会拉低圆形度。所以计算前最好对轮廓做一次平滑,或者用最小外接圆面积比来辅助判断。
参数说明:0.85 是常用阈值,实际项目中根据形状规整度调整。如果目标本身是手绘圆,阈值降到 0.7。
5. 避坑与排查:颜色形状识别最容易翻车的五个地方
5.1 现象:颜色识别在白天正常,晚上全错
原因:HSV 的 V 通道随光照变化,晚上 V 值整体偏低,原来设的 V 下限 100 把目标也过滤掉了。
解决:不要用固定阈值。要么加自动白平衡预处理,要么根据图像平均亮度动态调整 V 的下限。简单做法是算全图 V 通道均值,下限设为均值的 0.6 倍。
5.2 现象:形状识别把正方形识别成圆形
原因:approxPolyDP的 epsilon 设得太大,正方形的四个角被“抹圆”,逼近后顶点数超过 8,落入圆形判定分支。
解决:epsilon 系数从 0.02 降到 0.01,或者先用contourArea和最小外接矩形面积比判断是否为矩形——矩形填充度高,圆形填充度低(约 0.785)。
5.3 现象:findContours 报错或返回空
原因:传入的 Mat 不是单通道二值图。findContours要求输入是 CV_8UC1,如果传了三通道彩色图或浮点图会抛异常。
解决:确保findContours之前做了cvtColor转灰度和threshold二值化。二值图的类型用binary.type()确认,应该是CV_8UC1。
5.4 现象:Java 程序在 IDE 里跑得好,打包成 jar 后报 UnsatisfiedLinkError
原因:JavaCV 的原生库在 jar 里是压缩的,运行时需要解压到临时目录。如果打包时把原生库排除了,或者临时目录没有写权限,就会加载失败。
解决:用 Maven Shade 或 Assembly 打包时不要过滤.so和.dll。检查java.io.tmpdir是否有写权限。生产环境可以用-Dorg.bytedeco.javacpp.cachedir=/opt/cache指定缓存目录。
5.5 现象:处理一帧要几百毫秒,达不到实时
原因:每帧都创建新的 Mat 对象,JVM 频繁 GC。或者图像分辨率太高,全图处理耗时。
解决:复用 Mat 对象,用Mat.create()而不是new Mat()。先把图像缩放到 640 宽再处理,识别完把坐标映射回原图。findContours的CHAIN_APPROX_SIMPLE比CHAIN_APPROX_NONE快很多,除非需要完整轮廓点,否则用 SIMPLE。
6. 颜色与形状联合判定:一个可复用的检测管线
单独的颜色识别和形状识别都不难,难的是把两者串起来,还要在 Java 里跑得稳。我实际项目里的做法是:先做颜色掩膜,在掩膜上找轮廓,再对轮廓做形状判定。这样颜色和形状天然绑定在同一个目标上,不会出现“红色区域”和“方形轮廓”对不上的情况。
public class CombinedDetector { public static void detect(Mat bgr) { // 第一步:颜色掩膜 Mat mask = ColorDetect.detectRed(bgr); // 第二步:在掩膜上找轮廓 MatVector contours = new MatVector(); Mat hierarchy = new Mat(); opencv_imgproc.findContours(mask, contours, hierarchy, opencv_imgproc.RETR_EXTERNAL, opencv_imgproc.CHAIN_APPROX_SIMPLE); for (int i = 0; i < contours.size(); i++) { Mat contour = contours.get(i); double area = opencv_imgproc.contourArea(contour); if (area < 500) continue; // 第三步:形状判定 Mat approx = new Mat(); double epsilon = 0.02 * opencv_imgproc.arcLength(contour, true); opencv_imgproc.approxPolyDP(contour, approx, epsilon, true); int vertexCount = (int) approx.total(); // 第四步:输出颜色+形状 String shape = classifyByVertex(vertexCount); System.out.println("检测到红色 " + shape + ",面积 " + area); } } }逻辑说明:颜色掩膜已经把非目标颜色过滤掉了,在掩膜上找轮廓比在原图上找再逐个判断颜色效率高得多。每个轮廓同时携带颜色信息(来自掩膜)和形状信息(来自逼近),输出的是“红色三角形”这样的组合结果。
参数说明:面积过滤阈值和 epsilon 系数需要根据实际目标大小调。如果目标颜色多样,对每种颜色各跑一遍这个管线,最后合并结果。
验证方法:拿一张已知答案的测试图,人工标注每个目标的颜色和形状,跑管线后对比输出。准确率低于 90% 时,先查颜色掩膜是否干净,再查 epsilon 是否合适。我一般会保存中间掩膜图,出问题时肉眼一看就知道是颜色分割错了还是形状逼近错了。
这套方案值不值得做,取决于你的场景是否规则。几何图像、色块、零件、卡片这类目标,Java OpenCV 完全够用,部署也简单。但如果是自然场景下的任意物体识别,还是得上深度学习模型,OpenCV 做预处理就好。我自己的习惯是:先用 OpenCV 快速验证可行性,跑不通再考虑换方案,别一上来就上重武器。希望帮到你。
本文还有配套的精品资源,点击获取