使用Python和PIL分割图像,计算多个矩形物体的重心和旋转角度。

4

我正在使用Python和PIL来查找640x480图像中各种矩形(和正方形)的重心和旋转角度,类似于这个示例图片:enter image description here

目前,我的代码仅适用于图像中单个矩形的情况。

import Image, math

def find_centroid(im):
    width, height = im.size
    XX, YY, count = 0, 0, 0
    for x in xrange(0, width, 1):
        for y in xrange(0, height, 1):
            if im.getpixel((x, y)) == 0:
                XX += x
                YY += y
                count += 1
    return XX/count, YY/count

#Top Left Vertex
def find_vertex1(im):
    width, height = im.size
    for y in xrange(0, height, 1):
        for x in xrange (0, width, 1):
            if im.getpixel((x, y)) == 0:
                X1=x
                Y1=y
                return X1, Y1

#Bottom Left Vertex
def find_vertex2(im):
    width, height = im.size
    for x in xrange(0, width, 1):
        for y in xrange (height-1, 0, -1):
            if im.getpixel((x, y)) == 0:
                X2=x
                Y2=y
                return X2, Y2

#Top Right Vertex
def find_vertex3(im):
    width, height = im.size
    for x in xrange(width-1, 0, -1):
        for y in xrange (0, height, 1):
            if im.getpixel((x, y)) == 0:
                X3=x
                Y3=y
                return X3, Y3

#Bottom Right Vertex
def find_vertex4 (im):
    width, height = im.size
    for y in xrange(height-1, 0, -1):
        for x in xrange (width-1, 0, -1):
            if im.getpixel((x, y)) == 0:
                X4=x
                Y4=y
                return X4, Y4

def find_angle (V1, V2, direction):
    side1=math.sqrt(((V1[0]-V2[0])**2))
    side2=math.sqrt(((V1[1]-V2[1])**2))
    if direction == 0:
        return math.degrees(math.atan(side2/side1)), 'Clockwise'
    return 90-math.degrees(math.atan(side2/side1)), 'Counter Clockwise'

#Find direction of Rotation; 0 = CW, 1 = CCW
def find_direction (vertices, C):
    high=480
    for i in range (0,4):
        if vertices[i][1]<high:
            high = vertices[i][1]
            index = i
    if vertices[index][0]<C[0]:
        return 0
    return 1

def main():
    im = Image.open('hopperrotated2.png')
    im = im.convert('1') # convert image to black and white
    print 'Centroid ', find_centroid(im)
    print 'Top Left ', find_vertex1 (im)
    print 'Bottom Left ', find_vertex2 (im)
    print 'Top Right', find_vertex3 (im)
    print 'Bottom Right ', find_vertex4 (im)
    C = find_centroid (im)
    V1 = find_vertex1 (im)
    V2 = find_vertex3 (im)
    V3 = find_vertex2 (im)
    V4 = find_vertex4 (im)
    vertices = [V1,V2,V3,V4]
    direction = find_direction(vertices, C)
    print 'angle: ', find_angle(V1,V2,direction)

if __name__ == '__main__':
  main()

我的问题在于当图像中存在多个对象时。

我知道PIL有一个find_edges方法,可以得到仅包含边缘的图像,但我不知道如何使用这个新的边缘图像将图像分割成单独的对象。

from PIL import Image, ImageFilter

im = Image.open('hopperrotated2.png')

im1 = im.filter(ImageFilter.FIND_EDGES)
im1 = im1.convert('1')
print im1
im1.save("EDGES.jpg")

如果我可以使用边缘来将图像分割成单独的矩形,那么我只需要在每个矩形上运行我的第一行代码来获取质心和旋转角度。
但更好的方法是能够使用边缘来计算每个矩形的旋转角度和质心,而无需拆分图像。
非常感谢大家的帮助!

我认为你应该查看scipy.ndimage,特别是label来检测你的矩形,以及find_objectscenter_of_mass - deinonychusaur
2个回答

6
在找到角落之前,您需要识别每个对象。您只需要对象的边框,所以您也可以将初始输入减少到该边框。然后,只需按照每个不同的边框找到您的角落,一旦您知道每个不同的边框,质心就直接找到了。
使用下面的代码,您会得到以下结果(质心为红点,白色文本是以度为单位的旋转):
请注意,您的输入不是二进制的,所以我对此使用了非常简单的阈值。此外,以下代码是实现此目的的最简单方法,在任何一个体面的库中都有更快的方法。
import sys
import math
from PIL import Image, ImageOps, ImageDraw

orig = ImageOps.grayscale(Image.open(sys.argv[1]))
orig_bin = orig.point(lambda x: 0 if x < 128 else 255)
im = orig_bin.load()

border = Image.new('1', orig.size, 'white')
width, height = orig.size
bim = border.load()
# Keep only border points
for x in xrange(width):
    for y in xrange(height):
        if im[x, y] == 255:
            continue
        if im[x+1, y] or im[x-1, y] or im[x, y+1] or im[x, y-1]:
            bim[x, y] = 0
        else:
            bim[x, y] = 255

# Find each border (the trivial dummy way).
def follow_border(im, x, y, used):
    work = [(x, y)]
    border = []
    while work:
        x, y = work.pop()
        used.add((x, y))
        border.append((x, y))
        for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1),
                (1, 1), (-1, -1), (1, -1), (-1, 1)):
            px, py = x + dx, y + dy
            if im[px, py] == 255 or (px, py) in used:
                continue
            work.append((px, py))

    return border

used = set()
border = []
for x in xrange(width):
    for y in xrange(height):
        if bim[x, y] == 255 or (x, y) in used:
            continue
        b = follow_border(bim, x, y, used)
        border.append(b)

# Find the corners and centroid of each rectangle.
rectangle = []
for b in border:
    xmin, xmax, ymin, ymax = width, 0, height, 0
    mean_x, mean_y = 0, 0
    b = sorted(b)
    top_left, bottom_right = b[0], b[-1]
    for x, y in b:
        mean_x += x
        mean_y += y
    centroid = (mean_x / float(len(b)), mean_y / float(len(b)))
    b = sorted(b, key=lambda x: x[1])
    curr = 0
    while b[curr][1] == b[curr + 1][1]:
        curr += 1
    top_right = b[curr]
    curr = len(b) - 1
    while b[curr][1] == b[curr - 1][1]:
        curr -= 1
    bottom_left = b[curr]

    rectangle.append([
        [top_left, top_right, bottom_right, bottom_left], centroid])


result = orig.convert('RGB')
draw = ImageDraw.Draw(result)
for corner, centroid in rectangle:
    draw.line(corner + [corner[0]], fill='red', width=2)
    cx, cy = centroid
    draw.ellipse((cx - 2, cy - 2, cx + 2, cy + 2), fill='red')
    rotation = math.atan2(corner[0][1] - corner[1][1],
            corner[1][0] - corner[0][0])
    rdeg = math.degrees(rotation)
    draw.text((cx + 10, cy), text='%.2f' % rdeg)

result.save(sys.argv[2])

再次查看代码,我发现当顺时针旋转时,“top_left”角不会是图形的实际左上角。一种检测方法是检查下两个顶点是否比第一个顶点更高,在上面的代码中,这意味着if corner[0][1] > corner[1][1] and corner[0][1] > corner[2][1]: corner = corner[1:] + [corner[0]](将添加在循环for corner, centroid in rectangle:开始后面)。重新排序现在将给出正确的角度(顺时针为负)。这是你想要的吗? - mmgp
@aroushan 根据你如何看待矩形,有两个不同的旋转角度,因此你必须决定要测量哪一个。如果你有一种总是适用于你的解决方案,我建议你坚持使用它。 - mmgp
我有一个解决方案...但它似乎太原始了,不适合添加到你的代码中。我喜欢你建议的添加那行代码使顺时针旋转变为负数的想法。但是它没有起作用 :( - aroushan
你说得完全正确。谢谢。很抱歉,我以为我可以选择两个答案作为被接受的答案。我会选择你的,因为你使用了PIL。对此我很抱歉,也非常感谢你的帮助 :) - aroushan
1
哦,好的,抱歉刚才有些失礼。最后一个建议对您在角度测量问题上有帮助吗? - mmgp
显示剩余4条评论

3
这里是一个示例,展示如何通过标记图像并计算其质心来实现此操作。在scipy的ndimage中内置了此功能(以及其他很多有趣的图像处理功能)。对于角度,我使用矩形角点与边界切片相交的方法来计算。
import numpy as np
import scipy
from scipy import ndimage

im = scipy.misc.imread('6JYjd.png',flatten=1)
im = np.where(im > 128, 0, 1)
label_im, num = ndimage.label(im)
slices = ndimage.find_objects(label_im)
centroids = ndimage.measurements.center_of_mass(im, label_im, xrange(1,num+1))

angles = []
for s in slices:
    height, width = label_im[s].shape
    opp = height - np.where(im[s][:,-1]==1)[0][-1] - 1
    adj = width - np.where(im[s][-1,:]==1)[0][0] - 1
    angles.append(np.degrees(np.arctan2(opp,adj)))
print 'centers:', centroids
print 'angles:', angles

输出:

centers: [(157.17299748926865, 214.20652790151453), (219.91948280928594, 442.7146635321775), (363.06183745583041, 288.57169725293517)]
angles: [7.864024795499545, 26.306963825741803, 7.937188000622946]

你也可以用Matlab/Mathematica在2或3行中完成它。但这并不能帮助一个人学到什么东西,出于这个原因我只使用了非常基本的工具。 - mmgp
1
@mmgp - 我很想看看那个。它可能也可以写成成千上万行的代码。但我更偏爱使用适当工具编写简短的解决方案。对于图像分析,我通常会使用numpy、scipy和opencv,它们提供了快速(编写和运行)和强大的解决方案。我认为对于对图像分析感兴趣的人来说,值得花时间去研究它们。而且你错了...在编写这段代码时我学到了一些东西 ;) - fraxel
f = MorphologicalComponents[ColorNegate[Import["http://i.stack.imgur.com/6JYjd.png"]], 0.5]; cent = ComponentMeasurements [f, "Centroid"]; angle = ArcTan @@ (#[[1]] - #[[2]])/Degree & /@ ComponentMeasurements[f, "MinimalBoundingBox"][[All, 2, ;; 2]]; - mmgp
@mmgp - 谢谢!结果发现opencv中有一个类似的函数,可以用于角度,但是将数据整理成正确的形状有点棘手。 [cv2.minAreaRect(np.array(np.where(im[s]==1)).reshape(-1,1,2))[2] + 90 for s in slices] 现在我学到了两件事 :) - fraxel
@fraxel - 感谢您提供这个优美的代码!您和mmgp已经非常有帮助了。我现在唯一遇到的问题是,顺时针旋转的对象不能显示正确的角度。我需要能够区分旋转方向,以便可以将90度减去输出角度,从而得到适用于顺时针旋转的正确角度。 - aroushan
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接