How to obtain the skeleton of a binary image with scikit-image - python

I want to draw the skeleton of the following image:
I've tried with the following Python code:
import cv2
from skimage import morphology, color
import matplotlib.pyplot as plt
image = cv2.imread(r'C:\Users\Administrator\Desktop\sample.jpg')
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
image=color.rgb2gray(image)
skeleton =morphology.medial_axis(image)
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(8, 4))
ax1.imshow(image, cmap=plt.cm.gray)
ax1.axis('off')
ax1.set_title('original', fontsize=20)
ax2.imshow(skeleton, cmap=plt.cm.gray)
ax2.axis('off')
ax2.set_title('skeleton', fontsize=20)
fig.tight_layout()
plt.show()
And I got the following skeleton:
This doesn't look like the right skeleton image. I don't know what's going wrong.
Can anyone help me on this? Any help would be appreciated!!!

Applying the medial axis as in your example:
from skimage import img_as_bool, io, color, morphology
import matplotlib.pyplot as plt
image = img_as_bool(color.rgb2gray(io.imread('CIBUv.png')))
out = morphology.medial_axis(image)
f, (ax0, ax1) = plt.subplots(1, 2)
ax0.imshow(image, cmap='gray', interpolation='nearest')
ax1.imshow(out, cmap='gray', interpolation='nearest')
plt.show()
yields
Note that, as #Aaron mentions below, converting to a boolean image first helps here, because your original image was stored in JPEG which could introduce small fluctuations in pixel values.
You can also replace medial_axis with skeletonize for a different algorithm.
If you want the outline as described in #Tonechas's answer, then look at edge detection methods.

Related

plotting a grid of png with matplotlib

I have found multiple similar questions with this subject but so far I couldn't adapt any solution to my needs, so I'm sorry for reposting.
I'm trying to plot a grid of png images using matplotlib, the closest I've got to what I want is using the code below, which can be found here https://matplotlib.org/stable/gallery/axes_grid1/simple_axesgrid.html .
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import ImageGrid
import numpy as np
im1 = np.arange(100).reshape((10, 10))
im2 = im1.T
im3 = np.flipud(im1)
im4 = np.fliplr(im2)
fig = plt.figure(figsize=(4., 4.))
grid = ImageGrid(fig, 111, # similar to subplot(111)
nrows_ncols=(2, 2), # creates 2x2 grid of axes
axes_pad=0.1, # pad between axes in inch.
)
for ax, im in zip(grid, [im1, im2, im3, im4]):
# Iterating over the grid returns the Axes.
ax.imshow(im)
plt.show()
My question is, how do I get rid of the x and y ticks/labels and also give each image a title?
Again, I'm sorry for repeating the question.
This code
import matplotlib.pyplot as plt
image = plt.imread("sample.png")
fig, axes = plt.subplots(2, 3)
for row in [0, 1]:
for column in [0, 1, 2]:
ax = axes[row, column]
ax.set_title(f"Image ({row}, {column})")
ax.axis('off')
ax.imshow(image)
plt.show()
is going to produce

Erosion and Dilated getting swapped

I tried using morphological operations: Erosion and Dilation using skimage module. However, the results seem interchanged for me. Dilation should add pixels to the boundaries and erosion should remove them. But in my case, it is happening just the opposite.
Code:
from skimage import data, morphology
from matplotlib import pyplot as plt
def plot_comparison(original, first, second, title1, title2):
fig, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3, sharex=True, sharey=True, figsize=(10,8))
ax1.imshow(original, cmap='gray')
ax1.set_title('Original')
ax1.axis('off')
ax2.imshow(first, cmap='gray')
ax2.set_title(title1)
ax2.axis('off')
ax3.imshow(second, cmap='gray')
ax3.set_title(title2)
ax3.axis('off')
plt.show()
selem = morphology.rectangle(12, 6)
horse = data.horse()
eroded = morphology.binary_erosion(horse, selem=selem)
dilated = morphology.binary_dilation(horse, selem=selem)
plot_comparison(horse, eroded, dilated, 'Eroded', 'Dilated')
Output:

How to save a series of matplotlib plots as an image file?

I'm trying to save a series of matplotlib figures as one single image file with many slices. To put things in perspective, the following is the code I'm using:
for n in range(len(image.shape[0])): #this image here is a timelapse image
plt.imshow(image[n, :, :], cmap='gray')
ax = plt.gca()
for acontour in contour_list:
ax.add_patch(patches.Polygon(acontour[:, [1, 0]],linewidth=1,edgecolor='r',facecolor='none'))
plt.show()
I'm trying to overlay the corresponding contour on the original image for every slice and save all the images.
Thanks in advance.
Will this help:
import matplotlib.pyplot as plt
fig, axs = plt.subplots(2,len(image.shape[0])//2, figsize=(15, 6))
fig.subplots_adjust(hspace = .5, wspace=.001)
axs = axs.ravel()
for n in range(len(image.shape[0])):
# Your plot code
plt.savefig('image.png')

Converting pyplot figure to array

I am trying to convert a figure drawn using pyplot to an array, but I would like to eliminate any space outside of the plot before doing so. In my current approach, I am saving the figure to a temporary file (using the functionality of plt.savefig to eliminate any space outside the plot, i.e. using bbox_inches='tight' and pad_inches = 0), and then loading the image from the temporary file. Here's an MWE:
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
ax.plot([0,1], color='black', linewidth=4)
plt.xlim([0,1])
plt.ylim([0,1])
ax.set_aspect('equal', adjustable='box')
plt.axis('off')
plt.savefig('./tmp.png', bbox_inches='tight', pad_inches = 0)
plt.close()
img_size = 128
img = Image.open('./tmp.png')
X = np.array(img)
This approach is undesirable, because of the time required to write the file and read it. I'm aware of the following method for going directly from the pixel buffer to an array:
from PIL import Image
import matplotlib.pyplot as plt
from matplotlib.backends.backend_agg import FigureCanvas
import numpy as np
fig, ax = plt.subplots()
canvas = FigureCanvas(fig)
ax.plot([0,1], color='black', linewidth=4)
plt.xlim([0,1])
plt.ylim([0,1])
ax.set_aspect('equal', adjustable='box')
plt.axis('off')
canvas.draw()
X = np.array(canvas.renderer.buffer_rgba())
However, with this approach, I'm not sure how to eliminate the space around the plot before converting to an array. Is there an equivalent to bbox_inches='tight' and pad_inches = 0 that doesn't involve using plt.savefig()?
Improved Answer
This seems to work for your case and should be fast. There may be better ways - I am happy to delete it if anyone knows something better:
#!/usr/bin/env python3
from PIL import Image
import matplotlib.pyplot as plt
from matplotlib.backends.backend_agg import FigureCanvas
import numpy as np
fig, ax = plt.subplots()
canvas = FigureCanvas(fig)
ax.plot([0,1], color='red', linewidth=4)
plt.xlim([0,1])
plt.ylim([0,1])
ax.set_aspect('equal', adjustable='box')
plt.axis('off')
canvas.draw()
X = np.array(canvas.renderer.buffer_rgba())
The code above is yours, the code below is mine:
# Get width and height of cnvas for reshaping
w, h = canvas.get_width_height()
Y = np.frombuffer(X,dtype=np.uint8).reshape((h,w,4))[...,0:3]
# Work out extent of image by inverting and looking for black - ASSUMES CANVAS IS WHITE
extent = np.nonzero(~Y)
top = extent[0].min()
bottom = extent[0].max()
left = extent[1].min()
right = extent[1].max()
tight_img = Y[top:bottom,left:right,:]
# Save as image just to test - you don't want this bit
Image.fromarray(tight_img).save('tight.png')
Original Answer
There may be a better way, but you could avoid writing to disk by writing to a memory-based BytesIO instead:
from io import BytesIO
buffer = BytesIO()
plt.savefig(buffer, format='png', bbox_inches='tight', pad_inches = 0)
Then do:
x = np.array(Image.open(buffer))
In fact, if you use:
plt.savefig(buffer, format='rgba', bbox_inches='tight', pad_inches = 0)
the buffer already has your array and you can avoid the PNG encoding/decoding as well as the disk I/O. The only issue is that, because it is raw, we don't know the dimensions of the image to reshape() the buffer. It is actually this on my machine but I got the dimensions by writing a PNG and checking its width and height:
arr = buffer.getvalue()
x = np.frombuffer(arr, dtype=np.uint8).reshape((398,412,4))
If someone comes up with something better, I'll delete this.

How to detect circlular region in images and centre it with Python?

I have a figure flame of the form shown below:
I am trying to detect the outer edge of the camera's view and centre the figure so that circular view of the flame is exactly at the centre of the plot. As the position of the circle might change with the image capture date. Sometimes it might be at the upper half, sometimes lower half, etc.
Are there any modules in Python that can detect the view and centre it?
Reproducible code
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
img=mpimg.imread('flame.png')
lum_img = img[:,:,0]
img_plot = plt.imshow(lum_img)
img_plot.set_cmap('jet')
plt.axis('Off')
plt.show()
Adapted from this answer, do an edge detection and robustly fit a circle to the outline using RANSAC:
from __future__ import print_function
from skimage import io, feature, color, measure, draw, img_as_float
import numpy as np
image = img_as_float(color.rgb2gray(io.imread('flame.png')))
edges = feature.canny(image)
coords = np.column_stack(np.nonzero(edges))
model, inliers = measure.ransac(coords, measure.CircleModel,
min_samples=3, residual_threshold=1,
max_trials=1000)
print(model.params)
rr, cc = draw.circle_perimeter(int(model.params[0]),
int(model.params[1]),
int(model.params[2]),
shape=image.shape)
image[rr, cc] = 1
import matplotlib.pyplot as plt
plt.imshow(image, cmap='gray')
plt.scatter(model.params[1], model.params[0], s=50, c='red')
plt.axis('off')
plt.savefig('/tmp/flame_center.png', bbox_inches='tight')
plt.show()
This yields:
I think you have plenty of options. Two easy approaches that come to my mind would be to threshold your input image on a low intensity value which will give you a white circle. Then you could run the Hough transform for circles on it to find the center.
Or you can use the distance transform of the thresholded white pixels and take the maximum of this distance transform:
# code derived from watershed example of scikit-image
# http://scikit-image.org/docs/dev/auto_examples/plot_watershed.html
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage as ndi
from skimage.morphology import watershed
from skimage.feature import peak_local_max
from skimage.color import rgb2gray
from skimage.io import imread
img = imread('flame.png')
image = rgb2gray(img) > 0.01
# Now we want to separate the two objects in image
# Generate the markers as local maxima of the distance to the background
distance = ndi.distance_transform_edt(image)
# get global maximum like described in
# http://stackoverflow.com/a/3584260/2156909
max_loc = unravel_index(distance.argmax(), distance.shape)
fig, axes = plt.subplots(ncols=4, figsize=(10, 2.7))
ax0, ax1, ax2, ax3 = axes
ax0.imshow(img,interpolation='nearest')
ax0.set_title('Image')
ax1.imshow(image, cmap=plt.cm.gray, interpolation='nearest')
ax1.set_title('Thresholded')
ax2.imshow(-distance, cmap=plt.cm.jet, interpolation='nearest')
ax2.set_title('Distances')
ax3.imshow(rgb2gray(img), cmap=plt.cm.gray, interpolation='nearest')
ax3.set_title('Detected centre')
ax3.scatter(max_loc[1], max_loc[0], color='red')
for ax in axes:
ax.axis('off')
fig.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0,
right=1)
plt.show()
Just to give you an idea how robust this method is, if I pick a very bad threshold (image = rgb2gray(img) > 0.001 -- far too low to get a nice circle), the result is almost the same:

Categories