> 技术文档 > 程序员之电工基础-CV程序解决目标检测

程序员之电工基础-CV程序解决目标检测


一、背景

        兴趣爱好来了,决定研发一个产品。涉及到电工和机械等知识,所以记录一下相关的基础知识。今天的内容又回到了我的主营板块!!哈哈!!为后续整体集成做准备,先测试目标检测部分的能力。

二、需求描述

       我的需求是流水线上的工业相机拍摄不同产品的图片,同批次生产产品的第1个,需要设别特征或者某个标识物,把标识物设定为标识物模版。后面,同批次生产产品来到这个作业环节的时候,我们从新拍的图片中找到标识物模板所处位置就成功啦!!!

        我期望是自动寻找标识物,人工可以选择自动寻找的标识物,也可以手工选择标识物。

三、完整代码

        1.开发环境搭建

        就不多说了,参考我之前的文章工业生产安全-安全帽第一篇-opencv及java开发环境搭建_安全帽识别Java开发入门-CSDN博客

        2.标识模版制作程序

        (1)功能代码MarkerDetector.java        

import java.awt.Point;import java.awt.event.MouseAdapter;import java.awt.event.MouseEvent;import java.awt.image.BufferedImage;import java.util.ArrayList;import java.util.Comparator;import java.util.List;import javax.swing.ImageIcon;import javax.swing.JFrame;import javax.swing.JLabel;import javax.swing.SwingUtilities;import org.opencv.core.Core;import org.opencv.core.Mat;import org.opencv.core.MatOfPoint;import org.opencv.core.MatOfPoint2f;import org.opencv.core.Rect;import org.opencv.core.Scalar;import org.opencv.core.Size;import org.opencv.imgcodecs.Imgcodecs;import org.opencv.imgproc.Imgproc;public class MarkerDetector {// 加载OpenCV库(需提前配置)static { System.loadLibrary(Core.NATIVE_LIBRARY_NAME);} // 标识物筛选参数(可配置) private static final int MIN_AREA = 100; // 最小面积(像素) private static final int MAX_AREA = 5000; // 最大面积(像素) private static final double MIN_ASPECT_RATIO = 0.3; // 最小宽高比 private static final double MAX_ASPECT_RATIO = 3.0; // 最大宽高比 // 检测到的标识物列表 private List detectedMarkers = new ArrayList(); // 选中的标识物模板 private Marker selectedMarker; String templateRootPath=\"d:/test/\"; /** * 从拼接图像中自动检测标识物 * @param stitchedImagePath 拼接后的图像路径 * @return 排序后的前3个标识物 */ public List autoDetectMarkers(String stitchedImagePath) { // 1. 读取图像并预处理 Mat image = Imgcodecs.imread(stitchedImagePath); if (image.empty()) { throw new RuntimeException(\"无法读取图像: \" + stitchedImagePath); } // 灰度化→降噪→二值化 Mat gray = new Mat(); Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY); Mat blurred = new Mat(); Imgproc.GaussianBlur(gray, blurred, new Size(5, 5), 0); Mat thresh = new Mat(); Imgproc.threshold(blurred, thresh, 0, 255, Imgproc.THRESH_BINARY_INV + Imgproc.THRESH_OTSU); // 2. 轮廓检测 List contours = new ArrayList(); Mat hierarchy = new Mat(); Imgproc.findContours(thresh.clone(), contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE); // 3. 筛选符合条件的轮廓(作为候选标识物) for (MatOfPoint contour : contours) { // 计算轮廓面积 double area = Imgproc.contourArea(contour); if (area  MAX_AREA) { continue; } // 计算边界框 Rect rect = Imgproc.boundingRect(contour); double aspectRatio = (double) rect.width / rect.height; if (aspectRatio  MAX_ASPECT_RATIO) { continue; } // 计算轮廓复杂度(形状越规则,越可能是标识物) MatOfPoint2f contour2f = new MatOfPoint2f(contour.toArray()); double perimeter = Imgproc.arcLength(contour2f, true); MatOfPoint2f approx = new MatOfPoint2f(); Imgproc.approxPolyDP(contour2f, approx, 0.02 * perimeter, true); int vertices = approx.toArray().length; // 保存标识物信息(按\"面积+规则度\"排序) Marker marker = new Marker(rect, area, vertices, image.submat(rect)); detectedMarkers.add(marker); } // 4. 按优先级排序(面积大的优先,形状规则的优先) detectedMarkers.sort(Comparator.comparingDouble(Marker::getPriority).reversed()); // 返回前3个标识物 return detectedMarkers.size() > 3 ? detectedMarkers.subList(0, 3) : detectedMarkers; } /** * 显示标识物供用户选择 * @param imagePath 原始图像路径 * @param candidates 候选标识物列表 */ public void showMarkerSelectionDialog(String imagePath, List candidates) { Mat image = Imgcodecs.imread(imagePath); if (image.empty()) { throw new RuntimeException(\"无法读取图像: \" + imagePath); } // 在图像上绘制候选标识物边框 for (int i = 0; i = 0 && key  MIN_AREA) { selectedMarker = new Marker(selectionRect, selectionRect.area(), 0, image.submat(selectionRect)); System.out.println(\"已手动选择标识物区域\"); saveMarkerTemplate(selectedMarker); ((JFrame) SwingUtilities.getWindowAncestor(label)).dispose(); } } } /** * 保存标识物模板(用于后续匹配) */ private void saveMarkerTemplate(Marker marker) { String templatePath = templateRootPath+\"marker_template_\" + System.currentTimeMillis() + \".png\"; Imgcodecs.imwrite(templatePath, marker.getMat()); // 同时保存标识物在原图中的坐标(用于计算角度) System.out.println(\"标识物模板已保存至: \" + templatePath); System.out.println(\"标识物位置: \" + marker.getRect()); } /** * Mat转BufferedImage(用于Swing显示) */ private BufferedImage matToBufferedImage(Mat mat) { int type = BufferedImage.TYPE_BYTE_GRAY; if (mat.channels() > 1) { type = BufferedImage.TYPE_3BYTE_BGR; } int bufferSize = mat.channels() * mat.cols() * mat.rows(); byte[] buffer = new byte[bufferSize]; mat.get(0, 0, buffer); BufferedImage image = new BufferedImage(mat.cols(), mat.rows(), type); final byte[] targetPixels = ((java.awt.image.DataBufferByte) image.getRaster().getDataBuffer()).getData(); System.arraycopy(buffer, 0, targetPixels, 0, buffer.length); return image; } /** * 标识物实体类 */ public static class Marker { private Rect rect; // 位置和大小 private double area; // 面积 private int vertices; // 顶点数(形状规则度) private Mat mat; // 图像数据 public Marker(Rect rect, double area, int vertices, Mat mat) { this.rect = rect; this.area = area; this.vertices = vertices; this.mat = mat; } // 优先级计算(面积越大、形状越规则,优先级越高) public double getPriority() { // 顶点数4(矩形)或3(三角形)的标识物加分 double shapeScore = (vertices == 4 || vertices == 3) ? 1.5 : 1.0; return area * shapeScore; } // getter方法 public Rect getRect() { return rect; } public Mat getMat() { return mat; } }} 

       (2)测试代码Test.java

import java.util.List;/** * 测试自动寻找模版 */public class Test {public static void main(String[] args) {String stitchedImagePath=\"d:/test/3-2.jpg\";MarkerDetector md=new MarkerDetector();List ms=md.autoDetectMarkers(stitchedImagePath);md.showMarkerSelectionDialog(stitchedImagePath,ms);}}

(3)测试效果

测试输入的图片3-2.jpg是下图:

运行效果如下图:

我选择保存的是最右边的标识物,保存名称为marker_template_1756440153260,见下图:

3.标识物模版匹配实际图片程序

        (1)模版匹配实际图片MarkerTemplateMatcher.java

import java.awt.Color;import java.awt.Font;import java.awt.Graphics2D;import java.awt.image.BufferedImage;import java.awt.image.DataBufferByte;import java.util.ArrayList;import java.util.List;import javax.swing.ImageIcon;import javax.swing.JFrame;import javax.swing.JLabel;import org.opencv.core.Core;import org.opencv.core.CvType;import org.opencv.core.Mat;import org.opencv.core.Point;import org.opencv.core.Rect;import org.opencv.core.Scalar;import org.opencv.core.Size;import org.opencv.imgcodecs.Imgcodecs;import org.opencv.imgproc.Imgproc;public class MarkerTemplateMatcher {// 加载OpenCV库static { System.loadLibrary(Core.NATIVE_LIBRARY_NAME);} // 匹配阈值(0-1,值越大匹配越严格) private static final double MATCH_THRESHOLD = 0.8; // 模板图像 private Mat template; // 模板宽度和高度 private int templateWidth; private int templateHeight; /** * 加载模板图像 * @param templatePath 模板图像路径 * @return 是否加载成功 */ public boolean loadTemplate(String templatePath) { template = Imgcodecs.imread(templatePath); if (template.empty()) { System.err.println(\"无法加载模板图像: \" + templatePath); return false; } // 转换为灰度图(提高匹配效率) Imgproc.cvtColor(template, template, Imgproc.COLOR_BGR2GRAY); templateWidth = template.cols(); templateHeight = template.rows(); System.out.println(\"模板加载成功,尺寸: \" + templateWidth + \"x\" + templateHeight); return true; } /** * 在输入图像中寻找模板匹配位置 * @param inputImagePath 输入图像路径 * @return 匹配位置的矩形列表 */ public List findTemplateMatches(String inputImagePath) { List matches = new ArrayList(); // 读取输入图像并转换为灰度图 Mat inputImage = Imgcodecs.imread(inputImagePath); if (inputImage.empty()) { System.err.println(\"无法读取输入图像: \" + inputImagePath); return matches; } Mat grayImage = new Mat(); Imgproc.cvtColor(inputImage, grayImage, Imgproc.COLOR_BGR2GRAY); // 创建结果矩阵(存储匹配程度) int resultCols = grayImage.cols() - templateWidth + 1; int resultRows = grayImage.rows() - templateHeight + 1; Mat result = new Mat(resultRows, resultCols, CvType.CV_32FC1); // 执行模板匹配 Imgproc.matchTemplate(grayImage, template, result, Imgproc.TM_CCOEFF_NORMED); // 寻找匹配值超过阈值的位置 Core.MinMaxLocResult mmr = Core.minMaxLoc(result); // 对于多匹配情况,遍历结果矩阵 if (Imgproc.TM_CCOEFF_NORMED == Imgproc.TM_CCOEFF_NORMED) { // 单模板最佳匹配 if (mmr.maxVal >= MATCH_THRESHOLD) { Point matchLoc = mmr.maxLoc; Rect matchRect = new Rect(  new Point(matchLoc.x, matchLoc.y),  new Size(templateWidth, templateHeight) ); matches.add(matchRect); System.out.println(\"找到匹配位置,置信度: \" + mmr.maxVal); } else { System.out.println(\"未找到符合阈值的匹配,最高置信度: \" + mmr.maxVal); } } return matches; } /** * 显示带有匹配框的图像 * @param imagePath 原始图像路径 * @param matches 匹配位置矩形列表 */ public void showMatchingResult(String imagePath, List matches) { Mat image = Imgcodecs.imread(imagePath); if (image.empty()) { System.err.println(\"无法读取图像用于显示: \" + imagePath); return; } // 绘制所有匹配位置的矩形框 for (int i = 0; i  1) { type = BufferedImage.TYPE_3BYTE_BGR; } int bufferSize = mat.channels() * mat.cols() * mat.rows(); byte[] buffer = new byte[bufferSize]; mat.get(0, 0, buffer); BufferedImage image = new BufferedImage(mat.cols(), mat.rows(), type); byte[] targetPixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); System.arraycopy(buffer, 0, targetPixels, 0, buffer.length); return image; } /** * BufferedImage转Mat */ private Mat bufferedImageToMat(BufferedImage image) { Mat mat = new Mat(image.getHeight(), image.getWidth(), CvType.CV_8UC3); byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); mat.put(0, 0, data); return mat; } /** * 主方法:演示模板匹配流程 */ public static void main(String[] args) { // 创建匹配器实例 MarkerTemplateMatcher matcher = new MarkerTemplateMatcher(); // 1. 加载模板(替换为你的模板图像路径) String templatePath = \"marker_template.jpg\"; // 之前保存的标识物模板 if (!matcher.loadTemplate(templatePath)) { return; } // 2. 在新图像中寻找匹配(替换为你的测试图像路径) String testImagePath = \"new_wine_bottle.jpg\"; // 新的酒瓶图像 List matches = matcher.findTemplateMatches(testImagePath); // 3. 显示匹配结果 matcher.showMatchingResult(testImagePath, matches); }}

        (2)测试程序Test2.java

import java.util.List;import org.opencv.core.Rect;/** * 测试模版加载寻找 */public class Test2 {public static void main(String[] args) {long a1=System.currentTimeMillis();String stitchedImagePath=\"d:/test/3-5.jpg\";String templateImagePath=\"d:/test/marker_template_1756440153260.png\";MarkerTemplateMatcher mtm = new MarkerTemplateMatcher();System.out.println(System.currentTimeMillis()-a1);mtm.loadTemplate(templateImagePath);System.out.println(System.currentTimeMillis()-a1);List ms=mtm.findTemplateMatches(stitchedImagePath);System.out.println(System.currentTimeMillis()-a1);mtm.showMatchingResult(stitchedImagePath, ms);}}

        (3)测试效果

       测试用的3-5.jpg,我模拟了实际情况,让标识物的内部打了写空心点,并且将标识物水平方向缩短了2个像素(模拟线扫相机丢帧,少了1根线),如下图:

        实际上,人工模拟的干扰,也没有影响预期的效果,程序运行还是找到了目标:

四、总结

 1.还是学习速度,AI大大的帮助了我。我虽然有一点点CV的基础,但是这一次AI写的代码,比我写的好,时间也大大缩短了。但是AI仍然有理解不到位的地方,在我的引导下,快速完成了代码。

2.为什么选择CV而不是AI模型呢?因为AI模型目前做目标检测已经很好了,为什么不选择呢?因为时间,工业场景中,特别是流水线上,对节拍要求很高,基于AI的目标检测达不到这个处理效率。