时隔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))
- 提取当前小方格的图像,并对方格图像进行预处理操作,这里用一个函数
extract_grid()
实现,具体代码见下文。函数返回方格图像、方格二值图像、方格二值图像非零像素数。 - 条件1,判断方格二值图像的非零像素数是否大于阈值,若大于阈值,认为可能存在数字。
- 使用
find_biggest_bounding_box()
函数找出数字的外接矩形,具体代码见下文。函数返回外接矩形的左上坐标、宽度、高度。 - 计算矩形中心,计算矩形中心与方格中心距离。
- 条件2,判断矩形中心是否离方格中心足够近,若小于阈值(方格宽度的四分之一),认为存在数字。
- 使用 Python 的切片取出方格中数字,之后扩充数字图像为正方形,正方形边长取长宽较大者。
- 将数字缩放为统一的标准大小。注意! cv2.resize() 函数在缩放二值化的图像时,默认插值方法输出不再是二值图像,若令参数
interpolation=cv2.INTER_NEAREST
输出还是二值图像,但因为像素较低效果并不好。因此涉及缩放的地方均使用原图而不是使用二值化后的图像。 - 最后,转换为一维数组返回。
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]
- 取出坐标为 (x,y) 的方格图像 im_number。使用Python中的二维数组切片功能,类似OpenCV中感兴趣区域(ROI)功能,可以方便地提取图像的某一部分,对图像进行分割。
- 方格图像自适应二值化。
- 由于校正后图像中有白色边框,在分割后会在每一个方格四周存在白色像素,需要去除离中心较远的像素点(排除边框干扰)。这里遍历小方格每一个像素,判断该像素与中心的距离,远于阈值的将其值设为0,简单来说就是以方格中心为圆心画一个圆,圆内的保留,圆外的去除。
- 使用
cv2.countNonZero()
统计现在方格中的非零像素数。 - 将当前坐标方格图像和非零像素数返回。
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]
- 对方格二值图像使用轮廓检测。
- 遍历找到的轮廓,使用
cv2.boundingRect()
去轮廓的外接矩形, 求外接矩形面积,找出最大的面积。 - 将外接矩形扩大一个像素,有利于识别,返回外接矩形坐标、长、宽。
效果展示
通过以上步骤,可找出哪些方格有数字,并将数字存储,如下图所示。
1->2 去除了四周的边框干扰;
2->3 找出外接矩形面积最大的轮廓并提取;
3->4 扩充为正方形,再进行缩放二值化等;
最终数字提取结果如图所示。
提取了数字,并统一成标准大小,下一步就可以进行数字识别了。事实上数字提取的过程还有一些需要改进的地方:比如从原图上提取出的数字,未缩放前为29x29,而标准大小定义为20x20,这样一缩放就会损失信息,有可能影响识别精度,等等。