python-opencv答题卡识别
1:生成答题卡
第一种:用底层代码调取word.dll动态链接库
第二种:前端页面生成
第三种:使用通用答题卡
第四种:自己word画答题卡
本代码是自己手画答题卡
2:获取图片
第一种:手机拍摄
第二种:摄像头获取(csdn众多大佬可见其思路)
第三种:扫描仪获取
由于项目的需要:采取第三种获取方式
2.1首先了解twain协议
这个协议是摄像头和扫描仪国际通用的协议:
由于扫描仪用的32位.dll,这里可用java或者python、c++也行
这里采用java调取扫描仪驱动获取图片:
主代码:
import com.hime.unit.DateUtil;
import org.scavino.twain.JTwain;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.Timer;
import java.util.*;
public class ScanGUI<pri, pris> extends JFrame implements ActionListener {
/**
*
*/
private static final long serialVersionUID = 7241095149717774695L;
private JPanel mainPanel;
private JPanel topPanel = new JPanel();
private JPanel leftPanel = new JPanel();
private JPanel rightPanel = new JPanel();
private JPanel bottomPanel = new JPanel();
// private JToolBar mToolBar;
private JTextField dateTxt, batTxt, pageNumTxt;
private DefaultListModel fileListModel;
private JList fileJList=new JList();
private JButton scanBtn = new JButton("扫描");
// private JButton uploadBtn = new JButton("上传");
private JButton exitBtn = new JButton("退出");
private BufferedImage mBufferedImage;
private JPEGPanel mJpegPanel=new JPEGPanel();
private JComboBox mSourcesCombo;
private JPopupMenu popMenu ;
private JMenuItem delItem ;
private Map<String, String> fileMap = new HashMap<String, String>();// 用于存放扫描后的图片信息。
private Map<String, String> newFileMap = new HashMap<String, String>();//用于存放新扫描的信息。
private List<String> list =new LinkedList<String>();//用于存放文件顺序。
private AcquireHelper acquire;
static int i = 1;
//扫描仪图形界面
public ScanGUI() {
centerUI();
this.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
this.setSize(800, 550);
//扫描仪的标题
this.setTitle("实证优师");
this.setResizable(false);
this.setVisible(true);
buildUI();
//建立试图
this.setVisible(true);
// repaint();
// 最大化窗口
// this.setExtendedState(JFrame.MAXIMIZED_BOTH);
}
// 界面屏幕居中
public void centerUI() {
Dimension screenSize = java.awt.Toolkit.getDefaultToolkit()
.getScreenSize();
Dimension frameSize = this.getSize();
this.setLocation((screenSize.width - frameSize.width) / 4,
(screenSize.height - frameSize.height) / 4);
}
// 装载界面
public void buildUI() {
mainPanel = (JPanel) getContentPane();
initTopPanel();
initLeftPanel();
initRightPanel();
initBottomPanel();
mainPanel.add(topPanel, BorderLayout.NORTH);
mainPanel.add(leftPanel, BorderLayout.WEST);
mainPanel.add(rightPanel, BorderLayout.CENTER);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
}
// 加载topPanle
public void initTopPanel() {
topPanel.setLayout(new BorderLayout());
topPanel.setBorder(BorderFactory.createEtchedBorder());
JLabel logo = new JLabel();
//可加logo
logo.setIcon(new ImageIcon("D:\\爬虫青蛙\\30\\0011.jpg"));
logo.setText(" 实证优师文件扫描上传工具");
//设置字体
logo.setFont(new Font("baclk", Font.BOLD, 30));
//logoPanel面板
JPanel LogoPanel = new JPanel();
LogoPanel.add(logo);
//顶部面板
topPanel.add(LogoPanel, BorderLayout.NORTH);
//文本框的面板
JPanel txtPanel = new JPanel(new FlowLayout(1, 41, FlowLayout.CENTER));
//日期框
txtPanel.add(new JLabel("日期"));
//日期的文本框
dateTxt = new JTextField(10);
//日期
String date = DateUtil.dateToStr(new Date(), "yyyy-MM-dd");
dateTxt.setText(date);
//灰色框
// dateTxt.setEnabled(false);
// dateTxt.setVisible(true);
//日期框加载到文本框
txtPanel.add(dateTxt);
dateTxt.setEnabled(false);
//批次号
// txtPanel.add(new JLabel("批次号"));
batTxt = new JTextField(10);
//文本框
// txtPanel.add(batTxt);
// txtPanel.add(new JLabel("上传张数"));
// pageNumTxt = new JTextField(5);
// txtPanel.add(pageNumTxt);
topPanel.add(txtPanel, BorderLayout.SOUTH);
}
// 加载 leftPanel
private void initLeftPanel() {
leftPanel.setLayout(new BorderLayout());
// leftPanel.setBorder(BorderFactory.createTitledBorder("文件列表"));
popMenu = new JPopupMenu();
delItem = new JMenuItem("从列表中删除");
popMenu.add(delItem);
fileListModel = new DefaultListModel();
//设置字体
fileListModel.setSize(100);
// fileJList = new JList(fileListModel);
fileJList.setModel(fileListModel);
//文本框
// fileJList.setPreferredSize(new Dimension(150, 350));
//logo框
fileJList.setMaximumSize(new Dimension(150,800));
System.out.println(fileJList.getScrollableTracksViewportWidth());
//收听鼠标动作事件的工作
MyMouseAdapter myAdapter=new MyMouseAdapter();
//文件列表添加鼠标监听事件
fileJList.addMouseListener(myAdapter);
//设置弹出菜单的调用者,即弹出菜单在其中显示的组件
popMenu.setInvoker(fileJList);
// JScrollPane fileSrcPane = new JScrollPane(fileJList);
//图片扫描显示框
// fileSrcPane.setSize(new Dimension(150,350));
// leftPanel.add(fileSrcPane);
// 右键删除文件事件。
delItem.addMouseListener(myAdapter);
// 右键弹出事件
fileJList.addMouseListener(myAdapter );
}
// 加载rightPanel
private void initRightPanel() {
rightPanel.setLayout(new BorderLayout());
mJpegPanel = new JPEGPanel();
//边框 右边框 和下底框
// JScrollPane ps = new JScrollPane(
// mJpegPanel,
// JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
// JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
// rightPanel.add(ps, BorderLayout.CENTER);
}
//"WIA-KV-S1037","Panasonic KV-S1038/ KV-S1037"
static public final String PanasonicScanner = "WIA-KV-S1037";
// 加载 bottomPanel
private void initBottomPanel() {
bottomPanel.setLayout(new FlowLayout(1, 41, FlowLayout.CENTER));
//mTooBar=Twain
JToolBar mToolBar = new JToolBar("Twain");
//设置 floatable 属性,如果要移动工具栏,此属性必须设置为 true
mToolBar.setFloatable(false);
//当驱动可以获得的时候,用数组装起来
if (JTwain.getInstance().isTwainAvailble()) {
String[] twainSources = JTwain.getInstance().getAvailableSources();
//判断驱动没有获得时候
if (twainSources == null && twainSources.length == 0) {
//提示页面后,退出系统、延时
Timer timer=new Timer();
timer.schedule(new TimerTask() {
public void run() {
System.out.println(" 退出");
this.cancel();
}
}, 500);// 这里百毫秒
System.out.println("本程序存在5秒后自动退出");
}
//检查扫描仪驱动
for (String source : twainSources) {
if(PanasonicScanner.equals(source)){
twainSources = new String[]{source};
System.out.println(twainSources[0]);
}
//判断驱动是否存在
if (twainSources == null && twainSources.length == 0) {
//提示页面后,退出系统
Timer timer=new Timer();
timer.schedule(new TimerTask() {
public void run() {
System.out.println(" 退出");
this.cancel();
}
}, 500);// 这里百毫秒
System.out.println("本程序存在5秒后自动退出");
System.exit(-1);
}
}
//下拉列表组件 显示驱动名字
mSourcesCombo = new JComboBox(twainSources);
}
//低层框框
bottomPanel.add(mSourcesCombo);
//不可用框
mSourcesCombo.setEnabled(false);
//分隔线
mToolBar.addSeparator();
//扫描添加事件
scanBtn.addActionListener(this);
//扫描文本框添加到扫按钮
bottomPanel.add(scanBtn);
//锁住框框 点击应该是文本 没有进去
// scanBtn.setBorder(BorderFactory.createLoweredBevelBorder ());
scanBtn.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
super.mouseClicked(e);
if(e.getClickCount()==1)
scanBtn.setEnabled(false);
}
});
// scanBtn.setPressedIcon((Icon) scanBtn);
//// 上传
// uploadBtn.addActionListener(this);
// uploadBtn.setEnabled(false);
// bottomPanel.add(uploadBtn);
//退出添加事件
exitBtn.addActionListener(this);
//退出添加到文本框中
bottomPanel.add(exitBtn);
//
// bottomPanel.add(mToolBar);
}
/**
* 执行动作 不允许重复点击
* @param e
*/
public void actionPerformed(ActionEvent e) {
if (e.getSource().equals(scanBtn)) {
// scanBtn.setEnabled(false);
//连续按下
acquire = new AcquireHelper();
String source = (String) mSourcesCombo.getSelectedItem();
//开始获得信号
acquire.startAcquireOne(source);
//重置页面
repaint();
//这里可以设置灰框。不可点击
// scanBtn.setEnabled(true);
}
//获取事件是否等于退出框按钮
if(e.getSource().equals(exitBtn)){
//为真,系统就是退出
System.exit(0);
}
}
// 列表框中文件列表清除
private void clearfileListModel() {
//文件列表为空时
if (fileListModel.isEmpty())
//返回上一级
return;
else {
//文件列表就清空
fileListModel.clear();
//文件数组清空
fileMap.clear();
// System.out.println("上传成功 ");
}
}
// 增加列表框中文件显示
// private void addFileListModel() {
// if (JTwain.getInstance().isTwainAvailble()) {
// if (mSourcesCombo.getItemCount() > 0) {
//
//
// list.addAll(newFileMap.keySet());
//// //保存
// for (Iterator<String> it = newFileMap.keySet().iterator(); it.hasNext(); ) {
// fileListModel.add(i++, it.next());
// fileListModel.addElement(it.next());
// list.add(it.next());
// System.out.println(fileListModel.elementAt(fileListModel.size() - 1));
// }
// // 在文件列表框中显示文件
// fileListModel.setSize(list.size());
// clearfileListModel();
// for (int i = 0; i < list.size(); i++) {
// fileListModel.addElement(list.get(i));
// }
// fileMap.putAll(newFileMap);
// newFileMap.clear();
// for (String filename : fileMap.keySet()) {
// fileListModel.addElement(filename);
// System.out.println("add " + filename);
// JOptionPane.showMessageDialog(null, "扫描了" + i + "个文件!");
// }
//
// }
// }
// }
// 删除选中的列表框中的文件
// private void deleteFileListModel() {
// //当文件列表存在时
// if (!fileJList.isSelectionEmpty()) {
// Object[] fileStr = fileJList.getSelectedValues();
// if (fileStr == null || fileStr.length == 0)
// return;
// for (Object file : fileStr) {
// fileListModel.removeElement(file);
// fileMap.remove(file);
// list.remove(file);
// }
// //上传
//// if(fileMap.size()<1){
//// uploadBtn.setEnabled(false);
//// }
// }
// }
//图像打印
// private void imagePrint(Object o) {
// if (o != null) {
// String filename = o.toString();
// String fileuri = fileMap.get(filename) + filename;
// File file = new File(fileuri);
// if (file == null || !file.exists()) {
// file = new File("D:\\爬虫青蛙\\30\\300.jpg");
// }
// showImage(file);
//
// }
//
// }
//显示图片
// private void showImage(final File file) {
// if (file == null || !file.exists()) {
// return;
// }
// setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
//
// Thread runner = new Thread() {
// public void run() {
// //保存图片
// try {
// FileInputStream in = new FileInputStream(file);
// JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(in);
// mBufferedImage = decoder.decodeAsBufferedImage();
// in.close();
// SwingUtilities.invokeLater(new Runnable() {
// public void run() {
// reset();
// }
// });
// } catch (Exception ex) {
// ex.printStackTrace();
// }
// setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
// }
// };
// runner.start();
// }
// private void reset() {
// if (mBufferedImage != null) {
// mJpegPanel.setBufferedImage(mBufferedImage);
// }
// }
//主函数 扫描仪启动程序 扔出异常
public static void main(String[] args) throws Exception {
//
javax.swing.UIManager.setLookAndFeel(javax.swing.UIManager.getSystemLookAndFeelClassName());
// @SuppressWarnings("unused")
//图形化GUI
new ScanGUI();
}
//JPEGPanel继承JPanel类
class JPEGPanel extends JPanel {
//静态属性
private static final long serialVersionUID = 1L;
/** Image for the inner class */
//保护 不能强制访问
protected BufferedImage mJPEGPanelBufferedImage;
/** Pnale to diaply the image */
public JPEGPanel() {
// no op
}
/**
* Sets the bufferedimage into the class
*/
public void setBufferedImage(BufferedImage bi) {
if (bi == null) {
return;
}
// System.out.println(bi.toString());
mJPEGPanelBufferedImage = bi;
Dimension d = new Dimension(mJPEGPanelBufferedImage.getWidth(this),
mJPEGPanelBufferedImage.getHeight(this));
//仅仅是设置最好的大小,这个不一定与实际显示出来的控件大小一致(根据界面整体的变化而变化)
setPreferredSize(d);
//
revalidate();
//重新刷新
//重复
repaint();
}
/**
* Paints the component. Graphics object used for the painting
*/
public void paintComponent(Graphics g) {
super.paintComponent(g);
Dimension d = getSize();
//得到矩形颜色
g.setColor(getBackground());
g.fillRect(0, 0, d.width, d.height);
if (mJPEGPanelBufferedImage != null) {
g.drawImage(mJPEGPanelBufferedImage, 0, 0, this);
}
}
}
//鼠标触发事件
class MyMouseAdapter extends MouseAdapter {
public void mouseClicked(MouseEvent mouseEvent) {
if (mouseEvent.getSource().equals(fileJList)) {
JList theList = (JList) mouseEvent.getSource();
if (mouseEvent.getClickCount() == 2) {
int index = theList.locationToIndex(mouseEvent.getPoint());
if (index >= 0) {
Object o = theList.getModel().getElementAt(index);
//双击
System.out.println("Double-clicked on: " +
o.toString());
// imagePrint(o);
}
}
}
}
//鼠标触发事件释放
public void mouseReleased(MouseEvent e) {
if(e.getSource().equals(fileJList)){
if (popMenu.isPopupTrigger(e) && !fileJList.isSelectionEmpty()) {
popMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
if (e.getSource().equals(delItem)) {
if (e.getButton() == MouseEvent.BUTTON1
&& !fileJList.isSelectionEmpty()) {
// deleteFileListModel();
}
}
}
}
}
这也是在前人基础上修改的,github可以搜索。
这是扫描仪读取的图片
3:答题卡识别
不限于任何代码,网上资料很多,主要有python和c++居多。
接下来使用python参考别人的博客
3.1首先就是画坐标系
坐标系代码:
# coding: utf-8
import cv2
import numpy as np
#根据图片点击得出(x,y)
img = cv2.imread("img/SampleCard2.jpg")
# print img.shape
def on_EVENT_LBUTTONDOWN(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
xy = "%d,%d" % (x, y)
xy
cv2.circle(img, (x, y), 1, (255, 0, 0), thickness=-1)
cv2.putText(img, xy, (x, y), cv2.FONT_HERSHEY_PLAIN,
1.0, (0, 0, 0), thickness=1)
cv2.imshow("image", img)
cv2.namedWindow("image")
cv2.resizeWindow("image", 800, 600)
cv2.setMouseCallback("image", on_EVENT_LBUTTONDOWN)
cv2.imshow("image", img)
while (True):
try:
cv2.waitKey(100)
except Exception:
cv2.destroyWindow("image")
break
cv2.waitKey(0)
cv2.destroyAllWindow()
3.2图像预处理
先是灰度处理:
orginImg = cv.imread("img/SampleCard2.jpg")
二值化处理:
binaryImg = cv.adaptiveThreshold(grayImg, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 11, 2)
边缘检测:
cannyImg = cv.Canny(binaryImg, 30, 120)
检测轮廓:
contours = cv.findContours(cannyImg, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
'''参数:
image -- 要查找轮廓的原图像
mode -- 轮廓的检索模式,它有四种模式:
cv2.RETR_EXTERNAL 表示只检测外轮廓
cv2.RETR_LIST 检测的轮廓不建立等级关系
cv2.RETR_CCOMP 建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,
这个物体的边界也在顶层。
cv2.RETR_TREE 建立一个等级树结构的轮廓。
method -- 轮廓的近似办法:
cv2.CHAIN_APPROX_NONE 存储所有的轮廓点,相邻的两个点的像素位置差不超过1,即max (abs (x1 - x2), abs(y2 - y1) == 1
cv2.CHAIN_APPROX_SIMPLE压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需
4个点来保存轮廓信息
cv2.CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
返回值:
cv2.findContours()函数返回两个值:
contours -- 轮廓本身,它是一个list,list 中每个元素都是图像中的一个轮廓,用Numpy中的ndarray表示 ,每个轮廓是一个ndarray,每个ndarray
是轮廓上的点的集合。轮廓中并不存储轮廓上所有的点,而只存储可以用直线描述轮廓的点的个数,比如一个“正立”的矩形只有4个
点元素。
还有一个是每条轮廓对应的属性
'''
透视变换:是处理图像歪曲的
orginWImg = four_point_transform(orginImg, approx.reshape(4, 2))
自适应阈值:
binaryWImg = cv.adaptiveThreshold(warpedImg, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 101, 2)
'''interpolation - 插值方法。共有5种:
1)INTER_NEAREST - 最近邻插值法
2)INTER_LINEAR - 双线性插值法(默认)
3)INTER_AREA - 基于局部像素的重采样(resampling using pixel area relation)。对于图像抽取(image decimation)来说,这可能是一个更好的方法。但如果是放大图像时,它和最近邻法的效果类似。
4)INTER_CUBIC - 基于4x4像素邻域的3次插值法
5)INTER_LANCZOS4 - 基于8x8像素邻域的Lanczos插值
收缩图像,那么使用重采样差值法效果最好;如果想要放大图像,那么最好使用3次差值法或者线性差值法
'''
均值滤波
meanImg = cv.blur(binaryWImg, (25, 25))
最后结果:
控制台输出:
[(1, 'A'), (2, 'C'), (3, 'B'), (4, 'C'), (5, 'C'), (6, 'A'), (7, 'B'), (8, 'C'), (9, 'B'), (10, 'C'), (11, 'D'), (12, 'D'), (13, 'D'), (14, 'D'), (15, 'D'), (16, 'B'), (17, 'D'), (18, 'C'), (19, 'B'), (20, 'A'), (21, 'A'), (22, 'B'), (23, 'C'), (24, 'C'), (25, 'C'), (26, 'A'), (27, 'B'), (28, 'C'), (29, 'D'), (30, 'C'), (31, 'A'), (32, 'B'), (33, 'C'), (34, 'D'), (35, 'B'), (36, 'B'), (37, 'C'), (38, 'B'), (39, 'A'), (40, 'C')]
识别逻辑:
#根据试卷的宽度确定每个选择题的宽度
widthAnswer = meanImg.shape[1] / 20
#每5个选择题的宽度
wNUm = widthAnswer * 5
#答案的坐标
answerP = []
#遍历所有选择题坐标
for contour in contours:
#跳过第一个最大的(全部)
if isFirst:
isFirst = False
continue
#得到最小矩阵
cRect = cv.minAreaRect(contour)
#得到当前选择题的中心点坐标
cX = int(cRect[0][0])
cY = int(cRect[0][1])
#存入答案的中心点坐标
answerP.append((cX,cY))
#print(int(cX / widthAnswer))
#colA = (int(cX / widthAnswer) + 1) % 5 -1
#print(str(colA) + " 答案" + Answer(colA))
#在原图上画圆圈,标识出答案
cv.circle(orginWImg, (cX,cY), 7, (0, 0, 255), -1)
#对答案坐标 进行 高度 排序
answerP = sorted(answerP, key=lambda a:a[1])
#记录已经存入答案数量
countNum = 1
#每一行第一个题号
aNum = 1
#根据试卷每列首题编号
rowNum = [1,2,3,4,5,21,22,23,24,25,41,42,43,44,45,0]
# 存储的选择题答案
answer = []
#遍历每个答案坐标
for i in answerP:
#print(i,end=' ')
#计算第几列,确定选项 0ABCD
colA = int(i[0] / widthAnswer)%5
#print(Answer(colA),end=" ")
#确定题号
numT = aNum + int(i[0] / wNUm)*5
#print(numT)
answer.append((numT,Answer(colA)))
if countNum <= 40:
if countNum % 4 == 0:
aNum = rowNum[int(countNum/4)]
else:
if countNum % 2 == 0
aNum = rowNum[int((countNum-40) / 4) + 10]
countNum += 1
#对答案序号进行排序,便于展示
answer = sorted(answer,key=lambda x:x[0] )
把 1 【A】【B】【C】【D】把这5个框框看成一组,类似滑动窗口的效果一次扫描
最后准考证号码我还在仔细研究,用了tesseract和百度API效果不理想,还在思考中...