|
import array |
|
import Image |
|
from cmath import sqrt, atan |
|
import random |
|
import Queue |
|
import sys |
|
|
|
|
|
class ConnectedComponent(object): |
|
def __init__(self, label_id): |
|
""" |
|
:type label_id: integer |
|
""" |
|
self.id = label_id |
|
self.points = [] |
|
self.perimeter = 0 |
|
self.area = 0 |
|
self.mass_center = (0, 0) |
|
self.m20 = 0 |
|
self.m02 = 0 |
|
self.m11 = 0 |
|
self.density = 0 |
|
self.elongation = 0 |
|
self.orientation = 0 |
|
self.__is_done = False |
|
|
|
# draw connected component on canvas |
|
def draw(self, canvas, color): |
|
""" |
|
:type canvas: Image.Image |
|
""" |
|
for x, y in self.points: |
|
try: |
|
canvas.putpixel((x, y), color) |
|
except IndexError as e: |
|
pass |
|
|
|
return canvas |
|
|
|
# get boundary box for object |
|
def boundary_box(self): |
|
# find box corners |
|
xs = [point[0] for point in self.points] |
|
ys = [point[1] for point in self.points] |
|
|
|
# return box properties |
|
return min(xs), min(ys), max(xs), max(ys) |
|
|
|
# This is internal method |
|
# that finishes calculation of params |
|
def done(self): |
|
if not self.__is_done: |
|
self.__is_done = True |
|
self.area = float(len(self.points)) |
|
self.density = pow(self.perimeter, 2) / self.area |
|
# Center of mass |
|
mx, my = 0, 0 |
|
for x, y in self.points: |
|
mx += x |
|
my += y |
|
self.mass_center = (mx/self.area, my/self.area) |
|
# Discrete central moments |
|
for x, y in self.points: |
|
self.m20 += pow(x - self.mass_center[0], 2) |
|
self.m02 += pow(y - self.mass_center[1], 2) |
|
self.m11 += (x - self.mass_center[0]) * (y - self.mass_center[1]) |
|
## elongation |
|
m20, m02, m11 = self.m20, self.m02, self.m11 |
|
d1, d2 = m20+m02, abs(sqrt(pow(m20-m02, 2) + 4.0*pow(m11, 2))) |
|
# todo: fix calculations? |
|
try: |
|
self.elongation = (d1 + d2) / (d1 - d2) |
|
# orientation |
|
self.orientation = atan(2*m11 / (m20 - m02)) / 2 |
|
except ZeroDivisionError as e: |
|
pass |
|
else: |
|
raise StandardError("You can't change state of an connected component") |
|
|
|
def distance(self, obj): |
|
area_diff = abs(self.area - obj.area) |
|
perimetr_diff = abs(self.perimeter - obj.perimeter) |
|
density_diff = abs(self.density - obj.density) |
|
elong_diff = abs(self.elongation - obj.elongation) |
|
|
|
dist = 0 |
|
dist = sqrt(pow(dist, 2) + pow(area_diff, 2)) |
|
#dist = sqrt (pow (dist,2) + pow(perimetr_diff,2)) |
|
dist = sqrt(pow(dist, 2) + pow(density_diff, 2)) |
|
dist = sqrt(pow(dist, 2) + pow(elong_diff, 2)) |
|
|
|
return abs(dist) |
|
|
|
|
|
def select_label(used): |
|
""" |
|
:type used: set |
|
:return: int |
|
""" |
|
prev = 0 |
|
for label in used: |
|
if label-1 is not prev: |
|
used.add(prev+1) |
|
return prev+1 |
|
else: |
|
prev = label |
|
if prev+1 not in used: |
|
used.add(prev+1) |
|
return prev+1 |
|
|
|
|
|
def __labeling_step(img_data, labels, current_label, point, size, q): |
|
""" |
|
:type img_data: list |
|
:type labels: array.array |
|
:type current_label: ConnectedComponent |
|
:type point: integer |
|
:type size: tuple |
|
:type q: Queue.Queue |
|
""" |
|
# prepare all params of object |
|
width, height = size |
|
col, row = point % width, int(point / height) + 1 |
|
|
|
if img_data[point] is 0 or labels[point] is not 0: |
|
return False |
|
|
|
# todo: border is wrong! Fix it! |
|
border = 0 |
|
labels[point] = current_label.id |
|
if col > 0: |
|
border += img_data[point-1] is 0 or labels[point-1] is not 0 |
|
q.put(point-1) |
|
if col < width - 1: |
|
border += img_data[point+1] is 0 or labels[point+1] is not 0 |
|
q.put(point+1) |
|
if row > 0: |
|
border += img_data[point-width] is 0 or labels[point-width] is not 0 |
|
q.put(point-width) |
|
if row < height - 1: |
|
border += img_data[point+width] is 0 or labels[point+width] is not 0 |
|
q.put(point+width) |
|
|
|
# we should consider all 8 points to get correct outer border... |
|
if border < 3: |
|
current_label.perimeter += 1 |
|
|
|
current_label.area += 1 |
|
current_label.points.append((col, row)) |
|
|
|
return True |
|
|
|
|
|
def find_connected_components(bin_img): |
|
""" |
|
:type bin_img: Image.Image |
|
:return: ConnectedComponent |
|
""" |
|
img_data = list(bin_img.getdata()) |
|
labels = array.array('i', (0 for i in xrange(len(img_data)-1))) |
|
# simple hash map with all properties |
|
# of our connected component |
|
elements = [] |
|
# reduce amount of gaps in id range |
|
# this is not necessary |
|
component_ids = set() |
|
q = Queue.Queue() |
|
for i in xrange(len(img_data)-1): |
|
current_label = ConnectedComponent(select_label(component_ids)) |
|
if __labeling_step(img_data, labels, current_label, i, bin_img.size, q): |
|
# finish calculation of params |
|
elements.append(current_label) |
|
#Breadth First Search optimisation |
|
while not q.empty(): |
|
j = q.get() |
|
__labeling_step(img_data, labels, current_label, j, bin_img.size, q) |
|
for element in elements: |
|
element.done() |
|
|
|
return elements |
|
|
|
def random_color(): |
|
return random.randint(20, 255), random.randint(20, 255), random.randint(20, 255) |