I bought a tea strainer in goudong new year ago , The store owner claims to have 921 Holes , I feel a little “ Exaggeration ”, Always want to confirm .
The naked eye must be unable to count , So I thought about writing a program to count , Just took a picture and put it . Years ago, I was too busy , Then the new year , I was finally free yesterday , He wrote a 80 I counted the small scripts in line .
Because it's white porcelain , So the holes had better be completely black , So when taking photos, I put a black non-woven bag under it , The effect of shooting is ok .
Send it to the computer via wechat , Then simply use macOS Self contained image app With the , It is mainly to remove the irrelevant elements of the edge and adjust the brightness .
After pretreatment, see the following figure :
On the whole , This is a very simple task , The general process is to binarize the image ( Black and white ), Only black holes and white edges are left , Here's the picture .
You can see some disturbing factors , For example, the shadow directly below and the reflection of the hole at the upper right , You need to do an open operation to remove the interference . At this time, we can find that we only need to count the black areas ( Connected domain ) The number of holes is the number of holes , So the overall code framework is as follows :
# open
im = Image.open("WechatIMG1039.png")
w, h = im.size
# Two valued
im = im.convert('1')
pixel_data = im.load()
# Do several opening and closing operations to improve the contrast
for i in range(3):
# Use corrosion algorithm to remove impurities
corrode(pixel_data, w, h)
# Fill in the blank with inflation algorithm
expand(pixel_data, w, h)
# Statistical connected domain
print(f'holes count: {count_connected_area(pixel_data, w, h)}')
Corrosion algorithm can remove the soliton , It is a method of denoising . Here, simply put the surrounding 8 There is no black dot in the adjacent dot 2 Points of are defined as solitary points , Then set it to white to remove .
def corrode(pixel_data, w, h):
for x in range(w):
for y in range(h):
if count_black8(pixel_data, x, y, w, h) < 2:
# Set to white
pixel_data[x, y] = WHITE
Inflation algorithm is the reverse operation of corrosion algorithm , Here, simply put the surrounding 8 Black dots in adjacent dots exceed 4 Points of are defined as set to black , In this way, holes can be filled and black areas can be expanded .
def expand(pixel_data, w, h):
for x in range(w):
for y in range(h):
if count_black8(pixel_data, x, y, w, h) > 4:
# Set to black
pixel_data[x, y] = BLACK
The so-called connected domain is a black block , Let's traverse from the upper left corner , Find a black and expand its 8 Adjacency , Then expand step by step , Finally, count the number of its lattice .
def count_connected_area(pixel_data, w, h):
connected = []
for x in range(w):
for y in range(h):
if pixel_data[x, y] != BLACK:
continue
pixel_data[x, y] = WHITE
pixels = [(x, y)]
area = probe(pixel_data, x, y, w, h)
while area:
pixels.extend(area)
tmp = []
for px, py in area:
tmp.extend(probe(pixel_data, px, py, w, h))
area = tmp
connected.append(len(pixels))
return reduce(lambda s, x: s + 1 if 30 < x < 500 else s, connected, 0)
What I use here is Seed-Filling Method , It's slow , But the victory lies in simplicity , Anyway, I just write a little script to play , Don't care about performance , It doesn't matter . There is another kind. Two-Pass Method , Faster , But it's more complicated ; You can also use it directly OpenCV The algorithm of , It may be ten or twenty times faster than writing by yourself .
The script is simple , All the code 80 That's ok .
from functools import reduce
from PIL import Image
BLACK = 0
WHITE = 1
def extend8(x, y, w, h):
return ((x1, y1) for x1, y1 in (
(x-1, y-1), (x, y-1), (x+1, y-1),
(x-1, y), (x+1, y),
(x-1, y+1), (x, y+1), (x+1, y+1),
) if x1 >= 0 and y1 >= 0 and x1 < w and y1 < h)
def count_black8(pixel_data, x, y, w, h):
return reduce(lambda s, x: s + 1 if pixel_data[x] == BLACK else s, extend8(x, y, w, h), 0)
def corrode(pixel_data, w, h):
for x in range(w):
for y in range(h):
if count_black8(pixel_data, x, y, w, h) < 2:
# Set to white
pixel_data[x, y] = WHITE
def expand(pixel_data, w, h):
for x in range(w):
for y in range(h):
if count_black8(pixel_data, x, y, w, h) > 4:
# Set to black
pixel_data[x, y] = BLACK
def probe(pixel_data, x, y, w, h):
area = []
for x1, y1 in extend8(x, y, w, h):
if pixel_data[x1, y1] == BLACK:
pixel_data[x1, y1] = WHITE
area.append((x1, y1))
return area
def count_connected_area(pixel_data, w, h):
connected = []
for x in range(w):
for y in range(h):
if pixel_data[x, y] != BLACK:
continue
pixel_data[x, y] = WHITE
pixels = [(x, y)]
area = probe(pixel_data, x, y, w, h)
while area:
pixels.extend(area)
tmp = []
for px, py in area:
tmp.extend(probe(pixel_data, px, py, w, h))
area = tmp
connected.append(len(pixels))
return reduce(lambda s, x: s + 1 if 30 < x < 500 else s, connected, 0)
# open
im = Image.open("WechatIMG1039.png")
w, h = im.size
# Two valued
im = im.convert('1')
pixel_data = im.load()
# Do several open operations to improve the contrast
for i in range(3):
# Use corrosion algorithm to remove impurities
corrode(pixel_data, w, h)
# Fill in the blank with inflation algorithm
expand(pixel_data, w, h)
# Statistical connected domain
print(f'holes count: {count_connected_area(pixel_data, w, h)}')
# Visually check whether the images are all white .
im.show()
The final result is 922 Holes , It's more than the store said 1 individual , It seems that he is an honest shopkeeper , You can buy it !
The main theoretical basis of this method is mathematical morphology , It is a very general subject , It can not only be used to count holes , It can also be used to count platelets 、 Bacteria, etc , Widely applied .
another , I found that I wrote it more than ten years ago 《 use Python Do image processing 》 It is the most concise and easy to use PIL course , I used it to relearn PIL.