Related
I have a binary black and white images that looks like this
I want to fill in those white circles to be solid white disks. How can I do this in Python, preferrably using skimage?
You can detect circles with skimage's methods hough_circle and hough_circle_peaks and then draw over them to "fill" them.
In the following example most of the code is doing "hierarchy" computation for the best fitting circles to avoid drawing circles which are one inside another:
# skimage version 0.14.0
import math
import numpy as np
import matplotlib.pyplot as plt
from skimage import color
from skimage.io import imread
from skimage.transform import hough_circle, hough_circle_peaks
from skimage.feature import canny
from skimage.draw import circle
from skimage.util import img_as_ubyte
INPUT_IMAGE = 'circles.png' # input image name
BEST_COUNT = 6 # how many circles to draw
MIN_RADIUS = 20 # min radius should be bigger than noise
MAX_RADIUS = 60 # max radius of circles to be detected (in pixels)
LARGER_THRESH = 1.2 # circle is considered significantly larger than another one if its radius is at least so much bigger
OVERLAP_THRESH = 0.1 # circles are considered overlapping if this part of the smaller circle is overlapping
def circle_overlap_percent(centers_distance, radius1, radius2):
'''
Calculating the percentage area overlap between circles
See Gist for comments:
https://gist.github.com/amakukha/5019bfd4694304d85c617df0ca123854
'''
R, r = max(radius1, radius2), min(radius1, radius2)
if centers_distance >= R + r:
return 0.0
elif R >= centers_distance + r:
return 1.0
R2, r2 = R**2, r**2
x1 = (centers_distance**2 - R2 + r2 )/(2*centers_distance)
x2 = abs(centers_distance - x1)
y = math.sqrt(R2 - x1**2)
a1 = R2 * math.atan2(y, x1) - x1*y
if x1 <= centers_distance:
a2 = r2 * math.atan2(y, x2) - x2*y
else:
a2 = math.pi * r2 - a2
overlap_area = a1 + a2
return overlap_area / (math.pi * r2)
def circle_overlap(c1, c2):
d = math.sqrt((c1[0]-c2[0])**2 + (c1[1]-c2[1])**2)
return circle_overlap_percent(d, c1[2], c2[2])
def inner_circle(cs, c, thresh):
'''Is circle `c` is "inside" one of the `cs` circles?'''
for dc in cs:
# if new circle is larger than existing -> it's not inside
if c[2] > dc[2]*LARGER_THRESH: continue
# if new circle is smaller than existing one...
if circle_overlap(dc, c)>thresh:
# ...and there is a significant overlap -> it's inner circle
return True
return False
# Load picture and detect edges
image = imread(INPUT_IMAGE, 1)
image = img_as_ubyte(image)
edges = canny(image, sigma=3, low_threshold=10, high_threshold=50)
# Detect circles of specific radii
hough_radii = np.arange(MIN_RADIUS, MAX_RADIUS, 2)
hough_res = hough_circle(edges, hough_radii)
# Select the most prominent circles (in order from best to worst)
accums, cx, cy, radii = hough_circle_peaks(hough_res, hough_radii)
# Determine BEST_COUNT circles to be drawn
drawn_circles = []
for crcl in zip(cy, cx, radii):
# Do not draw circles if they are mostly inside better fitting ones
if not inner_circle(drawn_circles, crcl, OVERLAP_THRESH):
# A good circle found: exclude smaller circles it covers
i = 0
while i<len(drawn_circles):
if circle_overlap(crcl, drawn_circles[i]) > OVERLAP_THRESH:
t = drawn_circles.pop(i)
else:
i += 1
# Remember the new circle
drawn_circles.append(crcl)
# Stop after have found more circles than needed
if len(drawn_circles)>BEST_COUNT:
break
drawn_circles = drawn_circles[:BEST_COUNT]
# Actually draw circles
colors = [(250, 0, 0), (0, 250, 0), (0, 0, 250)]
colors += [(200, 200, 0), (0, 200, 200), (200, 0, 200)]
fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(10, 4))
image = color.gray2rgb(image)
for center_y, center_x, radius in drawn_circles:
circy, circx = circle(center_y, center_x, radius, image.shape)
color = colors.pop(0)
image[circy, circx] = color
colors.append(color)
ax.imshow(image, cmap=plt.cm.gray)
plt.show()
Result:
Do a morphological closing (explanation) to fill those tiny gaps, to complete the circles. Then fill the resulting binary image.
Code :
from skimage import io
from skimage.morphology import binary_closing, disk
import scipy.ndimage as nd
import matplotlib.pyplot as plt
# Read image, binarize
I = io.imread("FillHoles.png")
bwI =I[:,:,1] > 0
fig=plt.figure(figsize=(24, 8))
# Original image
fig.add_subplot(1,3,1)
plt.imshow(bwI, cmap='gray')
# Dilate -> Erode. You might not want to use a disk in this case,
# more asymmetric structuring elements might work better
strel = disk(4)
I_closed = binary_closing(bwI, strel)
# Closed image
fig.add_subplot(1,3,2)
plt.imshow(I_closed, cmap='gray')
I_closed_filled = nd.morphology.binary_fill_holes(I_closed)
# Filled image
fig.add_subplot(1,3,3)
plt.imshow(I_closed_filled, cmap='gray')
Result :
Note how the segmentation trash has melded to your object on the lower right and the small cape on the lower part of the middle object has been closed. You might want to continue with an morphological erosion or opening after this.
EDIT: Long response to comments below
The disk(4) was just the example I used to produce the results seen in the image. You will need to find a suitable value yourself. Too big of a value will lead to small objects being melded into bigger objects near them, like on the right side cluster in the image. It will also close gaps between objects, whether you want it or not. Too small of a value will lead to the algorithm failing to complete the circles, so the filling operation will then fail.
Morphological erosion will erase a structuring element sized zone from the borders of the objects. Morphological opening is the inverse operation of closing, so instead of dilate->erode it will do erode->dilate. The net effect of opening is that all objects and capes smaller than the structuring element will vanish. If you do it after filling then the large objects will stay relatively the same. Ideally it should remove a lot of the segmentation artifacts caused by the morphological closing I used in the code example, which might or might not be pertinent to you based on your application.
I don't know skimage but if you'd use OpenCv, I would do a Hough transform for circles, and then just draw them over.
Hough Transform is robust, if there are some small holes in the circles that is no problem.
Something like:
circles = cv2.HoughCircles(gray, cv2.cv.CV_HOUGH_GRADIENT, 1.2, 100)
# ensure at least some circles were found
if circles is not None:
# convert the (x, y) coordinates and radius of the circles to integers
circles = np.round(circles[0, :]).astype("int")
# loop over the (x, y) coordinates and radius of the circles
# you can check size etc here.
for (x, y, r) in circles:
# draw the circle in the output image
# you can fill here.
cv2.circle(output, (x, y), r, (0, 255, 0), 4)
# show the output image
cv2.imshow("output", np.hstack([image, output]))
cv2.waitKey(0)
See more info here: https://www.pyimagesearch.com/2014/07/21/detecting-circles-images-using-opencv-hough-circles/
I am dealing with this kind of image
(upper is post-processed)
(lower is raw)
So, first I converted the grayscale image into pure black and white binary image. I am interested in detecting the white blobs, and want to get rid of the arc-like smears in the corners. How can I do that?
I general, I know that my targets are almost circular in shape, not too big, but I want to encode something that automatically gets rid of everything else, like the lighter arcs in the upper left and right corners.
How would I do this in python, ideally skimage?
You can just detect circle of the right size with skimage's methods hough_circle and hough_circle_peaks and cut it out.
Here I adapted my previous answer to your other question to do this:
# skimage version 0.14.0
import math
import numpy as np
import matplotlib.pyplot as plt
from skimage import color
from skimage.io import imread
from skimage.transform import hough_circle, hough_circle_peaks
from skimage.feature import canny
from skimage.draw import circle
from skimage.util import img_as_ubyte
INPUT_IMAGE = 'dish1.png' # input image name
BEST_COUNT = 1 # how many circles to detect (one dish)
MIN_RADIUS = 100 # min radius of the Petri dish
MAX_RADIUS = 122 # max radius of the Petri dish (in pixels)
LARGER_THRESH = 1.2 # circle is considered significantly larger than another one if its radius is at least so much bigger
OVERLAP_THRESH = 0.1 # circles are considered overlapping if this part of the smaller circle is overlapping
def circle_overlap_percent(centers_distance, radius1, radius2):
'''
Calculating the percentage area overlap between circles
See Gist for comments:
https://gist.github.com/amakukha/5019bfd4694304d85c617df0ca123854
'''
R, r = max(radius1, radius2), min(radius1, radius2)
if centers_distance >= R + r:
return 0.0
elif R >= centers_distance + r:
return 1.0
R2, r2 = R**2, r**2
x1 = (centers_distance**2 - R2 + r2 )/(2*centers_distance)
x2 = abs(centers_distance - x1)
y = math.sqrt(R2 - x1**2)
a1 = R2 * math.atan2(y, x1) - x1*y
if x1 <= centers_distance:
a2 = r2 * math.atan2(y, x2) - x2*y
else:
a2 = math.pi * r2 - a2
overlap_area = a1 + a2
return overlap_area / (math.pi * r2)
def circle_overlap(c1, c2):
d = math.sqrt((c1[0]-c2[0])**2 + (c1[1]-c2[1])**2)
return circle_overlap_percent(d, c1[2], c2[2])
def inner_circle(cs, c, thresh):
'''Is circle `c` is "inside" one of the `cs` circles?'''
for dc in cs:
# if new circle is larger than existing -> it's not inside
if c[2] > dc[2]*LARGER_THRESH: continue
# if new circle is smaller than existing one...
if circle_overlap(dc, c)>thresh:
# ...and there is a significant overlap -> it's inner circle
return True
return False
# Load picture and detect edges
image = imread(INPUT_IMAGE, 1)
image = img_as_ubyte(image)
edges = canny(image, sigma=3, low_threshold=10, high_threshold=50)
# Detect circles of specific radii
hough_radii = np.arange(MIN_RADIUS, MAX_RADIUS, 2)
hough_res = hough_circle(edges, hough_radii)
# Select the most prominent circles (in order from best to worst)
accums, cx, cy, radii = hough_circle_peaks(hough_res, hough_radii)
# Determine BEST_COUNT circles to be drawn
drawn_circles = []
for crcl in zip(cy, cx, radii):
# Do not draw circles if they are mostly inside better fitting ones
if not inner_circle(drawn_circles, crcl, OVERLAP_THRESH):
# A good circle found: exclude smaller circles it covers
i = 0
while i<len(drawn_circles):
if circle_overlap(crcl, drawn_circles[i]) > OVERLAP_THRESH:
t = drawn_circles.pop(i)
else:
i += 1
# Remember the new circle
drawn_circles.append(crcl)
# Stop after have found more circles than needed
if len(drawn_circles)>BEST_COUNT:
break
drawn_circles = drawn_circles[:BEST_COUNT]
# Draw circle and cut it out
colors = [(250, 0, 0), (0, 250, 0), (0, 0, 250)]
fig, ax = plt.subplots(ncols=1, nrows=3, figsize=(10, 4))
color_image = color.gray2rgb(image)
black_image = np.zeros_like(image)
for center_y, center_x, radius in drawn_circles[:1]:
circy, circx = circle(center_y, center_x, radius, image.shape)
color = colors.pop(0)
color_image[circy, circx] = color
black_image[circy, circx] = image[circy, circx]
colors.append(color)
# Output
ax[0].imshow(image, cmap=plt.cm.gray) # original image
ax[1].imshow(color_image) # detected circle
ax[2].imshow(black_image, cmap=plt.cm.gray) # cutout
plt.show()
Output:
Again, as in my previous answer, most of the code here is doing "hierarchy" computation to find the biggest best fitting circle.
My work requires applying Local Binary Operator on Images. For that I have already converted the images in Gray then implemented a Connected Components analysis on the image also.
Here is the Code:
Adding Libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from skimage.io import imread, imshow
from skimage.color import rgb2gray
from skimage.morphology import (erosion, dilation, closing, opening,area_closing, area_opening)
from skimage.measure import label, regionprops, regionprops_table
Rendering the image
plt.figure(figsize=(6,6))
painting = imread("E:/Project/for_annotation/Gupi Gain0032.jpg")
plt.imshow(painting);
plt.figure(figsize=(6,6))
Binarizing Image
gray_painting = rgb2gray(painting)
binarized = gray_painting<0.55
plt.imshow(binarized);
4.Declaring Kernel
square = np.array([[1,1,1],
[1,1,1],
[1,1,1]])
Dilation function
def multi_dil(im, num, element=square):
for i in range(num):
im = dilation(im, element)
return im
Erosion function
def multi_ero(im, num, element=square):
for i in range(num):
im = erosion(im, element)
return im
Functions Applied
plt.figure(figsize=(6,6))
multi_dilated = multi_dil(binarized, 7)
area_closed = area_closing(multi_dilated, 50000)
multi_eroded = multi_ero(area_closed, 7)
opened = opening(multi_eroded)
plt.imshow(opened);
Label function
plt.figure(figsize=(6,6))
label_im = label(opened)
regions = regionprops(label_im)
plt.imshow(label_im);
Extract features
properties = ['area','convex_area','bbox_area', 'extent', 'mean_intensity','solidity', 'eccentricity', 'orientation']
pd.DataFrame(regionprops_table(label_im, gray_painting,
properties=properties))
Filtering Regions
masks = []
bbox = []
list_of_index = []
for num, x in enumerate(regions):
area = x.area
convex_area = x.convex_area
if (num!=0 and (area>100) and (convex_area/area <1.05)
and (convex_area/area >0.95)):
masks.append(regions[num].convex_image)
bbox.append(regions[num].bbox)
list_of_index.append(num)
count = len(masks)
Extracting Images
fig, ax = plt.subplots(2, int(count/2), figsize=(15,8))
for axis, box, mask in zip(ax.flatten(), bbox, masks):
red = painting[:,:,0][box[0]:box[2], box[1]:box[3]] * mask
green = painting[:,:,1][box[0]:box[2], box[1]:box[3]] * mask
blue = painting[:,:,2][box[0]:box[2], box[1]:box[3]] * mask
image = np.dstack([red,green,blue])
axis.imshow(image)
plt.tight_layout()
plt.figure(figsize=(6,6))
rgb_mask = np.zeros_like(label_im)
for x in list_of_index:
rgb_mask += (label_im==x+1).astype(int)
red = painting[:,:,0] * rgb_mask
green = painting[:,:,1] * rgb_mask
blue = painting[:,:,2] * rgb_mask
image = np.dstack([red,green,blue])
plt.imshow(image);
I am getting an error.
ValueError: Number of columns must be a positive integer, not 0
There is a possible approach which is not very far from what you attempted. Assume the background pixels are assigned the label 0, and the object pixels the value 1.
scan the image row by row;
when you meet a pixel 1, set a new label and perform a flood fill operation, replacing 1 by the new label.
Flood filling can be implemented very simply:
set the starting pixel to the new label;
recursively fill the eight neighbors, if they have a 1.
https://en.wikipedia.org/wiki/Flood_fill
The code of this version is pretty simple. But you will notice that it can easily overflow the stack because the number of pending fills can be as large as the image size.
def FloodFill(X, Y, Label):
I[X,Y]= Label
for all 8-way neighbors (X'=X±1, Y'=Y±1, inside image):
if I[X',Y'] == 1:
FloodFill(X', Y', Label)
def CCL(Image I):
Label= 1
for Y in range(I.Height):
for X in range(I.Width):
if I[X, Y] == 1:
Label+= 1
FloodFill(X, Y, Label)
So I would recommend the scanline version, which is a little more involved.
https://en.wikipedia.org/wiki/Flood_fill#Scanline_fill
I have a Numpy array of colors that is returned when using K-means quantization on an image. After K-means is used, I need to take those centers and calculate the convex hull of the set of color centers.
Essentially I need to find which points/colors don't have enough contrast from another color. I think the answer to this would be to find the points inside the hull (simplices?) that aren't the vertices of the hull.
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
from scipy.spatial import ConvexHull
from skimage import color, exposure
from sklearn import cluster
def quantize(pixels, n_colors):
width, height, depth = pixels.shape
reshaped_pixels = np.reshape(pixels, (width * height, depth))
model = cluster.KMeans(n_clusters=n_colors, n_init=100, precompute_distances=True)
labels = model.fit_predict(reshaped_pixels)
centers = model.cluster_centers_
quantized_pixels = np.reshape(centers[labels], (width, height, centers.shape[1]))
return quantized_pixels, centers, labels
def scale_image(img, max_width=100):
img_width, img_height = img.size
ratio = (max_width / float(img_width))
new_height = int((float(img_height)*float(ratio)))
img.thumbnail((max_width, new_height), Image.NEAREST)
return img
# Open image, convert to RGB just in case
img = Image.open('image2.jpg').convert('RGB')
# Scale image to speed up processing
img = scale_image(img, 80)
# Convert to numpy array
np_img_array = np.array(img)
# Convert rgb to lab colorspace
lab_pixels = color.rgb2lab(np_img_array)
# Kmeans quantization
quantized_lab_pixels, centers, labels = quantize(lab_pixels, 16)
# Convert pixels back to rgb
quantized_rgb_pixels = (color.lab2rgb(quantized_lab_pixels)*255).astype('uint8')
# Attempt to use convex hull around cluster centers
hull = ConvexHull(centers)
for simplex in hull.simplices:
plt.plot(centers[simplex, 0], centers[simplex, 1])
plt.scatter(*centers.T, alpha=.5, color='k', marker='v')
for p in centers:
point_is_in_hull = point_in_hull(p, hull)
marker = 'x' if point_is_in_hull else 'd'
color = 'g' if point_is_in_hull else 'm'
plt.scatter(p[0], p[1], marker=marker, color=color)
plt.show()
Update: Here is my rendered plot output. I'm not using the color labels in the plotting in this render.
Here is more so what would be helpful to see. However, the plots aren't what I need... I need to find out which of the cluster centers (colors) make up the vertices of the hull and then pair them back to the vertices labels.
Here is an example of the RGB colors outputted (on the left), and then on the right you would have the final colors, which would be after excluding cluster centers that fall outside of the region.
I am trying to create particles in an image ( and later displace, rotate, shear the image) for PIV. I use the following code to get my particles.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Tue Sep 12 09:47:24 2017
#author: arun
Program to create synthetic particles.
Makeparticles : Version 1
updates : 1) change particle diameter variation
2) Image transformations using transformation matrix
3) Time saved = 15 seconds faster
"""
import cv2
import numpy as np
import time
from PIL import Image
import mahotas
from affine import Affine
#from skimage import io
#from skimage import transform as tf
#import matplotlib.pyplot as plt
#import math
#import matplotlib as mpl
#import matplotlib.pyplot as plt
#import trackpy as tp
#import pims
"""---------------------make particles--------------------------------"""
class Particles(object):
def __init__(self,n,extra,w,h,r1,r2):
self.x = np.random.uniform(low=0.0, high=1680.0, size=n)
self.y = np.random.uniform(low=0.0, high=1424.0, size=n)
self.rad=np.random.uniform(low=r1, high=r2, size=n)
self.n=n
self.extra=extra
self.w=w
self.h=h
# Initial location of particles. We use floating point ( not integer)
# to specify random location
def randomloc(self) :
for i in range(0,n) :
frame.add_spot((self.x[i], self.y[i]), 255, self.rad[i])
return self.x,self.y
def displacement(self,xdisp=20,ydisp=0):
img = cv2.imread('initial_image.jpg',0)
rows,cols = img.shape
M = np.float32([[1,0,xdisp],[0,1,ydisp]])
dst = cv2.warpAffine(img,M,(cols,rows))
cropped = dst[self.extra:self.w+self.extra, \
self.extra:self.h+self.extra]
return cropped
#plt.imshow(dst)
def rotate(self, ox,oy,angle):
img = cv2.imread('initial_image.jpg',0)
rows,cols = img.shape
M = cv2.getRotationMatrix2D((ox,oy),angle,1)
# the format is cv2.getRotationMatrix2D(center, angle, scale)
dst = cv2.warpAffine(img,M,(cols,rows),flags=cv2.INTER_LANCZOS4)
# area = (200, 200,1480,1224)
# cropped_img = dst.crop(area)
cropped = dst[200:1480, 200:1224]
return cropped
def shear2(self,anglex,angley):
img = cv2.imread('initial_image.jpg',0)
rows,cols = img.shape
M = Affine.shear(anglex, angley)
M=M[0:6]
a = np.array(M).reshape(2,3)
dst = cv2.warpAffine(img,a,(cols,rows),flags=cv2.INTER_LANCZOS4)
cropped = dst[200:1480, 200:1224]
return cropped
#
# def shear0(self,angle):
# image = io.imread("initial_image.jpg")
#
# # Create Afine transform
# afine_tf = tf.AffineTransform(shear=angle)
#
# # Apply transform to image data
# modified = tf.warp(image, inverse_map=afine_tf)
#
# # Display the result
# cropped = modified[200:1480, 200:1224]
# #cropped_img.save('sheared_image.jpg')
# return modified
# def shear1(self,):
# img = cv2.imread("initial_image.jpg")
# rows,cols,ch = img.shape
#
# pts1 = np.float32([[50,50],[200,50],[50,200]])
# pts2 = np.float32([[10,100],[200,50],[100,250]])
# M = cv2.getAffineTransform(pts1,pts2)
# dst = cv2.warpAffine(img,M,(cols,rows))
class SimulatedFrame(object):
def __init__(self, shape, dtype=np.uint8):
self.image = np.zeros(shape, dtype=dtype)
self._saturation = np.iinfo(dtype).max
self.shape = shape
self.dtype =dtype
#""" A gaussian distribution of intensities is obtained. Eccentricity
# (ecc) means how much the particle is elongated . """
def add_spot(self, pos, amplitude, r, ecc=0):
"Add a Gaussian spot to the frame."
x, y = np.meshgrid(*np.array(list(map(np.arange, self.shape))) \
- np.asarray(pos))
spot = amplitude*np.exp(-((x/(1 - ecc))**2 + (y*(1 - ecc))**2)/\
(2*r**2)).T
self.image += np.clip(spot, 0, self._saturation).astype\
(self.dtype)
def save_frame(self, filename='initial_image1.jpg'):
img = Image.fromarray(self.image.astype(np.uint8))
#cropped = img[200:1480, 200:1224]
img.save('initial_image.jpg')
area = (200, 200,1224,1480)
cropped = img.crop(area)
cropped.save(filename)
#-------------------------------------------------------------------------
""" Definition of parameters
This part is used to create an image with particles. This is to verify and
test the program that it works as intended An initial image at time T0,
with the particles having a circular and guassian distribution of their
intensities is made using the Makepart function definition. Then these
particles are displaced through translation, rotation and shear motions,
and this image is used to compute the velocity and vorticity vectors later
by cross correlation .
The variables used in this part are described as follows
w = width of the image
h = height of the image
n = Number of particles
r1 = Radius of Particle in pixels: Lower limit
r2 = Radius of particle in pixels: Upper limit
xdisp = Displacement required in the x direction
ydisp = Displacement required in the y direction
rotangle = Rotation requried in degrees (Anti-clockwise rotation)
ox,oy = location of the point about which rotation occurs
shearx= shear angle in x direction.Positive value indicates bottom side
shift to right
sheary= shear angle in y direction.Positive value indicates right side
shift to down
"""
start1=time.time()
w=1280
h=1024
n=500
r1 =3.0
r2= 5.0
xdisp=20
ydisp=0
rotangle=1
ox=840
oy=712
shearx=1.0
sheary=0
extra=200
"""creating an object for the class SimulatedFrame.
Input - total window size, dtype of image
Output- Black image of the desired pixel size
"""
frame = SimulatedFrame((w+extra, h+extra), dtype=np.int)
#we send the number of particles to the class,
#so that we can make locations
coordinates=Particles(n,extra,w,h,r1,r2)
x,y=coordinates.randomloc()
frame.save_frame()
x1=coordinates.displacement(xdisp,ydisp)
mahotas.imsave('displaced.jpg', x1)
x2=coordinates.rotate(ox,oy,rotangle)
mahotas.imsave('rotated.jpg', x2)
x3=coordinates.shear2(shearx,sheary)
mahotas.imsave('sheared.jpg',x3)
print("--- %s seconds ---" % (time.time() - start1))
however, in the places where two particles overlap, I get a black spot which I would like to avoid. I believe it is due to the pixel light intensity counter going back to 0 once it reaches 255. Is there a way to avoid these black spots?
This is the image output