Java+OpenCV实战:HSV颜色分离与几何图形识别
简介这份PDF面向Java开发者与计算机视觉初学者聚焦如何用Java调用OpenCV视觉库完成几何图像的颜色与形状识别可解决传统RGB识别率不高、Java实现案例偏少的问题。资源共1个PDF文件压缩包约894KB内容为完整技术论文涵盖HSV颜色空间转换、颜色分离、滤波灰度化、二值化、轮廓提取、干扰轮廓去除、轮廓形状拟合及凸点距离判断等关键环节并给出三角形、圆、矩形、正方形、多边形等几何图形的识别思路。目前已有169人学习。读者可从中获得一套可复用的JavaOpenCV识别流程理解HSV各分量取值范围与阈值分割方法掌握轮廓面积过滤、腐蚀膨胀等排错技巧并借鉴交通信号灯、信号转向灯等颜色与形状识别场景的落地思路适合作为课程设计、毕业设计或视觉入门项目的参考。1. 用 Java 做几何图形识别一份被低估的 HSV 颜色分离实战文档大多数做图像识别的团队第一反应是 Python OpenCV毕竟资料多、上手快。但如果你手上跑的是 Java 技术栈的桌面端或服务端系统为了一个颜色形状识别模块再单独搭一套 Python 环境运维和联调成本都不低。这份文档讲的就是另一条路直接用 Java 调用 OpenCV 视觉库把图像转到 HSV 颜色空间按 H、S、V 的取值范围做颜色分离再对每种颜色的子图做滤波、灰度、二值化、轮廓提取和形状拟合最终识别出三角形、圆、矩形、正方形、多边形等几何图形的颜色和形状。它适合有 Java 基础、需要在现有工程里嵌入视觉识别能力的开发者也适合想理解 HSV 颜色分割完整链路的学生和工程师。2. 环境搭建与 OpenCV Java 库导入从零跑通第一个 Mat2.1 为什么选 Java 调 OpenCV 而不是换语言先说选型理由。这份文档的场景是几何图像的颜色与形状识别核心操作是颜色空间转换、通道分离、轮廓查找和形状拟合这些在 OpenCV 的 Java 绑定里都有对应 API功能覆盖上没有短板。真正需要权衡的是工程成本如果现有系统是 Java 写的引入 OpenCV Java 库只需要加一个 jar 和对应的动态链接库不需要跨进程通信也不需要额外维护一套 Python 运行时。对于中小规模的识别任务这个方案在部署复杂度上明显更省事。另一个容易被忽略的点是Java 的 Mat 对象和 OpenCV 原生 Mat 之间是直接映射的图像数据不需要序列化和反序列化处理链路短调试时也能直接在 IDE 里查看中间结果。常见做法是先把整个流程在 Java 里跑通确认识别率达标后再考虑要不要把计算密集的部分抽出去做优化。2.2 导入 OpenCV Java 库的具体步骤文档里用的是 OpenCV 3.0.0 版本解压后在 Eclipse 项目里导入 build\java 目录下的库文件。这里的关键是区分操作系统位数32 位系统选 OpenCV-x86 包64 位系统选 x64 包。导入之后项目里就能正常调用 OpenCV-Java 的类和方法了。具体操作分三步。第一步在 OpenCV 官网下载对应版本的压缩包并解压。第二步在 Eclipse 里右键项目进入 Build Path 配置把 build\java 下的 opencv-300.jar 添加到 Libraries。第三步配置 Native Library Location指向解压目录里 build\java\x86 或 x64 文件夹这一步是让 JVM 能找到 OpenCV 的动态链接库漏掉的话运行时会报 UnsatisfiedLinkError。# 以 Windows 32 位为例目录结构大致如下 opencv/ build/ java/ opencv-300.jar # Java 绑定的 jar 包 x86/ opencv_java300.dll # 32 位动态链接库 x64/ opencv_java300.dll # 64 位动态链接库提示jar 包和动态链接库的位数必须和 JVM 一致。如果 JDK 是 64 位的即使操作系统是 32 位也要用 x64 的库否则会在加载时直接失败。2.3 验证环境是否可用导入完成后写一个最小的测试类加载一张图片并打印它的尺寸。如果能正常输出宽高说明环境通了。import org.opencv.core.Core; import org.opencv.core.Mat; import org.opencv.imgcodecs.Imgcodecs; public class EnvCheck { public static void main(String[] args) { // 加载 OpenCV 动态链接库必须在所有 OpenCV 操作之前执行 System.loadLibrary(Core.NATIVE_LIBRARY_NAME); // 读取测试图片路径按实际情况替换 Mat img Imgcodecs.imread(test.jpg); if (img.empty()) { System.out.println(图片读取失败检查路径和格式); return; } // 输出图像的行数、列数和通道数 System.out.println(height img.rows() width img.cols() channels img.channels()); } }这段代码里System.loadLibrary 是入口参数 Core.NATIVE_LIBRARY_NAME 是 OpenCV 约定的库名不需要写全文件名。Imgcodecs.imread 返回的 Mat 如果为空通常是路径写错或者图片格式不被支持。rows 和 cols 分别对应图像的高和宽channels 是通道数彩色图一般是 3。环境跑通之后后面所有的颜色分割和形状识别都在这套基础上展开。如果这一步就卡住了先检查动态链接库的路径配置再看 JDK 位数是否匹配这两个是最高频的翻车点。3. HSV 颜色空间分割把七种颜色从图像里拆出来3.1 为什么 BGR 做颜色识别不靠谱OpenCV 默认读进来的图像是 BGR 顺序三个通道分别对应蓝、绿、红。用 BGR 做颜色判断的麻烦在于同一种颜色在不同光照下三个通道的值会同时漂移你很难划出一个固定的范围把红色和橙色分开。比如红色在暗光下 R 值可能只有 120在强光下能到 255而橙色在某个光照条件下 R 值也可能落在 120 附近单靠 R 通道根本分不清。HSV 把颜色拆成色调 H、饱和度 S、明度 V 三个维度。H 表示颜色种类S 表示颜色深浅V 表示亮度。这样一来光照变化主要影响 V颜色种类主要由 H 决定划分范围就稳定得多。文档里给出的各颜色 H、S、V 范围就是基于这个逻辑比如绿色的 H 范围是 35 到 77S 是 43 到 255V 是 46 到 255。3.2 图像缩小与颜色空间转换处理像素级操作时图像越大耗时越长。文档里第一步是缩小图像用 Imgproc.resize 把原图缩到更小的尺寸减少后续遍历的像素数量。这个操作对识别精度的影响可控因为几何图形的形状特征在缩小后依然保留。import org.opencv.core.Mat; import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; // src 为原始图像dsize 为目标尺寸 Mat resized new Mat(); Imgproc.resize(src, resized, new Size(320, 240)); // 先 RGB 转 BGR再 BGR 转 HSV Mat bgr new Mat(); Imgproc.cvtColor(resized, bgr, Imgproc.COLOR_RGB2BGR); Mat hsv new Mat(); Imgproc.cvtColor(bgr, hsv, Imgproc.COLOR_BGR2HSV);resize 的第三个参数是目标尺寸这里设成 320x240实际项目里可以根据识别精度要求调整。两次 cvtColor 的顺序不能反因为 OpenCV 读图默认是 BGR而文档里的原图可能是 RGB 格式先转成 BGR 再转 HSV 才能保证 H 值落在正确的区间。如果跳过 RGB2BGR 这一步红色和蓝色的 H 值会对调分割结果完全错位。3.3 遍历像素点统计各颜色数量转到 HSV 之后需要遍历每个像素点读取 H、S、V 三个分量按预设范围判断它属于哪种颜色并累计每种颜色的像素个数。int h, s, v; int greenCount 0; for (int i 0; i hsv.rows(); i) { for (int j 0; j hsv.cols(); j) { // clone 取出该像素的 HSV 三个分量 double[] pixel hsv.get(i, j).clone(); h (int) pixel[0]; s (int) pixel[1]; v (int) pixel[2]; // 绿色范围判断 if (h 35 h 77 s 43 s 255 v 46 v 255) { greenCount; } } }这段双重循环是性能敏感点。图像缩小到 320x240 之后总像素是 76800 个每个像素做一次 clone 和三次类型转换在普通机器上耗时可接受。如果图像不缩小直接遍历像素数可能翻几十倍处理时间会明显拉长。clone 的作用是复制当前像素的数据避免后续操作影响原 Mat。统计完各颜色像素数之后文档里设了一个阈值 200某颜色的像素数超过 200 才认为图中存在该颜色否则忽略。这个阈值的作用是过滤环境噪声和反光带来的零星像素防止把背景里的杂色误判成目标图形。3.4 用 inRange 和 bitwise_and 做颜色分割统计只是判断有没有真正要把某种颜色的区域从原图里抠出来用的是 Core.inRange 和 Core.bitwise_and 两个函数配合。import org.opencv.core.Core; import org.opencv.core.Scalar; // 绿色的 HSV 上下界 Scalar minVal new Scalar(35, 43, 46); Scalar maxVal new Scalar(77, 255, 255); // mask 中绿色区域为 255其余为 0 Mat mask new Mat(); Core.inRange(hsv, minVal, maxVal, mask); // 用 mask 和原图做与运算得到只保留绿色区域的图像 Mat greenOnly new Mat(); Core.bitwise_and(resized, resized, greenOnly, mask);inRange 的输出是一张单通道二值图落在范围内的像素是 255范围外是 0。bitwise_and 的第四个参数是 mask它决定哪些像素参与运算mask 为 255 的位置保留原图像素为 0 的位置置黑。这样得到的 greenOnly 就是只包含绿色图形的图像。其他颜色的分割用同样的方式只需要替换 minVal 和 maxVal 里的 H、S、V 范围。文档里覆盖了七种常见颜色每种颜色的范围划分是这套方案的核心参数范围偏一点就会导致漏分割或者多分割。注意inRange 的输入必须是 HSV 图像不能直接传 BGR 图。如果传错mask 的结果会完全不可用而且不会报错排查起来很费时间。4. 形状识别链路从轮廓提取到凸点距离判断4.1 分割后图像的三通道转换与滤波颜色分割得到的 greenOnly 是单通道图像后续的灰度化和二值化操作需要三通道输入所以先用 convertTo 把它转成 CV_8UC3。import org.opencv.core.CvType; import org.opencv.imgproc.Imgproc; // 单通道转三通道 greenOnly.convertTo(greenOnly, CvType.CV_8UC3); // 高斯滤波去除光线和反光干扰 Mat blurred new Mat(); Imgproc.GaussianBlur(greenOnly, blurred, new Size(5, 5), 0);GaussianBlur 的第三个参数是卷积核大小这里用 5x5。核越大平滑效果越强但也会模糊边缘影响轮廓精度。第四个参数是标准差设 0 表示由核大小自动推算。这一步的目的是把图像里因为光照不均产生的噪点压下去让后续的二值化结果更干净。4.2 灰度化与二值化滤波之后做灰度化把三通道图变成单通道灰度图再用 threshold 做二值化。Mat gray new Mat(); Imgproc.cvtColor(blurred, gray, Imgproc.COLOR_BGR2GRAY); Mat binary new Mat(); // 阈值设为 60大于 60 的置 255其余置 0 Imgproc.threshold(gray, binary, 60, 255, Imgproc.THRESH_BINARY);threshold 的第二个参数是阈值第三个是最大值第四个是阈值类型。THRESH_BINARY 表示大于阈值的像素设为最大值小于的设为 0。阈值 60 是文档里给出的经验值实际使用时可以根据图像亮度和对比度微调。阈值太低会把背景噪点也算进前景太高会丢掉图形边缘。4.3 腐蚀膨胀与轮廓提取二值化之后图像里可能还有零散的噪点和断裂的边缘。文档里先用 erode 腐蚀去掉不连通的像素点再用 dilate 膨胀把已有轮廓连通。import org.opencv.core.MatOfPoint; import java.util.ArrayList; import java.util.List; Mat kernel Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3)); // 腐蚀去掉零散像素 Mat eroded new Mat(); Imgproc.erode(binary, eroded, kernel); // 膨胀连通轮廓 Mat dilated new Mat(); Imgproc.dilate(eroded, dilated, kernel); // 查找轮廓 ListMatOfPoint contours new ArrayList(); Mat hierarchy new Mat(); Imgproc.findContours(dilated, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);getStructuringElement 生成一个 3x3 的矩形核腐蚀和膨胀都用它。findContours 的第四个参数 RETR_EXTERNAL 表示只提取最外层轮廓忽略嵌套轮廓这对几何图形识别是合适的。第五个参数 CHAIN_APPROX_SIMPLE 会压缩轮廓点只保留拐点减少后续计算量。4.4 去除干扰轮廓与形状拟合提取到的轮廓里有些面积过小可能是噪点有些面积过大可能是背景边缘。文档里用 contourArea 计算面积把面积小于等于 10 或大于等于 80000 的轮廓移除。import org.opencv.core.MatOfPoint2f; for (int j 0; j contours.size(); j) { double area Imgproc.contourArea(contours.get(j)); if (area 10 || area 80000) { contours.remove(j); j--; // 移除后索引回退避免跳过下一个轮廓 } }面积阈值 10 和 80000 是文档里的经验值实际项目里要根据图像分辨率和目标图形大小调整。移除轮廓时索引回退是一个容易忽略的细节不移的话会跳过被移除元素后面的那个轮廓。过滤完之后对每个轮廓做多边形拟合用 approxPolyDP 把轮廓近似成多边形再通过凸点个数和相邻凸点距离判断形状。for (MatOfPoint contour : contours) { MatOfPoint2f contour2f new MatOfPoint2f(contour.toArray()); MatOfPoint2f approx new MatOfPoint2f(); // 拟合精度为轮廓周长的 2% double epsilon 0.02 * Imgproc.arcLength(contour2f, true); Imgproc.approxPolyDP(contour2f, approx, epsilon, true); int vertexCount approx.toArray().length; // 根据凸点个数初步判断形状 if (vertexCount 3) { System.out.println(三角形); } else if (vertexCount 4) { // 四个点需要进一步区分正方形和矩形 System.out.println(四边形需判断边长); } else if (vertexCount 4) { System.out.println(多边形或圆形); } }approxPolyDP 的第三个参数 epsilon 控制拟合精度取周长的 2% 是一个常见起点。epsilon 越小拟合出的多边形越贴近原始轮廓凸点可能偏多epsilon 越大拟合越粗糙凸点可能偏少。第四个参数 true 表示轮廓是闭合的。凸点个数只能区分三角形、四边形和多边形正方形和矩形都是四个点圆和多边形在凸点数量上也可能混淆。文档里用相邻凸点距离做进一步判断正方形四条边长度接近矩形有两组对边长度不同圆的凸点距离则趋于均匀但点数更多。这套判断逻辑不复杂但参数需要根据实际图像反复调试。5. 避坑与排查颜色分割和形状识别里最容易翻车的五个点5.1 红色被分成两段H 值范围接不上现象分割红色图形时mask 里红色区域出现断裂或者干脆什么都没分出来。原因HSV 颜色空间里 H 是环形分布的红色跨越了 0 和 180 两个端点。如果只写一个范围比如 H 从 0 到 10就会漏掉 H 在 170 到 180 的那部分红色。解决红色需要分两段判断H 在 0 到 10 和 170 到 180 各做一次 inRange然后把两个 mask 用 Core.bitwise_or 合并。其他颜色如果靠近红色端点也要检查 H 范围是否完整覆盖。5.2 阈值 200 过滤掉了小图形现象图像里明明有目标颜色的小三角形但程序判断该颜色不存在。原因文档里的像素数阈值是 200如果图形本身很小或者图像缩小比例较大目标颜色的像素总数可能不到 200被直接过滤掉。解决阈值不要写死根据图像缩小后的总像素数和目标图形的最小预期面积来定。一个可行的做法是先统计各颜色像素数并打印出来观察实际分布再设一个能区分目标和噪声的值。如果目标图形可能很小阈值要相应降低。5.3 二值化阈值不合适导致轮廓断裂现象findContours 找到的轮廓不闭合或者一个图形被拆成好几段。原因threshold 的阈值设得偏高或偏低。偏高会把图形边缘的过渡像素判为背景导致轮廓断开偏低会把背景噪点判为前景轮廓膨胀变形。解决不要固定用 60先对灰度图做一次直方图观察找到前景和背景之间的谷底作为阈值。如果光照不均可以改用自适应阈值 Imgproc.adaptiveThreshold它对局部亮度变化更鲁棒。5.4 腐蚀膨胀核太大小图形被吃掉现象小三角形或小圆在腐蚀之后消失了后续找不到轮廓。原因getStructuringElement 生成的核是 3x3如果图形本身只有几个像素宽腐蚀一次就可能把它完全抹掉。解决核的大小要跟目标图形的最小尺寸匹配。图形小的时候用 2x2 甚至不做腐蚀只做膨胀。或者调整操作顺序先膨胀再腐蚀这样对小图形更友好。5.5 正方形和矩形区分不出来现象四个凸点的图形全部被判成同一种形状。原因approxPolyDP 只给出凸点个数四个点既可能是正方形也可能是矩形需要额外计算边长。解决拿到四个凸点之后计算相邻两点的距离得到四条边长。如果四条边长度接近判为正方形如果两组对边长度差异明显判为矩形。距离比较时设一个容差比例比如最长边和最短边的比值超过 1.2 才认为是矩形避免因为拟合误差把正方形误判成矩形。6. 进阶技巧用轮廓面积比和凸包把圆和多边形分开凸点个数能搞定三角形和四边形但圆和多边形在凸点数量上容易混。一个圆用 approxPolyDP 拟合时凸点个数取决于 epsilonepsilon 小的时候可能拟合出十几个点epsilon 大的时候可能只有五六个点跟五边形、六边形撞在一起。我一般会再加两个判据轮廓面积与凸包面积的比值以及凸点距离的方差。先算轮廓面积和凸包面积。圆的轮廓面积和凸包面积几乎相等比值接近 1多边形的凸包就是它自己比值也接近 1所以这个指标区分不了圆和多边形。真正有用的是凸点距离的方差圆的相邻凸点距离波动很小方差低多边形的边长如果有长短差异方差会明显偏高。把凸点个数、距离方差和面积比三个指标组合起来判断准确率比单看凸点个数稳得多。import org.opencv.core.MatOfInt; import org.opencv.core.Point; // 计算凸包 MatOfInt hull new MatOfInt(); Imgproc.convexHull(contour, hull); // 取凸包对应的点 Point[] hullPoints new Point[hull.toArray().length]; Point[] contourPoints contour.toArray(); for (int k 0; k hull.toArray().length; k) { hullPoints[k] contourPoints[hull.toArray()[k]]; } // 计算相邻凸点距离 double[] distances new double[hullPoints.length]; for (int k 0; k hullPoints.length; k) { Point p1 hullPoints[k]; Point p2 hullPoints[(k 1) % hullPoints.length]; distances[k] Math.sqrt(Math.pow(p1.x - p2.x, 2) Math.pow(p1.y - p2.y, 2)); } // 计算距离均值和方差 double mean 0; for (double d : distances) mean d; mean / distances.length; double variance 0; for (double d : distances) variance Math.pow(d - mean, 2); variance / distances.length; // 方差小且凸点多的倾向判为圆方差大的倾向判为多边形 if (variance mean * 0.05 hullPoints.length 6) { System.out.println(圆形); } else if (hullPoints.length 5) { System.out.println(五边形); } else { System.out.println(其他多边形); }这段代码里convexHull 返回的是凸包点在原轮廓点数组里的索引需要映射回坐标。距离方差和均值的比值是一个归一化指标不受图形绝对大小影响。比值小于 0.05 且凸点数超过 6 的时候基本可以判为圆。五边形单独判因为五个凸点且方差偏大的情况五边形的概率最高。实际调参时我习惯先把一批测试图跑一遍把每个轮廓的凸点数、距离方差、面积比打印出来人工看一眼哪些判对了哪些判错了再回头调 epsilon 和方差阈值。这套流程走两三遍参数就能稳定下来。从那以后我每次换数据集都强制先跑一遍参数打印确认分布没漂移再上正式识别省得后面批量处理时才发现阈值不对。希望帮到你。本文还有配套的精品资源点击获取