使用Python,是否有方法自动检测图像中的像素框?
我现在在用的电脑上没有安装openCV,这要是有的话就简单多了,省得我抓狂。下面是我处理过的一个图像,我已经得到了它的阈值,现在我想找个办法,得到一个四个值的元组,表示围绕像素聚集区域的框。
原始阈值图像
目前我使用的代码是:
box = image.getbbox()
draw = ImageDraw.Draw(area) # Create a draw object
draw.rectangle(area.getbbox(), outline="red")
结果图像
但我真正想做的是在顶部的白色区域或中间的灰色区域画一个框。我想避免裁剪,因为我想把这个写成一个自动化的函数,而我不知道阈值会在哪里。下面是我在火焰特效软件中做的一个例子,想要达到的效果:
新的理想结果希望这样说清楚了!我已经两天没睡了!任何建议或指导都非常感谢!
2 个回答
如果这对你还有帮助的话,有一个叫 scipy.ndimage.measurements.label
的工具,它可以在图片中找到一些小块区域(我们称之为“斑点”),并且可以用来找到这些区域的边框。
完成这个任务需要一些基本的工具和参数:
- 一种连接组件标记的方法;
- 用来判断是否丢弃或保留一个连接组件的阈值;
- 计算连接组件之间距离的指标,以及判断是否合并它们的阈值(这一步只有在你真的想这么做时才需要,目前还不太清楚)。
第一种方法在 PIL
中不可用,但 scipy
提供了这个功能。如果你也不想使用 scipy
,可以参考这个链接的答案:https://stackoverflow.com/a/14350691/1832154。我使用了那个答案中的代码,并进行了调整,使其可以处理 PIL
图像,而不是普通的列表,并假设那里的函数被放在一个叫 wu_ccl
的模块里。在第三步中,我使用了简单的棋盘距离,复杂度为 O(n^2)
。
接着,丢弃少于 200 像素的组件,认为距离小于 100 像素的组件应该在同一个边界框内,并在边界框外加 10 像素的填充,最终得到的结果是:
你可以简单地把组件的阈值调高,这样就只保留最大的一个。此外,你也可以把前面提到的两个步骤反过来做:先合并相近的组件,再丢弃(不过下面的代码并没有这样做)。
虽然这些任务相对简单,但代码并不短,因为我们没有依赖任何库来完成这些任务。下面是一个实现上面图像的示例代码,合并连接组件的部分特别多,我想是因为匆忙写的代码比需要的要长得多。
import sys
from collections import defaultdict
from PIL import Image, ImageDraw
from wu_ccl import scan, flatten_label
def borders(img):
result = img.copy()
res = result.load()
im = img.load()
width, height = img.size
for x in xrange(1, width - 1):
for y in xrange(1, height - 1):
if not im[x, y]: continue
if im[x, y-1] and im[x, y+1] and im[x-1, y] and im[x+1, y]:
res[x, y] = 0
return result
def do_wu_ccl(img):
label, p = scan(img)
ncc = flatten_label(p)
# Relabel.
l = label.load()
for x in xrange(width):
for y in xrange(height):
if l[x, y]:
l[x, y] = p[l[x, y]]
return label, ncc
def calc_dist(a, b):
dist = float('inf')
for p1 in a:
for p2 in b:
p1p2_chessboard = max(abs(p1[0] - p2[0]), abs(p1[1] - p2[1]))
if p1p2_chessboard < dist:
dist = p1p2_chessboard
return dist
img = Image.open(sys.argv[1]).convert('RGB')
width, height = img.size
# Pad image.
img_padded = Image.new('L', (width + 2, height + 2), 0)
width, height = img_padded.size
# "discard" jpeg artifacts.
img_padded.paste(img.convert('L').point(lambda x: 255 if x > 30 else 0), (1, 1))
# Label the connected components.
label, ncc = do_wu_ccl(img_padded)
# Count number of pixels in each component and discard those too small.
minsize = 200
cc_size = defaultdict(int)
l = label.load()
for x in xrange(width):
for y in xrange(height):
cc_size[l[x, y]] += 1
cc_filtered = dict((k, v) for k, v in cc_size.items() if k > 0 and v > minsize)
# Consider only the borders of the remaining components.
result = Image.new('L', img.size)
res = result.load()
im = img_padded.load()
l = label.load()
for x in xrange(1, width - 1):
for y in xrange(1, height - 1):
if im[x, y] and l[x, y] in cc_filtered:
res[x-1, y-1] = l[x, y]
result = borders(result)
width, height = result.size
result.save(sys.argv[2])
# Collect the border points for each of the remainig components.
res = result.load()
cc_points = defaultdict(list)
for x in xrange(width):
for y in xrange(height):
if res[x, y]:
cc_points[res[x, y]].append((x, y))
cc_points_l = list(cc_points.items())
# Perform a dummy O(n^2) method to determine whether two components are close.
grouped_cc = defaultdict(set)
dist_threshold = 100 # pixels
for i in xrange(len(cc_points_l)):
ki = cc_points_l[i][0]
grouped_cc[ki].add(ki)
for j in xrange(i + 1, len(cc_points_l)):
vi = cc_points_l[i][1]
vj = cc_points_l[j][1]
kj = cc_points_l[j][0]
dist = calc_dist(vi, vj)
if dist < dist_threshold:
grouped_cc[ki].add(kj)
grouped_cc[kj].add(ki)
# Flatten groups.
flat_groups = defaultdict(set)
used = set()
for group, v in grouped_cc.items():
work = set(v)
if group in used:
continue
while work:
gi = work.pop()
if gi in flat_groups[group] or gi in used:
continue
used.add(gi)
flat_groups[group].add(gi)
new = grouped_cc[gi]
if not flat_groups[group].issuperset(new):
work.update(new)
# Draw a bounding box around each group.
draw = ImageDraw.Draw(img)
bpad = 10
for cc in flat_groups.values():
data = []
for vi in cc:
data.extend(cc_points[vi])
xsort = sorted(data)
ysort = sorted(data, key=lambda x: x[1])
# Padded bounding box.
bbox = (xsort[0][0] - bpad, ysort[0][1] - bpad,
xsort[-1][0] + bpad, ysort[-1][1] + bpad)
draw.rectangle(bbox, outline=(0, 255, 0))
img.save(sys.argv[2])
再次强调,wu_ccl.scan
函数需要调整(取自前面提到的答案),为此可以考虑在里面创建一个模式为 'I'
的图像,而不是使用嵌套的 Python 列表。我还对 flatten_label
做了些小改动,使其返回连接组件的数量(但在这个最终呈现的代码中并没有实际使用)。