How to detect circlular region in images and centre it with Python? - 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:

Related

Matplotlib image plot - nan values shown as lowest color of colormaps instead of transparent

I am using matplotlib 3.0.3 and want to create an animation of image plots using the FuncAnimation module. For plotting speed, I update the image data using im.set_data for imshow and im.set_array() for plt.pcolormesh (set_data is not available as an attribute). If I update the data partially with NaN values, imshow displays them as blank pixels, while pcolormesh shows them as the lowest color from the colormap (blue for viridis).
Is this intended and if not, why is this the behavior? It seems related to set_array(), since pcolormesh normally does plot NaN as blank pixels.
Minimal example:
import numpy as np
import matplotlib.pyplot as plt
fig1 = plt.figure()
im1 = plt.pcolormesh(np.random.rand(10,10))
im1.set_array((np.zeros((10,10)) * np.nan).ravel())
fig2 = plt.figure()
im2 = plt.imshow(np.random.rand(10,10))
im2.set_data(np.zeros((10,10)) * np.nan)
Matplotlib images do not work well with nans. One should instead use masked arrays. Then both cases are the same (except for the need to flatten the pcolormesh array).
import numpy as np
import matplotlib.pyplot as plt
A = np.random.rand(10,10)
B = np.ma.array(A, mask=np.ones((10,10)))
fig1 = plt.figure()
im1 = plt.pcolormesh(A)
im1.set_array(B.ravel())
plt.colorbar()
fig2 = plt.figure()
im2 = plt.imshow(A)
im2.set_array(B)
plt.colorbar()
plt.show()

Generate 3D Surface Map from Skimage Elevation Map (2D numpy.ndarray)

In the skimage Segmentation tutorial, a 3D surface plot of the elevation map generated from the sobel function was plotted.
>>> from skimage.filters import sobel
>>> elevation_map = sobel(coins)
Question: elevation_map appears to be a 2D numpy.ndarray. How do we generate the 3D map shown using this?
This is likely produced using Paraview/VTK;
Try to play around the following:
from skimage import data
from skimage.filters import sobel
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
from matplotlib import cm
from scipy.ndimage import zoom
coins = data.coins()
coins = zoom(coins, 10)
elevation_map = sobel(coins)
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
m, n=elevation_map.shape
X, Y = np.meshgrid(np.arange(n), np.arange(m))
ax.plot_surface(X, Y, elevation_map, cmap=cm.viridis, antialiased=False)
ax.axis("off")
ax.set_facecolor('black')
plt.show()

How display one white pixel with mathplot imshow

I want to display one white pixel with mathplot:
import numpy as np
import matplotlib.pyplot as plt
plt.imshow([[0.99]], cmap='gray', interpolation='nearest')
plt.show()
but it shows black. Why?
The problem is that you only give imshow one value, so the colour scale is set around that value and it gets painted as the minimum value of the scale (thus black).
Specify vmin and vmax, as shown here:
import numpy as np
import matplotlib.pyplot as plt
plt.imshow([[0.99]], cmap='gray', interpolation='nearest', vmin=0, vmax=1)
plt.show()
More importantly, you need vmax, which will be mapped to white, to be the value you give imshow, and vmin to be smaller than that:
import numpy as np
import matplotlib.pyplot as plt
max_value = np.random.random()
min_value = -max_value # for instance
plt.imshow([[max_value]], cmap='gray', interpolation='nearest',
vmin=min_value, vmax=max_value)
plt.show()

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

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.

matplotlib: how to draw a rectangle on image

How to draw a rectangle on an image, like this:
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np
im = np.array(Image.open('dog.png'), dtype=np.uint8)
plt.imshow(im)
I don't know how to proceed.
You can add a Rectangle patch to the matplotlib Axes.
For example (using the image from the tutorial here):
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from PIL import Image
im = Image.open('stinkbug.png')
# Create figure and axes
fig, ax = plt.subplots()
# Display the image
ax.imshow(im)
# Create a Rectangle patch
rect = patches.Rectangle((50, 100), 40, 30, linewidth=1, edgecolor='r', facecolor='none')
# Add the patch to the Axes
ax.add_patch(rect)
plt.show()
There is no need for subplots, and pyplot can display PIL images, so this can be simplified further:
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from PIL import Image
im = Image.open('stinkbug.png')
# Display the image
plt.imshow(im)
# Get the current reference
ax = plt.gca()
# Create a Rectangle patch
rect = Rectangle((50,100),40,30,linewidth=1,edgecolor='r',facecolor='none')
# Add the patch to the Axes
ax.add_patch(rect)
Or, the short version:
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from PIL import Image
# Display the image
plt.imshow(Image.open('stinkbug.png'))
# Add the patch to the Axes
plt.gca().add_patch(Rectangle((50,100),40,30,linewidth=1,edgecolor='r',facecolor='none'))
You need use patches.
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig2 = plt.figure()
ax2 = fig2.add_subplot(111, aspect='equal')
ax2.add_patch(
patches.Rectangle(
(0.1, 0.1),
0.5,
0.5,
fill=False # remove background
) )
fig2.savefig('rect2.png', dpi=90, bbox_inches='tight')
From my understanding matplotlib is a plotting library.
If you want to change the image data (e.g. draw a rectangle on an image), you could use PIL's ImageDraw, OpenCV, or something similar.
Here is PIL's ImageDraw method to draw a rectangle.
Here is one of OpenCV's methods for drawing a rectangle.
Your question asked about Matplotlib, but probably should have just asked about drawing a rectangle on an image.
Here is another question which addresses what I think you wanted to know:
Draw a rectangle and a text in it using PIL
If you have a set of coordinates of ordered points you can also use the plot function and plot them directly without using the Rect patch. Here I recreate the example proposed by #tmdavison using that:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from PIL import Image
im = Image.open('/content/stinkbug.png')
# Create figure and axes
fig, ax = plt.subplots()
# Display the image
ax.imshow(im)
# Coordinates of rectangle vertices
# in clockwise order
xs = [50, 90, 90, 50, 50]
ys = [100, 100, 130, 130, 100]
ax.plot(xs, ys, color="red")
plt.show()

Categories