纯Java不依赖OpenCV及第三方库实现图像模板匹配问题求解
问题描述
需要在不引入OpenCV或任何第三方依赖的前提下,使用Java实现图像模板匹配功能。当前测试场景为:在红桃10扑克牌图像中匹配红桃花色图案模板,现有代码参考Matlab版本的逐位静态比对逻辑改写,存在两个核心问题:
- 静态逐像素比对逻辑鲁棒性差,图像存在压缩、轻微形变时会直接匹配失效
- 当前代码运行后只会在图像左上角固定绘制50*70的红色矩形,无法定位红桃模板的实际坐标,不能输出正确的匹配位置标注
现有实现代码如下:
import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.awt.image.DataBufferByte; public class pro { public static void main(String[] args) { try { // RGB pixel values byte[] pixels; File inp=new File("TenCardG.jpg"); BufferedImage image = ImageIO.read(inp); int width = image.getWidth(); int height = image.getHeight(); // System.out.println("Type: "+image.getColorModel()); pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); System.out.println("Dimension of image" + width + " "+height+" "+" pixels length "+ pixels.length); //rgb2gray in a 2D array grayImage int pr;// red int pg;// green int pb;// blue short [][] grayImage =new short [height][width]; int cord; for (int i=0; i<height;i++) for(int j=0;j<width;j++) { cord= 3*(i*width+j); pr= ((short) pixels[cord] & 0xff); // red pg= (((short) pixels[cord+1] & 0xff));// green pb= (((short) pixels[cord+2] & 0xff));// blue grayImage[i][j]=(short)Math.round(0.299 *pr + 0.587 * pg + 0.114 * pb); } Image scaledImage = image.getScaledInstance(-1,-1, 0); ImageIO.write( add_Rectangle(scaledImage), "jpg", new File("TenCardG2.jpg")); } catch (IOException e) { e.printStackTrace(); } System.out.println("Done, check the generated TenCardG2 image at its left top corner"); } // Add a rectangle of side size 50, 70 at coordinate 0.0 in image img public static BufferedImage add_Rectangle(Image img) { if (img instanceof BufferedImage) { return (BufferedImage) img; } // Create a buffered image with transparency BufferedImage bi = new BufferedImage( img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_RGB); Graphics2D g2D = bi.createGraphics(); g2D.drawImage(img, 0, 0, null); g2D.setColor(Color.RED); g2D.drawRect(0, 0, 50, 70); g2D.dispose(); return bi; } }
注:原代码缺失
javax.imageio.ImageIO导入,直接运行会抛出编译错误,调试前需要手动补全该导入。
实现方案
不引入第三方依赖的前提下,替换原有逐像素绝对差比对的静态逻辑,采用归一化互相关(NCC)模板匹配实现鲁棒匹配,该算法对图像亮度小幅变化、JPEG压缩噪声容忍度更高,实现步骤如下:
- 提前加载待匹配的红桃花色模板,同样完成灰度转换,计算模板自身的灰度均值、标准差,避免滑动窗口时重复计算
- 在源图像上按照模板尺寸滑动窗口,对每个窗口位置计算窗口区域与模板的归一化互相关系数,系数取值范围为[-1,1],值越接近1代表匹配度越高
- 遍历所有窗口位置,找到互相关系数最高的坐标,即为模板匹配的最佳位置,将该坐标传入矩形绘制方法即可完成标注
核心匹配逻辑代码可直接整合到现有灰度转换逻辑之后:
// 提前将红桃花色裁剪为单独模板文件存储 File tplFile = new File("heart_template.jpg"); BufferedImage tplImg = ImageIO.read(tplFile); int tplW = tplImg.getWidth(); int tplH = tplImg.getHeight(); // 模板转灰度 byte[] tplPixels = ((DataBufferByte) tplImg.getRaster().getDataBuffer()).getData(); short[][] tplGray = new short[tplH][tplW]; long tplSum = 0; for(int i=0;i<tplH;i++){ for(int j=0;j<tplW;j++){ int tplCord = 3*(i*tplW +j); int tpr = ((short)tplPixels[tplCord] & 0xff); int tpg = ((short)tplPixels[tplCord+1] & 0xff); int tpb = ((short)tplPixels[tplCord+2] & 0xff); tplGray[i][j] = (short)Math.round(0.299*tpr + 0.587*tpg + 0.114*tpb); tplSum += tplGray[i][j]; } } double tplMean = tplSum *1.0 / (tplW * tplH); // 计算模板标准差 double tplStdSum = 0; for(int i=0;i<tplH;i++){ for(int j=0;j<tplW;j++){ tplStdSum += Math.pow(tplGray[i][j] - tplMean, 2); } } double tplStd = Math.sqrt(tplStdSum / (tplW*tplH)); // 滑动窗口计算NCC系数 double maxNcc = -1; int matchX = 0, matchY =0; // 遍历所有能完整放下模板的窗口位置 for(int y=0; y <= height - tplH; y++){ for(int x=0; x <= width - tplW; x++){ // 计算当前窗口灰度均值 long winSum =0; for(int i=0;i<tplH;i++){ for(int j=0;j<tplW;j++){ winSum += grayImage[y+i][x+j]; } } double winMean = winSum *1.0/(tplW*tplH); // 计算窗口标准差、与模板的协方差 double winStdSum =0, covSum =0; for(int i=0;i<tplH;i++){ for(int j=0;j<tplW;j++){ double winDiff = grayImage[y+i][x+j] - winMean; double tplDiff = tplGray[i][j] - tplMean; winStdSum += Math.pow(winDiff,2); covSum += winDiff * tplDiff; } } double winStd = Math.sqrt(winStdSum/(tplW*tplH)); // 纯色区域跳过,避免除零错误 if(winStd < 1e-6 || tplStd <1e-6) continue; double ncc = covSum/(tplW*tplH*tplStd*winStd); // 更新最佳匹配位置 if(ncc > maxNcc){ maxNcc = ncc; matchX = x; matchY = y; } } } // 修改原矩形绘制方法,支持传入匹配坐标和模板尺寸,替换固定(0,0)坐标 ImageIO.write( drawMatchRect(image, matchX, matchY, tplW, tplH), "jpg", new File("TenCardG2.jpg"));
对应的矩形绘制方法需要调整参数,不再硬编码坐标和尺寸:
public static BufferedImage drawMatchRect(Image img, int x, int y, int rectW, int rectH) { if (img instanceof BufferedImage) { BufferedImage bi = (BufferedImage) img; Graphics2D g2D = bi.createGraphics(); g2D.setColor(Color.RED); g2D.drawRect(x, y, rectW, rectH); g2D.dispose(); return bi; } BufferedImage bi = new BufferedImage( img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_RGB); Graphics2D g2D = bi.createGraphics(); g2D.drawImage(img, 0, 0, null); g2D.setColor(Color.RED); g2D.drawRect(x, y, rectW, rectH); g2D.dispose(); return bi; }
优化提示
- 基础NCC实现匹配大尺寸图像时速度较慢,可以先对源图像和模板做2~4倍下采样做粗匹配找到大致范围,再在小范围做精匹配,能大幅提升运行速度
- 可以设置匹配阈值(比如NCC大于0.7才判定为匹配成功),避免图中不存在对应模板时出现误标注
- 原代码中
getScaledInstance(-1,-1,0)没有实际缩放效果,可以删除,直接使用原始图像绘制标注即可 - JPG是有损压缩格式,测试时模板尽量从同一张源图中裁剪得到,匹配准确率会更高
内容的提问来源于stack exchange,提问作者quadri quadri
相关产品推荐
相关产品推荐

