You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

扫描图像按行统计已填充单元格数量的技术实现难题

核心需求

按行统计网格中已填充(hatched)单元格的数量:每个单元格对应5分钟,每行对应一项操作;输出需包含所有行的填充单元格数量(一维/二维列表均可),可通过行号关联操作名称,需适配多份填充情况不同的文档。

当前进展

已完成PDF导入、图像纠偏、裁剪,仅保留目标区域;现有mask处理代码如下:

import cv2
import numpy as np

image = cv2.imread('result.jpg')
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
se=cv2.getStructuringElement(cv2.MORPH_RECT , (8,8))
bg=cv2.morphologyEx(gray, cv2.MORPH_DILATE, se)
out_gray=cv2.divide(gray, bg, scale=255)

thresh = cv2.threshold(out_gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

blur = cv2.GaussianBlur(thresh,(5,5),0)
kernel = np.ones((3, 3), np.uint8)
mask = cv2.erode(blur, kernel, iterations=2)

执行后得到对应mask图像,但仍存在识别问题。

遇到的困难

  • 无法准确识别网格结构,无法遍历每个单元格;原计划识别网格后通过轮廓内黑白占比判断填充单元格,但网格识别受阻。
  • 尝试过Canny边缘检测、HoughLines直线检测,仅能捕获网格左侧线条或导致图像布满冗余红线;参考常规网格行列统计方案,仍无法忽略擦除痕迹、处理网格不规则问题,遍历易丢失单元格位置。
  • 考虑过生成标准网格(如复制第3行并偏移多次)但不知实现方法,请求可行技术方案。

注:单元格上方的手写数字为噪声,可忽略;示例图像中第12行有29个填充单元格,对应2.42小时工时。


可行技术方案

方法1:行分割+列投影统计(绕开网格识别)

适合网格行对齐较好的场景,直接按行分割后统计填充单元格数量:

  1. 行分割:通过行投影定位每行边界
# 计算每行像素和,过滤空白行
row_proj = np.sum(mask, axis=1)
row_threshold = np.max(row_proj) * 0.1
row_indices = np.where(row_proj > row_threshold)[0]

# 合并连续行索引,得到每行上下边界
rows = []
current_start = row_indices[0]
for i in range(1, len(row_indices)):
    if row_indices[i] != row_indices[i-1] + 1:
        rows.append((current_start, row_indices[i-1]))
        current_start = row_indices[i]
rows.append((current_start, row_indices[-1]))
  1. 每行列投影统计填充数:通过列投影峰值判断填充单元格
fill_counts = []
for (y_start, y_end) in rows:
    row_img = mask[y_start:y_end+1, :]
    col_proj = np.sum(row_img, axis=0)
    col_threshold = np.max(col_proj) * 0.3
    col_indices = np.where(col_proj > col_threshold)[0]
    
    if len(col_indices) == 0:
        fill_counts.append(0)
        continue
    
    # 统计连续高投影段的数量(即填充单元格数)
    count = 1
    for i in range(1, len(col_indices)):
        if col_indices[i] > col_indices[i-1] + 5:  # 单元格间距阈值可调整
            count += 1
    fill_counts.append(count)

方法2:标准网格模板匹配

若某一行网格线清晰,可提取该行网格作为模板,应用到所有行:

  1. 提取参考行网格分割点(示例取第3行)
# 获取参考行(假设第3行网格清晰,对应rows[2])
ref_y_start, ref_y_end = rows[2]
ref_row = mask[ref_y_start:ref_y_end+1, :]
ref_col_proj = np.sum(ref_row, axis=0)

# 定位网格线列索引(低投影区域为网格线)
grid_cols = np.where(ref_col_proj < np.max(ref_col_proj)*0.1)[0]

# 去重得到网格分割点
unique_grid_cols = []
current_col = grid_cols[0]
for col in grid_cols[1:]:
    if col - current_col > 2:
        unique_grid_cols.append((current_col + col)//2)
        current_col = col
unique_grid_cols.append(current_col)
  1. 应用模板到所有行
fill_counts = []
for (y_start, y_end) in rows:
    row_img = mask[y_start:y_end+1, :]
    count = 0
    for i in range(len(unique_grid_cols)-1):
        col_start = unique_grid_cols[i]
        col_end = unique_grid_cols[i+1]
        if col_end >= row_img.shape[1]:
            col_end = row_img.shape[1]-1
        
        cell = row_img[:, col_start:col_end]
        fill_ratio = np.sum(cell == 255) / (cell.shape[0]*cell.shape[1])
        if fill_ratio > 0.2:  # 填充占比阈值可调整
            count += 1
    fill_counts.append(count)

方法3:优化mask减少噪声干扰

在原mask处理后添加两步操作,去除擦除痕迹和手写数字:

# 开运算去除小噪声点
kernel_open = np.ones((5,5), np.uint8)
mask_clean = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel_open)

# 垂直闭运算去除横向手写数字
kernel_vertical = np.ones((15,1), np.uint8)
mask_clean = cv2.morphologyEx(mask_clean, cv2.MORPH_CLOSE, kernel_vertical)

内容的提问来源于stack exchange,提问作者Doons

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 15:15:27