#!/usr/bin/env python
__version__ = '1.0'
__author__ = "Avinash Kak (kak@purdue.edu)"
__date__ = '2012-November-8'
__url__ = 'https://engineering.purdue.edu/kak/distWatershed/Watershed-1.0.html'
__copyright__ = "(C) 2012 Avinash Kak. Python Software Foundation."
import Image
import ImageDraw
import scipy
import numpy
import ImageTk
import Tkinter
import sys
import glob
import os
from Tkconstants import *
import math
import sets
#_______________________________ Utility functions ______________________________
def _coalesce(lists, result_sets):
'''
This function is used by the connected-components labeling algorithm of the
module. Connected components labeling of pixels in a binary image is usually
carried out by raster scanning the image and assigning new labels to isolated
pixels first encountered and propagated labels to pixels whose neighbors were
previously assigned new or propagated labels. When two pixels with two different
previously assigned labels are discovered to be each other's 8-neighbors, we
record the equivalence of those labels in a list. Ultimately, this list must be
resolved to discover the truly unique labels. This function resolves such
equivalence lists.
'''
if len(lists) == 0:
return result_sets
if len(result_sets) == 0:
result_sets.append( sets.Set(lists[0]) )
return _coalesce(lists[1:], result_sets )
for eachlist in lists:
(a,b) = eachlist
for newset in result_sets:
if a in newset:
for checkset in result_sets:
if b in checkset:
newset.union_update(checkset)
if checkset != newset:
result_sets.remove(checkset)
_coalesce( lists[1:], result_sets )
return result_sets
newset.add(b)
_coalesce( lists[1:], result_sets )
return result_sets
elif b in newset:
for checkset in result_sets:
if a in checkset:
newset.union_update(checkset)
if checkset != newset:
result_sets.remove(checkset)
_coalesce( lists[1:], result_sets )
return result_sets
newset.add(a)
_coalesce( lists[1:], result_sets )
return result_sets
result_sets.append(sets.Set(eachlist))
_coalesce( lists[1:], result_sets )
return result_sets
def _display_portion_of_array(array, blob_center=0, blob_diameter=0):
'''
This is a convenience function that is very useful for visualizing
the watershed in a portion of the array used for a given Z level.
'''
height,width = array.shape
if blob_center == 0 and blob_diameter == 0:
imin = 0
imax = height
jmin = 0
jmax = width
else:
imin = int(blob_center[0]) - int(blob_diameter/2.0)
imax = int(blob_center[0]) + int(blob_diameter/2.0) + 1
jmin = int(blob_center[1]) - int(blob_diameter/2.0)
jmax = int(blob_center[1]) + int(blob_diameter/2.0) + 1
for i in range(imin, imax):
lineoutput = ""
for j in range(jmin, jmax):
if array[(i,j)] < 0:
lineoutput += " X"
elif array[(i,j)] >= 0 and array[(i,j)] < 10:
lineoutput += " " + str(array[(i,j)])
elif array[(i,j)] >= 10 and array[(i,j)] < 99:
lineoutput += " " + str(array[(i,j)])
else:
lineoutput += str(array[(i,j)])
print lineoutput
print "\n\n"
def _display_portion_of_array_binary(array, blob_center, blob_diameter):
'''
This is a convenience function for the visualization of distance
transformations of small binary patterns.
'''
height,width = array.shape
imin = int(blob_center[0]) - int(blob_diameter/2.0)
imax = int(blob_center[0]) + int(blob_diameter/2.0) + 1
jmin = int(blob_center[1]) - int(blob_diameter/2.0)
jmax = int(blob_center[1]) + int(blob_diameter/2.0) + 1
for i in range(imin, imax):
lineoutput = ""
for j in range(jmin, jmax):
if array[(i,j)] < 10:
lineoutput += " " + str(array[(i,j)])
else:
lineoutput += str(array[(i,j)])
print lineoutput
print "\n\n"
def _gaussian(sigma):
'''
A 1-D Gaussian smoothing operator is generated by assuming that the pixel
sampling interval is a unit distance. We truncate the operator a 3 times the
value of sigma. So when sigma is set to 1, you get a 7-element operator. On the
other hand, when sigma is set to 2, you get a 13-element operator, and so on.
'''
win_half_width = int(3 * sigma)
xvals = range(-win_half_width, win_half_width+1)
gauss = lambda x: math.exp(-((x**2)/(2*float(sigma**2))))
operator = [gauss(x) for x in xvals]
summed = reduce( lambda x, y: x+y, operator )
operator = [x/summed for x in operator]
return operator
def _convolution_1D(input_array, operator):
'''
Since the Gaussian kernel is separable in its x and y dependencies, 2D convolution
of an image with the kernel can be decomposed into a sequence of 1D convolutions
first with the rows of the image and then another sequence of 1D convolutions
with the columns of the output from the first. This function carried out a 1D
convolution.
'''
height,width = input_array.shape
result_array = numpy.zeros((height, width), dtype="float")
w = len(operator) # should be an odd number
op_half_width = (w-1)/2
for i in range(height):
for j in range(width):
accumulated = 0.0
for k in range(-op_half_width,op_half_width+1):
if (j+k) >= 0 and (j+k) < width:
accumulated += input_array[i,(j+k)] * operator[k + op_half_width]
result_array[(i,j)] = accumulated
return result_array
def _convolution_2D(input_array, operator):
'''
Since the Gaussian kernel is separable in its x and y dependencies, 2D convolution
of an image with the kernel can be decomposed into a sequence of 1D convolutions
first with the rows of the image and then another sequence of 1D convolutions
with the columns of the output from the first. This function orchestrates the
invocation of 1D convolutions.
'''
result_conv_along_x = _convolution_1D(input_array, operator)
result_conv_along_y = _convolution_1D(result_conv_along_x.transpose(), operator)
final_result = result_conv_along_y.transpose()
return final_result
def _line_intersection(line1, line2):
'''
Each line is defined by a 4-tuple, with its first two elements defining the
coordinates of the first end point and the two elements defining the coordinates
of the second end point. This function defines a predicate that tells us whether
or not two given line segments intersect.
'''
line1_endpoint1_x = line1[0]
line1_endpoint1_y = line1[1]
line1_endpoint2_x = line1[2]
line1_endpoint2_y = line1[3]
line2_endpoint1_x = line2[0] + 0.5
line2_endpoint1_y = line2[1] + 0.5
line2_endpoint2_x = line2[2] + 0.5
line2_endpoint2_y = line2[3] + 0.5
if max([line1_endpoint1_x,line1_endpoint2_x]) <= min([line2_endpoint1_x,line2_endpoint2_x]):
return 0
elif max([line1_endpoint1_y,line1_endpoint2_y]) <= min([line2_endpoint1_y,line2_endpoint2_y]):
return 0
elif max([line2_endpoint1_x,line2_endpoint2_x]) <= min([line1_endpoint1_x,line1_endpoint2_x]):
return 0
elif max([line2_endpoint1_y,line2_endpoint2_y]) <= min([line1_endpoint1_y,line1_endpoint2_y]):
return 0
# Use homogeneous representation of lines:
hom_rep_line1 = _cross_product((line1_endpoint1_x,line1_endpoint1_y,1),(line1_endpoint2_x,line1_endpoint2_y,1))
hom_rep_line2 = _cross_product((line2_endpoint1_x,line2_endpoint1_y,1),(line2_endpoint2_x,line2_endpoint2_y,1))
hom_intersection = _cross_product(hom_rep_line1, hom_rep_line2)
if hom_intersection[2] == 0:
return 0
intersection_x = hom_intersection[0] / (hom_intersection[2] * 1.0)
intersection_y = hom_intersection[1] / (hom_intersection[2] * 1.0)
if intersection_x >= line1_endpoint1_x and intersection_x <= line1_endpoint2_x and intersection_y >= line1_endpoint1_y and intersection_y <= line1_endpoint2_y:
return 1
return 0
def _cross_product(vector1, vector2):
'''
Returns the vector cross product of two triples
'''
(a1,b1,c1) = vector1
(a2,b2,c2) = vector2
p1 = b1*c2 - b2*c1
p2 = a2*c1 - a1*c2
p3 = a1*b2 - a2*b1
return (p1,p2,p3)
#_________________________ Watershed Class Definition ______________________________
class Watershed(object):
# Class variables:
region_mark_coords = {}
drawEnable = startX = startY = 0
canvas = None
def __init__(self, *args, **kwargs ):
if args:
raise ValueError(
'''Watershed constructor can only be called with keyword arguments for
the following keywords: data_image, binary_or_gray_or_color,
gradient_threshold_as_fraction, level_decimation_factor,
size_for_calculations, max_gradient_to_be_reached_as_fraction,
sigma, and debug''')
data_image = sigma = size_for_calculations = None
level_decimation_factor = gradient_threshold_as_fraction = debug = None
max_gradient_to_be_reached_as_fraction = binary_or_gray_or_color = None
if kwargs.has_key('data_image'): data_image=kwargs.pop('data_image')
if kwargs.has_key('binary_or_gray_or_color'): \
binary_or_gray_or_color=kwargs.pop('binary_or_gray_or_color')
if kwargs.has_key('gradient_threshold_as_fraction'): \
gradient_threshold_as_fraction=kwargs.pop('gradient_threshold_as_fraction')
if kwargs.has_key('max_gradient_to_be_reached_as_fraction'): \
max_gradient_to_be_reached_as_fraction=\
kwargs.pop('max_gradient_to_be_reached_as_fraction')
if kwargs.has_key('sigma'): sigma = kwargs.pop('sigma')
if kwargs.has_key('size_for_calculations'): \
size_for_calculations = kwargs.pop('size_for_calculations')
if kwargs.has_key('level_decimation_factor'): \
level_decimation_factor = kwargs.pop('level_decimation_factor')
if kwargs.has_key('debug'): debug=kwargs.pop('debug')
if len(kwargs) != 0:
raise ValueError('''You have provided unrecognizable keyword args''')
if data_image:
self.data_im_name = data_image
self.data_im = Image.open(data_image)
self.original_im = Image.open(data_image)
else:
raise ValueError('''You must specify a data image''')
if binary_or_gray_or_color:
if binary_or_gray_or_color not in ['binary','gray','color']:
raise ValueError('''You must specify either "binary" or "gray" or "color" for your input image''')
self.binary_or_gray_or_color = binary_or_gray_or_color
else:
raise ValueError('''You must specify either "binary" or "gray" or "color" for your input image''')
if sigma:
self.sigma = sigma
else:
self.sigma = 1
if size_for_calculations:
self.size_for_calculations = size_for_calculations
else:
self.size_for_calculations = 128
if level_decimation_factor:
self.level_decimation_factor = level_decimation_factor
else:
self.level_decimation = 1
if gradient_threshold_as_fraction:
self.gradient_threshold_as_fraction = gradient_threshold_as_fraction
else:
self.gradient_threshold_as_fraction = 0.1
if max_gradient_to_be_reached_as_fraction:
self.max_gradient_to_be_reached_as_fraction = \
max_gradient_to_be_reached_as_fraction
else:
self.max_gradient_to_be_reached_as_fraction = 1
self.display_size = () # Set to a tuple of two values, first val
# for width and the second for height
self.componentID_array = None # This numpy 2D array holds labels for
# the different components in a binary
# image. This is initialized by
# a call to connected_components()
self.markID_data_array = None # This numpy 2D array holds the labels for
# for the different markers in all of
# the blobs in an image.
self.marker_image_data = None # A numpy array of size the same as the
# original image that holds all the
# marks
self.blob_dictionary = {} # The keys are integers and
# values are the corresponding
# blobs in the data image, each a
# numpy array of the same size as image
self.marks_dictionary = {} # The keys are integers and the
# values are the corresponding
# mark pixels as numpy arrays
self.blob_dia_dict = {} # Upperbound on diameters of all blobs
self.blob_center_dict = {} # Rough estimate of the center of a blob
self.marks_to_blobs_mapping_dict = {} # Shows which image blob goes with each
# mark
self.blobs_to_marks_mapping_dict = {} # Shows which marks belong to each image
# blob. Note that that the value for a
# key is now a list if blobs
self.number_of_components = None # This is the number of connected blobs
# in a binary image
self.number_of_marks = None # This is the number of marks created
# by a user
self.gray_image = None # Stores the gray image as an Image object
self.gray_image_as_array = None # Stores the gray image as a numpy array
self.gradient_image_as_array = None # Stores gradient image as a numpy array
self.max_number_of_levels_in_gradient = None # self explanatory
self.max_grad_level = None # Max int value of normalized gradient
self.min_grad_level = None # Min int value of normalized gradient
self.Z_levels_dict = {} # A key is a gradient level and value
# the set where gradient values
# are equal to or less than the key
self.blob_dictionary_by_level_for_Z = {} # A key in this dict represents
# a specific gradient level. And
# the corresponding value is a
# dict that serves as the blob
# dictionary for just that level
self.level_vs_number_of_components = {}
self.start_label_for_labeling_blobs_vs_level = {}
self.label_pool_at_level = {}
self.componentID_array_dict = {} # Each key is a gradient level and the
# corresponding value the componentID array
# for that level. In a componentID array,
# each blob caries a distinct integer label.
self.watershed_contours = [] # This is a list of lists. Each inner list
# is a watershed contour as extracted by the
# watershed contour extractor.
self.marks_scale = () # A pair of numbers. The first element is
# for the horizontal direction and the second
# for the vertical
self.region_marks_centers = {} # Each key is region index and the corresponding
# value a list of tuples, with each tuple
# the coordinates of the mark centers in that
# region.
self.marked_valley_for_region={} # These are new valleys injected by the user
# with the help of marks.
if debug:
self.debug = debug
else:
self.debug = 0
def extract_data_pixels(self):
'''
Gets the binary, grayscale, and color images ready for watershed processing.
If the images are too large, they are reduced to the size set by the
constructor. Color images are converted into grayscale images.
'''
if self.binary_or_gray_or_color == 'color':
size_for_calculations = self.size_for_calculations
im = self.data_im.convert("L")
width,height = im.size
scaling = 1.0
newwidth,newheight = width,height
if width > height:
if width > size_for_calculations:
scaling = (size_for_calculations * 1.0) / width
newwidth,newheight = int(newwidth * scaling),int(newheight * scaling)
elif height > width:
if height > size_for_calculations:
scaling = (size_for_calculations * 1.0) / height
newwidth,newheight = int(newwidth * scaling),int(newheight * scaling)
im = im.resize((newwidth,newheight), Image.ANTIALIAS)
self.data_im = im
elif self.binary_or_gray_or_color == 'gray':
size_for_calculations = self.size_for_calculations
im = self.data_im
width,height = im.size
scaling = 1.0
newwidth,newheight = width,height
if width > height:
if width > size_for_calculations:
scaling = (size_for_calculations * 1.0) / width
newwidth,newheight = int(newwidth * scaling),int(newheight * scaling)
elif height > width:
if height > size_for_calculations:
scaling = (size_for_calculations * 1.0) / height
newwidth,newheight = int(newwidth * scaling),int(newheight * scaling)
im = im.resize((newwidth,newheight), Image.ANTIALIAS)
self.data_im = im
elif self.binary_or_gray_or_color == 'binary':
# The "L" option, which stands for "luminance" first converts the image
# to gray (in case it was color) and then the "1" option converts into
# a binary image:
converted = self.data_im.convert("L").convert("1")
self.data_im = self._return_binarized_image(converted)
else:
sys.exit("The image must be declared to be either binary, or gray, or color")
def compute_gradient_image(self):
'''
The Watershed algorithm is applied to the gradient of the input image. This
module compute the gradient image. The gradient calculation is carried out
after the image is smoothed with a Gaussian kernel whose sigma is set in the
constructor.
'''
gray_image = self.data_im
width,height = gray_image.size
if self.debug:
print "For the gray-level image: width: %d height: %d" % (width,height)
sigma = self.sigma
gray_image_as_array = numpy.zeros((height, width), dtype="float")
for i in range(0, height):
for j in range(0, width):
gray_image_as_array[(i,j)] = gray_image.getpixel((j,i))
self.gray_image_as_array = gray_image_as_array
smoothing_op = _gaussian(sigma)
smoothed_image_array = _convolution_2D(gray_image_as_array, smoothing_op)
if self.debug:
self._display_image_array_row(smoothed_image_array,32,"smoothed image array")
gradient_image_array = numpy.zeros((height, width), dtype="float")
for i in range(1, height):
for j in range(1, width):
x_partial = smoothed_image_array[(i,j)] - smoothed_image_array[(i,j-1)]
y_partial = smoothed_image_array[(i,j)] - smoothed_image_array[(i-1,j)]
magnitude = math.sqrt( x_partial**2 + y_partial**2 )
gradient_image_array[(i,j)] = magnitude
for j in range(1,width):
gradient_image_array[(0,j)] = gradient_image_array[(1,j)]
for i in range(1,height):
gradient_image_array[(i,0)] = gradient_image_array[(i,1)]
gradient_image_array[(0,0)] = ( gradient_image_array[(0,1)] + gradient_image_array[(1,0)] + gradient_image_array[(1,1)] ) / 3.0
maxGradientVal = gradient_image_array.max()
minGradientVal =gradient_image_array.min()
normalized_grad_image_array = numpy.zeros((height, width), dtype="int")
for i in range(height):
for j in range(width):
newval = int( (gradient_image_array[(i,j)] - minGradientVal) * (255/(maxGradientVal-minGradientVal)) )
if newval < (255 * self.gradient_threshold_as_fraction):
newval = 0
normalized_grad_image_array[(i,j)] = newval
gradient_image_array = normalized_grad_image_array
self._display_and_save_array_as_image( gradient_image_array, "_gradient__" + str(sigma) )
self.gradient_image_as_array = gradient_image_array
maxValOfGradientMag = gradient_image_array.max()
minValOfGradientMag = gradient_image_array.min()
self.max_number_of_levels_in_gradient = maxValOfGradientMag +1
self.min_grad_level = minValOfGradientMag
self.max_grad_level = int(maxValOfGradientMag * self.max_gradient_to_be_reached_as_fraction)
def compute_LoG_image(self):
'''
This method computes the Laplacian-of-Gaussian (LoG) of an image. The LoG is
calculated as the difference of two Gaussian-smoothed versions of the input
image at two slightly difference scales. The LoG itself is NOT used in
watershed calculations.
'''
sigma = self.sigma
width,height = self.data_im.size
gray_image = self.data_im
gray_image_as_array = numpy.zeros((height, width), dtype="float")
for i in range(0, height):
for j in range(0, width):
gray_image_as_array[(i,j)] = gray_image.getpixel((j,i))
self.gray_image_as_array = gray_image_as_array
smoothing_op1 = _gaussian(sigma)
smoothing_op2 = _gaussian(sigma*1.20)
smoothed_image_array1 = _convolution_2D(gray_image_as_array, smoothing_op1)
smoothed_image_array2 = _convolution_2D(gray_image_as_array, smoothing_op2)
diff_image_array = smoothed_image_array1 - smoothed_image_array2 # DoG
self._display_and_save_array_as_image( diff_image_array, "_LoG__" + str(sigma) )
def dilate(self, structuring_element_rad):
'''
This is to just demonstrate the basic idea of dilation of a binary pattern by
a disk structuring element whose radius is supplied as the argument. This
method itself is NOT used in the watershed calculations. For large binary
patterns, it would be more efficient to carry out the dilations only at the
border pixels.
'''
argimage = self.data_im
(width,height) = argimage.size
im_out = Image.new("1", (width,height), 0)
a = structuring_element_rad
for j in range(0,height):
for i in range(0,width):
if argimage.getpixel((i,j)) != 0:
for l in range(-a,a+1):
for k in range(-a,a+1):
if j+l >= 0 and j+l < width and i+k >= 0 and i+k < height:
im_out.putpixel((i+k,j+l), 255)
im_out.save("dilation.jpg")
self.displayImage2(im_out, "Dilated Pattern (close window when done viewing)")
return im_out
def erode(self, argimage, structuring_element_rad):
'''
This is to just demonstrate the basic idea of erosion of a binary pattern by
a disk structuring element whose radius is supplied as the argument. This
method itself is NOT used in the watershed calculations.
'''
(width,height) = argimage.size
erosion_out = Image.new("1", (width,height), 0)
a = structuring_element_rad
for j in range(0,height):
for i in range(0,width):
if argimage.getpixel((i,j)) ==255:
image_pixels_at_struct_ele_offsets_all_okay = 1
for l in range(-a,a+1):
for k in range(-a,a+1):
if j+l >= 0 and j+l < width and i+k >= 0 and i+k < height:
if argimage.getpixel((i+k,j+l)) != 255:
image_pixels_at_struct_ele_offsets_all_okay = 0
break
if image_pixels_at_struct_ele_offsets_all_okay == 0:
break
if image_pixels_at_struct_ele_offsets_all_okay == 1:
erosion_out.putpixel((i,j), 255)
image_pixels_at_struct_ele_offsets_all_okay = 1
erosion_out.save("erosion.jpg")
self.displayImage2(erosion_out, "Pattern After Erosion (close window when done viewing)")
return erosion_out
def dilate_mark_in_its_blob(self, mark_index):
'''
This method illustrates distance mapping of a blob in a binary image with
respect to a mark created by clicking at a point within the blob.
'''
mark_region = self.marks_dictionary[mark_index]
relevant_blob_index = self.marks_to_blobs_mapping_dict[mark_index]
blob = self.blob_dictionary[relevant_blob_index]
blob_center = self.blob_center_dict[relevant_blob_index]
blob_diameter = self.blob_dia_dict[relevant_blob_index]
print "blob center is: ", blob_center # blob always refers to image
print "blob diameter is: ", blob_diameter
print "\n\nImage blob selected by mark:\n"
_display_portion_of_array_binary( blob, blob_center, blob_diameter )
print "\n\nOriginal mark:\n"
_display_portion_of_array_binary( mark_region, blob_center, blob_diameter )
dilated_mark = \
self._unit_dilation_of_marker_in_its_blob(mark_region, mark_index, 1)
if dilated_mark != None:
print "\n\nDilated mark for dilation of %d:\n" % (1)
_display_portion_of_array_binary( dilated_mark, blob_center, blob_diameter )
for i in range(2,40):
dilated_mark = \
self._unit_dilation_of_marker_in_its_blob(dilated_mark, mark_index, i)
if dilated_mark == None:
print "\n\nMax number of dilations reached at distance", i-1
break
print "\n\nDilated mark for dilation level of %d\n" % (i)
_display_portion_of_array_binary( dilated_mark, blob_center, blob_diameter )
def connected_components(self, data_or_marks):
'''
This method is the basic connected components algorithm in the Watershed
module. Just for programming convenience related to the I/O from this
method, I have made a distinction between carrying out a connected-components
labeling of a binary image and doing the same for a binary pattern that
contains all of the marks made by the user.
'''
if data_or_marks == "data":
binaryImage = self.data_im
print "Applying connected components algorithm to the binary pattern"
elif data_or_marks == "marks":
binaryImage = self.marker_image_data
print "Applying connected components algorithm to the marks"
else:
sys.exit("Wrong argument supplied to connected_components method")
equivalences = sets.Set() # set of pairs of equivalent labels
width,height = binaryImage.size
componentID_array = numpy.zeros((height, width), dtype="int")
markID_data_array = numpy.zeros((height, width), dtype="int")
current_componentID = 1
if binaryImage.getpixel((0,0)) ==255:
componentID_array[(0,0)] = current_componentID
current_componentID += 1
for i in range(1, width):
if binaryImage.getpixel((i,0)) == 255:
if componentID_array[(0,i-1)] != 0:
componentID_array[(0,i)] = componentID_array[(0,i-1)]
else:
componentID_array[(0,i)] = current_componentID
current_componentID += 1
for j in range(1, height):
if binaryImage.getpixel((0,j)) == 255:
if componentID_array[(j-1,0)] != 0:
componentID_array[(j,0)] = componentID_array[(j-1,0)]
else:
componentID_array[(j,0)] = current_componentID
current_componentID += 1
for j in range(1, height):
for i in range(1, width):
if componentID_array[(j,i-1)] != 0 and \
componentID_array[(j-1,i)] != 0:
equivalences.add((componentID_array[(j,i-1)],\
componentID_array[(j-1,i)]))
if binaryImage.getpixel((i,j)) == 255:
if componentID_array[(j,i-1)] != 0:
componentID_array[(j,i)] = componentID_array[(j,i-1)]
elif componentID_array[(j-1,i-1)] != 0:
componentID_array[(j,i)] = componentID_array[(j-1,i-1)]
elif componentID_array[(j-1,i)] != 0:
componentID_array[(j,i)] = componentID_array[(j-1,i)]
else:
componentID_array[(j,i)] = current_componentID
current_componentID += 1
if self.debug:
print "equivalences: ", equivalences
equivalences_as_lists = []
for eachset in equivalences:
equivalences_as_lists.append(list(eachset))
propagated_equivalences = _coalesce(equivalences_as_lists, [] )
if self.debug:
print "propagated equivalences: ", propagated_equivalences
number_of_components = len( propagated_equivalences )
if self.debug:
print "number of components: ", number_of_components
if data_or_marks == "data":
self.number_of_components = number_of_components
elif data_or_marks == "marks":
self.number_of_marks = number_of_components
label_mappings = {}
mapped_label = 1
for equiv_list in propagated_equivalences:
for label in equiv_list:
label_mappings[label] = mapped_label
mapped_label += 1
if self.debug:
print label_mappings
if data_or_marks == "data":
for j in range(0, height):
for i in range(0, width):
if componentID_array[(i,j)] != 0:
componentID_array[(i,j)] = label_mappings[componentID_array[(i,j)]]
self.componentID_array = componentID_array
self._make_blob_dictionary()
elif data_or_marks == "marks":
for j in range(0, height):
for i in range(0, width):
if componentID_array[(i,j)] != 0:
markID_data_array[(i,j)] = label_mappings[componentID_array[(i,j)]]
self.markID_data_array = markID_data_array
self._make_marks_dictionary()
def mark_blobs(self):
'''
For demonstrations of distance mapping of a binary blob with respect to a
marker blob, this method allows a user to both select one or more blobs in a
binary image for the purpose of distance mapping and to also place marks on
the blobs.
'''
global marker_image
old_files_with_marks = glob.glob("_mark_for_*.jpg")
for oldfile in old_files_with_marks:
os.remove(oldfile)
old_files_with_mark_blobs = glob.glob("_component_image_for_mark_*.jpg")
for oldfile in old_files_with_mark_blobs:
os.remove(oldfile)
mw = Tkinter.Tk()
mw.title("Mark a blob by clicking (THEN CLICK SAVE and EXIT)")
width,height = self.original_im.size
marker_image = Image.new("1", (width, height), 0)
self.marker_image_file_name = "_mark_for_" + self.data_im_name
data_image_width, data_image_height = self.data_im.size
display_x,display_y = width,height
if width > height:
display_x = 600
display_y = int(600.0 * (height * 1.0 / width))
else:
display_y = 600
display_x = int(600.0 * (width * 1.0 / height))
self.display_size = (display_x,display_y)
mw.configure(height = display_y, width = display_x)
mw.resizable( 0, 0 )
display_im = self.original_im.copy()
display_im = display_im.resize((display_x,display_y), Image.ANTIALIAS)
mark_scale_x = data_image_width / (1.0 * display_x)
mark_scale_y = data_image_height / (1.0 * display_y)
# Even though the user will mark an expanded version of the image, the
# markers themselves will be stored in images of size the original data
# image:
photo_image = ImageTk.PhotoImage(display_im)
canvasM = Tkinter.Canvas( mw,
width = display_x,
height = display_y,
cursor = "crosshair" )
canvasM.pack( side = 'top' )
frame = Tkinter.Frame(mw)
frame.pack( side = 'bottom' )
Tkinter.Button( frame,
text = 'Save', #
command = lambda: self._resize_and_save_for_marked_blobs(marker_image,self.marker_image_file_name,width,height)
).pack( side = 'left' )
Tkinter.Button( frame,
text = 'Exit',
command = lambda: mw.destroy()
).pack( side = 'right' )
canvasM.bind("<Button-1>", lambda e: self._blobmarker(e,mark_scale_x,mark_scale_y))
canvasM.create_image( 0,0, anchor=NW, image=photo_image)
canvasM.pack(fill=BOTH, expand=1)
mw.mainloop()
self._binarize_marks()
def mark_blobs_no_image_scale_change(self):
'''
For demonstrations of distance mapping of a binary blob with respect to a
marker blob, this method allows a user to both select one or more blobs in a
binary image for the purpose of distance mapping and to also place marks on
the blobs.
'''
global marker_image
old_files_with_marks = glob.glob("_mark_for_*.jpg")
for oldfile in old_files_with_marks:
os.remove(oldfile)
old_files_with_mark_blobs = glob.glob("_component_image_for_mark_*.jpg")
for oldfile in old_files_with_mark_blobs:
os.remove(oldfile)
mw = Tkinter.Tk()
mw.title("Mark a blob by clicking (THEN CLICK SAVE and EXIT)")
width,height = self.data_im.size
marker_image = Image.new("1", (width, height), 0)
self.marker_image_file_name = "_mark_for_" + self.data_im_name
mw.configure(height = height, width = width)
mw.resizable( 0, 0 )
photo_image = ImageTk.PhotoImage(self.data_im)
canvasM = Tkinter.Canvas( mw,
width = width,
height = height,
cursor = "crosshair" )
canvasM.pack( side = 'top' )
frame = Tkinter.Frame(mw)
frame.pack( side = 'bottom' )
Tkinter.Button( frame,
text = 'Save',
command = lambda: marker_image.save(self.marker_image_file_name)
).pack( side = 'left' )
Tkinter.Button( frame,
text = 'Exit',
command = lambda: mw.destroy()
).pack( side = 'right' )
canvasM.bind("<Button-1>", lambda e: self._blobmarker(e))
canvasM.create_image( 0,0, anchor=NW, image=photo_image)
canvasM.pack(fill=BOTH, expand=1)
mw.mainloop()
self._binarize_marks()
def compute_influence_zones_for_marks(self):
'''
Calculates the influence zones in a binary blob with respect to the marks
placed inside the blob. The method also identifies the pixels at the
geodesic skeleton formed by the influence zones.
'''
width,height = self.data_im.size
for blob_index in self.blobs_to_marks_mapping_dict.keys():
blob_center = self.blob_center_dict[blob_index]
blob_diameter = self.blob_dia_dict[blob_index]
blob_array = self.blob_dictionary[blob_index]
set_of_marks_for_blob = sets.Set()
set_of_marks_for_blob.update(self.blobs_to_marks_mapping_dict[blob_index])
if len(set_of_marks_for_blob) == 0: continue
print "\n\nHere is the blob you selected for IZ calculation:"
_display_portion_of_array_binary(blob_array, blob_center, blob_diameter)
composite_mark_array = numpy.zeros((height, width), dtype="int")
for mark_index in set_of_marks_for_blob:
marked_region = self.marks_dictionary[mark_index]
marked_region *= mark_index
composite_mark_array += marked_region
_display_portion_of_array_binary(composite_mark_array, blob_center, blob_diameter)
dilated_IZ = self._unit_dilation_of_influence_zones(composite_mark_array, blob_array, set_of_marks_for_blob)
if dilated_IZ != None:
print "\n\nDilated IZ for dilation of %d:\n" % (1)
_display_portion_of_array_binary( dilated_IZ, blob_center, blob_diameter )
for i in range(2,40):
dilated_IZ = self._unit_dilation_of_influence_zones(dilated_IZ, blob_array, set_of_marks_for_blob)
if dilated_IZ == None:
print "\n\nMax number of dilations reached at distance", i-1
break
print "\n\nDilated IZ for dilation level of %d\n" % (i)
_display_portion_of_array_binary(dilated_IZ, blob_center, blob_diameter)
def compute_Z_level_sets_for_gradient_image(self):
'''
For any value of n between 0 and 255, both ends inclusive, a pixel is in the
set Z_n if the gradient value at that pixel is less than or equal to n. Note
that the gradient values are normalized to be between 0 and 255.
'''
gradient_image_as_array = self.gradient_image_as_array
height,width = gradient_image_as_array.shape
decimation_factor = self.level_decimation_factor
self.max_grad_level = int(self.max_grad_level / decimation_factor )
new_Z_levels_dict = {}
for level in range(self.max_grad_level):
new_Z_levels_dict[level] = numpy.zeros((height, width), dtype="int")
for i in range(height):
for j in range(width):
for level in range(self.max_grad_level):
if gradient_image_as_array[(i,j)] <= level * decimation_factor:
new_Z_levels_dict[level][(i,j)] = 1
self.Z_levels_dict = new_Z_levels_dict
def propagate_influence_zones_from_bottom_to_top_of_Z_levels(self):
'''
Basic to watershed computation is the calculation of influence zones of the
connected components for one Z level in the connected components in the next
Z level. Note that we stop at one level below the max level at which Z sets
are known. That is because the last IZ calculation consists of finding the
influence zones of the Z sets at the 'self.max_grad_level-1' level in the Z
sets at the 'self.max_grad_level' level.
'''
for level in range(self.min_grad_level, self.max_grad_level-1):
print "Propagating influence zones from level %d to level %d" % (level, level+1)
self._compute_influence_zone_of_one_Z_level_in_the_next(level)
def mark_image_regions_for_gradient_mods(self):
'''
For watershed segmentation that incorporates user-supplied modifications to
the image gradients, this method allows a user to demarcate through mouse
clicks polygonal regions where the gradient can be explicitly set to 0. For
each region thus demarcated, the mouse clicks must be supplied in a clockwise
fashion.
'''
files_with_region_marks = glob.glob("_region__*.jpg")
for oldfile in files_with_region_marks:
os.remove(oldfile)
done = 0
while done == 0:
region_index = 0
print "Enter marks for Region 1:"
Watershed.region_mark_coords[0] = []
self._get_marks_for_one_region(region_index)
while 1:
answer = raw_input( "More regions to be marked? Enter 'y' for 'yes' or 'n' for 'no': ")
answer = answer.strip()
if answer == "n":
done = 1
self.region_marks_centers = Watershed.region_mark_coords
break
elif answer == "y":
region_index += 1
Watershed.region_mark_coords[region_index] = []
self._get_marks_for_one_region(region_index)
else:
print "You can only enter 'y' or 'n' for 'yes' and 'no'. Let's try again."
palette = [(255,0,0), (0,255,0), (0,0,255), (255,255,255), (192,68,192)]
color_index = 0
width,height = self.display_size
# Now place all marks in a single image using different colors:
composite_image = self.original_im.copy()
composite_image = composite_image.resize((width,height), Image.ANTIALIAS)
if self.binary_or_gray_or_color == "gray":
new_composite_image = Image.new("RGB", (width,height), (0,0,0))
for j in range(0, height):
for i in range(0, width):
gray_val = composite_image.getpixel((i,j))
new_composite_image.putpixel((i,j), (gray_val,gray_val,gray_val))
composite_image = new_composite_image
for region_index in self.region_marks_centers:
color_index %= len(palette)
for mark_center in self.region_marks_centers[region_index]:
for j in range(0, height):
for i in range(0, width):
if ((i-mark_center[0])**2 + (j-mark_center[1])**2) < 100:
composite_image.putpixel((i,j), palette[color_index])
color_index += 1
composite_image.save("_composite_image_with_all_marks_" + self.data_im_name)
self.displayImage2(composite_image, "All Marks Entered (close window when done viewing)")
self._make_new_valleys_from_region_marks()
def modify_gradients_with_marker_minima(self):
'''
After a user has demarcated the regions in which the image gradients can be
modified, this method carries out the gradient modifications.
'''
new_gradient_array = self.gradient_image_as_array.copy()
for region_index in self.marked_valley_for_region:
new_gradient_array *= self.marked_valley_for_region[region_index]
self.gradient_image_as_array = new_gradient_array
self._display_and_save_array_as_image(new_gradient_array, "_marker_modified_gradient")
def extract_watershed_contours(self):
'''
This method uses the border following algorithm to extract the watershed
contours from the final propagation of influences by the propagate_influences
method.
'''
result_componentID_array = self.componentID_array_dict[self.max_grad_level-1]
height,width = result_componentID_array.shape
result_watershed_array = numpy.zeros((height, width), dtype="int")
for i in range(height):
for j in range(width):
if result_componentID_array[(i,j)] == -1:
result_watershed_array[(i,j)] = 1
if self.debug:
print "\n\nDisplay the watershed pixels as a binary image:\n"
_display_portion_of_array(result_watershed_array)
myarray = result_watershed_array
height,width = myarray.shape
contours = [] # To allow for multiple contours, each a list of points
while 1:
contour = [] # for one contour
start_flag = 0
# Find first point for starting contour
for i in range(height):
for j in range(width):
if myarray[(i,j)] == 1:
myarray[(i,j)] = -1
start_flag = 1
break
if start_flag == 1: break
if start_flag == 0:
if self.debug:
print "\n\nDisplay all contours: ", contours
break
start_i, start_j = i,j
i,j = start_i,start_j
contour.append((i,j))
if self.debug:
print "contour starts at: ", (i,j)
Rpoints = [(i-1,j-1),(i-1,j),(i-1,j+1),(i,j+1),(i+1,j+1),(i+1,j),(i+1,j-1)] # Rpoints_W
contour_done_flag = 0
while 1:
contour_extension_flag = 0
if self.debug:
print "contour is: ", contour
print "Rpoints are: ", Rpoints
for l in range(len(Rpoints)):
p,q = Rpoints[l]
if (p,q) == (start_i,start_j):
contour_done_flag = 1
break
elif p > 0 and p < height and q > 0 and q < width and myarray[(p,q)] == 1:
contour_extension_flag = 1
if self.debug:
print "new point on contour: ", (p,q)
contour.append(Rpoints[l])
m,n = i,j
i,j = p,q
if m == i-1 and n == j-1:
Rpoints = [(i-1,j),(i-1,j+1),(i,j+1),(i+1,j+1),(i+1,j),(i+1,j-1),(i,j-1)] # Rpoints_NW
break
elif m == i-1 and n == j:
Rpoints = [(i-1,j+1),(i,j+1),(i+1,j+1),(i+1,j),(i+1,j-1),(i,j-1),(i-1,j-1)] # Rpoints_N
break
elif m == i-1 and n == j+1:
Rpoints = [(i,j+1),(i+1,j+1),(i+1,j),(i+1,j-1),(i,j-1),(i-1,j-1),(i-1,j)] # Rpoints_NE
break
elif m == i and n == j+1:
Rpoints = [(i+1,j+1),(i+1,j),(i+1,j-1),(i,j-1),(i-1,j-1),(i-1,j),(i-1,j+1)] # Rpoints_E
break
elif m == i+1 and n == j+1:
Rpoints = [(i+1,j),(i+1,j-1),(i,j-1),(i-1,j-1),(i-1,j),(i-1,j+1),(i,j+1)] # Rpoints_SE
break
elif m == i+1 and n == j:
Rpoints = [(i+1,j-1),(i,j-1),(i-1,j-1),(i-1,j),(i-1,j+1),(i,j+1),(i+1,j+1)] # Rpoints_S
break
elif m == i+1 and n == j-1:
Rpoints = [(i,j-1),(i-1,j-1),(i-1,j),(i-1,j+1),(i,j+1),(i+1,j+1),(i+1,j)] # Rpoints_SW
break
elif m == i and n == j-1:
Rpoints = [(i-1,j-1),(i-1,j),(i-1,j+1),(i,j+1),(i+1,j+1),(i+1,j),(i+1,j-1)] # Rpoints_W
break
myarray[(i,j)] = -1
if contour_done_flag == 1: break
if contour_extension_flag == 0: break
if self.debug:
print "\n\nHere is the contour: ", contour
print myarray
contours.append(contour)
self.watershed_contours = contours
def display_data_image(self):
'''
This is just a convenience method for displaying the image that you want to
subject to watershed segmentation.
'''
image = self.original_im
self.displayImage2(image, "Original Image (close window when done viewing)")
def display_watershed(self):
'''
Displays the watershed segmentation of the image in the grayscale mode. That
is, the image shown is what the computations are carried out on --- a
grayscale version of the input image (assuming it was a color image).
'''
result_componentID_array = self.componentID_array_dict[self.max_grad_level -1]
height,width = result_componentID_array.shape
newimage = Image.new("RGB", (width,height), (0,0,0))
original_image = self.data_im.copy()
draw = ImageDraw.Draw(original_image)
for i in range(0, height):
for j in range(0, width):
if result_componentID_array[(i,j)] == -1:
draw.point((j,i), 255)
self.displayImage2(original_image, "Watershed Segmentation (close window when done viewing)")
def display_watershed_in_color(self):
'''
Displays the watershed segmentation on top of the original color image
(assuming that the input image was a color image to begin with.)
'''
im = self.data_im
im_width,im_height = im.size
result_componentID_array = self.componentID_array_dict[self.max_grad_level -1]
watershed_points = []
for i in range(im_height):
for j in range(im_width):
if result_componentID_array[(i,j)] == -1:
watershed_points.append((i,j))
winsize_x,winsize_y = im_width,im_height
if im_width > im_height:
winsize_x = 600
winsize_y = int(600.0 * (im_height * 1.0 / im_width))
else:
winsize_y = 600
winsize_x = int(600.0 * (im_width * 1.0 / im_height))
original_im = self.original_im.copy()
original_im = original_im.resize((winsize_x,winsize_y), Image.ANTIALIAS)
mw = Tkinter.Tk()
mw.title( "Watershed Segmentation (close window when done viewing)" )
mw.configure( height = winsize_y, width = winsize_x )
mw.resizable( 0, 0 )
canvas = Tkinter.Canvas( mw,
height = winsize_y,
width = winsize_x,
cursor = "crosshair" )
canvas.pack( side = 'top' )
frame = Tkinter.Frame(mw)
frame.pack( side = 'bottom' )
Tkinter.Button( frame,
text = 'Save',
command = lambda: canvas.postscript( file = "_watershed_point_output.jpg")
).pack( side = 'left' )
Tkinter.Button( frame,
text = 'Exit',
command = lambda: mw.destroy(),
).pack( side = 'right' )
x_scale = winsize_x / (im_width * 1.0)
y_scale = winsize_y / (im_height * 1.0)
photo = ImageTk.PhotoImage( original_im )
canvas.create_image(winsize_x/2,winsize_y/2,image=photo)
for point in watershed_points:
x1 = int(point[1] * x_scale)
y1 = int(point[0] * y_scale)
x2 = x1 + 5
y2 = y1 + 5
canvas.create_oval(x1,y1,x2,y2, fill='red')
Tkinter.mainloop()
def display_watershed_contours_in_color(self):
'''
Shows the watershed contours as extracted by the extract_watershed_contours()
method.
'''
im = self.data_im
im_width,im_height = im.size
contours = self.watershed_contours
winsize_x,winsize_y = im_width,im_height
if im_width > im_height:
winsize_x = 600
winsize_y = int(600.0 * (im_height * 1.0 / im_width))
else:
winsize_y = 600
winsize_x = int(600.0 * (im_width * 1.0 / im_height))
original_im = self.original_im.copy()
original_im = original_im.resize((winsize_x,winsize_y), Image.ANTIALIAS)
mw = Tkinter.Tk()
mw.title( "Watershed Contours (close window when done viewing)" )
mw.configure( height = winsize_y, width = winsize_x )
mw.resizable( 0, 0 )
canvas = Tkinter.Canvas( mw,
height = winsize_y,
width = winsize_x,
cursor = "crosshair" )
canvas.pack( side = 'top' )
frame = Tkinter.Frame(mw)
frame.pack( side = 'bottom' )
Tkinter.Button( frame,
text = 'Save',
command = lambda: canvas.postscript(file = "_watershed_contour_output.jpg")
).pack( side = 'left' )
Tkinter.Button( frame,
text = 'Exit',
command = lambda: mw.destroy(),
).pack( side = 'right' )
x_scale = winsize_x / (im_width * 1.0)
y_scale = winsize_y / (im_height * 1.0)
photo = ImageTk.PhotoImage( original_im )
canvas.create_image(winsize_x/2,winsize_y/2,image=photo)
for polygon in contours:
if len(polygon) < 10: continue
polygon = [(int(item[1]*x_scale), int(item[0]*y_scale)) for item in polygon]
final_polygon = []
map(lambda x: final_polygon.append(x), polygon)
canvas.create_line(final_polygon, width=4, fill='red')
Tkinter.mainloop()
def displayImage(self, argimage, title=""):
'''
Displays the argument image. The display stays on for the number of seconds
that is the first argument in the call to tk.after() divided by 1000.
'''
width,height = argimage.size
winsize_x,winsize_y = width,height
if width > height:
winsize_x = 600
winsize_y = int(600.0 * (height * 1.0 / width))
else:
winsize_y = 600
winsize_x = int(600.0 * (width * 1.0 / height))
display_image = argimage.resize((winsize_x,winsize_y), Image.ANTIALIAS)
tk = Tkinter.Tk()
tk.title(title)
frame = Tkinter.Frame(tk, relief=RIDGE, borderwidth=2)
frame.pack(fill=BOTH,expand=1)
photo_image = ImageTk.PhotoImage( display_image )
label = Tkinter.Label(frame, image=photo_image)
label.pack(fill=X, expand=1)
tk.after(1000, self._callbak, tk) # display will stay on for one second
tk.mainloop()
def displayImage2(self, argimage, title=""):
'''
Displays the argument image. The display stays on until the user closes the
window. If you want a display that automatically shuts off after a certain
number of seconds, use the previous method displayImage().
'''
width,height = argimage.size
winsize_x,winsize_y = width,height
if width > height:
winsize_x = 600
winsize_y = int(600.0 * (height * 1.0 / width))
else:
winsize_y = 600
winsize_x = int(600.0 * (width * 1.0 / height))
display_image = argimage.resize((winsize_x,winsize_y), Image.ANTIALIAS)
tk = Tkinter.Tk()
tk.title(title)
frame = Tkinter.Frame(tk, relief=RIDGE, borderwidth=2)
frame.pack(fill=BOTH,expand=1)
photo_image = ImageTk.PhotoImage( display_image )
label = Tkinter.Label(frame, image=photo_image)
label.pack(fill=X, expand=1)
tk.mainloop()
def __del__(self):
for filename in glob.glob('_component_image_for_mark*'): os.remove(filename)
for filename in glob.glob('_gradient__*.jpg'): os.remove(filename)
for filename in glob.glob('_binarized_valley_created*'): os.remove(filename)
for filename in glob.glob('_region__*.jpg'): os.remove(filename)
for filename in glob.glob('_marker_modified_gradient*'): os.remove(filename)
for filename in glob.glob('_mark_for_*.jpg'): os.remove(filename)
for filename in glob.glob('_LoG__*.jpg'): os.remove(filename)
#______________________ Static Methods of the Watershed Class ________________
@staticmethod
def _blobmarker(evt, mark_scale_x, mark_scale_y):
global marker_image
canvasM = evt.widget
markX, markY = evt.x, evt.y
print "Button pressed at: x=%s y=%s\n" % (markX, markY)
canvasM.create_oval( markX-10, markY-10, markX+10, markY+10, outline="red", fill="green", width = 2 )
width,height = marker_image.size
markX *= mark_scale_x
markY *= mark_scale_y
for i in range(0, width):
for j in range(0, height):
if ((i-markX)**2 + (j-markY)**2) < 100:
marker_image.putpixel((i,j), 255)
@staticmethod
def _region_marker(evt, region_index, data_image_width, data_image_height):
global region_marker_image
display_width, display_height = region_marker_image.size
canvasM = evt.widget
markX, markY = evt.x, evt.y
Watershed.region_mark_coords[region_index].append((markX,markY))
print "For region index %d, Button pressed at: x=%s y=%s\n" % (region_index, markX, markY)
canvasM.create_oval( markX-10, markY-10, markX+10, markY+10, outline="red", fill="green", width = 2 )
display_x,display_y = region_marker_image.size
for i in range(0, display_x):
for j in range(0, display_y):
if ((i-markX)**2 + (j-markY)**2) < 100:
region_marker_image.putpixel((i,j), 255)
@staticmethod
def _resize_and_save(region_marker_image, width, height, region_marker_image_file_name):
resized = region_marker_image.resize((width,height), Image.ANTIALIAS)
resized.save(region_marker_image_file_name)
@staticmethod
def _resize_and_save_for_marked_blobs(marked_image,file_name,width,height):
resized = marker_image.resize((width,height), Image.ANTIALIAS)
resized.save(file_name)
@staticmethod
def make_binary_pic_art_nouveau(under_what_name):
'''
Can be used to make "fun" binary images for demonstrating distance mapping of
binary blobs, calculation of influence zones, etc. This method is taken from
Chapter 13 of my book "Scripting with Objects".
'''
# global drawEnable, startX, startY, canvas
mw = Tkinter.Tk()
mw.title( "Art Nouveau" )
mw.configure( height = 650, width = 600 )
mw.resizable( 0, 0 )
Watershed.canvas = Tkinter.Canvas( mw,
height = 600,
width = 600,
bg = 'white',
cursor = "crosshair" )
Watershed.canvas.pack( side = 'top' )
frame = Tkinter.Frame(mw)
frame.pack( side = 'bottom' )
Tkinter.Button( frame,
text = 'Save',
command = lambda: Watershed.canvas.postscript( file = under_what_name + ".ps", colormode="color")
).pack( side = 'left' )
Tkinter.Button( frame,
text = 'Exit',
command = lambda: sys.exit(0),
).pack( side = 'right' )
Watershed.canvas.bind( "<Button-1>", lambda e: Watershed._drawingControl( e ) )
mw.mainloop()
@staticmethod
def _drawingControl(evt):
'''
Turn drawing on the canvas on and off with consecutive clicks of the left
button of the mouse
'''
Watershed.drawEnable = (Watershed.drawEnable + 1) % 2
if not Watershed.drawEnable:
Watershed.canvas.bind( "<Motion>", lambda e: "break" )
else:
Watershed.startX, Watershed.startY = evt.x, evt.y
print "Button pressed at: x=%s y=%s\n" % (Watershed.startX, Watershed.startY)
Watershed.canvas.bind( "<Motion>", lambda e: Watershed._draw( e ) )
@staticmethod
def _draw(evt):
'''
Event processing for the drawEnable() method
'''
canv, x, y = evt.widget, evt.x, evt.y
canv.create_arc( Watershed.startX, Watershed.startY, x, y, width = 4, fill = 'black' )
Watershed.startX, Watershed.startY = x, y
@staticmethod
def gendata( feature, imagesize, position, orientation, output_image_name ):
'''
This method is useful for generating simple binary patterns for checking the
validity of the logic used for dilation, erosion, IZ calculation, geodesic
skeleton calculation, etc. Note that the permissible values for the
'feature' parameter are: 'line', 'triangle', 'rectangle', and
'broken_rectangle'. The parameter 'imagesize' is supposed to be a tuple
(m,n) for the size of the output image desired. The parameter 'position' is
supposed to be a tuple (x,y) of pixel coordinates for specifying the position
of the binary pattern in your image. The parameter 'orientation' is an
integer value specifying the number of degrees of rotation that should be
applied to the pattern for a given 'feature'.
'''
width,height = imagesize
x,y = position
theta = orientation
tan_theta = scipy.tan( theta * scipy.pi / 180 )
cos_theta = scipy.cos( theta * scipy.pi / 180 )
sin_theta = scipy.sin( theta * scipy.pi / 180 )
im = Image.new( "L", imagesize, 0 )
draw = ImageDraw.Draw(im)
if feature == 'line':
delta = y / tan_theta
if delta <= x:
x1 = x - y / tan_theta
y1 = 0
else:
x1 = 0
y1 = y - x * tan_theta
x2 = x1 + height / tan_theta
y2 = height
if x2 > width:
excess = x2 - width
x2 = width
y2 = height - excess * tan_theta
draw.line( (x1,y1,x2,y2), fill=255)
del draw
im.save( output_image_name )
elif feature == "triangle":
x1 = int(width/2.0)
y1 = int(0.7*height)
x2 = x1 - int(width/4.0)
y2 = int(height/4.0)
x3 = x1 + int(width/4.0)
y3 = y2
draw.line( (x1,y1,x2,y2), fill=255 )
draw.line( (x1,y1,x3,y3), fill=255 )
draw.line( (x2,y2,x3,y3), fill=255 )
del draw
h2 = int(height/2)
w2 = int(width/2)
im = im.transform(imagesize, Image.AFFINE, \
(cos_theta,sin_theta,-x,-sin_theta,cos_theta,-y), Image.BICUBIC )
im.save( output_image_name )
elif feature == "rectangle":
x1 = int(width/3.0)
y1 = int(height/3.0)
x2 = int(2.0*width/3.0)
y2 = y1
x3 = x2
y3 = int(2.0*height/3.0)
x4 = x1
y4 = y3
draw.line( (x1,y1,x2,y2), fill=255 )
draw.line( (x2,y2,x3,y3), fill=255 )
draw.line( (x3,y3,x4,y4), fill=255 )
draw.line( (x4,y4,x1,y1), fill=255 )
del draw
h2 = int(height/2)
w2 = int(width/2)
im = im.transform(imagesize, Image.AFFINE, \
(cos_theta,sin_theta,-x,-sin_theta,cos_theta,-y), Image.BICUBIC )
im.save( output_image_name )
elif feature == "broken_rectangle":
x1 = int(width/3.0)
y1 = int(height/3.0)
x2 = int(2.0*width/3.0)
y2 = y1
x3 = x2
y3 = int(2.0*height/3.0)
x4 = x1
y4 = y3
draw.line( (x1,y1,x1+10,y2), fill=255 )
draw.line( (x1+15,y1,x2,y2), fill=255 )
draw.line( (x2,y2,x3,y2+30), fill=255 )
draw.line( (x2,y2+35,x3,y3), fill=255 )
draw.line( (x3,y3,x4,y4), fill=255 )
draw.line( (x4,y4,x1,y1), fill=255 )
del draw
h2 = int(height/2)
w2 = int(width/2)
im = im.transform(imagesize, Image.AFFINE, \
(cos_theta,sin_theta,-x,-sin_theta,cos_theta,-y), Image.BICUBIC )
im.save( output_image_name )
else:
print "unknown feature requested"
sys.exit(0)
#______________________ Private Methods of the Watershed Class ________________
def _display_and_save_array_as_image(self, array, what_type):
height,width = array.shape
maxVal = array.max()
minVal = array.min()
newimage = Image.new("L", (width,height), 0.0)
for i in range(0, height):
for j in range(0, width):
displayVal = int( (array[(i,j)] - minVal) * (255/(maxVal-minVal)) )
newimage.putpixel((j,i), displayVal)
self.displayImage2(newimage, what_type + " (close window when done viewing)")
image_name = what_type + "_" + self.data_im_name
newimage.save(image_name)
def _display_and_save_binary_array_as_image(self, array, what_type):
height,width = array.shape
newimage = Image.new("1", (width,height), 0.0)
for i in range(0, height):
for j in range(0, width):
if array[(i,j)] == 1:
newimage.putpixel((j,i), 255)
self.displayImage2(newimage, what_type)
image_name = what_type + "_" + self.data_im_name
newimage.save(image_name)
def _display_image_array_row(self, image_array, which_row, type_image='image'):
print "\nDisplaying row indexed %d for %s:" % (which_row,type_image)
row = image_array[which_row]
print "Number of elements in the row: ", len(row)
maxVal = row.max()
minVal = row.min()
print "The max and min values are %f %f" % (maxVal, minVal)
for i in range(len(row)):
# print "%.2f " % (row[i]),
print "%d " % (row[i]),
print "\n\n"
# The goal here is the find the SKIZ skeleton in the Z level (i+1)
# as created by the blobs at the level i. Note that each blob at level
# i will contained in some blob at level i+1. When 2 or more blobs at
# at level i are contained in the same blob at level i+1, that means
# the pixels between the level i blobs define a watershed since the
# the level i blobs are the mouths of the valleys that merge at level
# i+1. When no level i blobs are contained in a given blob at level
# i+1, then the (i+1) blob defines the beginning of a new valley for
# the rising flood as we continue to immerse the topographic relief map
# in a tub of water. (Note that SKIZ stands for Skeleton formed by
# Influence Zones.) The blobs at level i define the influence zones
# for the blobs at level i+1.
def _compute_influence_zone_of_one_Z_level_in_the_next(self, level):
width,height = self.data_im.size
if level == 0:
self.start_label_for_labeling_blobs_vs_level[0] = 1
self._gray_level_connected_components(0,1)
self.label_pool_at_level[0] = range(1,self.level_vs_number_of_components[0]+1)
self._make_blob_dictionary_at_one_Z_level(0)
self.start_label_for_labeling_blobs_vs_level[level+1] = self.start_label_for_labeling_blobs_vs_level[level] + self.level_vs_number_of_components[level]
self._gray_level_connected_components(level+1,self.start_label_for_labeling_blobs_vs_level[level+1])
self.label_pool_at_level[level+1] = range(self.start_label_for_labeling_blobs_vs_level[level+1], self.start_label_for_labeling_blobs_vs_level[level+1] + self.level_vs_number_of_components[level+1] + 1)
self._make_blob_dictionary_at_one_Z_level(level+1)
componentID_array_at_current_level = self.componentID_array_dict[level]
componentID_array_at_next_level = self.componentID_array_dict[level+1]
current_level_label_mapping_dict = {}
current_level_labels_used = sets.Set()
next_Z_level_containment = {}
for i in range(height):
for j in range(width):
current_level_label = componentID_array_at_current_level[(i,j)]
next_level_label = componentID_array_at_next_level[(i,j)]
if current_level_label > 0 and next_level_label > 0:
current_level_label_mapping_dict[current_level_label] = next_level_label
if next_level_label not in next_Z_level_containment:
next_Z_level_containment[next_level_label] = sets.Set([current_level_label])
else:
next_Z_level_containment[next_level_label].add(current_level_label)
all_labels_next_level = self.blob_dictionary_by_level_for_Z[level+1].keys()
if self.debug:
print "\n\nAt level %d: All labels next level: %s\n" % (level,all_labels_next_level)
nextlevel = level + 1
if self.debug:
for label in next_Z_level_containment:
print "\nLevel: %d --- blob labeled %d contains lower level blobs labeled %s" %(nextlevel, label, next_Z_level_containment[label])
print "Level: %d --- next_Z_level_containment has keys: %s " % (level, next_Z_level_containment.keys())
print "keys in the next level blob dictionary: ", self.blob_dictionary_by_level_for_Z[level+1].keys()
print "componentID array for level ", level
_display_portion_of_array(self.componentID_array_dict[level])
print "\n\nStarting BIG LOOP\n\n"
for blob_index in next_Z_level_containment:
if self.debug:
print "for next level, we are working with blob index ", blob_index
blob_array = self.blob_dictionary_by_level_for_Z[level+1][blob_index]
lower_level_labels_contained = next_Z_level_containment[blob_index]
if self.debug:
print "INSIDE THE BIG LOOP: lower_level_labels_contained: ", lower_level_labels_contained
if len(lower_level_labels_contained) == 0: continue
self.label_pool_at_level[level+1].remove(blob_index)
self.label_pool_at_level[level+1] += list(lower_level_labels_contained)
self.level_vs_number_of_components[level+1] += len(lower_level_labels_contained) - 1
composite_mark_array = numpy.zeros((height, width), dtype="int")
for lower_level_blob_index in next_Z_level_containment[blob_index]:
marked_region = self.blob_dictionary_by_level_for_Z[level][lower_level_blob_index]
marked_region *= lower_level_blob_index
composite_mark_array += marked_region
self.no_more_dilation_possible = 0
dilated_IZ = self._unit_dilation_of_influence_zones2(composite_mark_array, blob_array, lower_level_labels_contained)
if self.no_more_dilation_possible == 0:
for i in range(2,40):
dilated_IZ = self._unit_dilation_of_influence_zones2(dilated_IZ, blob_array, lower_level_labels_contained)
if self.no_more_dilation_possible == 1:
if self.debug:
print "\n\nMax number of dilations reached at distance %d for blob index %d at level %d" % (i-1, blob_index, level)
break
del self.blob_dictionary_by_level_for_Z[level+1][blob_index]
sub_blob_dictionary = {}
for label in lower_level_labels_contained:
blob_array = numpy.zeros((height, width), dtype="int")
for i in range(0, height):
for j in range(0, width):
if dilated_IZ[(i,j)] == label:
blob_array[(i,j)] = 1
elif dilated_IZ[(i,j)] < 0:
componentID_array_at_next_level[(i,j)] = dilated_IZ[(i,j)]
self.blob_dictionary_by_level_for_Z[level+1][label] = blob_array
new_componentID_array_at_next_level = numpy.zeros((height, width), dtype="int")
for i in range(0, height):
for j in range(0, width):
if componentID_array_at_next_level[(i,j)] <= 0:
new_componentID_array_at_next_level[(i,j)] = componentID_array_at_next_level[(i,j)]
for blob_index in self.blob_dictionary_by_level_for_Z[level+1].keys():
blob_array = self.blob_dictionary_by_level_for_Z[level+1][blob_index]
for i in range(0, height):
for j in range(0, width):
if blob_array[(i,j)] == 1:
new_componentID_array_at_next_level[(i,j)] = blob_index
if self.debug:
print "Reconstructed componentID array for level ", level + 1
_display_portion_of_array(new_componentID_array_at_next_level)
self.componentID_array_dict[level+1] = new_componentID_array_at_next_level
self._make_blob_dictionary_at_one_Z_level(level+1)
def _unit_dilation_of_influence_zones2(self, input_array, blob_array, set_of_marks_for_blob):
(width,height) = self.data_im.size
dilated_array = input_array.copy()
sorted_mark_labels = sorted(list(set_of_marks_for_blob), reverse=True)
skiz_label = -1
change_made = 0
for i in range(0,height):
for j in range(0,width):
if input_array[(i,j)] > 0:
if ( ( (j+1) >= 0 and (j+1) < width ) and input_array[(i,j+1)] == 0 ) or \
( ( (j-1) >= 0 and (j-1) < width ) and input_array[(i,j-1)] == 0 ) or \
( ( (i-1) >= 0 and (i-1) < height ) and input_array[(i-1,j)] == 0 ) or \
( ( (i+1) >= 0 and (i+1) < height ) and input_array[(i+1,j)] == 0 ):
ij_label = input_array[(i,j)]
other_labels = set_of_marks_for_blob.difference(sets.Set([ij_label]))
for k in range(-1,2):
for l in range(-1,2):
if (i+k) >= 0 and (i+k) < height and (j+l) >= 0 and (j+l) < width:
if blob_array[(i+k,j+l)] != 0:
if dilated_array[(i+k,j+l)] == 0:
test_bit = 1
for s in range(-1,2):
for t in range(-1,2):
if (i+k+s) >= 0 and (i+k+s) < height and (j+l+t) >= 0 and (j+l+t) < width:
if dilated_array[(i+k+s,j+l+t)] in other_labels:
test_bit = 0
if test_bit == 1:
change_made = 1
dilated_array[(i+k,j+l)] = dilated_array[(i,j)]
else:
dilated_array[(i+k,j+l)] = skiz_label
if change_made == 0:
self.no_more_dilation_possible = 1
return dilated_array
def _gray_level_connected_components(self, level, start_label):
binary_image_array = self.Z_levels_dict[level]
equivalences = sets.Set() # set of pairs of equivalent labels
height,width = binary_image_array.shape
componentID_array = numpy.zeros((height, width), dtype="int")
current_componentID = 1
if binary_image_array[(0,0)] == 1:
componentID_array[(0,0)] = current_componentID
current_componentID += 1
for j in range(1, width):
if binary_image_array[(0,j)] == 1:
if binary_image_array[(0,j-1)] == 1:
componentID_array[(0,j)] = componentID_array[(0,j-1)]
else:
componentID_array[(0,j)] = current_componentID
current_componentID += 1
for i in range(1, height):
if binary_image_array[(i,0)] == 1:
if binary_image_array[(i-1,0)] != 0:
componentID_array[(i,0)] = componentID_array[(i-1,0)]
else:
componentID_array[(i,0)] = current_componentID
current_componentID += 1
for i in range(1, height):
for j in range(1, width):
if binary_image_array[(i-1,j)] == 1 and binary_image_array[(i,j-1)] == 1:
equivalences.add((componentID_array[(i-1,j)],\
componentID_array[(i,j-1)]))
if binary_image_array[(i,j)] == 1:
if binary_image_array[(i-1,j)] == 1:
componentID_array[(i,j)] = componentID_array[(i-1,j)]
elif binary_image_array[(i-1,j-1)] == 1:
componentID_array[(i,j)] = componentID_array[(i-1,j-1)]
elif binary_image_array[(i,j-1)] ==1:
componentID_array[(i,j)] = componentID_array[(i,j-1)]
elif (j+1) < width and binary_image_array[(i-1,j+1)] == 1:
componentID_array[(i,j)] = componentID_array[(i-1,j+1)]
else:
componentID_array[(i,j)] = current_componentID
current_componentID += 1
if self.debug:
print "Inside Component Labeler --- equivalences: ", equivalences
equivalences_as_lists = []
for eachset in equivalences:
equivalences_as_lists.append(list(eachset))
propagated_equivalences = _coalesce(equivalences_as_lists, [] )
if self.debug:
print "Inside Component Labeler --- propagated equivalences at level %d: %s" % (level,propagated_equivalences)
all_labels_in_propagated_equivalences = sets.Set()
for eachlist in propagated_equivalences:
all_labels_in_propagated_equivalences.update(eachlist)
all_labels_set = sets.Set(range(1,current_componentID))
labels_not_used_in_equivalences = all_labels_set - all_labels_in_propagated_equivalences
number_of_components = len( propagated_equivalences ) + len(labels_not_used_in_equivalences)
if self.debug:
print "\nInside Component Labeler --- number of components at level %d: %d " % (level, number_of_components)
self.level_vs_number_of_components[level] = number_of_components
label_mappings = {}
mapped_label = start_label
for equiv_list in propagated_equivalences:
for label in equiv_list:
label_mappings[label] = mapped_label
mapped_label += 1
for label in labels_not_used_in_equivalences:
label_mappings[label] = mapped_label
mapped_label += 1
for i in range(0, height):
for j in range(0, width):
if componentID_array[(i,j)] != 0:
componentID_array[(i,j)] = label_mappings[componentID_array[(i,j)]]
self.componentID_array_dict[level] = componentID_array
def _make_blob_dictionary_at_one_Z_level(self, level):
componentID_array = self.componentID_array_dict[level]
height, width = componentID_array.shape
number_of_components = self.level_vs_number_of_components[level]
if self.debug:
print "\nInside _make_blob_dictionary_at_one_Z_level --- number of blobs at level %d is %d" % (level, number_of_components)
self.blob_dictionary_by_level_for_Z[level] = {}
for blob_index in self.label_pool_at_level[level]:
blob_array = numpy.zeros((height, width), dtype="int")
for i in range(0, height):
for j in range(0, width):
if componentID_array[(i,j)] == blob_index:
blob_array[(i,j)] = 1
self.blob_dictionary_by_level_for_Z[level][blob_index] = blob_array
def _unit_dilation_of_marker_in_its_blob(self, input_array, marker_index, iter_index):
(width,height) = self.data_im.size
dilated_array = input_array.copy()
componentID_array = self.componentID_array
blob_index_for_marker = self.marks_to_blobs_mapping_dict[marker_index]
change_made = 0
for j in range(0,height):
for i in range(0,width):
if input_array[(i,j)] != 0:
if ( ( (j+1) >= 0 and (j+1) < width ) and input_array[(i,j+1)] == 0 ) or \
( ( (j-1) >= 0 and (j-1) < width ) and input_array[(i,j-1)] == 0 ) or \
( ( (i-1) >= 0 and (i-1) < height ) and input_array[(i-1,j)] == 0 ) or \
( ( (i+1) >= 0 and (i+1) < height ) and input_array[(i+1,j)] == 0 ):
for l in range(-1,2):
for k in range(-1,2):
if componentID_array[(i+k,j+l)] == blob_index_for_marker:
if dilated_array[(i+k,j+l)] == 0:
change_made = 1
dilated_array[(i+k,j+l)] = iter_index
if change_made == 0: return None
return dilated_array
def _unit_erosion_of_marker_in_its_blob(self, input_array, marker_index):
(width,height) = self.data_im.size
eroded_array = numpy.zeros((height, width), dtype="int")
for j in range(0,height):
for i in range(0,width):
if ( input_array[(i,j)] == 1 ):
input_pixels_at_unit_ele_offsets_all_okay = 1
for l in range(-1,2):
for k in range(-1,2):
if j+l >= 0 and j+l < width and i+k >= 0 and i+k < height:
if input_array[(i+k,j+l)] == 0:
input_pixels_at_unit_ele_offsets_all_okay = 0
break
if iinput_pixels_at_unit_ele_offsets_all_okay == 0:
break
if input_pixels_at_struct_ele_offsets_all_okay == 1:
eroded_array[(i,j)] = 1
image_pixels_at_struct_ele_offsets_all_okay = 1
return eroded_array
def _make_blob_dictionary(self):
width,height = self.data_im.size
componentID_array = self.componentID_array
number_of_components = self.number_of_components
for blob_index in range(1,number_of_components+1):
blob_array = numpy.zeros((height, width), dtype="int")
for j in range(0, height):
for i in range(0, width):
if componentID_array[(i,j)] == blob_index:
blob_array[(i,j)] = 1
self.blob_dictionary[blob_index] = blob_array.copy()
self._compute_center_and_diameter_upper_bound_for_blobs()
def _make_marks_dictionary(self):
width,height = self.data_im.size
markID_data_array = self.markID_data_array
number_of_marks = self.number_of_marks
for marker_index in range(1,number_of_marks+1):
marker_array = numpy.zeros((height, width), dtype="int")
for j in range(0, height):
for i in range(0, width):
if markID_data_array[(i,j)] == marker_index:
marker_array[(i,j)] = 1
self.marks_dictionary[marker_index] = marker_array.copy()
self._construct_mapping_from_marks_to_blobs()
self._check_marks_fit_in_blobs()
def _return_binarized_image(self, openedimage):
argimage = openedimage
width,height = argimage.size
output_image = Image.new("1", (width,height), 0)
for j in range(0, height):
for i in range(0, width):
if argimage.getpixel((i,j)) >= 128:
output_image.putpixel((i,j), 255)
else:
output_image.putpixel((i,j), 0)
return output_image
def _binarize_marks(self):
open_marker_image = Image.open(self.marker_image_file_name)
marker_image_data = open_marker_image.convert("L").convert("1")
width,height = marker_image_data.size
output_image = Image.new("1", (width,height), 0)
for j in range(0, height):
for i in range(0, width):
if marker_image_data.getpixel((i,j)) >= 128:
output_image.putpixel((i,j), 255)
else:
output_image.putpixel((i,j), 0)
self.marker_image_data = output_image
if self.debug:
self.displayImage2(output_image)
# Assumes that the connected_components() has already been called
# on the marks image produced by mark_blobs() method. So we should
# already have a numpy array called markID_data_array.
def _construct_mapping_from_marks_to_blobs(self):
number_of_components = self.number_of_components
number_of_marks = self.number_of_marks
marker_image = self.marker_image_data
width,height = marker_image.size
markID_data_array = self.markID_data_array
indexed_mark_array = numpy.zeros((height, width), dtype="int")
for mark_index in range(1, number_of_marks+1):
mark_posX = 0
mark_posY = 0
how_many_pixels_in_mark = 0
for i in range(0, height):
for j in range(0, width):
if markID_data_array[(i,j)] == mark_index:
how_many_pixels_in_mark += 1
mark_posX += j
mark_posY += i
posX = int(mark_posX)/how_many_pixels_in_mark
posY = int(mark_posY)/how_many_pixels_in_mark
componentID_array = self.componentID_array
marked_component_index = componentID_array[(posY,posX)]
if self.debug:
print "Identity of image blob requested by the mark indexed %d is %d" % (mark_index, marked_component_index)
self.marks_to_blobs_mapping_dict[mark_index] = marked_component_index
component_image = Image.new("1", (width,height), 0)
for i in range(0, height):
for j in range(0, width):
if componentID_array[(i,j)] == marked_component_index:
component_image.putpixel((j,i), 255)
self.displayImage2(component_image, "Displaying The Blob Selected (close window when done viewing)")
output_image_name = "_component_image_for_mark_indexed_" + str(mark_index) + ".jpg"
component_image.save( output_image_name )
self._construct_mapping_from_blobs_to_marks()
def _dilate_for_unittest(self, structuring_element_rad):
argimage = self.data_im
(width,height) = argimage.size
im_out = Image.new("1", (width,height), 0)
a = structuring_element_rad
for j in range(0,height):
for i in range(0,width):
if argimage.getpixel((i,j)) != 0:
for l in range(-a,a+1):
for k in range(-a,a+1):
if j+l >= 0 and j+l < width and i+k >= 0 and i+k < height:
im_out.putpixel((i+k,j+l), 255)
im_out.save("_dilation.jpg")
self.displayImage(im_out, "Dilated Pattern")
return im_out
def _construct_mapping_from_blobs_to_marks(self):
for blob_index in self.blob_dictionary.keys():
list_of_marks_for_blob = []
for mark_index in self.marks_to_blobs_mapping_dict.keys():
if self.marks_to_blobs_mapping_dict[mark_index] == blob_index:
list_of_marks_for_blob.append(mark_index)
self.blobs_to_marks_mapping_dict[blob_index] = list_of_marks_for_blob
def _check_marks_fit_in_blobs(self):
for ii in self.marks_dictionary:
mark = self.marks_dictionary[ii]
relevant_blob_index = self.marks_to_blobs_mapping_dict[ii]
blob = self.blob_dictionary[relevant_blob_index]
height,width = mark.shape
for j in range(0,height):
for i in range(0,width):
if blob[(i,j)] == 0:
mark[(i,j)] = 0
self.marks_dictionary[ii] = mark
def _callbak(self,arg):
arg.destroy()
def _get_marks_for_one_region(self, region_index):
global region_marker_image
region_marker_index = region_index
mw = Tkinter.Tk()
mw.title("Mark Region " + str(region_index) + " by clicking CLOCKWISE in it (Must SAVE before Exit)")
width,height = self.original_im.size
data_image_width, data_image_height = self.data_im.size
display_x,display_y = width,height
if width > height:
display_x = 1000
display_y = int(1000.0 * (height * 1.0 / width))
else:
display_y = 1000
display_x = int(1000.0 * (width * 1.0 / height))
self.display_size = (display_x,display_y)
mw.configure(height = display_y, width = display_x)
mw.resizable( 0, 0 )
display_im = self.original_im.copy()
display_im = display_im.resize((display_x,display_y), Image.ANTIALIAS)
mark_scale_x = data_image_width / (1.0 * display_x)
mark_scale_y = data_image_height / (1.0 * display_y)
self.marks_scale = (mark_scale_x,mark_scale_y)
# Even though the user will mark an expanded version of the image, the
# markers themselves will be stored in images of size the original data
# image:
region_marker_image = Image.new("1", (display_x, display_y), 0)
region_marker_image_file_name = "_region__" + str(region_index) + "__markers_for_" + self.data_im_name
photo_image = ImageTk.PhotoImage(display_im)
canvasM = Tkinter.Canvas( mw,
width = display_x,
height = display_y,
cursor = "crosshair" )
canvasM.pack( side = 'top' )
frame = Tkinter.Frame(mw)
frame.pack( side = 'bottom' )
Tkinter.Button( frame,
text = 'Save',
command = lambda: self._resize_and_save(region_marker_image, data_image_width, data_image_height, region_marker_image_file_name)
).pack( side = 'left' )
Tkinter.Button( frame,
text = 'Exit',
command = lambda: mw.destroy()
).pack( side = 'right' )
canvasM.bind("<Button-1>", lambda e: self._region_marker(e, region_index, data_image_width, data_image_height))
canvasM.create_image( 0,0, anchor=NW, image=photo_image)
canvasM.pack(fill=BOTH, expand=1)
mw.mainloop()
def _make_new_valleys_from_region_marks(self):
width,height = self.data_im.size
(scale_x,scale_y) = self.marks_scale
for region_index in self.region_marks_centers:
list_of_marks = self.region_marks_centers[region_index]
if self.debug:
print "\n\nFor region %d the marks before scaling are at: %s" % (region_index, list_of_marks)
list_of_marks = [(int(item[0]*scale_x), int(item[1]*scale_y)) for item in list_of_marks]
if self.debug:
print "For region %d the marks after scaling are at: %s" % (region_index, list_of_marks)
valley_array_for_region = numpy.ones((height, width), dtype="int")
for x in range(0, width):
for y in range(0, height):
number_of_crossings = 0
raster_line = (0,y,x,y)
for l in range(0,len(list_of_marks)-1):
line = (list_of_marks[l][0],list_of_marks[l][1],list_of_marks[l+1][0],list_of_marks[l+1][1])
if _line_intersection(raster_line, line):
number_of_crossings += 1
last_line = (list_of_marks[l+1][0],list_of_marks[l+1][1],list_of_marks[0][0],list_of_marks[0][1])
number_of_crossings += _line_intersection(raster_line, last_line)
if number_of_crossings % 2 == 1:
valley_array_for_region[(y,x)] = 0
self.marked_valley_for_region[region_index] = valley_array_for_region
self._display_and_save_binary_array_as_image(valley_array_for_region, "_binarized_valley_created_by_user_" + str(region_index) + " (close window when done viewing)")
def _compute_center_and_diameter_upper_bound_for_blobs(self):
width,height = self.data_im.size
for blob_index in self.blob_dictionary.keys():
blob = self.blob_dictionary[blob_index]
xmin,xmax = width,0
ymin,ymax = height,0
for j in range(0,height):
for i in range(0,width):
if blob[(i,j)] == 1:
if i < xmin: xmin = i
if i > xmax: xmax = i
if j < ymin: ymin = j
if j > ymax: ymax = j
dia = int(math.sqrt( (xmax - xmin)**2 + (ymax - ymin)**2 ))
xavg = xmin + int( (xmax - xmin) / 2.0 )
yavg = ymin + int( (ymax - ymin) / 2.0 )
self.blob_center_dict[blob_index] = (xavg,yavg)
self.blob_dia_dict[blob_index] = dia
def _unit_dilation_of_influence_zones(self, input_array, blob_array, set_of_marks_for_blob):
(width,height) = self.data_im.size
dilated_array = input_array.copy()
sorted_mark_labels = sorted(list(set_of_marks_for_blob), reverse=True)
skiz_label = sorted_mark_labels[0] + 1
change_made = 0
for i in range(0,height):
for j in range(0,width):
if input_array[(i,j)] != 0:
if ( ( (j+1) >= 0 and (j+1) < width ) and input_array[(i,j+1)] == 0 ) or \
( ( (j-1) >= 0 and (j-1) < width ) and input_array[(i,j-1)] == 0 ) or \
( ( (i-1) >= 0 and (i-1) < height ) and input_array[(i-1,j)] == 0 ) or \
( ( (i+1) >= 0 and (i+1) < height ) and input_array[(i+1,j)] == 0 ):
ij_label = input_array[(i,j)]
other_labels = set_of_marks_for_blob.difference(sets.Set([ij_label]))
for k in range(-1,2):
for l in range(-1,2):
if blob_array[(i+k,j+l)] != 0:
if dilated_array[(i+k,j+l)] == 0:
test_bit = 1
for s in range(-1,2):
for t in range(-1,2):
if dilated_array[(i+k+s,j+l+t)] in other_labels:
test_bit = 0
if test_bit == 1:
change_made = 1
dilated_array[(i+k,j+l)] = dilated_array[(i,j)]
else:
dilated_array[(i+k,j+l)] = skiz_label
if change_made == 0: return None
return dilated_array
#_________________________ End of Watershed Class Definition ___________________________
#______________________________ Test code follows _________________________________
if __name__ == '__main__':
# Watershed.gendata( "broken_rectangle", (200,200), (0,0), 0, "broken_rectangle1.jpg" )
wtrshd = Watershed(
# data_image = "triangle1.jpg",
# data_image = "rectangle1.jpg",
# data_image = "broken_rectangle1.jpg",
# data_image = "artpic3.jpg",
# binary_or_gray_or_color = "binary",
data_image = "orchid0001.jpg",
# binary_or_gray_or_color = "gray",
binary_or_gray_or_color = "color",
size_for_calculations = 128,
sigma = 1,
level_decimation_factor = 16, # Number of grad levels: 256 / this_factor
gradient_threshold_as_fraction = 0.1,
max_gradient_to_be_reached_as_fraction = 1.0,
# data_image = "orchid0001.jpg",
# binary_or_gray_or_color = "color",
# size_for_calculations = 128,
# sigma = 1,
# level_decimation_factor = 16, # Number of grad levels: 256 / this_factor
# gradient_threshold_as_fraction = 0.1,
# max_gradient_to_be_reached_as_fraction = 1.0,
#
debug = 0 )
wtrshd.extract_data_pixels()
print "Displaying the original image:"
wtrshd.display_data_image()
'''
# This is a good demo of the LoG of an image calculated as the DoG.
# The function compute_LoG_image() smooths the image with two
# Gaussians, one at sigma and the other at 1.20*sigma. The difference
# of the two is shown as the LoG of the image.
wtrshd.compute_LoG_image()
'''
'''
# For demonstrating pure dilation and erosion of a binary image.
dilated_image = wtrshd.dilate(5)
wtrshd.erode(dilated_image, 5)
'''
'''
# This is a demonstration of how dilations and erosions can be used to repair
# small breaks in contours.
dilated_image = wtrshd.dilation(wtrshd.data_im, 5)
wtrshd.erosion( dilated_image, 5 )
'''
'''
# For illustrating distance transformation vis-a-vis a set of marks
# made by the user.
wtrshd.connected_components("data")
wtrshd.mark_blobs()
wtrshd.connected_components("marks")
print "\n\nStart dilating marked blob:\n\n"
wtrshd.dilate_mark_in_its_blob(1)
'''
'''
# For illustrating the computation of Influence Zones (IZ) and the
# the geodesic skeletons (SKIZ) where SKIZ stands for Skeleton by Zones
# of Influence. The SKIZ skeleton is a by-product of the call to the
# last call shown below --- the call to compute_influence_zones_for_marks.
# The SKIZ skeleton is shown with a pixel label that is one larger than the
# number of marks inside an image blob.
wtrshd.connected_components("data")
wtrshd.mark_blobs()
wtrshd.connected_components("marks")
wtrshd.compute_influence_zones_for_marks()
'''
'''
# For demonstrating MARKERLESS watersheds:
print "Calculating the gradient image"
wtrshd.compute_gradient_image()
print "Computing Z levels in the gradient image"
wtrshd.compute_Z_level_sets_for_gradient_image()
print "Propagating influences:"
wtrshd.propagate_influence_zones_from_bottom_to_top_of_Z_levels()
wtrshd.display_watershed()
wtrshd.extract_watershed_contours()
wtrshd.display_watershed_in_color()
wtrshd.display_watershed_contours_in_color()
'''
# For demonstrating MARKER-BASED watersheds:
wtrshd.mark_image_regions_for_gradient_mods()
wtrshd.compute_gradient_image()
wtrshd.modify_gradients_with_marker_minima()
print "Computing Z levels in the gradient image"
wtrshd.compute_Z_level_sets_for_gradient_image()
print "Propagating influences:"
wtrshd.propagate_influence_zones_from_bottom_to_top_of_Z_levels()
wtrshd.display_watershed()
wtrshd.display_watershed_in_color()
wtrshd.extract_watershed_contours()
wtrshd.display_watershed_contours_in_color()