diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/src/main/java/com/acts/opencv/base/Card2Controller.java b/src/main/java/com/acts/opencv/base/Card2Controller.java new file mode 100644 index 0000000..4d8d6a0 --- /dev/null +++ b/src/main/java/com/acts/opencv/base/Card2Controller.java @@ -0,0 +1,584 @@ +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:通过高斯滤波然后边缘检测膨胀来链接边缘,将轮廓连通便于轮廓识别 +// // 高斯滤波,降噪 +// 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); + 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 contours = new ArrayList<>(); + Mat hierarchy = new Mat(); + Imgproc.findContours(img, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE); + + + + // 找出轮廓对应凸包的四边形拟合 + List squares = new ArrayList<>(); + List 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 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); + if (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 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 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); + 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 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)); + MatOfPoint2f quadMat = new MatOfPoint2f(new Point(0, 0), + new Point(imgWidth, 0), + new Point(imgWidth, imgHeight), + new Point(0, imgHeight)); + + // 提取图像 + Mat transmtx = Imgproc.getPerspectiveTransform(cornerMat, quadMat); + Imgproc.warpPerspective(result, quad, transmtx, quad.size()); + return quad; + + } + + // 根据三个点计算中间那个点的夹角 pt1 pt0 pt2 + private 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 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; + } + else + return new Point(-1, -1); + } + + // 对多个点按顺时针排序 + private static void sortCorners(List 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); + } + } + } + } + + + //答题卡识别 + public String cardResult(Mat mat){ + 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); + Imgproc.threshold(img_gray,img_gray, 170, 255, Imgproc.THRESH_BINARY_INV|Imgproc.THRESH_OTSU); + Mat temp = img_gray.clone(); +// //方式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); + +// logger.info(temp.cols()); + 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 resultList = new ArrayList(); + List list1 = processByCol(cut1,cut_gray1,img_cut,5); + List list2 = processByCol(cut2,cut_gray2,img_cut,5); + List list3 = processByCol(cut3,cut_gray3,img_cut,4); + List list4 = processByCol(cut4,cut_gray4,img_cut,4); + //学号单独处理 + List 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 processByCol(Mat cut1,Mat cut_gray,Mat temp,int answerCols) { + List result = new ArrayList(); + List contours = new ArrayList(); + List answerList = new ArrayList(); + 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(temp.width()*80/2693-20) && rect.height>(temp.height()*80/2764-20) && rect.width50&&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() { + //按照y坐标升序排列 + @Override + public int compare(MatOfPoint o1, MatOfPoint o2) { + Rect rect1 = Imgproc.boundingRect(o1); + Rect rect2 = Imgproc.boundingRect(o2); + if(rect1.yrect2.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 newList = answerList.subList(i,toIndex); + Collections.sort(newList, new Comparator() { + //按照y坐标升序排列 + @Override + public int compare(MatOfPoint o1, MatOfPoint o2) { + Rect rect1 = Imgproc.boundingRect(o1); + Rect rect2 = Imgproc.boundingRect(o2); + if(rect1.xrect2.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); + //获取填涂百分比 + double p100 = Core.countNonZero(dst) * 100 / Core.countNonZero(mask); + String anno = index2ColName(j); + 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-B + public 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 resultList){ + int studentNo = 0; + for (int i = 0; i < resultList.size(); i++) { + String result = resultList.get(i); + if ("A".equals(result)) { + studentNo += 1000 * i; + } else if ("B".equals(result)) { + studentNo += 100 * i; + } else if ("C".equals(result)) { + studentNo += 10 * i; + } else if ("D".equals(result)) { + studentNo += i; + } + } + NumberFormat formatter = new DecimalFormat("0000"); + String number = formatter.format(studentNo); + return number; + } + +} diff --git a/src/main/java/com/acts/opencv/base/CardController.java b/src/main/java/com/acts/opencv/base/CardController.java index 161a46c..5eac754 100644 --- a/src/main/java/com/acts/opencv/base/CardController.java +++ b/src/main/java/com/acts/opencv/base/CardController.java @@ -28,13 +28,12 @@ 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; -import com.acts.opencv.demo.DemoController; @Controller @RequestMapping(value = "card") public class CardController extends BaseController { - private static final Logger logger = LoggerFactory.getLogger(DemoController.class); + private static final Logger logger = LoggerFactory.getLogger(CardController.class); /** * 答题卡识别 diff --git a/src/main/java/com/acts/opencv/base/CardPlusController.java b/src/main/java/com/acts/opencv/base/CardPlusController.java index 337da07..cc90ba8 100644 --- a/src/main/java/com/acts/opencv/base/CardPlusController.java +++ b/src/main/java/com/acts/opencv/base/CardPlusController.java @@ -30,14 +30,13 @@ import org.springframework.web.bind.annotation.RequestMapping; import com.acts.opencv.common.utils.Constants; import com.acts.opencv.common.utils.OpenCVUtil; import com.acts.opencv.common.web.BaseController; -import com.acts.opencv.demo.DemoController; @Controller @RequestMapping(value = "cardPlus") public class CardPlusController extends BaseController { - private static final Logger logger = LoggerFactory.getLogger(DemoController.class); + private static final Logger logger = LoggerFactory.getLogger(CardPlusController.class); /** * 答题卡识别优化 diff --git a/src/main/java/com/acts/opencv/base/PageController.java b/src/main/java/com/acts/opencv/base/PageController.java index f6beac5..1ad9d7d 100644 --- a/src/main/java/com/acts/opencv/base/PageController.java +++ b/src/main/java/com/acts/opencv/base/PageController.java @@ -26,13 +26,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import com.acts.opencv.common.utils.Constants; import com.acts.opencv.common.utils.OpenCVUtil; import com.acts.opencv.common.web.BaseController; -import com.acts.opencv.demo.DemoController; @Controller @RequestMapping(value = "page") public class PageController extends BaseController { - private static final Logger logger = LoggerFactory.getLogger(DemoController.class); + private static final Logger logger = LoggerFactory.getLogger(PageController.class); /** * 答题卡识别优化 diff --git a/src/main/java/com/acts/opencv/base/RealTestController.java b/src/main/java/com/acts/opencv/base/RealTestController.java index ca29bfd..9994f3f 100644 --- a/src/main/java/com/acts/opencv/base/RealTestController.java +++ b/src/main/java/com/acts/opencv/base/RealTestController.java @@ -24,13 +24,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import com.acts.opencv.common.utils.Constants; import com.acts.opencv.common.utils.OpenCVUtil; import com.acts.opencv.common.web.BaseController; -import com.acts.opencv.demo.DemoController; @Controller @RequestMapping(value = "real") public class RealTestController extends BaseController { - private static final Logger logger = LoggerFactory.getLogger(DemoController.class); + private static final Logger logger = LoggerFactory.getLogger(RealTestController.class); /** * 图像矫正透视变换 diff --git a/src/main/java/com/acts/opencv/base/RectificationController.java b/src/main/java/com/acts/opencv/base/RectificationController.java index 408b23a..86553db 100644 --- a/src/main/java/com/acts/opencv/base/RectificationController.java +++ b/src/main/java/com/acts/opencv/base/RectificationController.java @@ -25,13 +25,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import com.acts.opencv.common.utils.Constants; import com.acts.opencv.common.utils.OpenCVUtil; import com.acts.opencv.common.web.BaseController; -import com.acts.opencv.demo.DemoController; @Controller @RequestMapping(value = "rect") public class RectificationController extends BaseController { - private static final Logger logger = LoggerFactory.getLogger(DemoController.class); + private static final Logger logger = LoggerFactory.getLogger(RectificationController.class); /** * 图像矫正透视变换 diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp index 1242043..6895338 100644 --- a/src/main/webapp/index.jsp +++ b/src/main/webapp/index.jsp @@ -158,6 +158,7 @@ desired effect
  • 图像矫正
  • 校正真实测试
  • +
  • 无定位点答题卡识别
  • diff --git a/src/main/webapp/statics/destimage/readme.txt b/src/main/webapp/statics/destimage/readme.txt new file mode 100644 index 0000000..ff7443e --- /dev/null +++ b/src/main/webapp/statics/destimage/readme.txt @@ -0,0 +1 @@ +目标图片地址没什么作用,防止部署的时候空目录部署丢失文件夹 \ No newline at end of file diff --git a/src/main/webapp/statics/sourceimage/card2_1.jpg b/src/main/webapp/statics/sourceimage/card2_1.jpg new file mode 100644 index 0000000..c6c5992 Binary files /dev/null and b/src/main/webapp/statics/sourceimage/card2_1.jpg differ diff --git a/src/main/webapp/statics/sourceimage/card2_2.jpg b/src/main/webapp/statics/sourceimage/card2_2.jpg new file mode 100644 index 0000000..7bacd25 Binary files /dev/null and b/src/main/webapp/statics/sourceimage/card2_2.jpg differ diff --git a/src/main/webapp/statics/sourceimage/card2_3.jpg b/src/main/webapp/statics/sourceimage/card2_3.jpg new file mode 100644 index 0000000..7b9714b Binary files /dev/null and b/src/main/webapp/statics/sourceimage/card2_3.jpg differ diff --git a/src/main/webapp/statics/sourceimage/card2_4.jpg b/src/main/webapp/statics/sourceimage/card2_4.jpg new file mode 100644 index 0000000..239b755 Binary files /dev/null and b/src/main/webapp/statics/sourceimage/card2_4.jpg differ diff --git a/src/main/webapp/view/base/contours.jsp b/src/main/webapp/view/base/contours.jsp index 54c5178..49454ca 100644 --- a/src/main/webapp/view/base/contours.jsp +++ b/src/main/webapp/view/base/contours.jsp @@ -109,7 +109,7 @@ 1. Mat src - 源文件Mat对象8bit 单通道二值化图像 + 源文件Mat对象8bit 单通道二值化图像,注意:查找轮廓方法因为有内置的边缘检测,所以是会更改原图的,因此使用的时候,可以clone一个使用 2. @@ -177,7 +177,7 @@ - 查找颜色 + 查找轮廓 重置 diff --git a/src/main/webapp/view/card/card2.jsp b/src/main/webapp/view/card/card2.jsp new file mode 100644 index 0000000..d731850 --- /dev/null +++ b/src/main/webapp/view/card/card2.jsp @@ -0,0 +1,190 @@ +<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> +<%@include file="/module/include/common.jsp"%> + + + + + + + + + + + + +
    +
    + +
    + +
    +
    + 图1 + 图2 + 图3 + 图4
    + 网上有朋友咨询答题卡识别方案,趁国庆前帮忙做了个demo,如上图。要求只进行客观题部分的识别,主观题先不用考虑。图片是拍摄的一张常规答题卡,无定位点的圆形填图框答题卡。识别思路如下:
    + 1、图形预处理,找到识别区域,即图中的矩形黑框,结合腐蚀膨胀进阶教程中的形态学梯度算法更好的检测边缘。
    + 2、对找到的矩形框进行校正,得到较为正规的答题卡识别区域,减少误差。
    + 3、对识别区域进行边缘检测,同第1步操作,主要是检测圆形边缘。
    + 4、逻辑识别过程,根据规则前2列5个选项后2列4个选项,同时第4列又分为准考证上半部分和下半部分填涂部分,进行切割。
    + 5、对切图进行轮廓识别,根据轮廓内填图部分来验证是否填图涂。
    + 6、程序判断输出结果。
    + 参考资料:
    + https://blog.csdn.net/zhaoxfxy/article/details/74999695
    + https://www.cnblogs.com/josephkim/p/8319069.html
    + +
    +
    +
    + + +
    +
    + + +
    + + + + + + + + +
    选择识别的图片
    + 图片1   + 图片2  + 图片3  + 图片4  +
    + + + +
      + +
    • + + + +
    • + + + +
    • + + +
      +  识别过程 + +

      识别过程

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      识别过程过程图片
      原图此处展现识别过程图,请先执行
      1.形态学梯度算法(保留边缘效果很棒)+大律法OTSU二值化图片进行图片边缘处理此处展现识别过程图,请先执行
      2.利用多边形拟合凸包输出检测出来外矩形边框此处展现识别过程图,请先执行
      3.根据矩形边框切图并进行校正,得到校正后的图像此处展现识别过程图,请先执行
      4.对校正后的图像进行边缘检测,方法同1,保留圆形填涂框的外边缘方便轮廓识别此处展现识别过程图,请先执行
      5.图片按规则切片,轮廓识别,掩模按顺序取出各选项轮廓,根据面积计算填涂百分比判定是否填涂。此步骤输出了学号的第一个掩模处理后提取的图片样张此处展现识别过程图,请先执行
      6.图片按规则切片,轮廓识别,根据面积计算填涂百分比判定是否填涂。得出结果
      + + + +
      +
      +
    • + + +
    + + +
    + + + + +