My solution to the problem of Visual Task 1 of the 2022 Intelligent Manufacturing Competition

比赛赛题:

  1. 比赛时间为 180 min。
  2. 比赛任务及要求

任务一:实现对图片中的齿轮的检测以及测量,检测内容包括齿轮上是否有划痕,测量内容包括齿数、齿顶圆的直径以及齿根圆的直径(像素长度)。
Task1: For the gear in picture, check whether there are scratches on it, measure the number of teeth, the diameter of the addendum circle and the diameter of the root circle (in pixel length).

比赛时间比较紧,上午比完生产调试app已经很累了,下午任务量又很大,特别是这个task1并没有准备到,很多时候来不及思考,直接就上暴力方法了,完全没有思考怎么才能优雅地完成...现在把比赛的代码简单完善了一下发出来。

处理图片:
原图

(以下代码禁止转载)
(以下代码禁止转载)
(以下代码禁止转载)
(以下代码禁止转载)
(以下代码禁止转载)

show me the code

import cv2
from skimage import morphology
from skimage import filters
from skimage import feature
from skimage import io
from skimage import segmentation
from skimage import transform

import matplotlib.pyplot as plt
import numpy as np
import os

fontdict_prop = {
    'family' : 'youyuan',
    'size'   : 12,
}

plt.rc('font', family='courier', size='12')
plt.rc('axes', unicode_minus='False')
plt.rcParams['savefig.dpi'] = 300
plt.rcParams['figure.figsize'] = (7, 6)

blur、二值化。图片比较干净,并不需要做太多处理。
Blur and get binary image. The output image is pure, so no further operations are performed.

img_origin = io.imread(r"2.bmp")
img_origin = cv2.cvtColor(img_origin, cv2.COLOR_RGBA2GRAY)
img_blur = cv2.GaussianBlur(img_origin, (5, 5), 3)
thresh, img_bin = cv2.threshold(img_origin, 0, 255, cv2.THRESH_OTSU)
plt.imshow(img_bin)
plt.savefig('figs/1.jpg')

轮廓提取、过滤,个人习惯性写法,实际没必要过滤了,直接取面积最大的轮廓。
Get the contours and then filter them. Just simply pick the one with max area.

cnts, hier = cv2.findContours(img_bin, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
min_thresh = 10000
cnts_filtered = []
rects = []
areas = []
for cnt in cnts:
    # cnt.reshape(-1, 2)
    area = cv2.contourArea(cnt)
    if area < min_thresh:
        continue
    areas.append(area)
    cnts_filtered.append(cnt)
    rectb = cv2.boundingRect(cnt)
    rects.append(rectb)

areamax = np.argmax(areas)
use_rect = rects[areamax]
use_cnt = cnts_filtered[areamax]

直接用boudingbox求齿顶圆直径,这样其实也挺准确的。
Get the diameter of addendum circle by using 'boudingbox'. Simple and quite accurate.

img_copy = img_bin.copy()
rectb = use_rect
pt1 = (rectb[0], rectb[1])
pt2 = (rectb[0] + rectb[2], rectb[1] + rectb[3])
_ = cv2.rectangle(img_copy, pt1, pt2, (255, 0, 0), 5)
plt.imshow(img_copy)
plt.savefig("figs/2.jpg")

_a, _b, w, h = use_rect
print("齿顶圆直径:", (w+h)/2) # diameter of addendum circle

bouding_box

裁剪出来,填补中心的空缺,顺便求个质心(没啥用)直接用框出来的区域的宽高的一半位置作为质心是准确的。
Cut out the gear, fill the hole in center (use flood_fill). You don't have to culculate the center of the mass and just believe in 'boudingbox'.

x, y, w, h = use_rect
ofs = 20
img_bin_crop = img_bin[y-ofs:y+h+ofs, x-ofs:x+w+ofs]
img_bin_crop_bu = img_bin_crop.copy() # 备份用于划痕检测
img_bin_crop = img_bin_crop.astype('bool')
xofs, yofs = x-ofs, y-ofs
img_crop_bu = img_blur[y-ofs:y+h+ofs, x-ofs:x+w+ofs] # 备份用于划痕检测

h, w = img_bin_crop.shape[0] // 2, img_bin_crop.shape[1] // 2
img_bin_crop = segmentation.flood_fill(img_bin_crop, (h, w), 1)
img_bin_crop = morphology.remove_small_holes(img_bin_crop, 100)
img_bin_crop = cv2.medianBlur(img_bin_crop.astype(np.uint8)*255, 15)
M = cv2.moments(use_cnt)
cx = int(M["m10"] / M["m00"]) - xofs
cy = int(M["m01"] / M["m00"]) - yofs
img_copy = img_bin_crop.copy()
img_copy = cv2.circle(img_copy, (cx, cy), 10, (64), -1)
plt.imshow(img_copy)
plt.savefig('figs/3.jpg')

齿轮

用笨方法迭代求齿根圆。在中心画圆,让圆的半径越来越大,看会不会碰到齿轮外部的像素,注意compare是取反的。还可以用其他办法,比如logpolar做坐标变换会比较简洁。
In order to get the root circle diameter, I use a stupid iterative method. I draw a circle in the center of the binary image and check if it covers the pixels outside the gear. If not, increase the radius and try again util it is over. By doing this, I will get the root circle, see below. You can try to use 'logpolar' to get the diameter more elegantly.

compare = 1 - (img_bin_crop // 255)
growth = np.zeros_like(img_bin_crop, dtype=np.uint8)
for r in range(10, img_bin_crop.shape[0] // 2 - 20):
    growth = cv2.circle(growth, (cx, cy), r, 1, -1)
    comsum = cv2.bitwise_and(compare, compare, mask=growth).sum()
    if comsum > 0:
        break
growth = growth.astype(np.uint8) * 128
growth = cv2.add(growth, (img_bin_crop // 1.2).astype(np.uint8))
plt.imshow(growth)
print("齿根圆直径:", r*2) # root circle diameter
plt.savefig('figs/4.jpg')

还挺好看 :-) Looking good.
齿根圆

借用skimage骨架化,准备数齿轮。数齿轮还是用logpolar会比较简单。
Skeletonize and get ready to get the teeth.

rsize = np.array((152, 152))
img_r = cv2.resize(img_bin_crop.astype(np.uint8), rsize)

img_thin = morphology.thin(img_r)
plt.imshow(img_thin)
plt.savefig('figs/5.jpg')

骨架化

骨架化后,非常细~ 接下来把中心挖掉(画个圆),保留齿骨架,然后提取轮廓即可知道有多少齿。
Draw a circle in the center to take away the pixels and reserve the teeth.

cx, cy = rsize // 2
r = int(rsize.mean() // 2.5)
img_thin = img_thin.astype(np.uint8)*255
cv2.circle(img_thin, (cx, cy), r, 0, -1)
img_thin = morphology.closing(img_thin, morphology.star(2))

plt.imshow(img_thin)
plt.savefig('figs/6.jpg')

不要直接提取轮廓来数齿轮,先膨胀一下
Dilate its teeth before finding contours.

img_d = morphology.dilation(img_thin, morphology.star(1))
img_d = morphology.remove_small_objects(img_d)
plt.imshow(img_d)
plt.savefig("figs/7.jpg")
cnts, hier = cv2.findContours(img_d.astype(np.uint8), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
print("齿数:", len(cnts)) # number of teeth 

外围

补充另一种数齿轮的方法:根据凸包点数计算齿数。先用多边形拟合轮廓,然后计算凸包,凸包有多少个点就有多少齿数。
这个感觉比较不准确...齿数很多的时候凸包似乎计算不准,approxPolyDP的eps参数没设定好,看下面那张图大概可能猜出原因。
Another method: By getting convex hull length. Use 'approxPolyDP' to fit the gear contour, and use 'convexhull' to get the number of teeth. However, it's not that accurate compared to skeletonization. May be the poly points should be filtered.

cnts, hier = cv2.findContours(img_bin_crop, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
min_thresh = 10000
cnts_filtered = []
rects = []
areas = []
for cnt in cnts:
    area = cv2.contourArea(cnt)
    if area < min_thresh:
        continue
    areas.append(area)
    cnts_filtered.append(cnt)
    rectb = cv2.boundingRect(cnt)
    rects.append(rectb)

areamax = np.argmax(areas)
use_rect = rects[areamax]
use_cnt = cnts_filtered[areamax]

eps = img_bin_crop.shape[0] // 100 # 这个不好决定
approx = cv2.approxPolyDP(use_cnt, eps, 1)
img_copy = img_bin_crop.copy()
cv2.polylines(img_copy, [approx], True, (128), 10)
plt.imshow(img_copy)
plt.savefig("figs/8.jpg")

多边形拟合

hullpt = cv2.convexHull(approx)
img_copy = img_bin_crop.copy()
cv2.polylines(img_copy, [hullpt], True, (128), 10)
plt.imshow(img_copy)
plt.savefig("figs/9.jpg")
# print("齿轮数目:", len(hullpt)) # number of teeth

凸包计算

最后提取划痕。划痕非常不明显,处理起来比较棘手。如果是直方图均衡的话,会引入更多干扰因素。索性暴力上局部OTSU(不要用全局),提取后虽然有很多小点点,但是比直方图均衡更好过滤掉,可以运行代码自己对比看看。
二值化后,用medianBlur可以过滤掉大多数点点,只留下划痕。然后膨胀一下(合并分裂的划痕),就可以提取轮廓了。轮廓也要过滤,因为这里的轮廓是包括了齿轮的边缘、内孔啥的,设置面积阈值来过滤即可,面积最大值和最小值需要手动尝试出来。
The last step is to check scratches. These scratches are not obvious so I took effort to deal with it. Using histogram equalization will create more noises. So I use local OTSU roughly (do not use global OTSU threshold). It does create viele white points (noises) but can be filtered out by median blur.
After that, apply dilation to it (in order to merge splited scratches), find contours and filter contours by area. The contours filter operation aims at filtering the edges of gear to get the scratches by area, so the threshold should be set carefully. It's simple and efficient in competition, but not recommended in project. Try to filter edges in other way.

img_crop_msked = cv2.bitwise_and(img_crop_bu, img_crop_bu, mask=img_bin_crop_bu)
plt.imshow(img_crop_msked)
thresh = filters.rank.otsu(img_crop_msked, morphology.disk(17))
img_t = img_crop_msked < thresh
# img_t = segmentation.flood_fill(img_t, (img_t.shape[1]//2, img_t.shape[0]//2), 1)
img_t = img_t.astype("uint8")*255
img_t = cv2.medianBlur(img_t, 15)
img_t = morphology.dilation(img_t, morphology.disk(11))
cnts, hier = cv2.findContours(img_t, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
img_copy = img_t.copy()
flag = 0
for i, cnt in enumerate(cnts):
    area = cv2.contourArea(cnt)
    if area < 1000 or area > 20000:
        continue
    img_copy = cv2.drawContours(img_copy, cnts, i, 128, 10)
    flag = 1
    print("有划痕") # with scratches
if not flag:
    print("无划痕") # without scratches
plt.imshow(img_copy)
plt.savefig("figs/10.jpg")

划痕