Elemento: reconocimiento de hoja de respuestas
dirección de github
El proceso de solución es el siguiente
Pretratamiento
Primero realice la detección de bordes Canny en la imagen y luego realice la operación de expansión. El propósito de la operación de expansión es, si el contorno exterior del papel no es muy obvio, después de la detección de bordes Canny, el
contorno exterior del papel es discontinuo y hay pequeños agujeros, use la operación de expansión para llenar los pequeños agujeros
Los resultados del procesamiento son los siguientes:
Detección de contornos
Extraiga el contorno con el área más grande MaxContour
y realice una aproximación de contorno adaptativa.epsilon = 0.0001 * 周长
El código específico es el siguiente:
#步长设置为周长的0.0001倍,一般来说取epsilon = 0.001倍周长
step = 0.0001 * cv2.arcLength(cnts[0], True)
epsilon = step
#不断递增epsilon直到近似所得轮廓正好包含四个点
while len(cnt) != 4:
cnt = cv2.approxPolyDP(cnts[0], epsilon, True)
#步增epsilon
epsilon += step
Los resultados del procesamiento son los siguientes:
Transformación de perspectiva
Antes de la transformación de perspectiva, se requiere un procesamiento previo avanzado y los cuatro puntos del contorno 左上、右上、右下、左下
se ordenan en el orden, la parte de clasificación del código es la siguiente:
#将四个轮廓点排序
pts = np.zeros((4, 2), np.float32)
res = np.sum(points, axis=1)
pts[0] = points[np.argmin(res)]
pts[2] = points[np.argmax(res)]
res = np.diff(points, axis=1)
pts[1] = points[np.argmin(res)]
pts[3] = points[np.argmax(res)]
Luego encuentre el ancho máximo y la altura máxima, el código específico es el siguiente:
#计算边长
w1 = np.sqrt((pts[0][0] - pts[1][0]) ** 2 + (pts[0][1] - pts[1][1]) ** 2)
w2 = np.sqrt((pts[2][0] - pts[3][0]) ** 2 + (pts[2][1] - pts[3][1]) ** 2)
w = int(max(w1, w2))
h1 = np.sqrt((pts[1][0] - pts[2][0]) ** 2 + (pts[1][1] - pts[2][1]) ** 2)
h2 = np.sqrt((pts[0][0] - pts[3][0]) ** 2 + (pts[0][1] - pts[3][1]) ** 2)
h = int(max(h1, h2))
Después de todo el preprocesamiento, podemos comenzar nuestra última y más importante transformación de perspectiva escalonada, el código específico es el siguiente:
#目标四个点
dst = np.array([
[0, 0],
[w - 1, 0],
[w - 1, h - 1],
[0, h - 1]
], np.float32)
#透视变换
mat = cv2.getPerspectiveTransform(pts, dst)
paper1 = org1.copy()
paper1 = cv2.warpPerspective(paper1, mat, (w, h))
if show_process:
imshow(paper1)
Los resultados son los siguientes:
Pretratamiento
Después de obtener la imagen transformada en perspectiva, también es necesario realizar operaciones de preprocesamiento Primero, para eliminar la influencia de diferentes niveles de exposición de diferentes imágenes, la imagen debe ser primero una ecualización de histograma adaptativo.
Los resultados del procesamiento son los siguientes:
Luego, la imagen se binariza para la detección de contornos. Pero la imagen de binarización terminada hay un problema, que se pinta la hoja de respuestas cuando, si no se pinta por completo,
puede causar una detección inexacta, por lo que para que los resultados sean una detección más precisa, pero también la necesidad de cerrar la operación de operación , Los resultados después del procesamiento son los siguientes:
Detección de contorno + filtrado de contorno
Primero extraiga todos los contornos, y los resultados son los siguientes:
Puede ver que se han extraído muchos contornos, muchos de los cuales son contornos que no necesitamos, por lo que necesitamos usar algunos algoritmos de filtrado para mantener los contornos (25 elipses) que necesitamos.
Los pasos del algoritmo de filtrado aquí son los siguientes:
- Primero obtenga la figura circunscrita del contorno a detectar, si es un círculo, obtenga el círculo circunscrito del contorno
- Luego se puede filtrar por área, cuando la relación área del perfil / área del patrón externo
ratio
satisfaga:ratio > 0.8 and ratio < 1.2
tiempo para cumplir con los requisitos - Luego se puede filtrar de acuerdo con la circunferencia, cuando se
ratio
satisfaga la relación contorno de circunferencia / perímetro de los gráficos externos :ratio > 0.8 and ratio < 1.2
cuando se cumplan los requisitos
El código específico es más complicado, como sigue:
#用于保存保留下来的轮廓
cntsex = []
#上下边界阈值
thresh_lower = 0.8
thresh_upper = 1.2
eps = 1e-6
show = org1.copy()
for cnt in cnts:
cntcopy = cnt.copy()
#按照h方向坐标对轮廓的所有点排序,找到最大的y
cntcopy = sorted(cntcopy, key=lambda x: x[0][1], reverse=True)
maxy = cntcopy[0][0][1]
#按照w方向坐标对轮廓的所有点排序,找到最大的x
cntcopy = sorted(cntcopy, key=lambda x: x[0][0], reverse=True)
maxx = cntcopy[0][0][0]
#获得椭圆的中心
(x, y), radius = cv2.minEnclosingCircle(cnt)
center = (int(x), int(y))
radius = int(radius)
#获得椭圆的长轴和短轴
a = maxx - x
b = maxy - y
if b == 0:
continue
ratio = a / b;
if ratio > 2 or ratio < 0.5:
continue
if radius == 0:
continue
#面积过滤
areaex = np.pi * a * b
area = cv2.contourArea(cnt)
ratio = area / areaex
if ratio < thresh_upper and ratio > thresh_lower:
cntsex.append(cnt)
show = cv2.drawContours(show, [cnt], 0, (0, 255, 0), 1)
show = cv2.ellipse(show, center, (int(a), int(b)), 0, 0, 360, (0, 0, 255), 1)
Después de esto tenemos todos los contornos que son más como elipses, pero esto no es suficiente, porque también se han conservado algunas elipses utilizadas para encuadernación. Se puede observar que
la característica de estas elipses utilizadas para encuadernación es que su área es mejor que la respuesta. La elipse de es mucho más pequeña, por lo que ordenamos todos los contornos key = 轮廓的面积
y luego filtramos el área más pequeña a través de un algoritmo específico. El código específico es el siguiente:
#第二次过滤
cnts = []
maxarea = -1e6
for cnt in cntsex:
area = cv2.contourArea(cnt)
if area > maxarea:
maxarea = area
maxgap = 0.5 * maxarea
cntsex = sorted(cntsex, key=lambda x: cv2.contourArea(x), reverse=True)
prvarea = cv2.contourArea(cntsex[0])
cnts.append(cntsex[0])
for i in range(1, len(cntsex)):
if abs(prvarea - cv2.contourArea(cntsex[i])) > maxgap:
break
cnts.append(cntsex[i])
El resultado final del procesamiento es el siguiente:
Ordenar + comprobar
Luego, debe ordenar los contornos de arriba a abajo y de izquierda a derecha. Este programa completa la detección mientras ordena. El código específico es el siguiente:
#对多个轮廓按照从上到下的顺序排序
cnts = sorted(cnts, key=lambda x: x[0][0][1])
rows = int(len(cnts) / 5)
TAB = ['A', 'B', 'C', 'D', 'E']
ANS = []
#检查每一行(即每一题)的答案
for i in range(rows):
subcnts = cnts[i*5:(i+1)*5]
subcnts = sorted(subcnts, key=lambda x: x[0][0][0])
total = []
for (j, cnt) in enumerate(subcnts):
mask = np.zeros(paper1.shape, dtype=np.uint8)
cv2.drawContours(mask, [cnt], -1, 255, -1) #-1表示填充
mask = cv2.bitwise_and(paper1, paper1, mask=mask)
total.append(cv2.countNonZero(mask))
idx = np.argmax(np.array(total))
ANS.append(TAB[idx])
print(ANS)
Los resultados del procesamiento son los siguientes: