程序员之电工基础-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<Marker> detectedMarkers = new ArrayList<>();// 选中的标识物模板private Marker selectedMarker;String templateRootPath="d:/test/";/*** 从拼接图像中自动检测标识物* @param stitchedImagePath 拼接后的图像路径* @return 排序后的前3个标识物*/public List<Marker> 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<MatOfPoint> 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 < MIN_AREA || area > MAX_AREA) {continue;}// 计算边界框Rect rect = Imgproc.boundingRect(contour);double aspectRatio = (double) rect.width / rect.height;if (aspectRatio < MIN_ASPECT_RATIO || 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<Marker> candidates) {Mat image = Imgcodecs.imread(imagePath);if (image.empty()) {throw new RuntimeException("无法读取图像: " + imagePath);}// 在图像上绘制候选标识物边框for (int i = 0; i < candidates.size(); i++) {Rect rect = candidates.get(i).getRect();// 绘制不同颜色的边框(第1名绿色,第2名蓝色,第3名黄色)Scalar color = i == 0 ? new Scalar(0, 255, 0) : i == 1 ? new Scalar(255, 0, 0) : new Scalar(0, 255, 255);Imgproc.rectangle(image, rect.tl(), rect.br(), color, 2);Imgproc.putText(image, "Object" + (i+1), rect.tl(), Imgproc.FONT_HERSHEY_SIMPLEX, 0.8, color, 2);}// 转换为Swing可显示的图像BufferedImage bufferedImage = matToBufferedImage(image);// 创建交互窗口JFrame frame = new JFrame("选择标识物(按1-3键选择,鼠标框选可手动添加)");frame.setSize(image.cols(), image.rows());frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);JLabel label = new JLabel(new ImageIcon(bufferedImage));frame.add(label);// 键盘选择(1-3对应候选标识物)frame.addKeyListener(new java.awt.event.KeyAdapter() {public void keyPressed(java.awt.event.KeyEvent e) {int key = e.getKeyCode() - java.awt.event.KeyEvent.VK_1;if (key >= 0 && key < candidates.size()) {selectedMarker = candidates.get(key);System.out.println("已选择候选" + (key+1) + "作为标识物");saveMarkerTemplate(selectedMarker);frame.dispose();}}});// 鼠标框选(手动选择区域)MouseSelectionHandler mouseHandler = new MouseSelectionHandler(label, image);label.addMouseListener(mouseHandler);label.addMouseMotionListener(mouseHandler);frame.setVisible(true);}/*** 鼠标框选处理器(支持手动选择标识物)*/private class MouseSelectionHandler extends MouseAdapter {private JLabel label;private Mat image;private Point startPoint;private Rect selectionRect;public MouseSelectionHandler(JLabel label, Mat image) {this.label = label;this.image = image;}@Overridepublic void mousePressed(MouseEvent e) {startPoint = e.getPoint();}@Overridepublic void mouseDragged(MouseEvent e) {// 实时绘制框选区域Point endPoint = e.getPoint();selectionRect = new Rect(new org.opencv.core.Point(Math.min(startPoint.x, endPoint.x), Math.min(startPoint.y, endPoint.y)),new org.opencv.core.Point(Math.max(startPoint.x, endPoint.x), Math.max(startPoint.y, endPoint.y)));Mat temp = image.clone();Imgproc.rectangle(temp, selectionRect.tl(), selectionRect.br(), new Scalar(0, 0, 255), 2);label.setIcon(new ImageIcon(matToBufferedImage(temp)));}@Overridepublic void mouseReleased(MouseEvent e) {// 保存手动框选的区域作为标识物if (selectionRect != null && selectionRect.area() > 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<MarkerDetector.Marker> 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<Rect> findTemplateMatches(String inputImagePath) {List<Rect> 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<Rect> matches) {Mat image = Imgcodecs.imread(imagePath);if (image.empty()) {System.err.println("无法读取图像用于显示: " + imagePath);return;}// 绘制所有匹配位置的矩形框for (int i = 0; i < matches.size(); i++) {Rect rect = matches.get(i);// 绘制红色矩形框(BGR格式)Imgproc.rectangle(image, rect.tl(), rect.br(), new Scalar(0, 0, 255), 2);// 显示匹配序号和置信度putTextWithChinese(image, "匹配" + (i+1), rect.tl());}// 转换为Swing可显示的图像BufferedImage bufferedImage = matToBufferedImage(image);// 创建显示窗口JFrame frame = new JFrame("模板匹配结果");frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);frame.getContentPane().add(new JLabel(new ImageIcon(bufferedImage)));frame.pack();frame.setLocationRelativeTo(null); // 居中显示frame.setVisible(true);}/*** 绘制中文文本(解决OpenCV中文显示问题)*/private void putTextWithChinese(Mat image, String text, Point point) {try {// 转换为BufferedImageBufferedImage bufferedImage = matToBufferedImage(image);Graphics2D g2d = bufferedImage.createGraphics();// 设置字体(使用系统中的中文字体)Font font = new Font("SimHei", Font.PLAIN, 16); // 黑体g2d.setFont(font);g2d.setColor(Color.RED);// 绘制文本g2d.drawString(text, (int)point.x, (int)point.y - 5); // 显示在矩形上方g2d.dispose();// 转换回Matimage.setTo(bufferedImageToMat(bufferedImage));} catch (Exception e) {// 若字体设置失败,使用默认方式绘制(可能显示乱码)Imgproc.putText(image, text, point, Imgproc.FONT_HERSHEY_SIMPLEX, 0.6, new Scalar(0, 0, 255), 2);}}/*** Mat转BufferedImage*/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);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<Rect> 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<Rect> 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的目标检测达不到这个处理效率。