一、背景
兴趣爱好来了,决定研发一个产品。涉及到电工和机械等知识,所以记录一下相关的基础知识。今天的内容又回到了我的主营板块!!哈哈!!为后续整体集成做准备,先测试目标检测部分的能力。
二、需求描述
我的需求是流水线上的工业相机拍摄不同产品的图片,同批次生产产品的第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的目标检测达不到这个处理效率。