具体算法实现:具体算法实现——opencvhttps://blog.csdn.net/qq_39246466/article/details/123819795

1.安装:

配置linux或windows环境[Linux下配置OpenCV](http://note.youdao.com/noteshare?id=5de54af1ef6fef8352b8f3d3a9356845&sub=3D706CF274274B68B3BA2C9C42254747)
[Windows下配置OpenCV](http://note.youdao.com/noteshare?id=e0df335c7bba4d7633874375539c228a&sub=1015DEB3B7C847C28D0BBB87F21EDB59)

核心包:将下载的exe文件运行,取出其中的opencv.jar包

2.核心工具类:

package com.acts.opencv.common.utils;import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.IOException;
import java.util.Date;
import java.util.Vector;import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Point;
import org.opencv.core.Size;
import org.opencv.highgui.Highgui;
import org.opencv.imgproc.Imgproc;public class OpenCVUtil {public static BufferedImage covertMat2Buffer(Mat mat) throws IOException {long time1 = new Date().getTime();// Mat 转byte数组BufferedImage originalB = toBufferedImage(mat);long time3 = new Date().getTime();System.out.println("保存读取方法2转=" + (time3 - time1));return originalB;// ImageIO.write(originalB, "jpg", new File("D:\\test\\testImge\\ws2.jpg"));}public static byte[] covertMat2Byte(Mat mat) throws IOException {long time1 = new Date().getTime();// Mat 转byte数组byte[] return_buff = new byte[(int) (mat.total() * mat.channels())];Mat mat1 = new Mat();mat1.get(0, 0, return_buff);long time3 = new Date().getTime();System.out.println(mat.total() * mat.channels());System.out.println("保存读取方法2转=" + (time3 - time1));return return_buff;}public static byte[] covertMat2Byte1(Mat mat) throws IOException {long time1 = new Date().getTime();MatOfByte mob = new MatOfByte();Highgui.imencode(".jpg", mat, mob);long time3 = new Date().getTime();// System.out.println(mat.total() * mat.channels());System.out.println("Mat转byte[] 耗时=" + (time3 - time1));return mob.toArray();}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 pixelsBufferedImage 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 source* @return*/public static Mat eroding(Mat source) {return eroding(source, 1);}public static Mat eroding(Mat source, double erosion_size) {Mat resultMat = new Mat(source.rows(), source.cols(), source.type());Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(2 * erosion_size + 1,2 * erosion_size + 1));Imgproc.erode(source, resultMat, element);return resultMat;}/*** 腐蚀膨胀是针对于白色区域来说的,膨胀是膨胀白色区域* 膨胀算法(白色区域变大)* @param source* @return*/public static Mat dilation(Mat source) {return dilation(source, 1);}/*** 腐蚀膨胀是针对于白色区域来说的,膨胀是膨胀白色区域* @Author 王嵩* @param source* @param dilationSize 膨胀因子2*x+1 里的x* @return Mat* @Date 2018年2月5日* 更新日志* 2018年2月5日 王嵩  首次创建**/public static Mat dilation(Mat source, double dilation_size) {Mat resultMat = new Mat(source.rows(), source.cols(), source.type());Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(2 * dilation_size + 1,2 * dilation_size + 1));Imgproc.dilate(source, resultMat, element);return resultMat;}/*** 轮廓识别,使用最外轮廓发抽取轮廓RETR_EXTERNAL,轮廓识别方法为CHAIN_APPROX_SIMPLE* @param source 传入进来的图片Mat对象* @return 返回轮廓结果集*/public static Vector<MatOfPoint> findContours(Mat source) {Mat rs = new Mat();/*** 定义轮廓抽取模式*RETR_EXTERNAL:只检索最外面的轮廓;*RETR_LIST:检索所有的轮廓,并将其放入list中;*RETR_CCOMP:检索所有的轮廓,并将他们组织为两层:顶层是各部分的外部边界,第二层是空洞的边界;*RETR_TREE:检索所有的轮廓,并重构嵌套轮廓的整个层次。*/int mode = Imgproc.RETR_EXTERNAL;// int mode = Imgproc.RETR_TREE;/*** 定义轮廓识别方法* 边缘近似方法(除了RETR_RUNS使用内置的近似,其他模式均使用此设定的近似算法)。可取值如下:*CV_CHAIN_CODE:以Freeman链码的方式输出轮廓,所有其他方法输出多边形(顶点的序列)。*CHAIN_APPROX_NONE:将所有的连码点,转换成点。*CHAIN_APPROX_SIMPLE:压缩水平的、垂直的和斜的部分,也就是,函数只保留他们的终点部分。*CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS:使用the flavors of Teh-Chin chain近似算法的一种。*LINK_RUNS:通过连接水平段的1,使用完全不同的边缘提取算法。使用CV_RETR_LIST检索模式能使用此方法。*/int method = Imgproc.CHAIN_APPROX_SIMPLE;Vector<MatOfPoint> contours = new Vector<MatOfPoint>();Imgproc.findContours(source, contours, rs, mode, method, new Point());return contours;}
}

3.答题卡识别实现

controller

package com.acts.opencv.base;import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.TreeMap;
import java.util.Vector;import javax.servlet.http.HttpServletResponse;import org.apache.commons.lang3.StringUtils;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.highgui.Highgui;
import org.opencv.imgproc.Imgproc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;import com.acts.opencv.common.utils.Constants;
import com.acts.opencv.common.utils.RectComp;
import com.acts.opencv.common.web.BaseController;@Controller
@RequestMapping(value = "card")
public class CardController extends BaseController {private static final Logger logger = LoggerFactory.getLogger(CardController.class);/*** 答题卡识别* step1 高斯模糊* 创建者 Songer* 创建时间    2018年3月22日*/@RequestMapping(value = "step1")public void step1(HttpServletResponse response, String imagefile, Integer ksize) {System.loadLibrary(Core.NATIVE_LIBRARY_NAME);logger.info("\n 高斯模糊");String sourcePath = Constants.PATH + imagefile;logger.info("url==============" + sourcePath);// 加载为灰度图显示Mat source = Highgui.imread(sourcePath, Highgui.CV_LOAD_IMAGE_GRAYSCALE);Mat destination = new Mat(source.rows(), source.cols(), source.type());Imgproc.GaussianBlur(source, destination, new Size(2 * ksize + 1, 2 * ksize + 1), 0, 0);String destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "card1.png";File dstfile = new File(destPath);if (StringUtils.isNotBlank(destPath) && dstfile.isFile() && dstfile.exists()) {dstfile.delete();logger.info("删除图片:" + destPath);}Highgui.imwrite(destPath, destination);logger.info("生成目标图片==============" + destPath);renderString(response, Constants.DEST_IMAGE_PATH + "card1.png");}/*** 答题卡识别* step2 二值化,反向二值化* 创建者 Songer* 创建时间    2018年3月22日*/@RequestMapping(value = "step2")public void step2(HttpServletResponse response, String imagefile, Double thresh) {System.loadLibrary(Core.NATIVE_LIBRARY_NAME);logger.info("\n 二值化处理");// 灰度化// Imgproc.cvtColor(source, destination, Highgui.CV_LOAD_IMAGE_GRAYSCALE);String sourcePath = Constants.PATH + imagefile;logger.info("url==============" + sourcePath);// 加载为灰度图显示Mat source = Highgui.imread(sourcePath, Highgui.CV_LOAD_IMAGE_GRAYSCALE);Mat destination = new Mat(source.rows(), source.cols(), source.type());Imgproc.threshold(source, destination, thresh, 255, Imgproc.THRESH_BINARY_INV);String destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "card2.png";File dstfile = new File(destPath);if (StringUtils.isNotBlank(destPath) && dstfile.isFile() && dstfile.exists()) {dstfile.delete();logger.info("删除图片:" + destPath);}Highgui.imwrite(destPath, destination);logger.info("生成目标图片==============" + destPath);renderString(response, Constants.DEST_IMAGE_PATH + "card2.png");}/*** 答题卡识别* step3 膨胀腐蚀闭运算(针对反向二值图是开运算)* 创建者 Songer* 创建时间  2018年3月22日*/@RequestMapping(value = "step3")public void step3(HttpServletResponse response, String imagefile, Integer ksize) {System.loadLibrary(Core.NATIVE_LIBRARY_NAME);logger.info("\n 开运算");// 灰度化// Imgproc.cvtColor(source, destination, Highgui.CV_LOAD_IMAGE_GRAYSCALE);String sourcePath = Constants.PATH + imagefile;logger.info("url==============" + sourcePath);// 加载为灰度图显示Mat source = Highgui.imread(sourcePath, Highgui.CV_LOAD_IMAGE_GRAYSCALE);Mat destination = new Mat(source.rows(), source.cols(), source.type());Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(2 * ksize + 1, 2 * ksize + 1));Imgproc.morphologyEx(source, destination, Imgproc.MORPH_OPEN, element);String destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "card3.png";File dstfile = new File(destPath);if (StringUtils.isNotBlank(destPath) && dstfile.isFile() && dstfile.exists()) {dstfile.delete();logger.info("删除图片:" + destPath);}Highgui.imwrite(destPath, destination);logger.info("生成目标图片==============" + destPath);renderString(response, Constants.DEST_IMAGE_PATH + "card3.png");}/*** 答题卡识别* step4 轮廓识别* 创建者 Songer* 创建时间 2018年3月22日*/@RequestMapping(value = "step4")public void step4(HttpServletResponse response, String imagefile) {System.loadLibrary(Core.NATIVE_LIBRARY_NAME);logger.info("\n 轮廓识别");// 灰度化// Imgproc.cvtColor(source, destination, Highgui.CV_LOAD_IMAGE_GRAYSCALE);String sourcePath = Constants.PATH + imagefile;logger.info("url==============" + sourcePath);// 加载为灰度图显示Mat source = Highgui.imread(sourcePath, Highgui.CV_LOAD_IMAGE_GRAYSCALE);Highgui.imwrite("D:\\test\\abc\\source.png", source);//此处固定写死,取每一行选项,切割后进行轮廓识别Mat ch1 = source.submat(new Rect(170, 52, 294, 32));Mat ch2 = source.submat(new Rect(170, 104, 294, 32));Mat ch3 = source.submat(new Rect(170, 156, 294, 32));Mat ch4 = source.submat(new Rect(170, 208, 294, 32));Mat ch5 = source.submat(new Rect(170, 260, 294, 32));Mat ch6 = source.submat(new Rect(706, 50, 294, 32));Mat ch7 = source.submat(new Rect(706, 104, 294, 32));Mat ch8 = source.submat(new Rect(706, 156, 294, 32));Mat ch9 = source.submat(new Rect(706, 208, 294, 32));Mat ch10 = source.submat(new Rect(706, 260, 294, 32));Mat ch11 = source.submat(new Rect(1237, 50, 294, 32));Mat ch12 = source.submat(new Rect(1237, 104, 294, 32));Mat ch13 = source.submat(new Rect(1237, 156, 294, 32));Mat ch14 = source.submat(new Rect(1237, 208, 294, 32));Mat ch15 = source.submat(new Rect(1237, 260, 294, 32));Mat ch16 = source.submat(new Rect(1766, 50, 294, 32));Mat ch17 = source.submat(new Rect(1766, 104, 294, 32));Mat ch18 = source.submat(new Rect(1766, 156, 294, 32));Mat ch19 = source.submat(new Rect(1766, 208, 294, 32));Mat ch20 = source.submat(new Rect(1766, 260, 294, 32));Mat ch21 = source.submat(new Rect(170, 358, 294, 32));Mat ch22 = source.submat(new Rect(170, 410, 294, 32));Mat ch23 = source.submat(new Rect(170, 462, 294, 32));Mat ch24 = source.submat(new Rect(170, 514, 294, 32));Mat ch25 = source.submat(new Rect(170, 566, 294, 32));List<Mat> chlist = new ArrayList<Mat>();chlist.add(ch1);chlist.add(ch2);chlist.add(ch3);chlist.add(ch4);chlist.add(ch5);chlist.add(ch6);chlist.add(ch7);chlist.add(ch8);chlist.add(ch9);chlist.add(ch10);chlist.add(ch11);chlist.add(ch12);chlist.add(ch13);chlist.add(ch14);chlist.add(ch15);chlist.add(ch16);chlist.add(ch17);chlist.add(ch18);chlist.add(ch19);chlist.add(ch20);chlist.add(ch21);chlist.add(ch22);chlist.add(ch23);chlist.add(ch24);chlist.add(ch25);Mat hierarchy = new Mat();java.util.TreeMap<Integer,String> listenAnswer = new TreeMap<Integer,String>();for (int no=0;no<chlist.size();no++) {Vector<MatOfPoint> contours = new Vector<MatOfPoint>();Mat ch = chlist.get(no);Imgproc.findContours(ch, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE, new Point());Vector<RectComp> rectCompList = new Vector<RectComp>();for(int i = 0;i<contours.size();i++){MatOfPoint mop= contours.get(i);// 获取轮廓外矩,即使用最小矩形将轮廓包裹Rect rm = Imgproc.boundingRect(mop);RectComp rc = new RectComp(rm);rectCompList.add(rc);}// System.out.println(no+"size="+rectCompList.size());Collections.sort(rectCompList);// for(int t = 0;t<rectCompList.size();t++){// RectComp rect = rectCompList.get(t);// System.out.println(rect.getRm().area() + "--------" + rect.getRm().x);// if (rect.getRm().area() < 300) {// 小于300的pass,完美填图的话是≈1500// continue;// }// if (rect.getRm().x < 68) {// listenAnswer.put(Integer.valueOf(no), "A");// } else if ((rect.getRm().x > 68) && (rect.getRm().x < 148)) {// listenAnswer.put(Integer.valueOf(no), "B");// } else if ((rect.getRm().x > 148) && (rect.getRm().x < 228)) {// listenAnswer.put(Integer.valueOf(no), "C");// } else if (rect.getRm().x > 228) {// listenAnswer.put(Integer.valueOf(no), "D");// }// }// 因为已经按面积排序了,所以取第一个面积最大的轮廓即可RectComp rect = rectCompList.get(0);System.out.println(rect.getRm().area() + "--------" + rect.getRm().x);if (rect.getRm().area() > 300) {// 小于300的pass,说明未填写,完美填图的话是≈1500if (rect.getRm().x < 68) {listenAnswer.put(Integer.valueOf(no), "A");} else if ((rect.getRm().x > 68) && (rect.getRm().x < 148)) {listenAnswer.put(Integer.valueOf(no), "B");} else if ((rect.getRm().x > 148) && (rect.getRm().x < 228)) {listenAnswer.put(Integer.valueOf(no), "C");} else if (rect.getRm().x > 228) {listenAnswer.put(Integer.valueOf(no), "D");}} else {listenAnswer.put(Integer.valueOf(no), "未填写");}Mat result = new Mat(ch.size(), CvType.CV_8U, new Scalar(255));Imgproc.drawContours(result, contours, -1, new Scalar(0, 255, 0), 2);String destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "ch" + (no + 1) + ".png";File dstfile = new File(destPath);if (StringUtils.isNotBlank(destPath) && dstfile.isFile() && dstfile.exists()) {dstfile.delete();logger.info("删除图片:" + destPath);}Highgui.imwrite(destPath, result);logger.info("生成目标图片==============" + result);}String resultValue = "最终结果:试题编号-答案<br> ";for (Integer key : listenAnswer.keySet()) {resultValue += "【" + (key + 1) + ":" + listenAnswer.get(key) + "】";if ((key + 1) % 5 == 0) {resultValue += "<br>";}}renderString(response, resultValue);}}
package com.acts.opencv.base;import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;import javax.servlet.http.HttpServletResponse;import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfInt;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.highgui.Highgui;
import org.opencv.imgproc.Imgproc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;import com.acts.opencv.common.utils.Constants;
import com.acts.opencv.common.web.BaseController;@Controller
@RequestMapping(value = "card2")
public class Card2Controller extends BaseController {private static final Logger logger = LoggerFactory.getLogger(Card2Controller.class);/*** 答题卡识别* @Author Songer* @param response* @param imagefile void* @Date 2018年10月22日* 更新日志* 2018年10月22日 Songer  首次创建**/@RequestMapping(value = "cardMarking")public void cardMarking(HttpServletResponse response, String imagefile,Integer picno) {try {System.loadLibrary(Core.NATIVE_LIBRARY_NAME);long t1 = new Date().getTime();//String sourceimage1 = "D:\\test\\abc\\card\\test4.jpg";String sourceimage = Constants.PATH + imagefile;//表格检测,获取到表格内容,是查找识别区域的部分,返回的是校正之后的图像Mat mat = markingArea(sourceimage);String destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "cardResult_3.png";Highgui.imwrite(destPath, mat);
//          Highgui.imwrite("D:\\test\\abc\\card\\card111.png", mat);//具体的答题卡识别过程,主要就是答案识别部分了String result = cardResult(mat);renderString(response, result);long t2 = new Date().getTime();logger.info("===耗时"+(t2-t1));} catch (Exception e) {e.printStackTrace();logger.error("答题卡识别异常!", e);}}/*** 此方法主要是通过边缘检测凸包,查找识别区域。即客观题的框* @Author Songer* @Date 2018年9月21日* 更新日志* 2018年9月21日 Songer  首次创建**/public static Mat markingArea(String path){Mat source = Highgui.imread(path, Highgui.CV_LOAD_IMAGE_COLOR);Mat img = new Mat();;Mat result= source.clone();// 彩色转灰度Imgproc.cvtColor(source, img, Imgproc.COLOR_BGR2GRAY);//此处图像预处理,可以使用方式1也可以使用方式2都可以,自己也可以测试下2种方式的差异
//          //方式1:通过高斯滤波然后边缘检测膨胀来链接边缘,将轮廓连通便于轮廓识别
//          // 高斯滤波,降噪
//          Imgproc.GaussianBlur(img, img, new Size(3,3), 2, 2);
//          // Canny边缘检测
//          Imgproc.Canny(img, img, 20, 60, 3, false);
//          // 膨胀,连接边缘
//          Imgproc.dilate(img, img, new Mat(), new Point(-1,-1), 3, 1, new Scalar(1));
//          //方式2:使用形态学梯度算法,此算法来保留物体的边缘轮廓很有效果Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5,5));Imgproc.morphologyEx(img, img, Imgproc.MORPH_GRADIENT, element);//图像二值化,使用的是OTSU二值化,阈值为170,也可以使用自适用二值化
//          Imgproc.adaptiveThreshold(img,img, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C,  Imgproc.THRESH_BINARY_INV, 51, 10);Imgproc.threshold(img,img, 170, 255, Imgproc.THRESH_BINARY|Imgproc.THRESH_OTSU);String destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "cardResult_1.png";Highgui.imwrite(destPath, img);
//          Highgui.imwrite("D:\\test\\abc\\card\\card1.png", img);List<MatOfPoint> contours = new ArrayList<>();Mat hierarchy = new Mat();//轮廓查找,主要就是找最外表格框Imgproc.findContours(img, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);// 找出轮廓对应凸包的四边形拟合List<MatOfPoint> squares = new ArrayList<>();List<MatOfPoint> hulls = new ArrayList<>();MatOfInt hull = new MatOfInt();MatOfPoint2f approx = new MatOfPoint2f();approx.convertTo(approx, CvType.CV_32F);for (MatOfPoint contour: contours) {// 边框的凸包Imgproc.convexHull(contour, hull);// 用凸包计算出新的轮廓点Point[] contourPoints = contour.toArray();int[] indices = hull.toArray();List<Point> newPoints = new ArrayList<>();for (int index : indices) {newPoints.add(contourPoints[index]);}MatOfPoint2f contourHull = new MatOfPoint2f();contourHull.fromList(newPoints);// 多边形拟合凸包边框(此时的拟合的精度较低)Imgproc.approxPolyDP(contourHull, approx, Imgproc.arcLength(contourHull, true)*0.02, true);// 筛选出面积大于某一阈值的,且四边形的各个角度都接近直角的凸四边形MatOfPoint approxf1 = new MatOfPoint();approx.convertTo(approxf1, CvType.CV_32S);//此处是筛选表格框,面积大于40000if (approx.rows() == 4 && Math.abs(Imgproc.contourArea(approx)) > 40000 &&Imgproc.isContourConvex(approxf1)) {double maxCosine = 0;for (int j = 2; j < 5; j++) {double cosine = Math.abs(getAngle(approxf1.toArray()[j%4], approxf1.toArray()[j-2], approxf1.toArray()[j-1]));maxCosine = Math.max(maxCosine, cosine);}// 考虑到图片倾斜等情况,角度大概72度if (maxCosine < 0.3) {MatOfPoint tmp = new MatOfPoint();contourHull.convertTo(tmp, CvType.CV_32S);squares.add(approxf1);hulls.add(tmp);}}}// 找出外接矩形最大的四边形int index = findLargestSquare(squares);MatOfPoint largest_square = squares.get(index);if (largest_square.rows() == 0 || largest_square.cols() == 0)return result;// 找到这个最大的四边形对应的凸边框,再次进行多边形拟合,此次精度较高,拟合的结果可能是大于4条边的多边形MatOfPoint contourHull = hulls.get(index);MatOfPoint2f tmp = new MatOfPoint2f();contourHull.convertTo(tmp, CvType.CV_32F);Imgproc.approxPolyDP(tmp, approx, 3, true);List<Point> newPointList = new ArrayList<>();double maxL = Imgproc.arcLength(approx, true) * 0.02;// 找到高精度拟合时得到的顶点中 距离小于低精度拟合得到的四个顶点maxL的顶点,排除部分顶点的干扰for (Point p : approx.toArray()) {if (!(getSpacePointToPoint(p, largest_square.toList().get(0)) > maxL &&getSpacePointToPoint(p, largest_square.toList().get(1)) > maxL &&getSpacePointToPoint(p, largest_square.toList().get(2)) > maxL &&getSpacePointToPoint(p, largest_square.toList().get(3)) > maxL)) {newPointList.add(p);}}// 找到剩余顶点连线中,边长大于 2 * maxL的四条边作为四边形物体的四条边List<double[]> lines = new ArrayList<>();for (int i = 0; i < newPointList.size(); i++) {Point p1 = newPointList.get(i);Point p2 = newPointList.get((i+1) % newPointList.size());if (getSpacePointToPoint(p1, p2) > 2 * maxL) {lines.add(new double[]{p1.x, p1.y, p2.x, p2.y});logger.info("p1x:"+p1.x+"  p1y:"+p1.y+"  p2x:"+p2.x+"  p2y:"+p2.y);//画出4条边线,真正识别过程中这些都是可以注释掉的,只是为了方便观察Core.line(source, new Point(p1.x, p1.y), new Point( p2.x,  p2.y), new Scalar(255, 0, 0),4);}}destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "cardResult_2.png";Highgui.imwrite(destPath, source);
//          Highgui.imwrite("D:\\test\\abc\\card\\card2.png", source);// 计算出这四条边中 相邻两条边的交点,即物体的四个顶点List<Point> corners = new ArrayList<>();for (int i = 0; i < lines.size(); i++) {Point corner = computeIntersect(lines.get(i),lines.get((i+1) % lines.size()));corners.add(corner);}// 对顶点顺时针排序sortCorners(corners);// 计算目标图像的尺寸Point p0 = corners.get(0);Point p1 = corners.get(1);Point p2 = corners.get(2);Point p3 = corners.get(3);logger.info("   "+p0.x+"   "+p0.y);logger.info("   "+p1.x+"   "+p1.y);logger.info("   "+p2.x+"   "+p2.y);logger.info("   "+p3.x+"   "+p3.y);double space0 = getSpacePointToPoint(p0, p1);double space1 = getSpacePointToPoint(p1, p2);double space2 = getSpacePointToPoint(p2, p3);double space3 = getSpacePointToPoint(p3, p0);// 使用最宽和最长的边作为进行图像矫正目标图像的长宽double imgWidth = space1 > space3 ? space1 : space3;double imgHeight = space0 > space2 ? space0 : space2;logger.info("imgWidth:"+imgWidth+"    imgHeight:"+imgHeight);// 如果提取出的图片宽小于高,则旋转90度,因为示例中的矩形框是宽>高的,如果宽小于高应该是图片旋转了if (imgWidth > imgHeight) {logger.info("----in");double temp = imgWidth;imgWidth = imgHeight;imgHeight = temp;Point tempPoint = p0.clone();p0 = p1.clone();p1 = p2.clone();p2 = p3.clone();p3 = tempPoint.clone();}//          Mat quad = Mat.zeros((int)imgHeight * 2, (int)imgWidth * 2, CvType.CV_8UC3);Mat quad = Mat.zeros((int)imgHeight, (int)imgWidth, CvType.CV_8UC3);MatOfPoint2f cornerMat = new MatOfPoint2f(p0, p1, p2, p3);
//          MatOfPoint2f quadMat = new MatOfPoint2f(new Point(imgWidth*0.4, imgHeight*1.6),
//                  new Point(imgWidth*0.4, imgHeight*0.4),
//                  new Point(imgWidth*1.6, imgHeight*0.4),
//                  new Point(imgWidth*1.6, imgHeight*1.6));//quadMat目标图像的点设置,以之前取出的最长的长宽作为新图像的长宽,创建一个图层MatOfPoint2f quadMat = new MatOfPoint2f(new Point(0, 0),new Point(imgWidth, 0),new Point(imgWidth, imgHeight),new Point(0, imgHeight));// 提取图像,使用warpPerspective做图像的透视变换Mat transmtx = Imgproc.getPerspectiveTransform(cornerMat, quadMat);Imgproc.warpPerspective(result, quad, transmtx, quad.size());return quad;}// 根据三个点计算中间那个点的夹角   pt1 pt0 pt2private static double getAngle(Point pt1, Point pt2, Point pt0){double dx1 = pt1.x - pt0.x;double dy1 = pt1.y - pt0.y;double dx2 = pt2.x - pt0.x;double dy2 = pt2.y - pt0.y;return (dx1*dx2 + dy1*dy2)/Math.sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10);}// 找到最大的正方形轮廓private static int findLargestSquare(List<MatOfPoint> squares) {if (squares.size() == 0)return -1;int max_width = 0;int max_height = 0;int max_square_idx = 0;int currentIndex = 0;for (MatOfPoint square : squares) {Rect rectangle = Imgproc.boundingRect(square);if (rectangle.width >= max_width && rectangle.height >= max_height) {max_width = rectangle.width;max_height = rectangle.height;max_square_idx = currentIndex;}currentIndex++;}return max_square_idx;}// 点到点的距离private static double getSpacePointToPoint(Point p1, Point p2) {double a = p1.x - p2.x;double b = p1.y - p2.y;return Math.sqrt(a * a + b * b);}// 两直线的交点private static Point computeIntersect(double[] a, double[] b) {if (a.length != 4 || b.length != 4)throw new ClassFormatError();double x1 = a[0], y1 = a[1], x2 = a[2], y2 = a[3], x3 = b[0], y3 = b[1], x4 = b[2], y4 = b[3];double d = ((x1 - x2) * (y3 - y4)) - ((y1 - y2) * (x3 - x4));if (d != 0) {Point pt = new Point();pt.x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d;pt.y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d;return pt;}elsereturn new Point(-1, -1);}// 对多个点按顺时针排序private static void sortCorners(List<Point> corners) {if (corners.size() == 0) return;Point p1 = corners.get(0);int index = 0;for (int i = 1; i < corners.size(); i++) {Point point = corners.get(i);if (p1.x > point.x) {p1 = point;index = i;}}corners.set(index, corners.get(0));corners.set(0, p1);Point lp = corners.get(0);for (int i = 1; i < corners.size(); i++) {for (int j = i + 1; j < corners.size(); j++) {Point point1 = corners.get(i);Point point2 = corners.get(j);if ((point1.y-lp.y*1.0)/(point1.x-lp.x)>(point2.y-lp.y*1.0)/(point2.x-lp.x)) {Point temp = point1.clone();corners.set(i, corners.get(j));corners.set(j, temp);}}}}/*** 答题卡识别,增加注释* 已得到矫正后的图像后,进行后续答案识别算法过程* @Author Songer* @param mat* @return String* @Date 2018年12月19日* 更新日志* 2018年12月19日 王嵩  首次创建**/public String cardResult(Mat mat){//设置剪切的边距,目的是裁剪表格边框,防止边框影响轮廓查找,这里设置为20像素int cutsize = 20;Mat img_cut = mat.submat(cutsize,mat.rows()-cutsize,cutsize,mat.cols()-cutsize);new Mat();Mat img_gray = img_cut.clone();//图像灰度化Imgproc.cvtColor(img_cut, img_gray, Imgproc.COLOR_BGR2GRAY);//图像二值化,注意是反向二值化以及OTSU算法Imgproc.threshold(img_gray,img_gray, 170, 255, Imgproc.THRESH_BINARY_INV|Imgproc.THRESH_OTSU);Mat temp = img_gray.clone();//此处使用的是方式2,形态学梯度算法保留填图选项的边框
//          //方式1:通过高斯滤波然后边缘检测膨胀来链接边缘,将轮廓连通便于轮廓识别
//          // 高斯滤波,降噪
//          Imgproc.GaussianBlur(temp, temp, new Size(3,3), 2, 2);
//          // Canny边缘检测
//          Imgproc.Canny(temp, temp, 20, 60, 3, false);
//          // 膨胀,连接边缘
//          Imgproc.dilate(temp, temp, new Mat(), new Point(-1,-1), 3, 1, new Scalar(1));//方式2:使用形态学梯度算法,此算法来保留物体的边缘轮廓很有效果Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5,5));Imgproc.morphologyEx(temp, temp, Imgproc.MORPH_GRADIENT, element);Imgproc.threshold(temp, temp, 170, 255, Imgproc.THRESH_BINARY|Imgproc.THRESH_OTSU);String destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "cardResult_4.png";Highgui.imwrite(destPath, temp);
//       Highgui.imwrite("D:\\test\\abc\\card\\card3.png", temp);//按比例截取,此处是根据原答题卡的各列的比例截取成4列,学号那列还要横向分隔一下,上半部分是学号下半部分是答题卡Mat cut1 = temp.submat(0,temp.rows(),0,(int)(0.275*temp.cols()));Mat cut_gray1 = img_gray.submat(0,img_gray.rows(),0,(int)(0.275*img_gray.cols()));Mat cut2 = temp.submat(0,temp.rows(),(int)(0.275*temp.cols()),(int)(0.518*temp.cols()));Mat cut_gray2 = img_gray.submat(0,img_gray.rows(),(int)(0.275*img_gray.cols()),(int)(0.518*img_gray.cols()));Mat cut3 = temp.submat(0,temp.rows(),(int)(0.518*temp.cols()),(int)(0.743*temp.cols()));Mat cut_gray3 = img_gray.submat(0,img_gray.rows(),(int)(0.518*img_gray.cols()),(int)(0.743*img_gray.cols()));Mat cut4 = temp.submat((int)(0.387*temp.rows()),temp.rows(),(int)(0.743*temp.cols()),temp.cols());Mat cut_gray4 = img_gray.submat((int)(0.387*img_gray.rows()),img_gray.rows(),(int)(0.743*img_gray.cols()),img_gray.cols());//学号Mat cut5 = temp.submat(0,(int)(0.387*temp.rows()),(int)(0.743*temp.cols()),temp.cols());Mat cut_gray5 = img_gray.submat(0,(int)(0.387*img_gray.rows()),(int)(0.743*img_gray.cols()),img_gray.cols());//         Highgui.imwrite("D:\\test\\abc\\card\\card_cut1.png", cut1);
//       Highgui.imwrite("D:\\test\\abc\\card\\card_cut1_gary.png", cut_gray1);
//       Highgui.imwrite("D:\\test\\abc\\card\\card_cut2.png", cut2);
//       Highgui.imwrite("D:\\test\\abc\\card\\card_cut3.png", cut3);
//       Highgui.imwrite("D:\\test\\abc\\card\\card_cut4.png", cut4);
//       Highgui.imwrite("D:\\test\\abc\\card\\card_cut5.png", cut5);List<String> resultList = new ArrayList<String>();//按列处理List<String> list1 = processByCol(cut1,cut_gray1,img_cut,5);List<String> list2 = processByCol(cut2,cut_gray2,img_cut,5);List<String> list3 = processByCol(cut3,cut_gray3,img_cut,4);List<String> list4 = processByCol(cut4,cut_gray4,img_cut,4);//学号单独处理List<String> list5 = processByCol(cut5,cut_gray5,img_cut,4);resultList.addAll(list1);resultList.addAll(list2);resultList.addAll(list3);resultList.addAll(list4);String studentNo = getStudentNo(list5);logger.info("学生学号为:"+studentNo);StringBuffer result = new StringBuffer("学生学号为:"+studentNo);for (int i = 0; i < resultList.size(); i++) {result.append(resultList.get(i));logger.info(resultList.get(i));}return result.toString();}/*** 根据列单独处理* @Author Songer* @param cut1 传入的答案列,一般1-20一列* @param cut_gary 传入的答案列未处理过* @param temp 表格范围mat* @param toIndex 列答案数,即每几个答案一组* @Date 2018年9月20日* 更新日志* 2018年9月20日 Songer  首次创建**/private static List<String> processByCol(Mat cut1,Mat cut_gray,Mat temp,int answerCols) {List<String> result = new ArrayList<String>();List<MatOfPoint> contours = new ArrayList<MatOfPoint>();List<MatOfPoint> answerList = new ArrayList<MatOfPoint>();Mat hierarchy = new Mat();//进行轮廓查找,注意因为该答题卡特征是闭合填图区域,所以用这种方式,如果是非闭合填涂区域,则不适用Imgproc.findContours(cut1.clone(), contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
//       logger.info(contours.size());
//       logger.info("-----w------"+(temp.width()*80/2693-20));
//       logger.info("-----h------"+(temp.height()*80/2764-20));for(int i = 0;i<contours.size();i++){MatOfPoint mop= contours.get(i);//每个轮廓给出外包矩形,便于操作Rect rect = Imgproc.boundingRect(mop);
//          logger.info("-----------"+rect.width+" "+rect.height);//一个填图区域大概占整个表格的w:80/2733 h:0/2804,,所以排除掉太小的轮廓和过大的轮廓
//          //绘制每个轮廓图
//          Imgproc.drawContours(cut1, contours, i, new Scalar(170), 2);//此处是为了排除杂点,较小或较大的轮廓都是非填图选项,可以排除,可以按实际情况灵活变动限制条件,最好输出出轮廓便于观察if(rect.width>(temp.width()*80/2693-20) && rect.height>(temp.height()*80/2764-20) && rect.width<temp.width()*0.05 && rect.height<temp.height()*0.05){
//          if(rect.width>50&&rect.height>50&&rect.area()>2500&&rect.area()<10000){
//              Core.rectangle(img_cut, new Point(rect.x, rect.y), new Point(rect.x + rect.width, rect.y
//                      + rect.height), new Scalar(0, 255, 0), 2);answerList.add(mop);}}Collections.sort(answerList, new Comparator<MatOfPoint>() {//按照y坐标升序排列@Overridepublic int compare(MatOfPoint o1, MatOfPoint o2) {Rect rect1 = Imgproc.boundingRect(o1);Rect rect2 = Imgproc.boundingRect(o2);if(rect1.y<rect2.y){return -1;}else if(rect1.y>rect2.y){return 1;}else{return 0;}}});//每4/5个一组组成新list,并按x坐标排序//该方式依赖于预处理后的图片没有干扰轮廓。如果图像处理不佳,干扰项没排除,那么此处的分组就会错乱。//提供一个优化思路:对于闭合填涂区域的可以使用水平垂直投影进行坐标定位点的判定。int queno = 1;int totoSize = answerList.size();for (int i = 0; i < totoSize; i+=answerCols) {int toIndex = i+answerCols;if(toIndex>totoSize){toIndex = totoSize-1;}List<MatOfPoint> newList = answerList.subList(i,toIndex);Collections.sort(newList, new Comparator<MatOfPoint>() {//按照x坐标升序排列@Overridepublic int compare(MatOfPoint o1, MatOfPoint o2) {Rect rect1 = Imgproc.boundingRect(o1);Rect rect2 = Imgproc.boundingRect(o2);if(rect1.x<rect2.x){return -1;}else if(rect1.x>rect2.x){return 1;}else{return 0;}}});String resultChoose = "";for (int j = 0; j < newList.size(); j++) {Imgproc.drawContours(cut_gray, newList, j, new Scalar(170), 2);//掩模提取出轮廓Mat mask = Mat.zeros(cut_gray.size(), CvType.CV_8UC1);//绘制轮廓便于观察Imgproc.drawContours(mask, newList, j, new Scalar(255), -1);Mat dst = new Mat();Core.bitwise_and(cut_gray, mask, dst);//获取填涂百分比,填涂区域的二值化后取出非0点/掩模的轮廓面积double p100 = Core.countNonZero(dst) * 100 / Core.countNonZero(mask);String anno = index2ColName(j);//认为非0像素超过80%的算是填涂if(p100>80){resultChoose += anno;logger.info(p100+" 第"+queno+"行:选项("+anno+")填涂");}else{logger.info(p100+" 第"+queno+"行:选项("+anno+")未填涂");}
//              Highgui.imwrite("D:\\test\\abc\\card\\card_x"+i+j+".png", dst);if(i==0&&j==0){//输出一下第一个掩模String destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "cardResult_5.png";Highgui.imwrite(destPath, dst);}}result.add(resultChoose);queno++;}//       Highgui.imwrite("D:\\test\\abc\\card\\card4.png", cut1);return result;}//编号转答案0-A 1-Bpublic static String index2ColName(int index){if (index < 0) {return null;}int num = 65;// A的Unicode码String colName = "";do {if (colName.length() > 0) {index--;}int remainder = index % 26;colName = ((char) (remainder + num)) + colName;index = (int) ((index - remainder) / 26);} while (index > 0);return colName;}/**根据表元的列名转换为列号* @param colName 列名, 从A开始* @return A1->0; B1->1...AA1->26*/public static int colName2Index(String colName) {int index = -1;int num = 65;// A的Unicode码int length = colName.length();for (int i = 0; i < length; i++) {char c = colName.charAt(i);if (Character.isDigit(c)) break;// 确定指定的char值是否为数字index = (index + 1) * 26 + (int)c - num;}return index;}/*** 根据返回的结果集转换为学号* 因为学号部分的处理跟答案一样是一横行进行处理的,同时返回的是ABCDE等选项结果* 转换公式为:学生学号=遍历list的index值*(A=1000,B=100,C=10,D=1)相加* @Author Songer* @param resultList* @return String* @Date 2018年9月20日* 更新日志* 2018年9月20日 Songer  首次创建**/public static String getStudentNo(List<String> resultList){int studentNo = 0;for (int i = 0; i < resultList.size(); i++) {String result = resultList.get(i);if (result.contains("A")) {studentNo += 1000 * i;} if (result.contains("B")) {studentNo += 100 * i;} if (result.contains("C")) {studentNo += 10 * i;}if (result.contains("D")) {studentNo += i;}}NumberFormat formatter = new DecimalFormat("0000");String number = formatter.format(studentNo);return number;}}
/*** Copyright &copy; 2016-2020 公众学业 All rights reserved.*/
package com.acts.opencv.common.web;import java.beans.PropertyEditorSupport;
import java.io.IOException;
import java.io.PrintWriter;import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;import org.apache.commons.lang3.StringEscapeUtils;
import org.springframework.http.MediaType;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;import com.acts.opencv.common.mapper.JsonMapper;
import com.acts.opencv.common.utils.Constants;/*** 控制器支持类* 创建者  Songer* 创建时间    2016年7月21日**/
public abstract class BaseController{/*** 添加Model消息* @param message*/protected void addMessage(Model model, String... messages) {StringBuilder sb = new StringBuilder();for (String message : messages){sb.append(message).append(messages.length>1?"<br/>":"");}model.addAttribute("message", sb.toString());}/*** 添加Flash消息* @param message*/protected void addMessage(RedirectAttributes redirectAttributes, String... messages) {StringBuilder sb = new StringBuilder();for (String message : messages){sb.append(message).append(messages.length>1?"<br/>":"");}redirectAttributes.addFlashAttribute("message", sb.toString());}/*** 客户端返回JSON字符串* @param response* @param string* @return*/protected void renderString(HttpServletResponse response, Object object) {try {response.reset();response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);response.setHeader("Cache-Control", "no-cache, must-revalidate");PrintWriter writer = response.getWriter();writer.write(JsonMapper.toJsonString(object));writer.flush();writer.close();} catch (IOException e) {e.printStackTrace();}}/*** 客户端返回图片类型* @param response* @param object void* @throws IOException* @Date 2018年3月13日* 更新日志* 2018年3月13日 Songer  首次创建**/protected void renderImage(HttpServletResponse response, byte[] object) {try {response.reset();response.setContentType("image/*");ServletOutputStream output = response.getOutputStream();output.flush();output.write(object);output.close();// ServletOutputStream output = response.getOutputStream();// FileInputStream fis = new FileInputStream("E:\\tomcat7\\webapps\\java_opencv\\statics\\distimage\\lena.png");// byte[] buffer = new byte[1024];// int i = -1;// while ((i = fis.read(buffer)) != -1) {// output.write(buffer, 0, i);// }// output.flush();// output.close();// fis.close();} catch (IOException e) {// 如果是ClientAbortException异常,可以不用管,原因是页面参数变化太快,response请求被中断try {// response.reset();PrintWriter writer = response.getWriter();response.setContentType("text/html;charset=utf-8");writer.write("无法打开图片!");writer.close();} catch (IOException e1) {e1.printStackTrace();}e.printStackTrace();}}/*** 客户端返回字符串* @param response* @param string* @return*/protected void renderString(HttpServletResponse response) {renderString(response, Constants.SUCCESS);}/*** 初始化数据绑定* 1. 将所有传递进来的String进行HTML编码,防止XSS攻击* 2. 将字段中Date类型转换为String类型*/@InitBinderprotected void initBinder(WebDataBinder binder) {// String类型转换,将所有传递进来的String进行HTML编码,防止XSS攻击binder.registerCustomEditor(String.class, new PropertyEditorSupport() {@Overridepublic void setAsText(String text) {setValue(text == null ? null : StringEscapeUtils.escapeHtml4(text.trim()));}@Overridepublic String getAsText() {Object value = getValue();return value != null ? value.toString() : "";}});}}

4.其他工具类

package com.acts.opencv.common.utils;import org.springframework.web.context.ContextLoader;/*** 常量 创建者 Songer 创建时间 2018年3月09日**/
public class Constants {public static final String CURRENT_USER = "UserInfo";public static final String WECHAT_USER = "weChatUserInfo";public static final String REFERENCE_CODE = "referenceCode";public static final String SUCCESS = "success";public static final String ERROR = "error";public static final String SF_FILE_SEPARATOR = System.getProperty("file.separator");// 文件分隔符public static final String SF_LINE_SEPARATOR = System.getProperty("line.separator");// 行分隔符public static final String SF_PATH_SEPARATOR = System.getProperty("path.separator");// 路径分隔符public static final String PATH = ContextLoader.getCurrentWebApplicationContext().getServletContext().getRealPath("/");/*** 文件*/public static final String SOURCE_IMAGE_PATH = Constants.SF_FILE_SEPARATOR + "statics"+ Constants.SF_FILE_SEPARATOR + "sourceimage" + Constants.SF_FILE_SEPARATOR;// 图片原地址public static final String DEST_IMAGE_PATH = Constants.SF_FILE_SEPARATOR + "statics" + Constants.SF_FILE_SEPARATOR+ "destimage" + Constants.SF_FILE_SEPARATOR;// 图片生成地址/*** 返回参数规范*//** 区分类型 1 -- 无错误,Code重复 */public static final String CODE_DUPLICATE = "1";/** 区分类型 2 -- 无错误,名称重复 */public static final String NAME_DUPLICATE = "2";/** 区分类型 3 -- 数量超出 */public static final String NUMBER_OVER = "3";/** 区分类型 0 -- 无错误,程序正常执行 */public static final String NO_ERROR = "0";/** 区分类型 -1 -- 无错误,返回结果为空 */public static final String NULL_POINTER = "-1";/** 区分类型 -2 -- 错误,参数不正确 */public static final String INCORRECT_PARAMETER = "-2";/** 区分类型 -3 -- 错误,程序执行错误 */public static final String PROGRAM_EXECUTION_ERROR = "-3";/** 区分类型 -5 -- 错误,数据已删除 */public static final String DATA_DELETED = "-5";/** 区分类型 -6 -- 错误,参数不一致(验证码) */public static final String DATA_NOT_SAME = "-6";/**json文件缺失  */public static final String NO_JSON_FILE = "-7";/*** 分页中可能用到的常量*/public static final Integer PAGE_SIZE=10;//一页共有十条内容}
package com.acts.opencv.common.utils;import org.opencv.core.Rect;public class RectComp implements Comparable<Object> {private Rect rm;public Rect getRm() {return rm;}public void setRm(Rect rm) {this.rm = rm;}public RectComp() {super();}public RectComp(Rect rm) {super();this.rm = rm;}// @Override// public int compareTo(Object object) {// if(this == object){// return 0;// } else if (object != null && object instanceof RectComp) {// RectComp rect = (RectComp) object;// if (rm.x <= rect.rm.x) {// return -1;// }else{// return 1;// }// }else{// return -1;// }// }@Override// 按面积排序,最大的放第一个public int compareTo(Object object) {if(this == object){return 0;} else if (object != null && object instanceof RectComp) {RectComp rect = (RectComp) object;if (rm.area() >= rect.rm.area()) {return -1;} else {return 1;}} else {return -1;}}}

图像识别——(java使用opencv答题卡识别)相关推荐

  1. opencv答题卡识别 (一)

    背景:答题卡阅卷需要光标阅读机,有些小学校买不起光标阅读机. 主要开源库:opencv,版本3.0. 识别原理:把答题卡放在深色背景中,用查找轮廓定位好答题卡位置,用透视变换取出答题卡图像,根据位置判 ...

  2. OpenCV 答题卡识别

    1.预处理.轮廓检测 import cv2 import numpy as np # 正确答案 ANSWER_KEY = {0:1,1:4,2:0,3:3,4:1} def cv_show(name, ...

  3. python OpenCV 答题卡识别判卷

    完整代码: #导入工具包 import numpy as np import argparse import imutils import cv2# 设置参数 ap = argparse.Argume ...

  4. 基于 SpringMvc + OpenCV 实现的答题卡识别系统(附源码)

    点击关注公众号,实用技术文章及时了解 java_opencv 项目介绍 OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,它提供了一系列图像处理和计算机视觉方面很多通用算法.是研究图像 ...

  5. 用Python+OpenCV+PyQt开发的答题卡识别软件

    用Python+OpenCV+PyQt开发的答题卡识别软件 软件使用说明 软件设计思路 如何设置答案 界面风格 备注 这是一个可以识别定制答题卡的软件,它可以根据用户自定的答案来进行识别,校对正误并统 ...

  6. 基于 SpringMvc+OpenCV 实现的答题卡识别系统(附源码)

    java_opencv 项目介绍 OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,它提供了一系列图像处理和计算机视觉方面很多通用算法.是研究图像处理技术的一个很不错的工具.最初开始接 ...

  7. OpenCV C++案例实战五《答题卡识别》

    OpenCV C++案例实战五<答题卡识别> 前言 一.图像矫正 1.源码 二.获取选项区域 1.扣出每题选项 2.源码 三.获取答案 1.思路 2.辅助函数 3.源码 4.效果 总结 前 ...

  8. opencv图像处理—项目实战:答题卡识别判卷

    哔站唐宇迪opencv课程--项目实战:答题卡识别判卷 [计算机视觉-OpenCV]唐宇迪博士教会了我大学四年没学会的OpenCV OpenCV计算机视觉实战全套课程(附带课程课件资料+课件笔记+源码 ...

  9. 答题卡识别任务--opencv python(附代码)

    答题卡识别 项目理论和源码来自唐宇迪opencv项目实战 记一篇python-opencv 完成答题卡识别 项目的学习笔记 输入一张特定格式的答题卡图片(答题卡中题目数量和选项个数是固定的),能够输出 ...

最新文章

  1. java 文件md5校验_Java 获取 文件md5校验码
  2. java http 401_服务器返回HTTP响应代码:401,URL:https
  3. 一元三次方程求解matlab_初中数学最全函数/方程【实际应用题】分类详解提升必学必练!...
  4. linux kvm百度云,如何在 Ubuntu Linux 上使用 KVM 云镜像
  5. 如何阵列平面_Proe/Creo如何使用点阵列——通过内部草绘创建
  6. 信息提醒之Notification,兼容全部SDK-更新中
  7. 关于word中公式和图片对齐的简易设置
  8. shell 脚本逻辑判断
  9. 模块化加载_Java9模块化的类加载机制实现剖析
  10. nssl1470-X【并查集,素数】
  11. 在线颜色拾取器 - 资源篇
  12. 草稿 ktv 航版 1211 rs ga 打开文件控件 文件的复制操作
  13. java零碎要点---用java实现生成二维码,与解析代码实现
  14. 《C++ Primer Plus(第6版)中文版》——1.2 C++简史
  15. AndroidOpenCV摄像头预览全屏问题
  16. Cocos2dx-lua组件tableView的简单用法
  17. matlab结构体、数组和单元数组类型的创建
  18. java求闰年_JAVA中怎么计算闰年
  19. [Mysql] STR_TO_DATE函数
  20. 数据分析需要学习的技能有哪些?

热门文章

  1. 手机录屏怎样只录手机内部声音不录入外部声音?教你三种方法,一定能帮到你
  2. SQLserver 索引碎片
  3. CSS伪类(Pseudo-classes)、伪元素、伪类选择器
  4. 【C/C++】程序环境,探索程序的执行过程(习得无上内功《易筋经》的第一步)
  5. 24V输入防反接电路
  6. 英语敢死队 第三周学习总结感受
  7. 北京密云区携手锐捷打造新一代教育城域网 为互联网+教育“开山铺路”
  8. C/C++杂志订阅管理系统[2022-12-31]
  9. popcap地图卷动
  10. 用SHELL调度ORACLE存储过程