时隔10个月的时间,我终于决定继续更新这个系列博客……
所有代码已经放在GitHub,包括用mnist数据集做手写识别的尝试,文档和注释等正在完善,程序的结构也在优化。

在上一篇文章中得到了清晰、标准的数独问题图像,为下一步提取并识别数字做好了准备。

处理过程

数字提取的主要步骤是:将含有数字的图像,分为 9*9 即 81 个大小相同的方格,遍历这个81个位置,判断每个方格中是否有数字,记录数字所在位置(0~80),保存在 indexes_numbers 中,数字储存在数组sudoku中,主要的代码如下:

# 识别并记录序号 (SUDOKU_SIZE = 9)
indexes_numbers = []
for i in range(SUDOKU_SIZE):
    for j in range(SUDOKU_SIZE):
        img_number = img_puzzle[i * GRID_HEIGHT:(i + 1) * GRID_HEIGHT][:, j * GRID_WIDTH:(j + 1) * GRID_WIDTH]
        hasNumber, sudoku[i * 9 + j, :] = extractNumber.recognize_number(img_number, i, j)
        if hasNumber:
            indexes_numbers.append(i * 9 + j)

Recognize_number() 函数

数字提取部分的核心函数就是 Recognize_number(),用来判断方格中是否有数字,并存储数字,代码如下:

def recognize_number(im_number, x, y):
    """
    判断当前方格是否存在数字并存储该数字
    :param im_number: 方格图像
    :param x: 方格横坐标 (0~9)
    :param y: 方格纵坐标 (0~9)
    :return: 是否有数字,数字的一维数组
    """
    # 提取并处理方格图像
    [im_number_thresh, n_active_pixels] = extract_grid(im_number)

    # 条件1:非零像素大于设定的最小值
    if n_active_pixels > N_MIN_ACTIVE_PIXELS:

        # 找出外接矩形
        [x_b, y_b, w, h] = find_biggest_bounding_box(im_number_thresh)

        # 计算矩形中心与方格中心距离
        cX = x_b + w // 2
        cY = y_b + h // 2
        d = np.sqrt(np.square(cX - GRID_WIDTH // 2) + np.square(cY - GRID_HEIGHT // 2))

        # 条件2: 外接矩形中心与方格中心距离足够小
        if d < GRID_WIDTH // 4:

            # 取出方格中数字
            number_roi = im_number[y_b:y_b + h, x_b:x_b + w]

            # 扩充数字图像为正方形,边长取长宽较大者
            h1, w1 = np.shape(number_roi)
            if h1 > w1:
                number = np.zeros(shape=(h1, h1))
                number[:, (h1 - w1) // 2:(h1 - w1) // 2 + w1] = number_roi
            else:
                number = np.zeros(shape=(w1, w1))
                number[(w1 - h1) // 2:(w1 - h1) // 2 + h1, :] = number_roi

            # 将数字缩放为标准大小
            number = cv2.resize(number, (NUM_WIDTH, NUM_HEIGHT), interpolation=cv2.INTER_LINEAR)
			# 二值化
            retVal, number = cv2.threshold(number, 50, 255, cv2.THRESH_BINARY)

            # 转换为1维数组并返回
            return True, number.reshape(1, NUM_WIDTH * NUM_HEIGHT)

    # 没有数字,则返回全零1维数组
    return False, np.zeros(shape=(1, NUM_WIDTH * NUM_HEIGHT))
  1. 提取当前小方格的图像,并对方格图像进行预处理操作,这里用一个函数 extract_grid() 实现,具体代码见下文。函数返回方格图像、方格二值图像、方格二值图像非零像素数
  2. 条件1,判断方格二值图像的非零像素数是否大于阈值,若大于阈值,认为可能存在数字。
  3. 使用 find_biggest_bounding_box() 函数找出数字的外接矩形,具体代码见下文。函数返回外接矩形的左上坐标、宽度、高度
  4. 计算矩形中心,计算矩形中心与方格中心距离。
  5. 条件2,判断矩形中心是否离方格中心足够近,若小于阈值(方格宽度的四分之一),认为存在数字。
  6. 使用 Python 的切片取出方格中数字,之后扩充数字图像为正方形,正方形边长取长宽较大者。
  7. 将数字缩放为统一的标准大小。注意! cv2.resize() 函数在缩放二值化的图像时,默认插值方法输出不再是二值图像,若令参数 interpolation=cv2.INTER_NEAREST 输出还是二值图像,但因为像素较低效果并不好。因此涉及缩放的地方均使用原图而不是使用二值化后的图像。
  8. 最后,转换为一维数组返回。

extract_grid() 函数

该函数用来提取小方格的图像,然后对其进行预处理。

def extract_grid(im_number):
    """
    将校正后图像划分为9x9的棋盘,取出小方格;二值化;去除离中心较远的像素(排除边框干扰);统计非零像素数(判断方格中是否有数字)
    :param im_number: 方格图像
    :param x: 方格横坐标 (0~9)
    :param y: 方格纵坐标 (0~9)
    :return: im_number_thresh: 二值化及处理后图像
             n_active_pixels: 非零像素数
    """

    # 二值化
    retVal, im_number_thresh = cv2.threshold(im_number, 150, 255, cv2.THRESH_BINARY)

    # 去除离中心较远的像素点(排除边框干扰)
    for i in range(im_number.shape[0]):
        for j in range(im_number.shape[1]):
            dist_center = np.sqrt(np.square(GRID_WIDTH // 2 - i) + np.square(GRID_HEIGHT // 2 - j))
            if dist_center > GRID_WIDTH // 2 - 2:
                im_number_thresh[i, j] = 0

    # 统计非零像素数,以判断方格中是否有数字
    n_active_pixels = cv2.countNonZero(im_number_thresh)

    return [im_number_thresh, n_active_pixels]
  1. 取出坐标为 (x,y) 的方格图像 im_number。使用Python中的二维数组切片功能,类似OpenCV中感兴趣区域(ROI)功能,可以方便地提取图像的某一部分,对图像进行分割。
  2. 方格图像自适应二值化。
  3. 由于校正后图像中有白色边框,在分割后会在每一个方格四周存在白色像素,需要去除离中心较远的像素点(排除边框干扰)。这里遍历小方格每一个像素,判断该像素与中心的距离,远于阈值的将其值设为0,简单来说就是以方格中心为圆心画一个圆,圆内的保留,圆外的去除。
  4. 使用 cv2.countNonZero() 统计现在方格中的非零像素数。
  5. 将当前坐标方格图像和非零像素数返回。

find_biggest_bounding_box() 函数

该函数用来找出数字的外接矩形,输入参数为二值图像,输出为外接矩形的左上坐标和宽度、高度。

def find_biggest_bounding_box(im_number_thresh):
    """
    找出小方格中外接矩形面积最大的轮廓,返回其外接矩形参数
    :param im_number_thresh: 当前方格的二值化及处理后图像
    :return: 外接矩形参数(左上坐标及长和宽)
    """
    # 轮廓检测
    b, contour, hierarchy1 = cv2.findContours(im_number_thresh.copy(),
                                              cv2.RETR_EXTERNAL,
                                              cv2.CHAIN_APPROX_SIMPLE)
    # 找出外接矩形面积最大的轮廓
    biggest_bound_rect = []
    bound_rect_max_size = 0
    for i in range(len(contour)):
        bound_rect = cv2.boundingRect(contour[i])
        size_bound_rect = bound_rect[2] * bound_rect[3]
        if size_bound_rect > bound_rect_max_size:
            bound_rect_max_size = size_bound_rect
            biggest_bound_rect = bound_rect

    # 将外接矩形扩大一个像素
    x_b, y_b, w, h = biggest_bound_rect
    x_b = x_b - 1
    y_b = y_b - 1
    w = w + 2
    h = h + 2
    return [x_b, y_b, w, h]
  1. 对方格二值图像使用轮廓检测。
  2. 遍历找到的轮廓,使用 cv2.boundingRect() 去轮廓的外接矩形, 求外接矩形面积,找出最大的面积。
  3. 将外接矩形扩大一个像素,有利于识别,返回外接矩形坐标、长、宽。

效果展示

通过以上步骤,可找出哪些方格有数字,并将数字存储,如下图所示。

python提取数字并输出 python数字提取问题_python提取数字并输出

1->2 去除了四周的边框干扰;

2->3 找出外接矩形面积最大的轮廓并提取;

3->4 扩充为正方形,再进行缩放二值化等;

最终数字提取结果如图所示。

python提取数字并输出 python数字提取问题_opencv_02

提取了数字,并统一成标准大小,下一步就可以进行数字识别了。事实上数字提取的过程还有一些需要改进的地方:比如从原图上提取出的数字,未缩放前为29x29,而标准大小定义为20x20,这样一缩放就会损失信息,有可能影响识别精度,等等。