基于计算机视觉的试卷答题区表格识别与提取技术

基于计算机视觉的试卷答题区表格识别与提取技术

摘要

本文介绍了一种基于计算机视觉技术的试卷答题区表格识别与提取算法。该算法能够自动从试卷图像中定位答题区表格,执行图像方向矫正,精确识别表格网格线,并提取每个答案单元格。本技术可广泛应用于教育测评、考试管理系统等场景,极大提高答卷处理效率。

关键技术

  • 表格区域提取与分割
  • 图像二值化预处理
  • 多尺度形态学操作
  • 水平线与竖线精确检测
  • 单元格定位与提取

1. 系统架构

我们设计的试卷答题区表格处理工具由以下主要模块组成:

  1. 答题区定位:从整张试卷图像中提取右上角的答题区表格
  2. 图像预处理:进行二值化、去噪等操作以增强表格线条
  3. 表格网格识别:精确检测水平线和竖线位置
  4. 单元格提取:根据网格线交点切割并保存各个答案单元格

处理流程:

输入图像 -> 答题区定位 -> 方向矫正 -> 图像预处理 -> 
网格线检测 -> 单元格提取 -> 输出结果

2. 核心功能实现

2.1 答题区表格定位

我们假设答题区通常位于试卷右上角,首先提取该区域并应用轮廓检测算法:

# 提取右上角区域(答题区域通常在试卷右上角)
x_start = int(width * 0.6)
y_start = 0
w = width - x_start
h = int(height * 0.5)# 提取区域
region = img[y_start:y_start + h, x_start:x_start + w]

接着使用形态学操作提取线条并查找表格轮廓:

# 转为灰度图并二值化
gray = cv2.cvtColor(region, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY_INV, 11, 2)# 使用形态学操作检测线条
horizontal_lines = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_h, iterations=2)
vertical_lines = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_v, iterations=2)

2.2 图像预处理

为了增强表格线条特征,我们执行以下预处理步骤:

# 高斯平滑去噪
blurred = cv2.GaussianBlur(gray, (5, 5), 0)# 自适应阈值二值化
binary = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 21, 5)# 形态学操作填充小空隙
kernel = np.ones((3, 3), np.uint8)
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)

2.3 表格网格线识别

这是本算法的核心部分,我们分别检测水平线和竖线:

2.3.1 水平线检测

使用形态学开运算提取水平线,然后计算投影找到线条位置:

horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (horizontal_size, 1))
horizontal_lines = cv2.morphologyEx(binary_image, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)# 提取水平线坐标 - 基于行投影
h_coords = []
h_projection = np.sum(horizontal_lines, axis=1)
for i in range(1, len(h_projection) - 1):if h_projection[i] > h_projection[i - 1] and h_projection[i] > h_projection[i + 1] and h_projection[i] > width // 5:h_coords.append(i)
2.3.2 竖线检测

竖线检测采用多尺度策略,使用不同大小的结构元素,提高检测的鲁棒性:

# 使用不同大小的结构元素进行竖线检测
vertical_kernels = [cv2.getStructuringElement(cv2.MORPH_RECT, (1, (table_bottom - table_top) // 12)),  # 细线cv2.getStructuringElement(cv2.MORPH_RECT, (1, (table_bottom - table_top) // 8)),  # 中等cv2.getStructuringElement(cv2.MORPH_RECT, (1, (table_bottom - table_top) // 4))  # 粗线
]# 合并不同尺度的检测结果
vertical_lines = np.zeros_like(binary_image)
for kernel in vertical_kernels:v_lines = cv2.morphologyEx(binary_image, cv2.MORPH_OPEN, kernel, iterations=1)vertical_lines = cv2.bitwise_or(vertical_lines, v_lines)

2.4 表格竖线位置精确校正

由于竖线检测可能存在偏左问题,我们实现了复杂的位置校正算法:

# 竖线位置修正:解决偏左问题 - 检测实际线条中心位置
v_coords_corrected = []
for idx, v_coord in enumerate(v_coords_detected):# 第2-11根竖线特殊处理if 1 <= idx <= 10:  # 第2-11根竖线search_range_left = 2  # 左侧搜索范围更小search_range_right = 12  # 右侧搜索范围大幅增大else:search_range_left = 5search_range_right = 5# 在搜索范围内找到峰值中心位置# 对于特定竖线,使用加权平均来偏向右侧if 1 <= idx <= 10:window = col_sum[left_bound:right_bound+1]weights = np.linspace(0.3, 2.0, len(window))  # 更强的右侧权重weighted_window = window * weightsmax_pos = left_bound + np.argmax(weighted_window)# 强制向右偏移max_pos += 3else:max_pos = left_bound + np.argmax(col_sum[left_bound:right_bound+1])
2.4.1 不等间距网格处理

我们根据实际表格特点,处理了第一列宽度与其他列不同的情况:

# 设置第一列的宽度为其他列的1.3倍
first_column_width_ratio = 1.3# 计算除第一列外每列的宽度
remaining_width = right_bound - left_bound
regular_column_width = remaining_width / (expected_vlines - 1 + (first_column_width_ratio - 1))

2.5 单元格提取与保存

根据检测到的网格线,我们提取出每个单元格:

# 提取单元格的过程
cell_img = image[y1_m:y2_m, x1_m:x2_m].copy()# 保存单元格图片
cell_filename = f'cell_0{q_num:02d}.png'
cell_path = os.path.join(output_dir, cell_filename)
cv2.imwrite(cell_path, cell_img)

3. 技术创新点

  1. 多尺度形态学操作:使用不同尺寸的结构元素检测竖线,提高了检测的鲁棒性
  2. 表格线位置动态校正:针对不同位置的竖线采用不同的校正策略,解决了竖线偏左问题
  3. 不等间距网格处理:通过特殊计算处理第一列宽度不同的情况,更好地适应实际试卷样式
  4. 加权峰值搜索:使用加权策略进行峰值搜索,提高了线条中心位置的准确性

4. 使用示例

4.1 基本用法

from image_processing import process_image# 处理单张图像
input_image = "./images/1.jpg"
output_dir = "./output"
image_paths = process_image(input_image, output_dir)print(f"处理成功: 共生成{len(image_paths)}个单元格图片")

4.2 批量处理

我们还提供了批量处理多张试卷图像的功能:

# 批量处理目录中的所有图像
for img_file in image_files:img_path = os.path.join(images_dir, img_file)output_dir = os.path.join(output_base_dir, f"result_{img_name}")image_paths = process_image(img_path, output_dir)

4.3 完整代码

"""
试卷答题区表格处理工具
1. 从试卷提取答题区表格
2. 对表格进行方向矫正
3. 切割表格单元格并保存所有25道题的答案单元格
"""
import os
import cv2
import numpy as np
import argparse
import sys
import time
import shutilclass AnswerSheetProcessor:"""试卷答题区表格处理工具类"""def __init__(self):"""初始化处理器"""passdef process(self, input_image_path, output_dir):"""处理试卷答题区,提取表格并保存单元格Args:input_image_path: 输入图像路径output_dir: 输出单元格图像的目录Returns:处理后的图片路径列表,失败时返回空列表"""os.makedirs(output_dir, exist_ok=True)temp_dir = os.path.join(os.path.dirname(output_dir), f"temp_{time.strftime('%Y%m%d_%H%M%S')}")os.makedirs(temp_dir, exist_ok=True)try:# 1. 提取答题区表格table_img, _ = self._extract_answer_table(input_image_path, temp_dir)if table_img is None:print("无法提取答题区表格")return []# 保存提取的原始表格图像# original_table_path = os.path.join(output_dir, "original_table.png")# cv2.imwrite(original_table_path, table_img)# 2. 矫正表格方向corrected_table = self._correct_table_orientation(table_img)# cv2.imwrite(os.path.join(output_dir, "corrected_table.png"), corrected_table)# 3. 提取表格单元格image_paths = self._process_and_save_cells(corrected_table, temp_dir, output_dir)# 4. 清理临时目录shutil.rmtree(temp_dir, ignore_errors=True)return image_pathsexcept Exception as e:print(f"处理失败: {str(e)}")shutil.rmtree(temp_dir, ignore_errors=True)return []def _extract_answer_table(self, image, output_dir):"""提取试卷答题区表格"""# 读取图像if isinstance(image, str):img = cv2.imread(image)if img is None:return None, Noneelse:img = image# 调整图像大小以提高处理速度max_width = 1500if img.shape[1] > max_width:scale = max_width / img.shape[1]img = cv2.resize(img, None, fx=scale, fy=scale)# 获取图像尺寸height, width = img.shape[:2]# 提取右上角区域(答题区域通常在试卷右上角)x_start = int(width * 0.6)y_start = 0w = width - x_starth = int(height * 0.5)# 提取区域region = img[y_start:y_start + h, x_start:x_start + w]# 转为灰度图并二值化gray = cv2.cvtColor(region, cv2.COLOR_BGR2GRAY)binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY_INV, 11, 2)# 检测表格线kernel_h = cv2.getStructuringElement(cv2.MORPH_RECT, (max(25, w // 20), 1))kernel_v = cv2.getStructuringElement(cv2.MORPH_RECT, (1, max(25, h // 20)))horizontal_lines = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_h, iterations=2)vertical_lines = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_v, iterations=2)# 合并线条grid_lines = cv2.add(horizontal_lines, vertical_lines)# 膨胀线条kernel = np.ones((3, 3), np.uint8)dilated_lines = cv2.dilate(grid_lines, kernel, iterations=1)# 查找轮廓contours, _ = cv2.findContours(dilated_lines, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 筛选可能的表格轮廓valid_contours = []for contour in contours:x, y, w, h = cv2.boundingRect(contour)area = cv2.contourArea(contour)if area < 1000:continueaspect_ratio = float(w) / h if h > 0 else 0if 0.1 <= aspect_ratio <= 3.0:valid_contours.append((x, y, w, h, area))if valid_contours:# 选择面积最大的轮廓valid_contours.sort(key=lambda c: c[4], reverse=True)x, y, w, h, _ = valid_contours[0]# 调整回原图坐标x_abs = x_start + xy_abs = y_start + y# 提取表格区域并加一些padding确保完整padding = 10x_abs = max(0, x_abs - padding)y_abs = max(0, y_abs - padding)w_padded = min(width - x_abs, w + 2 * padding)h_padded = min(height - y_abs, h + 2 * padding)table_region = img[y_abs:y_abs + h_padded, x_abs:x_abs + w_padded]return table_region, (x_abs, y_abs, w_padded, h_padded)# 如果未找到有效轮廓,返回预估区域x_start = int(width * 0.75)y_start = int(height * 0.15)w = int(width * 0.2)h = int(height * 0.4)x_start = max(0, min(x_start, width - 1))y_start = max(0, min(y_start, height - 1))w = min(width - x_start, w)h = min(height - y_start, h)return img[y_start:y_start + h, x_start:x_start + w], (x_start, y_start, w, h)def _correct_table_orientation(self, table_img):"""矫正表格方向(逆时针旋转90度)"""if table_img is None:return Nonetry:return cv2.rotate(table_img, cv2.ROTATE_90_COUNTERCLOCKWISE)except Exception as e:print(f"表格方向矫正失败: {str(e)}")return table_imgdef _process_and_save_cells(self, table_img, temp_dir, output_dir):"""处理表格并保存单元格"""try:# 预处理图像binary = self._preprocess_image(table_img)# cv2.imwrite(os.path.join(output_dir, "binary_table.png"), binary)# 检测表格网格h_lines, v_lines = self._detect_table_cells(binary, table_img.shape, output_dir)# 如果未检测到足够的网格线if len(h_lines) < 2 or len(v_lines) < 2:print("未检测到足够的表格线")return []# 可视化并保存表格网格self._visualize_grid(table_img, h_lines, v_lines, output_dir)# 提取并直接保存单元格image_paths = self._extract_and_save_cells(table_img, h_lines, v_lines, output_dir)return image_pathsexcept Exception as e:print(f"表格处理错误: {str(e)}")return []def _preprocess_image(self, image):"""表格图像预处理"""if image is None:return None# 转为灰度图if len(image.shape) == 3:gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)else:gray = image.copy()# 高斯平滑blurred = cv2.GaussianBlur(gray, (5, 5), 0)# 自适应阈值二值化binary = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 21, 5)# 进行形态学操作,填充小空隙kernel = np.ones((3, 3), np.uint8)binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)return binarydef _detect_table_cells(self, binary_image, image_shape, output_dir):"""检测表格网格,基于图像真实表格线精确定位"""height, width = image_shape[:2]# 1. 先检测水平线horizontal_size = width // 10horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (horizontal_size, 1))horizontal_lines = cv2.morphologyEx(binary_image, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)# 提取水平线坐标h_coords = []h_projection = np.sum(horizontal_lines, axis=1)for i in range(1, len(h_projection) - 1):if h_projection[i] > h_projection[i - 1] and h_projection[i] > h_projection[i + 1] and h_projection[i] > width // 5:h_coords.append(i)# 使用聚类合并相近的水平线h_coords = self._cluster_coordinates(h_coords, eps=height // 30)# 2. 确保我们至少有足够的水平线定义表格区域if len(h_coords) < 2:print("警告: 水平线检测不足,无法确定表格范围")h_lines = [(0, int(y), width, int(y)) for y in h_coords]v_lines = []return h_lines, v_lines# 获取表格垂直范围h_coords.sort()table_top = int(h_coords[0])table_bottom = int(h_coords[-1])# 3. 增强竖线检测 - 使用多尺度检测策略# 使用不同大小的结构元素进行竖线检测vertical_kernels = [cv2.getStructuringElement(cv2.MORPH_RECT, (1, (table_bottom - table_top) // 12)),  # 细线cv2.getStructuringElement(cv2.MORPH_RECT, (1, (table_bottom - table_top) // 8)),  # 中等cv2.getStructuringElement(cv2.MORPH_RECT, (1, (table_bottom - table_top) // 4))  # 粗线]# 合并不同尺度的检测结果vertical_lines = np.zeros_like(binary_image)for kernel in vertical_kernels:v_lines = cv2.morphologyEx(binary_image, cv2.MORPH_OPEN, kernel, iterations=1)vertical_lines = cv2.bitwise_or(vertical_lines, v_lines)# 只关注表格区域内的竖线table_region_v = vertical_lines[table_top:table_bottom, :]# 计算列投影col_sum = np.sum(table_region_v, axis=0)# 4. 更精准地寻找竖线位置# 使用自适应阈值计算local_max_width = width // 25  # 更精细的局部最大值搜索窗口threshold_ratio = 0.15  # 降低阈值以捕获更多可能的竖线# 自适应阈值计算threshold = np.max(col_sum) * threshold_ratio# 扫描所有列查找峰值v_coords_raw = []i = 0while i < len(col_sum):# 查找局部范围内的峰值local_end = min(i + local_max_width, len(col_sum))local_peak = i# 找到局部最大值for j in range(i, local_end):if col_sum[j] > col_sum[local_peak]:local_peak = j# 如果局部最大值大于阈值,认为是竖线if col_sum[local_peak] > threshold:v_coords_raw.append(local_peak)# 跳过已处理的区域i = local_peak + local_max_width // 2else:i += 1# 5. 去除过于接近的竖线(可能是同一条线被重复检测)v_coords_detected = self._cluster_coordinates(v_coords_raw, eps=width // 50)  # 使用更小的合并阈值# 6. 检查找到的竖线数量expected_vlines = 15  # 预期应有15条竖线print(f"初步检测到竖线数量: {len(v_coords_detected)}")# 7. 处理识别结果if len(v_coords_detected) > 0:# 7.1 获取表格的左右边界v_coords_detected.sort()  # 确保按位置排序# 竖线位置修正:解决偏左问题 - 检测实际线条中心位置v_coords_corrected = []for idx, v_coord in enumerate(v_coords_detected):# 在竖线坐标附近寻找准确的线条中心# 对于第2-11根竖线,使用更大的搜索范围向右偏移if 1 <= idx <= 10:  # 第2-11根竖线search_range_left = 2  # 左侧搜索范围更小search_range_right = 12  # 右侧搜索范围大幅增大else:search_range_left = 5search_range_right = 5left_bound = max(0, v_coord - search_range_left)right_bound = min(width - 1, v_coord + search_range_right)if left_bound < right_bound and left_bound < len(col_sum) and right_bound < len(col_sum):# 在搜索范围内找到峰值中心位置# 对于第2-11根竖线,使用加权平均来偏向右侧if 1 <= idx <= 10:# 计算加权平均,右侧权重更大window = col_sum[left_bound:right_bound+1]weights = np.linspace(0.3, 2.0, len(window))  # 更强的右侧权重weighted_window = window * weightsmax_pos = left_bound + np.argmax(weighted_window)# 强制向右偏移2-3像素max_pos += 3max_pos = min(right_bound, max_pos)else:max_pos = left_bound + np.argmax(col_sum[left_bound:right_bound+1])v_coords_corrected.append(max_pos)else:v_coords_corrected.append(v_coord)# 使用修正后的坐标v_coords_detected = v_coords_correctedleft_bound = v_coords_detected[0]  # 最左边的竖线right_bound = v_coords_detected[-1]  # 最右边的竖线# 7.2 计算理想的等距离竖线位置,但使第一列宽度比其他列宽ideal_vlines = []# 设置第一列的宽度为其他列的1.5倍first_column_width_ratio = 1.3# 计算除第一列外每列的宽度remaining_width = right_bound - left_boundregular_column_width = remaining_width / (expected_vlines - 1 + (first_column_width_ratio - 1))# 设置第一列ideal_vlines.append(int(left_bound))# 设置第二列位置ideal_vlines.append(int(left_bound + regular_column_width * first_column_width_ratio))# 设置剩余列for i in range(2, expected_vlines):ideal_vlines.append(int(left_bound + regular_column_width * (i + (first_column_width_ratio - 1))))# 7.3 使用修正后的列位置v_coords = ideal_vlines# 进一步向右偏移第2-11根竖线(总共15根)for i in range(1, 11):if i < len(v_coords):v_coords[i] += 3  # 向右偏移3像素else:# 如果没有检测到竖线,使用预估等距离print("未检测到任何竖线,使用预估等距离")left_bound = width // 10right_bound = width * 9 // 10# 计算除第一列外每列的宽度first_column_width_ratio = 1.5remaining_width = right_bound - left_boundregular_column_width = remaining_width / (expected_vlines - 1 + (first_column_width_ratio - 1))# 设置列位置v_coords = []v_coords.append(int(left_bound))v_coords.append(int(left_bound + regular_column_width * first_column_width_ratio))for i in range(2, expected_vlines):v_coords.append(int(left_bound + regular_column_width * (i + (first_column_width_ratio - 1))))# 8. 检验最终的竖线位置是否合理if len(v_coords) == expected_vlines:# 计算相邻竖线间距spacings = [v_coords[i + 1] - v_coords[i] for i in range(len(v_coords) - 1)]avg_spacing = sum(spacings[1:]) / len(spacings[1:])  # 不计入第一列的宽度# 检查是否有间距异常的竖线(除第一列外)for i in range(1, len(spacings)):if abs(spacings[i] - avg_spacing) > avg_spacing * 0.2:  # 如果间距偏差超过20%print(f"警告: 第{i + 1}和第{i + 2}竖线之间间距异常, 实际:{spacings[i]}, 平均:{avg_spacing}")# 如果是最后一个间距异常,可能是最后一条竖线位置不准if i == len(spacings) - 1:v_coords[-1] = v_coords[-2] + int(avg_spacing)print(f"修正最后一条竖线位置: {v_coords[-1]}")# 9. 转换为线段表示h_lines = [(0, int(y), width, int(y)) for y in h_coords]v_lines = [(int(x), int(table_top), int(x), int(table_bottom)) for x in v_coords]# 10. 强制补充缺失的水平线 - 期望有5条水平线(4行表格)if len(h_lines) < 5 and len(h_lines) >= 2:h_lines.sort(key=lambda x: x[1])top_y = int(h_lines[0][1])bottom_y = int(h_lines[-1][1])height_range = bottom_y - top_y# 计算应有的4等分位置expected_y_positions = [top_y + int(height_range * i / 4) for i in range(1, 4)]# 添加缺失的水平线new_h_lines = list(h_lines)for y_pos in expected_y_positions:# 检查是否已存在接近该位置的线exist = Falsefor line in h_lines:if abs(line[1] - y_pos) < height // 20:exist = Truebreakif not exist:new_h_lines.append((0, int(y_pos), width, int(y_pos)))h_lines = new_h_lines# 11. 最终排序h_lines = sorted(h_lines, key=lambda x: x[1])v_lines = sorted(v_lines, key=lambda x: x[0])print(f"最终水平线数量: {len(h_lines)}")print(f"最终竖线数量: {len(v_lines)}")# 12. 计算并打印竖线间距,用于检验均匀性if len(v_lines) > 1:spacings = []for i in range(len(v_lines) - 1):spacing = v_lines[i + 1][0] - v_lines[i][0]spacings.append(spacing)avg_spacing = sum(spacings[1:]) / len(spacings[1:])  # 不计入第一列的宽度print(f"竖线平均间距: {avg_spacing:.2f}像素")print(f"竖线间距: {spacings}")return h_lines, v_linesdef _cluster_coordinates(self, coords, eps=10):"""合并相近的坐标"""if not coords:return []coords = sorted(coords)clusters = []current_cluster = [coords[0]]for i in range(1, len(coords)):if coords[i] - coords[i - 1] <= eps:current_cluster.append(coords[i])else:clusters.append(int(sum(current_cluster) / len(current_cluster)))current_cluster = [coords[i]]if current_cluster:clusters.append(int(sum(current_cluster) / len(current_cluster)))return clustersdef _visualize_grid(self, image, h_lines, v_lines, output_dir):"""可视化检测到的网格线并保存结果图像"""# 复制原图用于绘制result = image.copy()if len(result.shape) == 2:result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)# 绘制水平线for line in h_lines:x1, y1, x2, y2 = linecv2.line(result, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2)# 绘制垂直线for line in v_lines:x1, y1, x2, y2 = linecv2.line(result, (int(x1), int(y1)), (int(x2), int(y2)), (255, 0, 0), 2)# 绘制交点for h_line in h_lines:for v_line in v_lines:y = h_line[1]x = v_line[0]cv2.circle(result, (int(x), int(y)), 3, (0, 255, 0), -1)# 只保存grid_on_image.pngif output_dir:cv2.imwrite(os.path.join(output_dir, "grid_on_image.png"), result)def _extract_and_save_cells(self, image, h_lines, v_lines, output_dir, margin=3):"""提取单元格并保存到输出目录"""height, width = image.shape[:2]# 确保线条按坐标排序h_lines = sorted(h_lines, key=lambda x: x[1])v_lines = sorted(v_lines, key=lambda x: x[0])# 保存图片路径image_paths = []# 检查线条数量是否足够if len(h_lines) < 4 or len(v_lines) < 10:print(f"警告: 线条数量不足(水平线={len(h_lines)}, 垂直线={len(v_lines)})")if len(h_lines) < 2 or len(v_lines) < 2:print("错误: 线条数量太少,无法提取任何单元格")return image_paths# 记录表格结构print(f"表格结构: {len(h_lines)}行, {len(v_lines) - 1}列")# 创建题号到行列索引的映射question_mapping = {}# 第2行是1-13题(列索引从1开始,0列是题号列)for i in range(1, 14):if i < len(v_lines):question_mapping[i] = (1, i)# 第4行是14-25题(列索引从1开始,0列是题号列)for i in range(14, 26):col_idx = i - 13  # 14题对应第1列,15题对应第2列,...if col_idx < len(v_lines) and 3 < len(h_lines):question_mapping[i] = (3, col_idx)# 提取每道题的单元格saved_questions = []for q_num in range(1, 26):if q_num not in question_mapping:print(f"题号 {q_num} 没有对应的行列索引映射")continuerow_idx, col_idx = question_mapping[q_num]if row_idx >= len(h_lines) - 1 or col_idx >= len(v_lines) - 1:print(f"题号 {q_num} 的行列索引 ({row_idx}, {col_idx}) 超出表格范围")continuetry:# 获取单元格边界x1 = int(v_lines[col_idx][0])y1 = int(h_lines[row_idx][1])x2 = int(v_lines[col_idx + 1][0])y2 = int(h_lines[row_idx + 1][1])# 打印单元格信息用于调试if q_num in [1, 4, 13, 14, 25]:  # 打印关键单元格的位置信息print(f"题号 {q_num} 单元格: x1={x1}, y1={y1}, x2={x2}, y2={y2}, 宽={x2 - x1}, 高={y2 - y1}")# 添加边距,避免包含边框线x1_m = min(width - 1, max(0, x1 + margin))y1_m = min(height - 1, max(0, y1 + margin))x2_m = max(0, min(width, x2 - margin))y2_m = max(0, min(height, y2 - margin))# 检查单元格尺寸if x2_m <= x1_m or y2_m <= y1_m or (x2_m - x1_m) < 5 or (y2_m - y1_m) < 5:print(f"跳过无效单元格: 题号 {q_num}, 尺寸过小")continue# 提取单元格cell_img = image[y1_m:y2_m, x1_m:x2_m].copy()# 检查单元格是否为空图像if cell_img.size == 0 or cell_img.shape[0] == 0 or cell_img.shape[1] == 0:print(f"跳过空单元格: 题号 {q_num}")continue# 保存单元格图片cell_filename = f'cell_0{q_num:02d}.png'cell_path = os.path.join(output_dir, cell_filename)cv2.imwrite(cell_path, cell_img)# 添加到路径列表和已保存题号列表image_paths.append(cell_path)saved_questions.append(q_num)except Exception as e:print(f"提取题号 {q_num} 时出错: {str(e)}")print(f"已保存 {len(saved_questions)} 个单元格,题号: {sorted(saved_questions)}")return image_pathsdef process_image(input_image_path, output_dir):"""处理试卷答题区,提取表格并保存单元格"""processor = AnswerSheetProcessor()return processor.process(input_image_path, output_dir)def main():"""主函数:解析命令行参数并执行处理流程"""# 解析命令行参数parser = argparse.ArgumentParser(description='试卷答题区表格处理工具')parser.add_argument('--image', type=str, default="./images/12.jpg", help='输入图像路径')parser.add_argument('--output', type=str, default="./output", help='输出目录')args = parser.parse_args()# 检查图像是否存在if not os.path.exists(args.image):print(f"图像文件不存在: {args.image}")return 1# 确保输出目录存在os.makedirs(args.output, exist_ok=True)print(f"开始处理图像: {args.image}")print(f"输出目录: {args.output}")# 处理图像try:image_paths = process_image(args.image, args.output)if image_paths:print(f"处理成功: 共生成{len(image_paths)}个单元格图片")print(f"所有结果已保存到: {args.output}")return 0else:print("处理失败")return 1except Exception as e:print(f"处理过程中发生错误: {str(e)}")return 1if __name__ == "__main__":sys.exit(main())

5. 应用场景

  1. 考试批阅系统:大规模考试的答题卡批阅
  2. 教育测评平台:智能化教育测评系统
  3. 试卷数字化处理:将纸质试卷转换为电子数据
  4. 教学检测系统:快速评估学生答题情况

6. 算法效果展示

在这里插入图片描述
上图是测试的试卷图片,要求提取出填写的答题区。

在这里插入图片描述

上图展示了表格网格识别的效果,蓝色线条表示竖线,红色线条表示水平线,绿色点表示线条交点。
在这里插入图片描述
上图是从试卷中提取出的答案单元格。

7. 总结与展望

本文介绍的试卷答题区表格识别技术,通过计算机视觉算法实现了高效准确的表格定位和单元格提取。该技术有以下优势:

  1. 高精度:采用多尺度策略和位置校正算法,提高了表格线识别的精度
  2. 高适应性:能够处理不同样式的试卷答题区
  3. 高效率:自动化处理流程大幅提高了试卷处理效率

未来我们将继续优化算法,提高对更复杂表格的识别能力,并结合OCR技术实现答案内容的自动识别。

参考资料

  1. OpenCV官方文档: https://docs.opencv.org/
  2. 数字图像处理 - 冈萨雷斯
  3. 计算机视觉:算法与应用 - Richard Szeliski

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/904634.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringAI实现AI应用-自定义顾问(Advisor)

SpringAI实战链接 1.SpringAl实现AI应用-快速搭建-CSDN博客 2.SpringAI实现AI应用-搭建知识库-CSDN博客 3.SpringAI实现AI应用-内置顾问-CSDN博客 4.SpringAI实现AI应用-使用redis持久化聊天记忆-CSDN博客 5.SpringAI实现AI应用-自定义顾问&#xff08;Advisor&#xff09…

【HarmonyOS 5】App Linking 应用间跳转详解

目录 什么是 App Linking 使用场景 工作原理 如何开发 1.开通 App Linking 2.确定域名 3.服务端部署 applinking.json 文件 4.AGC绑定域名 5.项目配置 6.组装聚合链接 7.解析聚合链接中的参数 其他 如何获取应用ID 什么是 App Linking App Linking 是一款创建跨…

什么是变量提升?(形象的比喻)

当然&#xff01;可以用几个生活中的比喻来形象地解释变量提升&#xff1a; ​​1. 书架的占位符​​ 想象你有一个书架&#xff0c;但还没放书。 • 变量提升&#xff08;var&#xff09;&#xff1a; 你先在书架上贴了一个标签&#xff08;比如写“我的书”&#xff09;&…

C++面向对象编程入门:从类与对象说起(一)

C语言是面向过程&#xff0c;关注的是过程&#xff0c;分析出求解问题的步骤&#xff0c;通过函数调用逐步解决问题&#xff0c;而C面向的是对象&#xff0c;关注的是对象&#xff0c;将一件事拆解成多个对象&#xff0c;靠对象之间互交完成。 目录 类的定义 类的两种定义 …

uniapp tabBar 中设置“custom“: true 在H5和app中无效解决办法

uniapp小程序自定义底部tabbar&#xff0c;但是在转成H5和app时发现"custom": true 无效&#xff0c;原生tabbar会显示出来 解决办法如下 在tabbar的list中设置 “visible”:false 代码如下&#xff1a;"tabBar": {"custom": true,//"cust…

SpringBoot学生操行评分系统源码设计开发

概述 基于SpringBoot框架开发的学生操行评分系统完整项目&#xff0c;该系统采用主流技术栈开发&#xff0c;包含完善的评分管理功能模块&#xff0c;是学校管理、教育培训机构理想的数字化解决方案&#xff0c;非常适合作为设计参考或二次开发基础项目。 主要内容 5.1 管理…

从代码学习深度学习 - 单发多框检测(SSD)PyTorch版

文章目录 前言工具函数数据处理工具 (`utils_for_data.py`)训练工具 (`utils_for_train.py`)检测相关工具 (`utils_for_detection.py`)可视化工具 (`utils_for_huitu.py`)模型类别预测层边界框预测层连接多尺度预测高和宽减半块基础网络块完整的模型训练模型读取数据集和初始化…

基于STM32的温湿度光照强度仿真设计(Proteus仿真+程序设计+设计报告+讲解视频)

这里写目录标题 **1.****主要功能****2.仿真设计****3.程序设计****4.设计报告****5.下载链接** 基于STM32的温湿度光照强度仿真设计(Proteus仿真程序设计设计报告讲解视频&#xff09; 仿真图Proteus 8.9 程序编译器&#xff1a;keil 5 编程语言&#xff1a;C语言 设计编号…

SSH 服务部署指南

本指南涵盖 OpenSSH 服务端的安装、配置密码/公钥/多因素认证&#xff0c;以及连接测试方法。 适用系统&#xff1a;Ubuntu/Debian、CentOS/RHEL 等主流 Linux 发行版。 1. 安装 SSH 服务端 Ubuntu/Debian # 更新软件包索引 sudo apt update# 安装 OpenSSH 服务端 sudo apt i…

《Python星球日记》 第46天:决策树与随机森林

名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏:《Python星球日记》,限时特价订阅中ing 目录 一、前言二、决策树算法原理1. 决策树简介2. 决策树的分裂准则(1) 信息熵与信息增益(2) 基尼不纯…

Vue2:引入公共JS,通过this调用

tools.js // 图片加上前缀 baseurl 是请求域名 img 是图片路径export function getimgurl(img) {return ${this.$baseurl}${img}}main.js import baseUrl from "/api/baseUrl.js" Vue.prototype.$baseurl baseUrlimport {getimgurl} from /api/tool.js; Vue.protot…

【Hot 100】 146. LRU 缓存

目录 引言LRU 缓存官方解题LRU实现&#x1f4cc; 实现步骤分解步骤 1&#xff1a;定义双向链表节点步骤 2&#xff1a;创建伪头尾节点&#xff08;关键设计&#xff09;步骤 3&#xff1a;实现链表基础操作操作 1&#xff1a;添加节点到头部操作 2&#xff1a;移除任意节点 步骤…

【Linux】swap交换分区管理

目录 一、Swap 交换分区的功能 二、swap 交换分区的典型大小的设置 2.1 查看交换分区的大小 2.1.1 free 2.1.2 cat /proc/swaps 或 swapon -s 2.1.3 top 三、使用交换分区的整体流程 3.1 案例一 3.2 案例二 一、Swap 交换分区的功能 计算机运行一个程序首先会将外存&am…

【计算机网络】用户从输入网址到网页显示,期间发生了什么?

1.URL解析 浏览器分解URL&#xff1a;https://www.example.com/page 协议&#xff1a;https域名&#xff1a;www.example.com路径&#xff1a;/page 2.DNS查询&#xff1a; 浏览器向DNS服务器发送查询请求&#xff0c;将域名解析为对应的IP地址。 3.CDN检查(如果有)&#…

架空输电线巡检机器人轨迹优化设计

架空输电线巡检机器人轨迹优化 摘要 本论文针对架空输电线巡检机器人的轨迹优化问题展开研究,综合考虑输电线复杂环境、机器人运动特性及巡检任务需求,结合路径规划算法、智能优化算法与机器人动力学约束,构建了多目标轨迹优化模型。通过改进遗传算法与模拟退火算法,有效…

根据窗口大小自动调整页面缩放比例,并保持居中显示

vue 项目 直接上代码 图片u1.png 是个背景图片 图片u2.png 是个遮罩 <template><div id"app"><div class"viewBox"><divclass"screen":style"{ transform: translate(-50%,-50%…

初学Python爬虫

文章目录 前言一、 爬虫的初识1.1 什么是爬虫1.2 爬虫的核心1.3 爬虫的用途1.4 爬虫分类1.5 爬虫带来的风险1.6. 反爬手段1.7 爬虫网络请求1.8 爬虫基本流程 二、urllib库初识2.1 http和https协议2.2 编码解码的使用2.3 urllib的基本使用2.4 一个类型六个方法2.5 下载网页数据2…

oracle 数据库sql 语句处理过程

14.1SQL语句处理过程 在进行SQL语句处理优化前&#xff0c;需要先熟悉和了解SQL语句的处理过程。 每种类型的语句在执行时都需要如下阶段&#xff1a; 第1步: 创建游标。 第2步: 分析语句。 第5步: 绑定变量。 第7步: t运行语句。 第9步: 关闭游标。 如果使用了并行功能&#x…

pm2 list查询服务时如何通过name或者namespace进行区分

在 PM2 中&#xff0c;如果 pm2 list 显示的所有服务名称&#xff08;name&#xff09;相同&#xff0c;就无法直观地区分不同的进程。这时可以通过 --namespace&#xff08;命名空间&#xff09; 或 自定义 name 来区分服务。以下是解决方案&#xff1a; 方法 1&#xff1a;启…

[python] 函数基础

二 函数参数 2.1 必备参数(位置参数) 含义: 传递和定义参数的顺序及个数必须一致 格式: def func(a,b) def func_1(id,passwd):print("id ",id)print("passwd ",passwd) func_1(10086,123456) 2.2 默认参数 函数: 为函数的参数提供一个默认值,如果调…