简介:这份PDF面向Java开发者与计算机视觉初学者,聚焦如何用Java调用OpenCV视觉库完成几何图像的颜色与形状识别,可解决传统RGB识别率不高、Java实现案例偏少的问题。资源包共1个PDF文件,约894KB,内容为完整技术文档,便于按章节系统阅读与对照实践。文中围绕HSV颜色空间展开,先按H、S、V取值范围分割图像,再经滤波、灰度化、二值化、轮廓提取、去除干扰轮廓、轮廓形状拟合与凸点距离判断,识别三角形、圆、矩形、正方形、多边形等几何图形,并给出交通信号灯、信号转向灯等应用场景。目前已有169人学习,适合想掌握OpenCV-Java图像处理流程、补齐颜色分离与形状拟合思路的读者参考。
1. 从一张交通灯照片说起:Java 也能做几何图像颜色与形状识别
路口抓拍一张交通灯照片,人眼一秒就能分辨“红灯圆、绿灯圆、黄灯圆”,但要让程序自动说出“这是红色圆形”,背后其实是一条完整的图像处理流水线。这份资源给出的方案,是用 Java 调用 OpenCV 视觉库,把一张彩色几何图先转到 HSV 颜色空间,按 H、S、V 的取值范围把红、黄、绿、橙等颜色逐个分离出来,再对每种颜色的分割图做滤波、灰度化、二值化、轮廓提取、干扰轮廓剔除、形状拟合,最后靠凸点个数和相邻凸点距离判断它是三角形、圆、矩形、正方形还是多边形。整套流程不依赖深度学习模型,纯传统视觉算法,跑在普通电脑上就能出结果,适合想用 Java 做图像识别入门、又不想一上来就啃神经网络的开发者。如果你手里正好有交通信号灯、火焰识别、色块分拣这类“颜色加形状”的判别需求,这份材料能直接当骨架用。
2. 环境搭建与 OpenCV Java 包导入:从零把工程跑起来
2.1 为什么选 Java 调 OpenCV 而不是 Python
翻一圈资料会发现,彩色几何图像识别大多用 C++、Python 或 MATLAB 写,Java 实现的偏少。但实际项目里,很多后台服务、桌面工具、教学实验环境本身就是 Java 技术栈,为了一个识别功能再单独维护一套 Python 服务,部署和联调都多一层麻烦。这份资源的价值就在于把 OpenCV 的 Java 接口用起来,让识别逻辑直接长在 Java 工程里。另一个选型点是颜色空间:不少 Java 实现直接用 R、G、B 三原色做判断,但 RGB 三个分量都和亮度耦合,换个光照条件阈值就飘;HSV 把色调 H、饱和度 S、明度 V 拆开,颜色判断主要看 H,鲁棒性明显更好,这也是它识别率能上去的关键。
2.2 下载与导入 OpenCV Java 库
资源里用的是 OpenCV 3.0.0,解压后目录结构里跟 Java 相关的是build\java这一层。导入步骤不复杂,但位数选错会直接报UnsatisfiedLinkError,这是最常见的翻车点。
# 解压 OpenCV 后,进入 build/java 目录 # 32 位系统选 x86,64 位系统选 x64 opencv-3.0.0/ build/ java/ opencv-300.jar # Java 接口 jar 包 x86/ # 32 位动态库 opencv_java300.dll x64/ # 64 位动态库 opencv_java300.dll在 Eclipse 里右键项目 → Build Path → Configure Build Path → Libraries → Add External JARs,把opencv-300.jar加进来。然后还要让 JVM 找得到动态库,两种做法:一是把对应位数的 dll 所在目录加进java.library.path,二是在代码里显式加载。
// 静态代码块里加载本地库,路径按实际解压位置改 static { // 32 位系统用 x86 目录,64 位用 x64 System.load("D:/opencv-3.0.0/build/java/x86/opencv_java300.dll"); }参数说明:System.load接收的是 dll 的绝对路径,不是目录;如果写成目录会抛异常。用System.loadLibrary("opencv_java300")也可以,但前提是 dll 所在目录已经在java.library.path里。我一般会在项目启动时先跑一句System.out.println(System.getProperty("java.library.path")),确认路径到底有没有生效,比盲猜快得多。
2.3 最小可运行验证
导入完别急着写识别逻辑,先写个最小用例确认 OpenCV 真的能调起来。
import org.opencv.core.Core; import org.opencv.core.Mat; import org.opencv.imgcodecs.Imgcodecs; public class OpenCVSmokeTest { public static void main(String[] args) { // 加载本地库 System.load("D:/opencv-3.0.0/build/java/x86/opencv_java300.dll"); // 读一张图,路径换成你自己的 Mat img = Imgcodecs.imread("test.png"); if (img.empty()) { System.out.println("图片没读到,检查路径"); return; } System.out.println("宽=" + img.width() + " 高=" + img.height() + " 通道=" + img.channels()); } }逻辑说明:Imgcodecs.imread返回的Mat如果为空,说明路径错或格式不支持,后面所有处理都会连锁失败,所以这一步必须卡住。channels()返回 3 表示三通道彩色图,返回 1 是灰度图。这个冒烟测试过了,再往下做颜色分割才有意义。
3. 颜色分割核心链路:HSV 转换、阈值分割与像素统计
3.1 缩小图像与颜色空间转换
原图直接处理像素量大,遍历起来慢,所以第一步先缩小。资源里用Imgproc.resize把图缩到较小尺寸,处理速度能明显提升,代价是细节损失,但对几何图形这种大色块目标影响不大。
import org.opencv.core.Mat; import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; // src 原图,dst 缩小后的图,dsize 目标尺寸 Mat resized = new Mat(); Imgproc.resize(src, resized, new Size(320, 240)); // OpenCV 默认按 BGR 读图,先转 BGR 再转 HSV Mat hsv = new Mat(); Imgproc.cvtColor(resized, hsv, Imgproc.COLOR_BGR2HSV);参数说明:resize的第三个参数是目标尺寸,宽高按比例给就行,不用太精确。cvtColor的转换码这里直接用COLOR_BGR2HSV,因为imread读进来就是 BGR 顺序。资源原文里先 RGB 转 BGR 再 BGR 转 HSV,是因为它假设输入是 RGB 型图像;实际用imread读文件的话,一步COLOR_BGR2HSV就够了,少一次转换少一次误差。
3.2 遍历像素统计各颜色数量
HSV 空间里每种颜色都有对应的 H、S、V 范围,遍历像素点,落在哪个范围就归为哪种颜色,同时累计个数。这一步是后面设阈值判断“图里到底有没有这种颜色”的依据。
// 统计各颜色像素个数 int redCount = 0, greenCount = 0, yellowCount = 0, orangeCount = 0; for (int i = 0; i < hsv.rows(); i++) { for (int j = 0; j < hsv.cols(); j++) { double[] clone = hsv.get(i, j).clone(); int h = (int) clone[0]; // 色调 0-180 int s = (int) clone[1]; // 饱和度 0-255 int v = (int) clone[2]; // 明度 0-255 // 红色 H 在 0-10 或 156-180 两段 if ((h >= 0 && h <= 10) || (h >= 156 && h <= 180)) { if (s > 43 && v > 46) redCount++; } // 绿色 H 约 35-77 else if (h >= 35 && h <= 77 && s > 43 && v > 46) { greenCount++; } // 黄色 H 约 26-34 else if (h >= 26 && h <= 34 && s > 43 && v > 46) { yellowCount++; } // 橙色 H 约 11-25 else if (h >= 11 && h <= 25 && s > 43 && v > 46) { orangeCount++; } } }逻辑说明:hsv.get(i, j)返回的是double[],直接.clone()避免引用被后续操作改掉。H 在 OpenCV 里范围是 0 到 180,不是 0 到 360,这点跟很多资料不一样,写阈值时容易搞错。S 和 V 加了下限过滤,是为了排除灰白和过暗的像素,否则背景里的浅色区域会被误判成目标颜色。红色横跨 0 和 180 两端,必须写成两段判断,只写一段会漏掉一半红色像素。
3.3 阈值判断与 inRange 分割
统计完像素数,设一个阈值,资源里用的是大于 200。超过这个数才认为图里存在该颜色,否则视为环境反光或噪声带来的干扰。这个阈值不是固定的,图小就调小,图大就调大,本质是过滤掉零星噪点。
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); // 生成掩膜,落在范围内的像素为 255,其余为 0 Mat mask = new Mat(); Core.inRange(hsv, minVal, maxVal, mask); // 用掩膜和原图做与运算,得到只保留绿色的分割图 Mat greenPart = new Mat(); Core.bitwise_and(resized, resized, greenPart, mask);参数说明:inRange的三个参数分别是输入 HSV 图、下限 Scalar、上限 Scalar,输出是单通道掩膜。bitwise_and的第四个参数是掩膜,只有掩膜为 255 的位置才保留原图像素,其余置黑。这样每种颜色都能得到一张干净的分割图,后面形状识别就在这张图上做。资源里对红、黄、绿、橙都做了同样处理,其他颜色照搬这套上下限即可。
4. 形状识别流水线:从分割图到凸点判形
4.1 三通道转换与滤波去噪
分割出来的图是单通道掩膜结果,不能直接做灰度化,得先转回三通道。这一步不做的话,后面cvtColor会报通道数不匹配。
// 单通道转三通道 Mat rdst = new Mat(); greenPart.convertTo(rdst, CvType.CV_8UC3); // 高斯滤波去噪,核大小 5x5 Mat blurred = new Mat(); Imgproc.GaussianBlur(rdst, blurred, new Size(5, 5), 0);参数说明:convertTo的第二个参数CvType.CV_8UC3表示 8 位无符号三通道。GaussianBlur的核大小用奇数,5x5 是比较通用的起点,图噪点多就加到 7x7,但核越大边缘越糊,轮廓会变形,得权衡。高斯滤波主要对付光线和反光带来的细碎噪点,这一步做完图会柔和一些。
4.2 灰度化、二值化与形态学处理
找轮廓前必须把图变成黑白分明的二值图。先灰度化,再阈值二值化,然后用腐蚀和膨胀把零散像素清掉、把断开的轮廓连上。
// 灰度化 Mat gray = new Mat(); Imgproc.cvtColor(blurred, gray, Imgproc.COLOR_BGR2GRAY); // 二值化,阈值 127,最大值 255 Mat binary = new Mat(); Imgproc.threshold(gray, binary, 127, 255, Imgproc.THRESH_BINARY); // 腐蚀去掉零散像素点 Mat eroded = new Mat(); Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3)); Imgproc.erode(binary, eroded, kernel); // 膨胀把轮廓重新连通 Mat dilated = new Mat(); Imgproc.dilate(eroded, dilated, kernel);逻辑说明:threshold的阈值 127 是中间值,如果目标色块偏暗可以调低,偏亮调高。腐蚀和膨胀用的核是 3x3 矩形,先腐蚀后膨胀等于开运算,作用是去小噪点、保大轮廓。资源里特别提到这一步能去掉干扰直线和不封闭轮廓,实际调的时候如果发现目标轮廓也被腐蚀断了,就把核改小或者只做膨胀不做腐蚀。
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(); Imgproc.findContours(dilated, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE); // 剔除面积过小或过大的轮廓 for (int j = 0; j < contours.size(); j++) { double area = Imgproc.contourArea(contours.get(j)); if (area <= 10 || area >= 80000) { contours.remove(contours.get(j)); j--; // 删除后索引回退,否则会跳过一个 } }参数说明:RETR_EXTERNAL只取最外层轮廓,几何图形识别一般够用;CHAIN_APPROX_SIMPLE只存拐点,省内存。面积下限 10 过滤噪点,上限 80000 过滤整幅图边框这种超大轮廓。这里有个容易踩的坑:contours.remove之后列表长度变了,循环索引必须回退,否则会漏检。资源原文没写j--,实际跑的时候如果发现少识别了一个图形,大概率就是这里。
4.4 多边形拟合与凸点判形
最后一步是把轮廓拟合成多边形,数凸点个数,再结合相邻凸点距离判断具体形状。
import org.opencv.core.MatOfPoint2f; import org.opencv.core.Point; for (MatOfPoint contour : contours) { MatOfPoint2f contour2f = new MatOfPoint2f(contour.toArray()); MatOfPoint2f approx = new MatOfPoint2f(); // 拟合精度取周长的 2% double peri = Imgproc.arcLength(contour2f, true); Imgproc.approxPolyDP(contour2f, approx, 0.02 * peri, true); Point[] points = approx.toArray(); int vertexCount = points.length; // 按凸点个数初判 if (vertexCount == 3) { System.out.println("三角形"); } else if (vertexCount == 4) { // 四个点还要看边长区分正方形和矩形 double d1 = distance(points[0], points[1]); double d2 = distance(points[1], points[2]); if (Math.abs(d1 - d2) < 5) { System.out.println("正方形"); } else { System.out.println("矩形"); } } else if (vertexCount > 6) { System.out.println("圆形或多边形"); } } // 两点距离 private static double distance(Point a, Point b) { return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)); }逻辑说明:approxPolyDP的第三个参数是拟合精度,取周长的 2% 是经验值,精度越小拟合出的点越多,圆会被拟合成很多边形,精度越大点越少,矩形可能被拟合成三角形。四个点时用相邻边长差判断正方形还是矩形,差值阈值 5 像素是资源里的做法,实际按图的分辨率调。圆形因为拟合后凸点很多,通常超过 6 个,所以归到“圆形或多边形”一类,要更精确区分圆和多边形,可以再看凸点距离的均匀程度。资源里说识别准确率能到 99% 以上,前提是光照均匀、背景干净,复杂背景下这个数字会掉,后面避坑章节会细说。
5. 避坑与常见问题排查:那些让识别率掉下来的细节
5.1 现象:程序一启动就报 UnsatisfiedLinkError
原因:JVM 找不到 OpenCV 的本地动态库,或者位数不匹配。32 位 JVM 加载 64 位 dll、64 位 JVM 加载 32 位 dll 都会报这个错,报错信息里通常带Can't load IA 32-bit .dll on a AMD 64-bit platform之类的提示。
解决:先确认 JDK 位数,java -version输出里带 64-Bit 就是 64 位。然后选对应目录的 dll,用绝对路径System.load加载。如果路径里有中文或空格,换成纯英文路径再试。
5.2 现象:红色图形识别不出来,或者只识别出一半
原因:HSV 里红色 H 值横跨 0 和 180 两端,只写了一段范围,另一半像素被漏掉。另外红色在光照下容易偏橙或偏紫,H 值会漂。
解决:红色判断写成两段,(h >= 0 && h <= 10) || (h >= 156 && h <= 180)。如果还是漏,把两段范围各放宽 2 到 3 个值,同时把 S 下限调低一点,让偏暗的红色也能进来。
5.3 现象:背景里浅色区域被误判成目标颜色
原因:S 和 V 的下限设得太低,灰白背景的饱和度虽然低,但偶尔也会落进范围。或者阈值 200 太小,零星噪点被当成有效颜色。
解决:把 S 下限从 43 提到 60 以上,V 下限从 46 提到 80 以上,先把灰白区域挡掉。阈值根据图的大小调,小图 50 到 100,大图 500 以上,原则是比噪声像素数高一个量级。
5.4 现象:轮廓提取后图形边缘毛刺多,形状判断不准
原因:二值化阈值不合适,或者腐蚀膨胀的核太大,把轮廓啃变形了。高斯滤波核太大也会让边缘糊掉。
解决:二值化阈值用 Otsu 自动算,Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY + Imgproc.THRESH_OTSU),比固定 127 稳。形态学核从 3x3 起调,别一上来就 7x7。高斯核 5x5 够用,噪点实在多再考虑加大。
5.5 现象:圆形被识别成多边形,或者正方形被识别成矩形
原因:approxPolyDP的精度参数不合适。精度太小,圆被拟合成十几边形;精度太大,正方形四个角被合并成三个点。正方形和矩形的边长差阈值 5 像素也不一定通用。
解决:精度从周长的 2% 开始试,圆多就调到 3% 到 4%,方形多就调到 1%。正方形判断的边长差阈值按图的分辨率缩放,320x240 的图用 5 像素,640x480 就用 10 像素。更稳的做法是算边长比例,比值接近 1 就是正方形,比固定像素差更抗缩放。
6. 二次开发改造:把识别逻辑抽成可复用模块
这份资源的代码是论文式写法,逻辑都堆在 main 里,直接拿来做项目得先拆。我一般会按“颜色分割”和“形状识别”两条线抽成两个类,颜色阈值做成配置,形状判断做成策略,这样换场景不用改核心代码。
// 颜色配置类,阈值外置,方便调 public class ColorRange { private String name; private Scalar min; private Scalar max; private int pixelThreshold; // 构造、getter、setter 省略 } // 识别服务,输入 Mat,输出识别结果列表 public class ShapeRecognizer { private List<ColorRange> colorRanges; public List<RecognitionResult> recognize(Mat src) { List<RecognitionResult> results = new ArrayList<>(); Mat resized = new Mat(); Imgproc.resize(src, resized, new Size(320, 240)); Mat hsv = new Mat(); Imgproc.cvtColor(resized, hsv, Imgproc.COLOR_BGR2HSV); for (ColorRange range : colorRanges) { Mat mask = new Mat(); Core.inRange(hsv, range.getMin(), range.getMax(), mask); // 像素数不够就跳过,省得后面白算 if (Core.countNonZero(mask) < range.getPixelThreshold()) continue; Mat part = new Mat(); Core.bitwise_and(resized, resized, part, mask); String shape = detectShape(part); if (shape != null) { results.add(new RecognitionResult(range.getName(), shape)); } } return results; } private String detectShape(Mat part) { // 这里放第 4 章的滤波、二值化、轮廓、拟合逻辑 // 返回 "三角形" / "圆形" / "正方形" 等 return null; } }改造要点:颜色阈值从代码里挪到配置文件或数据库,加一种颜色不用重新编译。detectShape里把中间结果Mat及时release(),不然处理视频流时内存涨得很快。如果要做实时识别,把resize的目标尺寸再调小,320x240 在普通机器上跑单帧大概几十毫秒,够用;要更快就降到 160x120,代价是远处小图形识别不到。
验证改造是否成功,我习惯用一组固定测试图跑回归:红圆、绿三角、黄方、橙矩形各一张,再加一张纯背景图确认不误报。每次改阈值或精度参数,先跑这组图,结果对了再上真实场景。真实场景里光照变化大,建议加一步自动白平衡或者直方图均衡,能明显稳住识别率。
从那以后我每次调 OpenCV 阈值参数,都强制先跑一遍固定测试图再动真实数据,不然改一个数看着这边好了那边又崩,来回折腾的时间比写代码还长。希望帮到你。
本文还有配套的精品资源,点击获取