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

发布于:2025-09-01 ⋅ 阅读:(12) ⋅ 点赞:(0)

一、背景

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

二、需求描述

       我的需求是流水线上的工业相机拍摄不同产品的图片,同批次生产产品的第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;
        }

        @Override
        public void mousePressed(MouseEvent e) {
            startPoint = e.getPoint();
        }

        @Override
        public 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)));
        }

        @Override
        public 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 {
            // 转换为BufferedImage
            BufferedImage 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();
            
            // 转换回Mat
            image.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的目标检测达不到这个处理效率。