Matplotlib: Inconsistent results with images - python

I am trying to plot multiple images in a figure using matplotlib.
Basically, I read the images using PIl library, convert it to numpy array and do some operation on it (setting the elements in a row to zero). Everything works fine till this point. But when I try to save the results using matplotlib, I get inconsistent results.
Please have a look at my code.
Importing the libraries
import numpy as np
import matplotlib.pyplot as plt
import PIL.Image as PI
Loading the file
fileName = 'n01978287_43.jpg'
img = PI.open(fileName)
size = 224
img = img.resize((size, size))
img = np.asarray(img, dtype=np.uint8).astype(np.float32)
img = img/255
Result 1
temp_img = np.copy(img)
temp_img[51, :, :] = 0*temp_img[51, :, :]
fig = plt.figure()
ax1 = plt.subplot(1, 6, 1)
ax1.imshow(img, interpolation='none')
ax2 = plt.subplot(1, 6, 2)
ax2.imshow(temp_img, interpolation='none')
plt.savefig('test_516.png')
plt.close(fig)
Result 2
temp_img = np.copy(img)
temp_img[52, :, :] = 0*temp_img[52, :, :]
fig = plt.figure()
ax1 = plt.subplot(1, 6, 1)
ax1.imshow(img, interpolation='none')
ax2 = plt.subplot(1, 6, 2)
ax2.imshow(temp_img, interpolation='none')
plt.savefig('test_526.png')
plt.close(fig)
Result 3
temp_img = np.copy(img)
temp_img[51, :, :] = 0*temp_img[51, :, :]
fig = plt.figure()
ax1 = plt.subplot(1, 2, 1)
ax1.imshow(img, interpolation='none')
ax2 = plt.subplot(1, 2, 2)
ax2.imshow(temp_img, interpolation='none')
plt.savefig('test_512.png')
plt.close(fig)
Result 4
temp_img = np.copy(img)
temp_img[56, :, :] = 0*temp_img[56, :, :]
fig = plt.figure()
ax1 = plt.subplot(1, 2, 1)
ax1.imshow(img, interpolation='none')
ax2 = plt.subplot(1, 2, 2)
ax2.imshow(temp_img, interpolation='none')
plt.savefig('test_562.png')
plt.close(fig)
Now, if you look at the results, you would notice the inconsistency.
Firstly, for first two images (figure with 6 axes), you see the black line only in one of the image. (There is a pattern to this if you zero out all the rows (one at a time) and then try to save the results).
In the last two images, black line gets thicker. (I didn't find any pattern in this case).
System Setup - Python3, Matplotlib3, PIL, Numpy
Update:
After looking for ways to save a figure with the desired resolution (224*224 in this case), I wrote the following code (using multiple resources from web).
Importing libraries and loading the image file
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw, ImageFont
fileName = 'n01978287_43.jpg'
img = Image.open(fileName)
size = 224
img = img.resize((size, size))
img = np.asarray(img, dtype=np.uint8).astype(np.float32)
img = img/255
Function to plot the grid of images
def plt_save(grid, idx, name):
nRows = len(grid)
nCols = len(grid[0])
print('Clearing figure')
plt.rcParams.update({'font.size': 8})
wFig = (nCols+2) # Figure width (two more than nCols because I want to add ylabels on the very left and very right of figure)
hFig = (nRows+1) # Figure height (one more than nRows becasue I want to add xlabels to the top of figure)
fig = plt.figure(figsize=( wFig, hFig ))
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
fig.patch.set_facecolor('grey')
for r in range(nRows):
for c in range(nCols):
ax = plt.subplot2grid( shape=[hFig, wFig], loc=[r+1, c+1] )
im= ax.imshow(grid[r][c], interpolation='none')
ax.spines['bottom'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.set_xticks([])
ax.set_yticks([])
#fig.colorbar(im, ax=ax)
#ax.set_aspect('auto')
if not r:
ax.set_title('Image',
rotation=22.5,
horizontalalignment='left',
verticalalignment='bottom')
if not c:
ax.set_ylabel('leftLabel',
rotation=0,
horizontalalignment='right',
verticalalignment='center')
if c == wFig-3:
ax2 = ax.twinx()
#ax2.axis('off')
ax2.set_xticks([])
ax2.set_yticks([])
ax2.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)
ax2.spines['bottom'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax2.set_ylabel( 'rightLabel',
rotation=0,
verticalalignment='center',
horizontalalignment='left' )
print('Saving file')
plt.savefig( ( str(idx) + '_' + name + '_' + fileName.split('.')[0] + '.png'),
orientation='landscape',
#bbox_inches='tight',
facecolor = fig.get_facecolor(),
dpi=224, # DPI is 224 becasue the axis size is 1x1 inch and I want 224x224 pixels in each axis
transparent=True,
frameon=False )
plt.close(fig)
Loop to zero out the rows of the image (one at a time)
for i in range(0, 224):
temp_img = np.copy(img)
temp_img[i, :, :] = 0*temp_img[i, :, :]
# 1*4 Grid of images (can vary based on the requirement)
grid = [img, temp_img, img, temp_img]
grid = [grid, grid] #2*4 grid of images
plt_save(grid, i, 'PLT_')
Here is how one of the 224 images looks like.
The thing is that it works perfectly as long as I stick with this kind of plot. But the moment I try to make some changes (like adding a colorbar, having some spaces between each axis etc), the image resolution changes. If I use bbox_inches = 'tight' while saving my figure, it adjusts everything but changes the original resolution while keeping the figure size constant.
Is there any other way similar to bbox_inches='tight' such that it can keep the axis resolution fixed while adjusting the figure size accordingly. Or if there is no such thing in matplotlib, could you suggest me any other way to incorporate colorbar (small spaces between axis, ylabel for each axis etc) while keeping the image resolution fixed.

The image you start with has 224 pixels in height.
In the first two cases you distribute those over 72 pixels in the resulting image. This means any row of the image has a 72/224=32% chance of showing up in the final plot. In row number 52 you are lucky and hit this one third chance.
In the second two cases the resulting image is 226 pixels in height (i.e. just slightly larger than the original). Here you have a 2/224=0.9% chance that one row will occupy two pixels. In the case of row no. 56 you hit that unlucky chance.

Related

imshow subplot placement inside matplotlib figure

I have a Python script that draws a matrix of images, each image is read from disk and is 100x100 pixels. Current result is:
matrix of images
I don't know why Python adds vertical spacing between each row. I tried setting several parameters for plt.subplots. Rendering code is below:
fig, axs = plt.subplots(
gridRows, gridCols, sharex=True, sharey=False, constrained_layout={'w_pad': 0, 'h_pad': 0, 'wspace': 0, 'hspace': 0}, figsize=(9,9)
)
k = 0
for i in range(len(axs)):
for j in range(len(axs[i])):
if (k < paramsCount and dataset.iat[k,2]):
img = mpimg.imread(<some_folder_path>)
else:
img = mpimg.imread(<some_folder_path>)
ax = axs[i, j]
ax.imshow(img)
ax.axis('off')
if (i == 0): ax.set_title(dataset.iat[k,1])
if (j == 0): ax.text(-0.2, 0.5, dataset.iat[k,0], transform=ax.transAxes, verticalalignment='center', rotation='vertical', size=12)
axi = ax.axis()
rec = plt.Rectangle((axi[0], axi[2]), axi[1] - axi[0], axi[3] - axi[2], fill=False, lw=1, linestyle="dotted")
rec = ax.add_patch(rec)
rec.set_clip_on(False)
k = k + 1
plt.show()
Desired result is like:
desired result
Does anyone have ideas?
I'm sure there are many ways to do this other than the tashi answer, but the grid and subplot keywords are used in the subplot to remove the spacing and scale. In the loop process for each subplot, I set the graph spacing, remove the tick labels, and adjust the spacing by making the border dashed and the color gray. The title and y-axis labels are also added based on the loop counter value. Since the data was not provided, some of the data is written directly, so please replace it with your own data.
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(20220510)
grid = np.random.rand(4, 4)
gridRows, gridCols = 5, 10
titles = np.arange(5,51,5)
ylabels = [500,400,300,200,100]
fig, axs = plt.subplots(gridRows, gridCols,
figsize=(8,4),
gridspec_kw={'wspace':0, 'hspace':0},
subplot_kw={'xticks': [], 'yticks': []}
)
for i, ax in enumerate(axs.flat):
ax.imshow(grid, interpolation='lanczos', cmap='viridis', aspect='auto')
ax.margins(0, 0)
if i < 10:
ax.set_title(str(titles[i]))
if i in [0,10,20,30,40]:
ax.set_ylabel(ylabels[int(i/10)])
ax.set_xticklabels([])
ax.set_yticklabels([])
for s in ['bottom','top','left','right']:
ax.spines[s].set_linestyle('dashed')
ax.spines[s].set_capstyle("butt")
for spine in ax.spines.values():
spine.set_edgecolor('gray')
plt.show()
I realized it has to do with the dimensions passed to figsize. Since rows count is half the columns count, I need to pass figsize(width, width/2).

Remove precipitates oriented in line shape in microscopic image

I have some microscopic images where there are precipitates in single states and in some we have in horizontal or vertical lines. Now how should I remove these lines?
import matplotlib.pyplot as plt
import numpy as np
from scipy import ndimage as ndi
import cv2
import math
from skimage import (
color, feature, filters, measure, morphology, segmentation, util
)
# Sample1 - T61
image = cv2.imread(r"C:\Users\Stelle1.tif",cv2.IMREAD_GRAYSCALE)
assert not isinstance(image,type(None)), 'image not found'
fig, ax = plt.subplots()
ax.imshow(image, cmap='gray')
ax.axis('off')
plt.imshow()
click to view the image
fig, ax = plt.subplots(figsize=(5, 5))
qcs = ax.contour(image, origin='image')
ax.axis('off')
plt.show()
thresholds = filters.threshold_multiotsu(image, classes=3)
regions = np.digitize(image, bins=thresholds)
fig, ax = plt.subplots(ncols=2, figsize=(10, 5))
ax[0].imshow(image)
ax[0].set_title('Original')
ax[0].axis('off')
ax[1].imshow(regions)
ax[1].set_title('Multi-Otsu thresholding')
ax[1].axis('off')
plt.show()
cells = image > thresholds[0]
dividing = image > thresholds[1]
labeled_cells = measure.label(cells)
labeled_dividing = measure.label(dividing)
naive_mi = labeled_dividing.max() / labeled_cells.max()
print(naive_mi)
higher_threshold = 100
dividing = image > higher_threshold
smoother_dividing = filters.rank.mean(util.img_as_ubyte(dividing),
morphology.disk(4))
binary_smoother_dividing = smoother_dividing > 20
fig, ax = plt.subplots(figsize=(5, 5))
ax.imshow(binary_smoother_dividing)
ax.set_title('Dividing precipitate')
ax.axis('off')
plt.show()
click to view the image
Here is what I got if I increase the higher_threshold = 100, I will lose the ellipse shape precipitate where I need to count the area and other properties. Can you suggest some solution that the algorithm should not detect the line shape precipitates?
Have you thought about using something like a hough transform to detect straight lines?:
https://scikit-image.org/docs/dev/auto_examples/edges/plot_line_hough_transform.html
I basically lifted this straight from the above tutorial and got some pretty decent out of the box results.
from skimage import io
from skimage.transform import probabilistic_hough_line
from skimage.feature import canny
img = io.imread('GsSj9.png', as_gray=True) # read in the image
edges = canny(img) # use canny filter to detect edges
lines = probabilistic_hough_line(edges, threshold=20, line_length=20, line_gap=3)
# make plot of image and probabilistic_hough_line
fig, axes = plt.subplots(1, 2, sharex=True, sharey=True)
ax = axes.ravel()
ax[0].imshow(img)
ax[0].set_title('image')
ax[1].imshow(img * 0)
for line in lines:
p0, p1 = line
ax[1].plot((p0[0], p1[0]), (p0[1], p1[1]))
ax[1].set_xlim((0, img.shape[1]))
ax[1].set_ylim((img.shape[0], 0))
ax[1].set_title('Probabilistic Hough')
You would still need to figure out a good way to make a binary image from the transform lines but it could be useful in your endeavor.

Can you force the wspace and hspace of figure subplots to a fixed value in matplotlib regardless of figure size

I am trying to build a function to plot multiple images in a grid with a single colorbar and histogram. I would like the spacing between all the plots to be a fixed value and for the colorbar to span the height of a all images and histogram to span the width of the images/colorbar. I have some code that works, but it requires the figure size being set to a specific aspect ratio for it to work. This is not ideal because I want to use the function for images with varying aspect ratios and for a varying number of images 2x1, 1x2, 2x2, etc.
This code outputs 3 figures of varying aspect ratio. I would like if any excess dimension would be applied to the border spacing rather than the subplot wspace, hspace spacing.
fig wide: https://i.stack.imgur.com/BB1Cz.png
fig tall: https://i.stack.imgur.com/G5C34.png
fig nice: https://i.stack.imgur.com/AVX6C.png
Here is the code:
import math
import numpy as np
import matplotlib as mpl
from matplotlib import pyplot as plt
def compare_frames(frames, columns, bins=256, alpha=.5, vmin=None, vmax=None, fig=None):
if vmin is None:
vmin = min([f.min() for f in frames])
if vmax is None:
vmax = max([f.max() for f in frames])
if fig == None:
fig = plt.figure()
color_cycle = plt.get_cmap('tab10')
rows = math.ceil(len(frames)/columns)
width_ratios = [1 for col in range(columns)] + [.05]
gs = mpl.gridspec.GridSpec(rows + 1, columns + 1, figure=fig, width_ratios=width_ratios)
images = []
for row in range(rows):
for col in range(columns):
idx = row*columns + col
if idx < len(frames):
ax = fig.add_subplot(gs[row, col])
ax.get_xaxis().set_ticks([])
ax.get_yaxis().set_ticks([])
for spine in ['bottom', 'top', 'left', 'right']:
ax.spines[spine].set_color(color_cycle(idx))
ax.spines[spine].set_linewidth(3)
images.append(ax.imshow(frames[idx], vmin=vmin, vmax=vmax))
cax = fig.add_subplot(gs[0:-1, -1])
plt.colorbar(images[0], cax=cax)
hax = fig.add_subplot(gs[-1, :])
for i, frame in enumerate(frames):
hax.hist(frame.ravel(), bins=256, range=(vmin, vmax), color=color_cycle(i), alpha=alpha)
fig.subplots_adjust(wspace=.05, hspace=.05)
if __name__ == '__main__':
x_size = 640
y_size = 512
frames = []
for i in range(4):
frames.append(np.random.normal(i + 1, np.sqrt(i + 1), size=(y_size, x_size)))
fig_wide = plt.figure(figsize=(12, 8))
compare_frames(frames, 2, fig=fig_wide)
fig_tall = plt.figure(figsize=(6, 8))
compare_frames(frames, 2, fig=fig_tall)
fig_nice = plt.figure(figsize=(6.9, 8))
compare_frames(frames, 2, fig=fig_nice)
plt.show()
I've gathered that I should probably be using matplotlib axes_grid1 from mpl_toolkits. They have a built-in ImageGrid class which does a lot of what I would like to do (fixed spacing for images and colorbar):
def compare_frames(frames, columns, bins=256, alpha=.5, vmin=None, vmax=None, fig=None):
if vmin is None:
vmin = min([f.min() for f in frames])
if vmax is None:
vmax = max([f.max() for f in frames])
if fig == None:
fig = plt.figure()
color_cycle = plt.get_cmap('tab10')
rows = math.ceil(len(frames)/columns)
im_grid = axes_grid1.ImageGrid(fig, 111, nrows_ncols=(rows, columns), axes_pad=.1,
cbar_mode='single', cbar_pad=.1, cbar_size=.3)
for i, ax in enumerate(im_grid):
im = ax.imshow(frames[i], vmin=vmin, vmax=vmax)
ax.get_xaxis().set_ticks([])
ax.get_yaxis().set_ticks([])
for spine in ['bottom', 'top', 'left', 'right']:
ax.spines[spine].set_color(color_cycle(i))
ax.spines[spine].set_linewidth(3)
cbar = fig.colorbar(im, cax=im_grid.cbar_axes[0])
This is great, and I would love to figure out a way to use this ImageGrid class to do most of the work, and then add another axis at the bottom for the histogram. I haven't been able to crack how to do this however since all of the examples I've found use "append_axes()" on a Divider class. ImageGrid forms a SubplotDivider however, which doesn't have an append_axes function.

How to display a ground truth image segmentation mask image in python?

I tried this piece of code
from skimage import io
temp = io.imread(mask_input_path)
plt.imshow(temp)
This displays it a normal image, hence the output is black.
Just like a normal image. If your entire mask is black that means desired object is not present in your image.
But to select only masked area you need 2 extra lines on code
import matplotlib.pyplot as plt
input_img = plt.imread('img.jpg')
mask_img = plt.imread('mask.jpg')
# select only masked area below
masked = input_img.copy()
masked[mask_img == 0 ] = 0
fig, axes = plt.subplots(1, 3, figsize=(16, 12))
ax = axes.flatten()
ax[0].imshow(input_img, cmap="gray")
ax[0].set_axis_off()
ax[0].set_title("Original Imput Image", fontsize=12)
ax[1].imshow(mask_img, cmap="gray")
ax[1].set_axis_off()
ax[1].set_title("Mask", fontsize=12)
ax[2].imshow(masked, cmap="gray")
ax[2].set_axis_off()
ax[2].set_title("Masked", fontsize=12)
plt.show()
Actually using
masked[mask_img < 30 ] = 0
gives slightly better results because mask values are not exactly zero in my case

matplotlib Gridspec subplots unexpected different size

I am trying to create a grid of images using matplotlib.
The first row and column define the input to a function and the rest of the grid is the output.
Here's someone else's reference of how I would like it to look: reference.
Especially note that lines seperating the first row and column from everything else.
I was trying for the last couple of hours to make it work. The best I've come so far is using Gridspec to divide the image into four groups and construct the image using PIL.
However, for a reason I cannot understand the shapes of the different subplots don't match.
Attaching a minimal code and it's output.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import PIL
f = plt.figure(figsize=(20, 20))
resolution = 256
num_images = 6
h = w = num_images
main_grid = gridspec.GridSpec(h, w, hspace=0, wspace=0)
col = f.add_subplot(main_grid[0, 1:])
row = f.add_subplot(main_grid[1:, 0])
mid = f.add_subplot(main_grid[1:, 1:])
corner = f.add_subplot(main_grid[0, 0])
corner_canvas = PIL.Image.new('RGB', (resolution, resolution), 'gray')
mid_canvas = PIL.Image.new('RGB', (resolution * w, resolution * h), 'yellow')
col_canvas = PIL.Image.new('RGB', (resolution * w, resolution), 'blue')
row_canvas = PIL.Image.new('RGB', (resolution, resolution * h), 'red')
corner.imshow(corner_canvas)
col.imshow(col_canvas)
row.imshow(row_canvas)
mid.imshow(mid_canvas)
plt.savefig('fig.png')
As you can see here, the shapes don't match which make the grid not aligned.
Any solution producing an image in the style of the reference would be great !
I would use a combination of GridSpec and GridSpecFromSubplotSpec for this kind of layout:
Nx = 2
Ny = 3
sp = 0.5
fig = plt.figure()
gs0 = matplotlib.gridspec.GridSpec(2,2, width_ratios=[1,Nx+1], height_ratios=[1,Ny+1], wspace=sp, hspace=sp, figure=fig)
gs00 = matplotlib.gridspec.GridSpecFromSubplotSpec(1,Nx,subplot_spec=gs0[0,1:], wspace=0, hspace=0)
gs01 = matplotlib.gridspec.GridSpecFromSubplotSpec(Ny,1,subplot_spec=gs0[1:,0], wspace=0, hspace=0)
gs11 = matplotlib.gridspec.GridSpecFromSubplotSpec(Ny,Nx, subplot_spec=gs0[1:,1:], wspace=0, hspace=0)
top_axes = [fig.add_subplot(gs00[i]) for i in range(Nx)]
left_axes = [fig.add_subplot(gs01[i]) for i in range(Ny)]
center_axes = [fig.add_subplot(gs11[j,i]) for j in range(Ny) for i in range(Nx)]

Categories

Resources