前言
哈喽,小刀又来咕咕咕了,上次我们讲到利用 DFS来解决九宫格填数的问题,还说要做一个比较完整的小项目,好滴,这个坑挖好了,今天继续填!
友情提醒:今天的推文不短且有点硬核风,希望想认真看的小朋友找个空闲的时间认真阅读~
这次我们来讲解怎么用比较通用性的图像处理方法把一张九宫格题目的图片裁剪出我们想要的含有数字的区域
必备库+导入图片
这次我们需要用到的出装有:
import os
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
假设我们有下面的一张图:
记住我们的目的是为了裁剪出我们需要的数字,这样方方正正的图自然很好处理,等下我也会展示怎么处理有畸变的图像
首先我们读取该图像,然后进行取反(一般希望目标是白色,背景是黑色,方便后续操作)和二值操作(像canny或者findContours这些函数都是需要二值滴)
# 读取图像
img = cv.imread(img_name)
# cvtColor进行色彩空间转换:彩色图到灰色图
gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 取反灰度值
gray_img = ~gray_img
# 二值化阈值
threshold = 127 # np.mean(gray_img)
# 二值化
ret, thresh_img = cv.threshold(gray_img, 127, 255, cv.THRESH_BINARY)
结果如下:
根据九宫格的特点就是行列组成规整的矩形,所以我们只要找到九宫格所在的矩形框,并且平铺没有明显畸变,就可以来分割啦(案板上的九宫格,任你宰割
那就又回到CV里经典到不能再经典的轮廓查找,矩形框角点获取和直线检测等问题啦
轮廓检测+角点获取
# 轮廓查找函数(具体函数用法大家可以官网查询:)
contours,hierarchy = cv.findContours(erode_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
# 一般是最长的轮廓,即九宫格最外边的大矩形
# 画一下矩形框看看对不对
img_copy = cv.cvtColor(np.copy(thresh_img),cv.COLOR_GRAY2BGR)
img_copy_contour = cv.drawContours(img_copy,contours,np.argmax(contours_len),(0,0,255,),3)
结果如下:
获取四个顶点也是有函数滴,可以用角点检测,但是对这种方方正正的,我们直接用最小外包矩形也可以得到比较好的效果:
min_rect = cv.minAreaRect(contours[np.argmax(contours_len)])
rect_points = cv.boxPoints(min_rect).tolist()
sum_p = [sum(p) for p in rect_points]
# tl:top_left tr:top_right
# br:bottom_right bl:bottom_left
# 左上顶点坐标和最小,右下顶点坐标和最大
tl = rect_points[sum_p.index(min(sum_p))]
br = rect_points[sum_p.index(max(sum_p))]
# 剩下的顶点为左下和右上,根据x的大小区分
temp = sorted([r for r in rect_points if r != tl and r != br], key=lambda x: x[0])
bl = temp[0]
tr = temp[1]
# re_group
rect_points = [tl, tr, br, bl]
print(rect_points)
"""
[output]
[[12.0, 16.0], [534.0, 16.0], [534.0, 536.0], [12.0, 536.0]]
"""
我们来看看找得对不对:
for r in rect_points:
cv.circle(img_copy, tuple(r), 5, (0,0,255), 3)
plt.figure(1)
plt.title('check corner coordinates')
plt.imshow(cv.cvtColor(img_copy, cv.COLOR_BGR2RGB))
plt.show()
接下来我们来看看有线性畸变的图像怎么处理,比如下面这张图:
可以看到有明显的纵向拉伸,这个时候我们不能再用上面cv.minAreaRect函数来进行查找最小外包矩形,因为这样得不到真实的九宫格外部矩形角点,后续的仿射变换也会错误,当然啦,这种情况也有方法:
首先我们通过轮廓查找找到ROI(兴趣即目标)区域:
轮廓查找莫得问题,接着我们来进行角点的提取,这里用到的是多边形逼近拟合函数cv.approxPolyDP
"""
approxCurve = cv.approxPolyDP(curve, epsilon, closed[, approxCurve])
curve: 2D点集,即待拟合的轮廓点
approxCurve: 多边形拟合的结果
epsilon: 精度,表示拟合结果与原轮廓的最大容许距离
closed: 轮廓是否是闭合的
"""
rect_points = cv.approxPolyDP(contours[np.argmax(contours_len)], 0.02 * cv.arcLength(contours[np.argmax(contours_len)], closed=True), closed=True)
print(rect_points)
"""
[output]
[[[258 6]]
[[23 9]]
[[3 265]]
[[280 269]]]
"""
这里我们的轮廓本来就是拉扯后的矩形,所以结果应该是四个角点,容许精度我们设置为原轮廓周长的0.02(cv.arcLength可以求取一段轮廓的周长
注意这里的points的大小是 (4, 1, 2),我们来显示在原图上一波:
img_copy=np.copy(img)
for r in rect_points:
# 半径为5个像素,颜色为红色,线宽为3
cv.circle(img_copy, center = tuple(r[0]), radius = 5, color = (0,0,255), thickness = 3)
plt.figure(1)
plt.title('The corners of distorted img')
plt.imshow(cv.cvtColor(img_copy,cv.COLOR_BGR2RGB))
plt.show()
最后我们来区分出左上,右上,右下,左下顶点(因为approxPolyDP函数给出的点可能不按顺序:
# reshape -> (4,2) -> list
rect_points = rect_points.squeeze(1).tolist()
# 各个顶点下x,y坐标的和值,最小的为左上,最大的为右下
sum_rp = [sum(a) for a in rect_points]
tl = rect_points[sum_rp.index(min(sum_rp))]
br = rect_points[sum_rp.index(max(sum_rp))]
# 将剩下的顶点按x坐标排序,从小到大,小的为左下,大的为右上
temp = sorted([r for r in rect_points if r != tl and r != br], key = lambda x: x[0])
bl = temp