需求:
找到图片中的标记框,后期可以基于此实现确定纸张方向等功能。
思路:可以借鉴二维码的三点定位原理,进行实现。
巨人的肩膀:
效果:
代码运行后效果:
各个阶段的截图如图所示,右下角的三张图片(标记0-2)是能够定位出来的三个标记框
原图:
canny处理效果(找轮廓):
依赖:
需要OpenCV的运行环境,Windows直接通过exe安装即可,Mac有些繁琐,Linux需要通过源码进行本地编译。具体参见其他资料,此处不再赘述。
<dependency>
<groupId>org.opencv</groupId>
<artifactId>opencv</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
源码:
import com.stonelu.tools.opencv.OpencvUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import java.awt.*;
import java.awt.Point;
import java.util.*;
import java.util.List;
/**
* @author stonelu
* @date 2019-06-11 09:39
*/
public class Test {
public static void main(String[] args) {
findQRCode();
}
private static void showImg(Mat mat, String title, int idx) {
ImageForm.showImage(mat, new Dimension(400, 400), title, new Point(50 * idx, 50 * idx));
}
private static void findQRCode() {
// 测试图片
String srcFilePath = "/Users/edz/Desktop/corner-3.jpg";
int imIdx = 0;
// 子轮廓个数
int tarChildContourCnt = 4;
List<MatOfPoint> markContours = new LinkedList<>();
Mat srcMat = OpencvUtil.readImg(srcFilePath);
showImg(srcMat, "原图", imIdx++);
Mat grayMat = OpencvUtil.getGrayMat(srcMat);
showImg(grayMat, "灰度图", imIdx++);
Mat adaptThresholdMat = OpencvUtil.adaptiveThreshold(grayMat);
showImg(adaptThresholdMat, "二值图", imIdx++);
Mat gaussMat = new Mat();
Imgproc.GaussianBlur(adaptThresholdMat, gaussMat, new Size(5, 5), 0);
showImg(gaussMat, "高斯滤波", imIdx++);
Mat cannyMat = new Mat();
Imgproc.Canny(gaussMat, cannyMat, 100, 200);
OpencvUtil.saveImg(cannyMat, "/Users/edz/Desktop/canny.jpg");
showImg(cannyMat, "canny", imIdx++);
List<MatOfPoint> contours = new LinkedList<>();
Mat hierarchy = new Mat();
Imgproc.findContours(cannyMat, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_NONE);
Map<Integer, Integer> childContourCntMap = new HashMap<>();
for (int i = 0; i < contours.size(); i++) {
double[] ds = hierarchy.get(0, i);
if (ds != null && ds.length > 3) {
int childContourCnt = 0;
// 最外层轮廓,排除
if (getChildIdx(ds) == -1) {
continue;
}
// 计算此轮廓的所有子轮廓数量
while (getChildIdx(ds) != -1) {
childContourCnt++;
ds = hierarchy.get(0, (int) getChildIdx(ds));
}
addContour(childContourCntMap, childContourCnt);
if (childContourCnt >= tarChildContourCnt) {
// 子轮廓数量足够,则表示此为定位图形
markContours.add(contours.get(i));
}
}
}
System.out.println("一共找到["+ markContours.size() + "]个标记点");
// 需要进行轮廓过滤
List<MatOfPoint> contoursLast = filterContours(markContours);
System.out.println("经过过滤后,还有["+ contoursLast.size() + "]个标记点");
for (int i = 0; i < contoursLast.size(); i++) {
Mat m = srcMat.clone();
Imgproc.drawContours(m, contoursLast, i, new Scalar(0, 255, 0), -1);
// 找到标记点的中心坐标
Point center = getMatOfPointCenter(contoursLast.get(i));
System.out.println(String.format("坐标:(%d, %d)", center.x, center.y));
System.out.println(String.format("p: (%d, %d)", center.x, center.y));
m = OpencvUtil.paintCircle(m, new org.opencv.core.Point(center.x, center.y), 30, new Scalar(255, 0, 0));
showImg(m, "标记" + i, imIdx++);
}
// 输出标记框
for (Map.Entry<Integer, Integer> entry : childContourCntMap.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
}
/**
* 获取内嵌轮廓索引
* @param ds
* @return
*/
private static double getChildIdx(double[] ds) {
return ds[2];
}
/**
* 获取轮廓的中心点
* @param mp
* @return
*/
private static Point getMatOfPointCenter(MatOfPoint mp) {
if (mp == null) {
return null;
}
Rect rect = Imgproc.boundingRect(mp);
int centerX = rect.x + rect.width/ 2;
int centerY = rect.y + rect.height / 2;
return new Point(centerX, centerY);
}
/**
* 过滤标记点
* @return
*/
private static List<MatOfPoint> filterContours(List<MatOfPoint> list) {
List<MatOfPoint> retContours = new LinkedList<>();
// 按照x坐标递增排序
Map<Integer, PointMatOfPoint> xPointMatMap = new TreeMap<>(Comparator.naturalOrder());
for (MatOfPoint mp : list) {
Point center = getMatOfPointCenter(mp);
xPointMatMap.put(center.x, new PointMatOfPoint(center, mp));
}
int preX = 0;
int preY = 0;
boolean isFirstEntry = true;
for (Map.Entry<Integer, PointMatOfPoint> entry : xPointMatMap.entrySet()) {
PointMatOfPoint pointMatOfPoint = entry.getValue();
Point point = pointMatOfPoint.getPoint();
int currX = point.x;
int currY = point.y;
// 第一个标记点
if (isFirstEntry) {
preX = currX;
preY = currY;
isFirstEntry = false;
retContours.add(pointMatOfPoint.getMatOfPoint());
continue;
}
if (isTarTagBelievable(preX, preY, currX, currY)) {
preX = currX;
preY = currY;
retContours.add(pointMatOfPoint.getMatOfPoint());
}
}
return retContours;
}
/**
* 指定标记是否可信
* @return
*/
private static boolean isTarTagBelievable(int srcX, int srcY, int tarX, int tarY) {
// 为了避免相同定位点框的中心点被多个轮廓包含,此处需要做下兼容
int delta = 30;
return Math.abs(srcX - tarX) > delta || Math.abs(srcY - tarY) > delta;
}
/**
* 将内嵌轮廓个数添加到map中
* @param map
* @param childCnt
*/
private static void addContour(Map<Integer, Integer> map, Integer childCnt) {
Integer cnt = map.get(childCnt);
if (cnt == null) {
cnt = 0;
}
cnt++;
map.put(childCnt, cnt);
}
@Data
@AllArgsConstructor
private static class PointMatOfPoint {
private Point point;
private MatOfPoint matOfPoint;
}
}