3D nrrd image visualization (matplotlib)? - python

I have 3D image data in nrrd format, where the array is of shape (300, 256, 256) (meaning I have a 256x256 image and 300 slices of it, adding up to a 3d image). The array after reading from nrrd saves the opacity info for each 3D point (e.g. imgarray[x][y][z] would equal a number between 0-255, it's only for opacity, e.g. no rgb colors (this is on purpose)).
I'm trying to visualize this with matplotlib:
import matplotlib.pyplot as plt
from mpl_toolkits import mplot3dfig = plt.figure()
ax = plt.axes(projection='3d')
ax.scatter3D(`what do I put here?`)
However, matplotlib requires me to give x, y and z axes (plus a cmap), but my data is not in such format (if I do imgarray[0] that still contains an array of 256x256 opacity info).
I can easily view a slice of the 3D image by
plt.imshow(imgarray[100])
plt.show()
However, I'd like to view it in 3D. How can I do this?

Napari can render large multi-dimensional arrays using Maximum Intensity Projection, or as isosurfaces. It comes with a GUI that makes it easy to experiment with different display settings. (At time of writing it is still in alpha stage.)
Usage example:
import numpy as np
import napari
from skimage import data, filters # Just to generate some test data (3D blobs).
with napari.gui_qt():
# Generate some test data (smooth 3D blob shapes)
imgarray = filters.gaussian(np.squeeze(np.stack([data.binary_blobs(length=300, n_dim=3, blob_size_fraction=0.1, volume_fraction=0.05)[:, 0:256, 0:256]])).astype(float), sigma=(2.5, 2.5, 2.5))
print(imgarray.shape)
'''
# If imgarray values are bytes (0..255), convert to floats for display.
imgarray = imgarray.astype(float) / 255
'''
# Open viewer (Qt window) with axes = slice, row, column
viewer = napari.Viewer(title='volume test', ndisplay=3, order=(0, 1, 2))
viewer.add_image(data=imgarray, name='blobs', scale=[256/300, 1, 1], colormap='gray_trans', rendering='attenuated_mip', attenuation=2.0, contrast_limits=(0.25, 1))

Related

How do I produce images of closed loops with user-specified image dimensions? [duplicate]

Say I have an image of size 3841 x 7195 pixels. I would like to save the contents of the figure to disk, resulting in an image of the exact size I specify in pixels.
No axis, no titles. Just the image. I don't personally care about DPIs, as I only want to specify the size the image takes in the screen in disk in pixels.
I have read other threads, and they all seem to do conversions to inches and then specify the dimensions of the figure in inches and adjust dpi's in some way. I would like to avoid dealing with the potential loss of accuracy that could result from pixel-to-inches conversions.
I have tried with:
w = 7195
h = 3841
fig = plt.figure(frameon=False)
fig.set_size_inches(w,h)
ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)
ax.imshow(im_np, aspect='normal')
fig.savefig(some_path, dpi=1)
with no luck (Python complains that width and height must each be below 32768 (?))
From everything I have seen, matplotlib requires the figure size to be specified in inches and dpi, but I am only interested in the pixels the figure takes in disk. How can I do this?
To clarify: I am looking for a way to do this with matplotlib, and not with other image-saving libraries.
Matplotlib doesn't work with pixels directly, but rather physical sizes and DPI. If you want to display a figure with a certain pixel size, you need to know the DPI of your monitor. For example this link will detect that for you.
If you have an image of 3841x7195 pixels it is unlikely that you monitor will be that large, so you won't be able to show a figure of that size (matplotlib requires the figure to fit in the screen, if you ask for a size too large it will shrink to the screen size). Let's imagine you want an 800x800 pixel image just for an example. Here's how to show an 800x800 pixel image in my monitor (my_dpi=96):
plt.figure(figsize=(800/my_dpi, 800/my_dpi), dpi=my_dpi)
So you basically just divide the dimensions in inches by your DPI.
If you want to save a figure of a specific size, then it is a different matter. Screen DPIs are not so important anymore (unless you ask for a figure that won't fit in the screen). Using the same example of the 800x800 pixel figure, we can save it in different resolutions using the dpi keyword of savefig. To save it in the same resolution as the screen just use the same dpi:
plt.savefig('my_fig.png', dpi=my_dpi)
To to save it as an 8000x8000 pixel image, use a dpi 10 times larger:
plt.savefig('my_fig.png', dpi=my_dpi * 10)
Note that the setting of the DPI is not supported by all backends. Here, the PNG backend is used, but the pdf and ps backends will implement the size differently. Also, changing the DPI and sizes will also affect things like fontsize. A larger DPI will keep the same relative sizes of fonts and elements, but if you want smaller fonts for a larger figure you need to increase the physical size instead of the DPI.
Getting back to your example, if you want to save a image with 3841 x 7195 pixels, you could do the following:
plt.figure(figsize=(3.841, 7.195), dpi=100)
( your code ...)
plt.savefig('myfig.png', dpi=1000)
Note that I used the figure dpi of 100 to fit in most screens, but saved with dpi=1000 to achieve the required resolution. In my system this produces a png with 3840x7190 pixels -- it seems that the DPI saved is always 0.02 pixels/inch smaller than the selected value, which will have a (small) effect on large image sizes. Some more discussion of this here.
This worked for me, based on your code, generating a 93Mb png image with color noise and the desired dimensions:
import matplotlib.pyplot as plt
import numpy
w = 7195
h = 3841
im_np = numpy.random.rand(h, w)
fig = plt.figure(frameon=False)
fig.set_size_inches(w,h)
ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)
ax.imshow(im_np, aspect='normal')
fig.savefig('figure.png', dpi=1)
I am using the last PIP versions of the Python 2.7 libraries in Linux Mint 13.
Hope that helps!
The OP wants to preserve 1:1 pixel data. As an astronomer working with science images I cannot allow any interpolation of image data as it would introduce unknown and unpredictable noise or errors. For example, here is a snippet from a 480x480 image saved via pyplot.savefig():
Detail of pixels which matplotlib resampled to be roughly 2x2, but notice the column of 1x2 pixels
You can see that most pixels were simply doubled (so a 1x1 pixel becomes 2x2) but some columns and rows became 1x2 or 2x1 per pixel which means the the original science data has been altered.
As hinted at by Alka, plt.imsave() which will achieve what the OP is asking for. Say you have image data stored in image array im, then one can do something like
plt.imsave(fname='my_image.png', arr=im, cmap='gray_r', format='png')
where the filename has the "png" extension in this example (but you must still specify the format with format='png' anyway as far as I can tell), the image array is arr, and we chose the inverted grayscale "gray_r" as the colormap. I usually add vmin and vmax to specify the dynamic range but these are optional.
The end result is a png file of exactly the same pixel dimensions as the im array.
Note: the OP specified no axes, etc. which is what this solution does exactly. If one wants to add axes, ticks, etc. my preferred approach is to do that on a separate plot, saving with transparent=True (PNG or PDF) then overlay the latter on the image. This guarantees you have kept the original pixels intact.
Based on the accepted response by tiago, here is a small generic function that exports a numpy array to an image having the same resolution as the array:
import matplotlib.pyplot as plt
import numpy as np
def export_figure_matplotlib(arr, f_name, dpi=200, resize_fact=1, plt_show=False):
"""
Export array as figure in original resolution
:param arr: array of image to save in original resolution
:param f_name: name of file where to save figure
:param resize_fact: resize facter wrt shape of arr, in (0, np.infty)
:param dpi: dpi of your screen
:param plt_show: show plot or not
"""
fig = plt.figure(frameon=False)
fig.set_size_inches(arr.shape[1]/dpi, arr.shape[0]/dpi)
ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)
ax.imshow(arr)
plt.savefig(f_name, dpi=(dpi * resize_fact))
if plt_show:
plt.show()
else:
plt.close()
As said in the previous reply by tiago, the screen DPI needs to be found first, which can be done here for instance: http://dpi.lv
I've added an additional argument resize_fact in the function which which you can export the image to 50% (0.5) of the original resolution, for instance.
This solution works for matplotlib versions 3.0.1, 3.0.3 and 3.2.1.
def save_inp_as_output(_img, c_name, dpi=100):
h, w, _ = _img.shape
fig, axes = plt.subplots(figsize=(h/dpi, w/dpi))
fig.subplots_adjust(top=1.0, bottom=0, right=1.0, left=0, hspace=0, wspace=0)
axes.imshow(_img)
axes.axis('off')
plt.savefig(c_name, dpi=dpi, format='jpeg')
Because the subplots_adjust setting makes the axis fill the figure, you don't want to specify a bbox_inches='tight', as it actually creates whitespace padding in this case. This solution works when you have more than 1 subplot also.
I had same issue. I used PIL Image to load the images and converted to a numpy array then patched a rectangle using matplotlib. It was a jpg image, so there was no way for me to get the dpi from PIL img.info['dpi'], so the accepted solution did not work for me. But after some tinkering I figured out way to save the figure with the same size as the original.
I am adding the following solution here thinking that it will help somebody who had the same issue as mine.
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np
img = Image.open('my_image.jpg') #loading the image
image = np.array(img) #converting it to ndarray
dpi = plt.rcParams['figure.dpi'] #get the default dpi value
fig_size = (img.size[0]/dpi, img.size[1]/dpi) #saving the figure size
fig, ax = plt.subplots(1, figsize=fig_size) #applying figure size
#do whatver you want to do with the figure
fig.tight_layout() #just to be sure
fig.savefig('my_updated_image.jpg') #saving the image
This saved the image with the same resolution as the original image.
In case you are not working with a jupyter notebook. you can get the dpi in the following manner.
figure = plt.figure()
dpi = figure.dpi
The matplotlib reference has examples about han to set the figure size in different units. For pixels:
px = 1/plt.rcParams['figure.dpi'] # pixel in inches
plt.subplots(figsize=(600*px, 200*px))
plt.text(0.5, 0.5, '600px x 200px', **text_kwargs)
plt.show()
https://matplotlib.org/stable/gallery/subplots_axes_and_figures/figure_size_units.html#
plt.imsave worked for me.
You can find the documentation here: https://matplotlib.org/3.2.1/api/_as_gen/matplotlib.pyplot.imsave.html
#file_path = directory address where the image will be stored along with file name and extension
#array = variable where the image is stored. I think for the original post this variable is im_np
plt.imsave(file_path, array)
Why everyone keep using matplotlib?
If your image is an numpy array with shape (3841, 7195, 3), its data type is numpy.uint8 and rgb value ranges from 0 to 255, you can simply save this array as an image without using matplotlib:
from PIL import Image
im = Image.fromarray(A)
im.save("your_file.jpeg")
I found this code from another post

Write my own image function that takes a greyscale (0-255) to a r,g,b, alpha heat map

I have a 300*500 image. It's is in grayscale and ranges from 0-255. I want to iterate value by value and apply a heat map (say viridis but it doesn't matter) to each value.
My final heatmap image is in Red, Blue, Green and Alpha. I imagine the specific heat map function would take the grayscale values and output three values for each Red, Blue, Green and their appropriate weights.
f(0-255) = weightr(Red), weightb(Blue), weightg(Green).
My ending image would have dimensions (300,500,4) The four channels are r,b,g and an alpha channel.
What is the function that would achieve this? Almost certain it's going to be highly dependent on the specific heat map. Viridis is what I'm after, but I want to understand the concept as well.
The code below reads in a random image (the fact it's from unsplash does not matter) and turns it into a (300,500), 0-255 image called imgarray. I know matplotlib defaults to viridis, but I included the extra step to show what I would like to achieve with my own function.
import matplotlib.pyplot as plt
import requests
from PIL import Image
from io import BytesIO
img_src = 'https://unsplash.it/500/300'
response = requests.get(img_src)
imgarray = Image.open(BytesIO(response.content))
imgarray = np.asarray(imgarray.convert('L'))
from matplotlib import cm
print(cm.viridis(imgarray))
plt.imshow(cm.viridis(imgarray))
Matplotlib defines the viridis colormap as 256 RGB colors (one for each 8 bit gray scale value), where each color channel is a floating point value from [0, 1]. The definition can be found on github. The following code demonstrates how matplotlib applies the viridis colormap to a gray scale image.
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib._cm_listed import _viridis_data # the colormap look-up table
import requests
from PIL import Image
from io import BytesIO
img_src = 'https://unsplash.it/id/767/500/300'
response = requests.get(img_src)
imgarray = Image.open(BytesIO(response.content))
imgarray = np.asarray(imgarray.convert('L'))
plt.imshow(cm.viridis(imgarray))
plt.show()
# look-up table: from grayscale to RGB
viridis_lut = np.array(_viridis_data)
print(viridis_lut.shape) # (256, 3)
# convert grayscale to RGB using the LUT
img_viridis = viridis_lut.take(imgarray, axis=0, mode='clip')
plt.imshow(img_viridis)
plt.show()
# add an alpha channel
alpha = np.full(imgarray.shape + (1,), 1.) # shape: (300, 500, 1)
img_viridis_alpha = np.concatenate((img_viridis, alpha), axis=2)
assert (cm.viridis(imgarray) == img_viridis_alpha).all() # are both equal
Produces the following image:
The actual magic happens in the np.take(a, indices) method, which takes values from array a (the viridis LUT) at the given indices (gray scale values from 0..255 from the image). To get the same result as from the cm.viridis function, we just need to add an alpha channel (full of 1. = full opacity).
For reference, the same conversion happens around here in the matplotlib source code.

Converting numpy array to picture

So I have got a string of characters and I am representing it by a number between 1-5 in a numpy array. Now I want to convert it to a pictorial form by first repeating the string of numbers downwards so the picture becomes broad enough to be visible (since single string will give a thin line of picture). My main problem is how do I convert the array of numbers to a picture?
This would be a minimal working example to visualize with matplotlib:
import numpy as np
import matplotlib.pyplot as plt
# generate 256 by 1 vector of values 1-5
img = np.random.randint(1,6, 256)
# transpose for visualization
img = np.expand_dims(img, 1).T
# force aspect ratio
plt.imshow(img, aspect=100)
# or, alternatively use aspect='auto'
plt.show()
You can force the aspect ratio of the plotted figure by simply setting the aspect option of imshow()

matplotlib imshow plots different if using colormap or RGB array

I am having the following problem: I am saving 16-bit tiff images with a microscope and I need to analyze them. I want to do that with numpy and matplotlib, but when I want to do something as simple as plotting the image in green (I will later need to superpose other images), it fails.
Here is an example when I try to plot the image either as a RGB array, or with the default jet colormap.
import numpy as np
import matplotlib.pyplot as plt
import cv2
imageName = 'image.tif'
# image as luminance
img1 = cv2.imread(imageName,-1)
# image as RGB array
shape = (img1.shape[0], img1.shape[1], 3)
img2 = np.zeros(shape,dtype='uint16')
img2[...,1] += img1
fig = plt.figure(figsize=(20,8))
ax1 = fig.add_subplot(1,2,1)
ax2 = fig.add_subplot(1,2,2)
im1 = ax1.imshow(img1,interpolation='none')
im2 = ax2.imshow(img2,interpolation='none')
fig.show()
Which to me yields the following figure:
I am sorry if the question is too basic, but I have no idea why the right plot is showing this artifacts. I would like to get with the green scale, something like how the figure looks (imageJ also yields somthing similar to the left plot).
Thank you very much for your collaboration.
I find the right plot much more artistic...
matplotlib is rather complicated when it comes to interpreting images. It goes roughly as follows:
if the image is a NxM array of any type, it is interpreted through the colormap (autoscale, if not indicated otherwise). (In principle, if the array is a float array scaled to 0..1, it should be interpreted as a grayscale image. This is what the documentation says, but in practice this does not happen.)
if the image is a NxMx3 float array, the RGB components are interpreted as RGB components between 0..1. If the values are outside of this range, they are taken with positive modulo 1, i.e. 1.2 -> 0.2, -1.7 -> 0.3, etc.
if the image is a NxMx3 uint8 array, it is interpreted as a standard image (0..255 components)
if the image is NxMx4, the interpretation is as above, but the fourth component is the opacity (alpha)
So, if you give matplotlib a NxMx3 array of integers other than uint8 or float, the results are not defined. However, by looking at the source code, the odd behavour can be understood:
if A.dtype != np.uint8:
A = (255*A).astype(np.uint8)
where A is the image array. So, if you give it uint16 values 0, 1, 2, 3, 4..., you get 0, 255, 254, 253, ... Yes, it will look very odd. (IMHO, the interpretation could be a bit more intuitive, but this is how it is done.)
In this case the easiest solution is to divide the array by 65535., and then the image should be as expected. Also, if your original image is truly linear, then you'll need to make the reverse gamma correction:
img1_corr = (img1 / 65535.)**(1/2.2)
Otherwise your middle tones will be too dark.
I approached this by normalising the image by the maximum value of the given datatype, which said by DrV, for uint16 is 65535. The helper function would look something like:
def normalise_bits(img):
bits = 1.0 # catch all
try:
# Test integer value, e.g. np.uint16
bits = np.iinfo(img.dtype).max
except ValueError:
# Try float maximum, e.g. np.float32
bits = np.finfo(img.dtype).max
return (img / bits).astype(float)
Then the image can be handled by matplotlib as a float [0.0, 1.0]

How do I apply a DCT to an image in Python?

I want to apply a Discrete Cosine Transform (as well as the inverse) to an image in Python and I'm wondering what is the best way to do it and how. I've looked at PIL and OpenCV but I still don't understand how to use it.
Example with scipy.fftpack:
from scipy.fftpack import dct, idct
# implement 2D DCT
def dct2(a):
return dct(dct(a.T, norm='ortho').T, norm='ortho')
# implement 2D IDCT
def idct2(a):
return idct(idct(a.T, norm='ortho').T, norm='ortho')
from skimage.io import imread
from skimage.color import rgb2gray
import numpy as np
import matplotlib.pylab as plt
# read lena RGB image and convert to grayscale
im = rgb2gray(imread('images/lena.jpg'))
imF = dct2(im)
im1 = idct2(imF)
# check if the reconstructed image is nearly equal to the original image
np.allclose(im, im1)
# True
# plot original and reconstructed images with matplotlib.pylab
plt.gray()
plt.subplot(121), plt.imshow(im), plt.axis('off'), plt.title('original image', size=20)
plt.subplot(122), plt.imshow(im1), plt.axis('off'), plt.title('reconstructed image (DCT+IDCT)', size=20)
plt.show()
Also, if you plot a small slice of the 2D DCT coefficients array imF (in log domain), you will get a figure like the following (with a checkerboard pattern):
From OpenCV:
DCT(src, dst, flags) → None
Performs a forward or inverse Discrete Cosine transform of a 1D or 2D
floating-point array.
Parameters:
src (CvArr) – Source array, real 1D or 2D array
dst (CvArr) – Destination array of the same size and same type as the source
flags (int) –
Transformation flags, a combination of the following values
CV_DXT_FORWARD do a forward 1D or 2D transform.
CV_DXT_INVERSE do an inverse 1D or 2D transform.
CV_DXT_ROWS do a forward or inverse transform of every individual row of
the input matrix. This flag allows user to transform multiple vectors simultaneously
and can be used to decrease the overhead (which is sometimes several times larger
than the processing itself), to do 3D and higher-dimensional transforms and so forth.
Here is an example of it being used.
The DCT is also available in scipy.fftpack.

Categories