继上一次使用做二维码识别,公司需要高识别率的二维码识别,但ZXING实在是太弱了,对于那种二维码占比很小、图片对比度不高的,识别率低的令人咋舌,后来引入了opencv,加上恶补了一些图像处理的基础知识,总算有一个能看的过去的识别率了(但公司最后还是决定去买现成的产品。。。被嫌弃!!!)
上一篇文章说到了灰化、二值化处理图片来增加识别率,整体效果不是太明显,本次识别的主要思路是,先定位,再截取,最后再针对性的对二维码所在的那一小块图片进行常规的图像处理(例如灰化、二值化、去噪、直方图均值化。。。)
opencv的安装和导入网上一大堆,请自行百度,下载地址:https://opencv.org/releases/
部分关于二维码三点定位的代码参考了这位博主的博文:https://www.cnblogs.com/huacanfly/p/9908408.html
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.imageio.ImageIO;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.RotatedRect;
import org.opencv.core.Size;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.CLAHE;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.QRCodeDetector;
import org.opencv.utils.Converters;
import com.google.zxing.Binarizer;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.Result;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
public class QRcodeDecode {
// 定位后截取出来的二维码图片放大倍数
private static int TIMES = 4;
// 图像处理后的图片存放地址
private static String PATH = "F:\\output\\cc.jpg";
static {
// 加载Opencv的dll文件
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
public static void main(String[] args) {
decode("C:\\Users\\guanh\\Desktop\\vouchers");
}
/**
* @param directoryPath 要进行二维码识别的图片所在文件夹目录路径
*/
public static void decode(String directoryPath) {
int sum = 0; // 统计本次识别的总张数
int count = 0; // 统计识别成功的张数
int notFound = 0; // 记录未定位成功的图片张数
long startTime = System.currentTimeMillis();
// 需要进行识别的图片所在文件夹路径
File file = new File(directoryPath);
File[] vouchers = file.listFiles();
QRCodeDetector detector = new QRCodeDetector();
for (File voucher : vouchers) {
sum++;
/**
* 第一次识别,直接识别,若失败,则进行图像二维码定位处理
*/
String qRcode = decodeQRcode(detector, voucher.getAbsolutePath());
if (qRcode == null || "".equals(qRcode)) {
// 对图像进行处理,定位图像中的二维码,将其截取出来
findQRcodeAndCut(voucher.getAbsolutePath());
File file1 = new File(PATH);
if (file1.exists()) {
/**
* 第二次识别,若失败,则将定位后截取的二维码图片进行二值化处理再识别
*/
qRcode = decodeQRcode(detector, PATH);
if (qRcode == null || "".equals(qRcode)) {
Mat mat = Imgcodecs.imread(PATH, 1);
// 彩色图转灰度图
Imgproc.cvtColor(mat, mat, Imgproc.COLOR_RGB2GRAY);
// 对图像进行平滑处理
Imgproc.blur(mat, mat, new Size(3, 3));
// 中值去噪
Imgproc.medianBlur(mat, mat, 5);
// 这里定义一个新的Mat对象,主要是为了保留原图,未下次处理做准备
Mat mat2 = new Mat();
// 根据OTSU算法进行二值化
Imgproc.threshold(mat, mat2, 205, 255, Imgproc.THRESH_OTSU);
// 生成二值化后的图像
Imgcodecs.imwrite(PATH, mat2);
/**
* 第三次识别,若失败,则将图像进行限制对比度的自适应直方图均衡化处理
*/
qRcode = decodeQRcode(detector, PATH);
if (qRcode == null || "".equals(qRcode)) {
// 限制对比度的自适应直方图均衡化
CLAHE clahe = Imgproc.createCLAHE(2, new Size(8, 8));
clahe.apply(mat, mat);
Imgcodecs.imwrite(PATH, mat);
/**
* 第四次识别,失败就标红打印失败的图片名称
*/
qRcode = decodeQRcode(detector, PATH);
if (qRcode == null || "".equals(qRcode)) {
System.err.println(voucher.getName());
} else {
System.out.println(voucher.getName() + "---4---" + qRcode);
}
} else {
System.out.println(voucher.getName() + "---3---" + qRcode);
}
} else {
System.out.println(voucher.getName() + "---2---" + qRcode);
}
} else {
notFound++;
}
} else {
System.out.println(voucher.getName() + "---1---" + qRcode);
}
// 每次检查处理图片时是否有生成图片,若存在,则删除,避免干扰下一次图像识别结果
File file2 = new File(PATH);
if (file2.exists()) {
file2.delete();
}
if (qRcode != null && !"".equals(qRcode)) {
count++;
}
}
long endTime = System.currentTimeMillis();
long time = (endTime-startTime)/1000;
System.out.println("一共扫描" + vouchers.length + "张图片,耗时" + time + "秒,平均每张耗时:" + Math.round(100.0 * time/vouchers.length)*1.0/100 + "秒");
System.out.println(
"未定位到的图片数量:" + notFound + ",sun = " + sum + ",count = " + count + "识别率:" + 1.0 * count / sum + "%");
}
public static void findQRcodeAndCut(String filePath) {
Mat src_gray = new Mat();
Mat src = Imgcodecs.imread(filePath, 1);
List contours = new ArrayList();
List markContours = new ArrayList();
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
/** 图片太小就放大 **/
if (src.width() * src.height() < 90000) {
Imgproc.resize(src, src, new Size(800, 600));
}
// 彩色图转灰度图
Imgproc.cvtColor(src, src_gray, Imgproc.COLOR_RGB2GRAY);
// 对图像进行平滑处理
Imgproc.GaussianBlur(src_gray, src_gray, new Size(3, 3), 0);
Imgproc.Canny(src_gray, src_gray, 112, 255);
Mat hierarchy = new Mat();
Imgproc.findContours(src_gray, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_NONE);
for (int i = 0; i < contours.size(); i++) {
MatOfPoint2f newMtx = new MatOfPoint2f(contours.get(i).toArray());
RotatedRect rotRect = Imgproc.minAreaRect(newMtx);
double w = rotRect.size.width;
double h = rotRect.size.height;
double rate = Math.max(w, h) / Math.min(w, h);
/***
* 长短轴比小于1.3,总面积大于60
*/
if (rate < 1.3 && w < src_gray.cols() / 4 && h < src_gray.rows() / 4
&& Imgproc.contourArea(contours.get(i)) > 60) {
/***
* 计算层数,二维码角框有五层轮廓(有说六层),这里不计自己这一层,有4个以上子轮廓则标记这一点
*/
double[] ds = hierarchy.get(0, i);
if (ds != null && ds.length > 3) {
int count = 0;
if (ds[3] == -1) {/** 最外层轮廓排除 */
continue;
}
/***
* 计算所有子轮廓数量
*/
while ((int) ds[2] != -1) {
++count;
ds = hierarchy.get(0, (int) ds[2]);
}
if (count >= 4) {
markContours.add(contours.get(i));
}
}
}
}
/***
* 二维码有三个角轮廓,正常需要定位三个角才能确定坐标,但由于公司使用的凭证干扰因素较少,故当识别到两个点的时候也将二维码定位出来;
* 当识别到三个点时,根据三个点定位可以确定二维码位置和形状,根据三个点组成三角形形状最大角角度判断是不是二维码的三个角
* 当识别到两个点时,取两个点中间点,往四周扩散截取 当小于两个点时,直接返回
*/
if (markContours.size() == 0) {
return;
} else if (markContours.size() == 1) {
capture(markContours.get(0), src);
} else if (markContours.size() == 2) {
List threePointList = new ArrayList<>();
threePointList.add(markContours.get(0));
threePointList.add(markContours.get(1));
capture(threePointList, src);
} else {
for (int i = 0; i < markContours.size() - 2; i++) {
List threePointList = new ArrayList<>();
for (int j = i + 1; j < markContours.size() - 1; j++) {
for (int k = j + 1; k < markContours.size(); k++) {
threePointList.add(markContours.get(i));
threePointList.add(markContours.get(j));
threePointList.add(markContours.get(k));
capture(threePointList, src, i + "-" + j + "-" + k);
threePointList.clear();
}
}
}
}
}
/**
* 针对对比度不高的图片,只能识别到一个角的,直接以该点为中心截取
*
* @param matOfPoint
* @param src
*/
private static void capture(MatOfPoint matOfPoint, Mat src) {
Point centerPoint = centerCal(matOfPoint);
int width = 200;
Rect roiArea = new Rect((int) (centerPoint.x - width) > 0 ? (int) (centerPoint.x - width) : 0,
(int) (centerPoint.y - width) > 0 ? (int) (centerPoint.y - width) : 0, (int) (2 * width),
(int) (2 * width));
// 截取二维码
Mat dstRoi = new Mat(src, roiArea);
// 放大图片
Imgproc.resize(dstRoi, dstRoi, new Size(TIMES * width, TIMES * width));
Imgcodecs.imwrite(PATH, dstRoi);
}
/**
* 当只识别到二维码的两个定位点时,根据两个点的中点进行定位
*
* @param threePointList
* @param src
*/
private static void capture(List threePointList, Mat src) {
Point p1 = centerCal(threePointList.get(0));
Point p2 = centerCal(threePointList.get(1));
Point centerPoint = new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
double width = Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y) + 50;
// 设置截取规则
Rect roiArea = new Rect((int) (centerPoint.x - width) > 0 ? (int) (centerPoint.x - width) : 0,
(int) (centerPoint.y - width) > 0 ? (int) (centerPoint.y - width) : 0, (int) (2 * width),
(int) (2 * width));
// 截取二维码
Mat dstRoi = new Mat(src, roiArea);
// 放大图片
Imgproc.resize(dstRoi, dstRoi, new Size(TIMES * width, TIMES * width));
Imgcodecs.imwrite(PATH, dstRoi);
}
/**
* 对图片进行矫正,裁剪
*
* @param contours
* @param src
* @param idx
*/
private static void capture(List contours, Mat src, String idx) {
Point[] pointthree = new Point[3];
for (int i = 0; i < 3; i++) {
pointthree[i] = centerCal(contours.get(i));
}
double[] ca = new double[2];
double[] cb = new double[2];
ca[0] = pointthree[1].x - pointthree[0].x;
ca[1] = pointthree[1].y - pointthree[0].y;
cb[0] = pointthree[2].x - pointthree[0].x;
cb[1] = pointthree[2].y - pointthree[0].y;
/*
* angle1,angle2,angle3分别对应识别到的二维码定位角的三个点所组成三角形的三个角
*/
double angle1 = 180 / 3.1415 * Math.acos((ca[0] * cb[0] + ca[1] * cb[1])
/ (Math.sqrt(ca[0] * ca[0] + ca[1] * ca[1]) * Math.sqrt(cb[0] * cb[0] + cb[1] * cb[1])));
double ccw1;
if (ca[0] * cb[1] - ca[1] * cb[0] > 0) {
ccw1 = 0;
} else {
ccw1 = 1;
}
ca[0] = pointthree[0].x - pointthree[1].x;
ca[1] = pointthree[0].y - pointthree[1].y;
cb[0] = pointthree[2].x - pointthree[1].x;
cb[1] = pointthree[2].y - pointthree[1].y;
double angle2 = 180 / 3.1415 * Math.acos((ca[0] * cb[0] + ca[1] * cb[1])
/ (Math.sqrt(ca[0] * ca[0] + ca[1] * ca[1]) * Math.sqrt(cb[0] * cb[0] + cb[1] * cb[1])));
double ccw2;
if (ca[0] * cb[1] - ca[1] * cb[0] > 0) {
ccw2 = 0;
} else {
ccw2 = 1;
}
ca[0] = pointthree[1].x - pointthree[2].x;
ca[1] = pointthree[1].y - pointthree[2].y;
cb[0] = pointthree[0].x - pointthree[2].x;
cb[1] = pointthree[0].y - pointthree[2].y;
double angle3 = 180 / 3.1415 * Math.acos((ca[0] * cb[0] + ca[1] * cb[1])
/ (Math.sqrt(ca[0] * ca[0] + ca[1] * ca[1]) * Math.sqrt(cb[0] * cb[0] + cb[1] * cb[1])));
int ccw3;
if (ca[0] * cb[1] - ca[1] * cb[0] > 0) {
ccw3 = 0;
} else {
ccw3 = 1;
}
if (Double.isNaN(angle1) || Double.isNaN(angle2) || Double.isNaN(angle3)) {
return;
}
Point[] poly = new Point[4];
if (angle3 > angle2 && angle3 > angle1) {
if (ccw3 == 1) {
poly[1] = pointthree[1];
poly[3] = pointthree[0];
} else {
poly[1] = pointthree[0];
poly[3] = pointthree[1];
}
poly[0] = pointthree[2];
Point temp = new Point(pointthree[0].x + pointthree[1].x - pointthree[2].x,
pointthree[0].y + pointthree[1].y - pointthree[2].y);
poly[2] = temp;
} else if (angle2 > angle1 && angle2 > angle3) {
if (ccw2 == 1) {
poly[1] = pointthree[0];
poly[3] = pointthree[2];
} else {
poly[1] = pointthree[2];
poly[3] = pointthree[0];
}
poly[0] = pointthree[1];
Point temp = new Point(pointthree[0].x + pointthree[2].x - pointthree[1].x,
pointthree[0].y + pointthree[2].y - pointthree[1].y);
poly[2] = temp;
} else if (angle1 > angle2 && angle1 > angle3) {
if (ccw1 == 1) {
poly[1] = pointthree[1];
poly[3] = pointthree[2];
} else {
poly[1] = pointthree[2];
poly[3] = pointthree[1];
}
poly[0] = pointthree[0];
Point temp = new Point(pointthree[1].x + pointthree[2].x - pointthree[0].x,
pointthree[1].y + pointthree[2].y - pointthree[0].y);
poly[2] = temp;
}
Point[] trans = new Point[4];
int temp = 50;
trans[0] = new Point(0 + temp, 0 + temp);
trans[1] = new Point(0 + temp, 100 + temp);
trans[2] = new Point(100 + temp, 100 + temp);
trans[3] = new Point(100 + temp, 0 + temp);
double maxAngle = Math.max(angle3, Math.max(angle1, angle2));
// System.out.println("maxAngle:" + maxAngle);
// 二维码为直角,最大角过大或者过小都判断为不是二维码
if (maxAngle < 75 || maxAngle > 115) {
return;
}
Mat perspectiveMmat = Imgproc.getPerspectiveTransform(
Converters.vector_Point_to_Mat(Arrays.asList(poly), CvType.CV_32F),
Converters.vector_Point_to_Mat(Arrays.asList(trans), CvType.CV_32F)); // warp_mat
Mat dst = new Mat();
// 计算透视变换结果
Imgproc.warpPerspective(src, dst, perspectiveMmat, src.size(), Imgproc.INTER_LINEAR);
Rect roiArea = new Rect(0, 0, 200, 200);
Mat dstRoi = new Mat(dst, roiArea);
// 放大图片
Imgproc.resize(dstRoi, dstRoi, new Size(2 * dstRoi.width(), 2 * dstRoi.height()));
Imgcodecs.imwrite(PATH, dstRoi);
}
/**
* 将Mat转换为流,为了方便测试,代码中没有将Mat转换成流进行识别,若有需要,可以不落地文件
*
* @param m
* @return
*/
public static BufferedImage toBufferedImage(Mat m) {
int type = BufferedImage.TYPE_BYTE_GRAY;
if (m.channels() > 1) {
type = BufferedImage.TYPE_3BYTE_BGR;
}
int bufferSize = m.channels() * m.cols() * m.rows();
byte[] b = new byte[bufferSize];
m.get(0, 0, b); // get all the pixels
BufferedImage image = new BufferedImage(m.cols(), m.rows(), type);
final byte[] targetPixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
System.arraycopy(b, 0, targetPixels, 0, b.length);
return image;
}
/**
* 获取轮廓的中心坐标
*
* @param matOfPoint
* @return
*/
private static Point centerCal(MatOfPoint matOfPoint) {
double centerx = 0, centery = 0;
MatOfPoint2f mat2f = new MatOfPoint2f(matOfPoint.toArray());
RotatedRect rect = Imgproc.minAreaRect(mat2f);
Point vertices[] = new Point[4];
rect.points(vertices);
centerx = ((vertices[0].x + vertices[1].x) / 2 + (vertices[2].x + vertices[3].x) / 2) / 2;
centery = ((vertices[0].y + vertices[1].y) / 2 + (vertices[2].y + vertices[3].y) / 2) / 2;
Point point = new Point(centerx, centery);
return point;
}
/**
* 解析读取二维码
* 先使用ZXING二维码识别,若失败,使用OPENCV自带的二维码识别
* 个人测试,两者的识别率差不多,都不尽人意,但一起使用还是可以略微提高一点识别率,毕竟实现算法不一样
* 若还要其它的识别,类似Zbar,都可以集成进来
*
* @param qrCodePath 二维码图片路径
* @return 成功返回二维码识别结果,失败返回null
* @throws Exception
*/
public static String decodeQRcode(QRCodeDetector detector, String qrCodePath){
String qrCodeText = null;
try {
BufferedImage image = ImageIO.read(new File(qrCodePath));
LuminanceSource source = new BufferedImageLuminanceSource(image);
Binarizer binarizer = new HybridBinarizer(source);
BinaryBitmap binaryBitmap = new BinaryBitmap(binarizer);
Map hints = new HashMap();
hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
Result result = new MultiFormatReader().decode(binaryBitmap, hints);
qrCodeText = result.getText();
} catch (Exception e) {
qrCodeText = detector.detectAndDecode(Imgcodecs.imread(qrCodePath, 1));
}
return qrCodeText;
}
}
今天也参加了和专门做二维码识别的公司的会议,大致了解了他们的产品实现,整体思路差不多,但比不过人家专业,若公司真要求很高,还是买产品来的实在。