Pixel manipulation by python matplotlib - matplotlib

I found the python matplotlib function very useful. I would be much better for image processing if each individual pixel is fixed.
I would like to do pixel by pixel processing. The problem is, after using the "handles.append" function, the image margin changes.
from skimage.io import imread
import matplotlib.pyplot as plt
img = imread('uk_figure.png')
my_dpi = 96
plt.figure(figsize=(800/my_dpi, 800/my_dpi), dpi = my_dpi, frameon=False)
fig1 = plt.figure(1)
fig1.set_figheight(40) #image size is set here, pixel = 8 *100 = 800
fig1.set_figwidth(40) #image size is set here, pixel = 8 *100 = 800
imgplot = plt.imshow(img)
#to add code below
plt.axis("off")
plt.subplots_adjust(left = 0, right =1, top =1, bottom =0)
plt.savefig("uk_figure_addcolor4.png", pad_inches=0)
The figure saved is EXACTLY the same as original image. Thus, I think I could process the two image by pixel by pixel processing.
After added this code, matplotlib automatically added a margin for my image. Thus, pixel by pixel processing fails. Is there a way to instruct matplotlib to save image with fixed pixel?
x = [457,458,459,460]
y = [288,289,290,291]
handles = []
handles.append(plt.scatter(x,y, color='blue', marker='+')) #margin changes after adding this line

The margin is added due to a change in the axis-limits. If your image is 1000 by 1000 pixels big say, you can solve your problem by adding plt.gca().set_xlim(0, 1000) and plt.gca().set_ylim(0, 1000). plt.gca() will get you the current axis, and then to that axis set the x- and y-limits.
Note: You might need to invert the y-limits to plt.gca().set_ylim(1000, 0), since matplotlib usually chooses the upper left corner as the origin for images. I used random data so its hard to tell if the image is flipped or not, but if you have a motive in your image you can very easy tell if the image is flipped or not. If it is flipped, invert the ylim as above.

Related

Matplotlib, Inkscape, Spyder, plots and SVG compatibility (true axis size)

I have been plotting data for years during my PhD and always had to fight with something that unfortunately plagues the scientific community: negligent data manipulation.
My problem is that when I plot with matplotlib two graphics with different number lengths in the Y axis, the result is two graphics with two different X axis sizes.
When I copy the resulting SVG image directly from Spyder IPython console (Copy SVG) and paste in Inkscape for editing, matching the axis is a painful task which requires scaling them correctly with absolute precision. I am aware there plugins that are able to rescale plots in Inkscape and etc.
Bonus solved problem 1: for some reason, the size of an SVG created by matplotlib is scaled by 0.75 relative to Inkscape
Bonus solved problem 2: Matplotlib uses... inches, so the 25.4 that is in the following code lines is simply to convert from inch to millimeters.
Sometimes, having more control at the root is better than patching and patching and patching. So here is my solution to those who have been agonizing like me over being able to have two plots with the same absolute axis sizes:
from matplotlib import pyplot as plt
inch = False # Set to True if you want to use inch (blergh...).
width = 50 # The actual size in millimeters for the X axis to have.
height = 20 # The actual size in millimeters for the Y axis to have.
figsize = [(-0.212+width)/(1+24.4*(not inch)),(-0.212+height)/(1+24.4*(not inch))] # [W, H]
# Attention to the 0.212 mm which is thickness of the axis line; the cap at the end of the axis is half of thickness and is accounted for the size of the axis in Inkscape. So, when you use the size of a line from Inkscape as the desired size of the axis in a plot from matplotlib, ax.get_linewidth() by default should be 0.8 (whatever 0.8 is.. but it seems like 0.212/25.4 * 100).
height_scale = 3 # Scale to account for the axis title, labels and ticks.
width_scale = 2 # Scale to account for the axis title, labels and ticks.
figsize = [width_scale*figsize[0]/0.75, height_scale*figsize[1]/0.75]
fig = plt.figure(figsize = (figsize[0], figsize[1]))
wpos = (50/(1+24.4*(not inch)))/(figsize[0]/0.75) # Giving 50 mm mandatory position shift for the Y axis, to accommodate the title, labels and ticks.
hpos = (40/(1+24.4*(not inch)))/(figsize[1]/0.75) # Giving 40 mm mandatory position shift for the X axis to accommodate the title, labels and ticks.
# Now comes the problem. The AXIS size is defined relatively to the FIGURE size. The following values will simply use the rescaled FIGURE sizes:
wscale = 1/width_scale # = (width_scale*figsize[0]/0.75)/width_scale = figsize[0]/0.75 which is our target size for Inkscape.
hscale = 1/height_scale
ax = fig.add_axes([wpos, hpos, wscale, hscale])
Then you can plot at will, copy the SVG output (in Spyder's IPython console, at least) and paste it in Inkscape.
The only set back is that the whole FIGURE size will be abnormal and you'll have to remove the white background from it in Inkscape. But that is something probably all of us already do.
This is a minimal working code. You can paste it in your IPython console and copy the SVG output, paste it in Inkscape and check the axis line size. It will be with a width of 50 mm and a height of 20 mm.

Tensorflow how to sample large number of textures from small dataset of large images

I have 100 large-ish (1000x1000) images which I want to use as a training data set for a texture analysis system. I want to randomly generate texture swatches of about 200x200. What is the best way to do this? I would prefer to not preprocess all of the swatches so that each epoch is trained with slightly different swatches.
My initial (naive?) implementation included preprocessing layers in the model that do random crops on the image and just do a ton of epochs to accommodate the small number of large pictures, however after about ~400 epochs TF would crash without exception (it would just exit).
I now find myself coding a data generator (tf.keras.utils.Sequence) that will return a batch of swatches on request, but I feel like I'm reinventing the wheel and it is getting clunky - making me think this can't be the best way.
What is the best way to handle such a situation where you have a somewhat small dataset that you dynamically create more samples from?
I have written a function that will segment an image. Code is below
import cv2
def image_segment( image_path, img_resize, crop_size):
image_list=[]
img=cv2.imread(image_path)
img=cv2.resize(img, img_resize)
shape=img.shape
xsteps =int( shape[0]/crop_size[0])
ysteps = int( shape[1]/crop_size[1])
print (xsteps, ysteps)
for i in range (xsteps):
for j in range (ysteps):
x= i * crop_size[0]
xend=x + crop_size[0]
y= j * crop_size[1]
yend = y + crop_size[1]
cropped_image = cropped_image=img[x: xend, y: yend]
image_list.append(cropped_image)
return image_list
below is an example of use
# This code provides input to the image_segment function
image_path=r'c:\temp\landscape.jpg' # location of image
width=1000 # width to resize input image
height= 800 # height to resize input image
image_resize=( width, height) # specify original image (width, height)
crop_width=200 # width of desired cropped images
crop_height=400 # height of desired cropped images
# Note to get full set of cropped images width/cropped_width and height/cropped_height should be integer values
crop_size=(crop_height, crop_width)
images=image_segment(image_path, image_resize, crop_size) # call the function
The code below will display the resized input image and the resultant cropped images
# this code will display the resized input image and the resultant cropped images
import matplotlib.pyplot as plt
from matplotlib.pyplot import imshow
img=cv2.imread(image_path) # read in the image
input_resized_image=cv2.resize(img, image_resize) # resize the image
imshow(input_resized_image) # show the resized input image
r=len(images)
plt.figure(figsize=(20, 20))
for i in range(r):
plt.subplot(5, 5, i + 1)
image=images # scale images between 0 and 1 becaue pre-processor set them between -1 and +1
plt.imshow(image[i])
class_name=str(i)
plt.title(class_name, color='green', fontsize=16)
plt.axis('off')
plt.show()

Dynamically scaling axes during a matplotlib ArtistAnimation

It appears to be impossible to change the y and x axis view limits during an ArtistAnimation, and have the frames replayed with different axis limits.
The limits seem to fixed to those set last before the animation function is called.
In the code below, I have two plotting stages. The input data in the second plot is a much smaller subset of the data in the 1st frame. The data in the 1st stage has a much wider range.
So, I need to "zoom in" when displaying the second plot (otherwise the plot would be very tiny if the axis limits remain the same).
The two plots are overlaid on two different images (that are of the same size, but different content).
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib.image as mpimg
import random
# sample 640x480 image. Actual frame loops through
# many different images, but of same size
image = mpimg.imread('image_demo.png')
fig = plt.figure()
plt.axis('off')
ax = fig.gca()
artists = []
def plot_stage_1():
# both x, y axis limits automatically set to 0 - 100
# when we call ax.imshow with this extent
im_extent = (0, 100, 0, 100) # (xmin, xmax, ymin, ymax)
im = ax.imshow(image, extent=im_extent, animated=True)
# y axis is a list of 100 random numbers between 0 and 100
p, = ax.plot(range(100), random.choices(range(100), k=100))
# Text label at 90, 90
t = ax.text(im_extent[1]*0.9, im_extent[3]*0.9, "Frame 1")
artists.append([im, t, p])
def plot_stage_2():
# axes remain at the the 0 - 100 limit from the previous
# imshow extent so both the background image and plot are tiny
im_extent = (0, 10, 0, 10)
# so let's update the x, y axis limits
ax.set_xlim(im_extent[0], im_extent[1])
ax.set_ylim(im_extent[0], im_extent[3])
im = ax.imshow(image, extent=im_extent, animated=True)
p, = ax.plot(range(10), random.choices(range(10), k=10))
# Text label at 9, 9
t = ax.text(im_extent[1]*0.9, im_extent[3]*0.9, "Frame 2")
artists.append([im, t, p])
plot_stage_1()
plot_stage_2()
# clear white space around plot
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=None, hspace=None)
# set figure size
fig.set_size_inches(6.67, 5.0, True)
anim = animation.ArtistAnimation(fig, artists, interval=2000, repeat=False, blit=False)
plt.show()
If I call just one of the two functions above, the plot is fine. However, if I call both, the axis limits in both frames will be 0 - 10, 0 - 10. So frame 1 will be super zoomed in.
Also calling ax.set_xlim(0, 100), ax.set_ylim(0, 100) in plot_stage_1() doesn't help. The last set_xlim(), set_ylim() calls fix the axis limits throughout all frames in the animation.
I could keep the axis bounds fixed and apply a scaling function to the input data.
However, I'm curious to know whether I can simply change the axis limits -- my code will be better this way, because the actual code is complicated with multiple stages, zooming plots across many different ranges.
Or perhaps I have to rejig my code to use FuncAnimation, instead of ArtistAnimation?
FuncAnimation appears to result in the expected behavior. So I'm changing my code to use that instead of ArtistAnimation.
Still curious to know though, whether this can at all be done using ArtistAnimation.

Colorbar frame and color not aligned

I have a vexing issue with a colorbar and even after vigorous research I cannot find the question even being asked. I have a plot where I overlay a contour and a pcolormesh and I would like a colorbar to indicate values. That works fine except for one thing:
The colorbar frame and color are offset
The colorbar frame and the actual bar are offset such that below you have a white bit in the frame and on top the color is poking out. While the frame is aligned with the axis as desired, the colorbar is offset.
Here is a working example that emulates the situation I was in, i.e. multiple plots with insets.
import matplotlib.gridspec as gridspec
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
figheight = 4.2 - (2.1 - 49.519 / 25.4)
matplotlib.rcParams['figure.figsize'] = (5.25, figheight)
matplotlib.rcParams['axes.linewidth'] = 0.5
fig = plt.figure()
grid = gridspec.GridSpec(2, 1, height_ratios=[49.519 / 25.4 / figheight, 2.1 / figheight])
ax0 = plt.subplot(grid[0, 0])
ax1 = plt.subplot(grid[1, 0])
plt.tight_layout()
###############################################################################################
#
# Define position of inset
#
###############################################################################################
ax1.axis('off')
pos1 = ax1.get_position()
pos2 = matplotlib.transforms.Bbox([[pos1.x0, pos1.y0],
[.8*pos1.x1,
0.8*pos1.height + pos1.y0]])
left, bottom, width, height = [pos2.x0, pos2.y0, pos2.width, pos2.height]
ax2 = fig.add_axes([left, bottom, width, height])
###############################################################################################
#
# ax2 (inset) plot
#
###############################################################################################
pos2 = ax2.get_position()
ax2.axis('on')
x = np.linspace(0,5)
z = (np.outer(np.sin(x), np.cos(x))+1)*0.5
im = ax2.pcolormesh(z)
c = ax2.contour(z, linewidths=7)
ax2pos = ax2.get_position()
cbar_axis = fig.add_axes([ax2pos.x1+0.05,ax2pos.y0, .02, ax2pos.height])
colorbar = fig.colorbar(im, ax = ax2,
cax = cbar_axis, ticks = [0.1, .5, .9])
colorbar.outline.set_visible(True)
plot = 'Minimal.pdf'
fig.savefig(plot)
plt.close()
The problem persists in both the inline display and the saved .pdf if 'Inline' graphics backend is chosen. Using tight layout or not changes how badly the offset is depending on the size of the bar - same with using PyQT5 rather than inline graphics backend. I thought it was gone when I was changing between the various combinations, but I just realized it's still there.
I would appreciate any input.
As suggested by ImportanceOfBeingErnest I have tried using np.round on the figsize and that didn't change things. While you can fiddle around with sizes to make it look okay, it always stands over on one or the other side by some amount. When I change the graphics backend on Spyder 3 from 'Inline' to 'QT5' the problem becomes less severe with or without rounding. A summary of this is in this picture Colorbar overlap cases. Note that with not rounded and PyQT5 the problem still occurs, but is not as severe.
On inspection, it is clear that the colorbar is not only bleeding out over the top of its axes, but it's also positioned slightly to the left.
So, the problem here appears to be a conflict between the position of the colorbar axis and the colorbar itself when rasterization occurs. You can find more details on this issue in matplotlib's github repository, but I'll summarize what's going on here.
Colorbars are rasterized when the output is produced, so as to avoid artifacting issues during rendering. The position of the colorbar is snapped to the nearest integer pixels during the rasterization process, while the axis is kept where it is supposed to be. Then, when the output is produced, the colorbar falls within borders of fixed pixels of the image, despite the fact that the image is, itself, vectorized. Thus, there are two strategies that can be employed to avoid this mishap.
Use a finer DPI
The conversion from vectorized coordinates to rasterized coordinates takes place assuming a given DPI on the image. By default, this is set to be 72. However, by using more DPI, the overall shift induced by the rasterization process will be smaller, as the closest pixel the colorbar will snap to will be much nearer. Here, we change the output to have fig.savefig(plot,dpi=4000), and the problem goes away:
Note, however, that on my machine, the output size changed from 62 KB to 78 KB due to this change (although the DPI adjustment was also, admittedly, extreme). If you are worried about file sizes, you should pick a lower DPI that fixes the problem.
Use a different colormap
This rasterization happens when more than 50 colors are in the colorbar. Thus, we can do a quick test, setting our colormap to Pastel1 via
im = ax2.pcolormesh(z,cmap='Pastel1'). Here, the colorbar / axis mismatch is mitigated.
As a fallback, adopting a colorbar with fewer than 50 colors should mitigate this problem.
Rasterize the Axis
For completeness, there is also a third option. If you rasterize the colorbar axis, both the axis boundaries and the colormap will be rasterized, and you'll lose the offset. This will also rasterize your labels, and the axis will shift as one, breaking alignment with the nearby axis. For this, you just need to include cbar_axis.set_rasterized(True).
First, a way to overlay a contour and a pcolormesh and create a colorbar would be the following
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
import numpy as np
x = np.linspace(0,5)
z = (np.outer(np.sin(x), np.cos(x))+1)*0.5
fig = plt.figure(figsize=(4, 4))
ax = fig.add_subplot(111)
im = ax.pcolormesh(z)
c = ax.contour(z, linewidths=7)
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", "5%", pad="3%")
colorbar = fig.colorbar(im, cax=cax, ticks = [0.1, .5, .9])
plt.show()
Now to the problem from the question. It is of course possible to create the axes to put the colorbar in manually. Replacing the colorbar creation with the code from the question still produces a nice image.
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0,5)
z = (np.outer(np.sin(x), np.cos(x))+1)*0.5
fig = plt.figure(figsize=(4, 4))
ax = fig.add_subplot(111)
plt.subplots_adjust(right=0.8)
im = ax.pcolormesh(z)
c = ax.contour(z, linewidths=7)
ax2pos = ax.get_position()
cbar_axis = fig.add_axes([ax2pos.x1+0.05,ax2pos.y0, .05, ax2pos.height])
colorbar = fig.colorbar(im, ax = ax,
cax = cbar_axis, ticks = [0.1, .5, .9])
colorbar.outline.set_visible(True)
plt.show()
Conclusion so far: The issue is not reproducible, at least not without a Minimal, Complete, and Verifiable example.
I'm uncertain about the reasons for the behaviour in the example from the question. However, it seems that it can be overcome by rounding the figure size to 3 significant digits
matplotlib.rcParams['figure.figsize'] = (5.25, np.round(figheight,3))

Problems with zeros in matplotlib.colors.LogNorm

I am plotting a histogram using
plt.imshow(hist2d, norm = LogNorm(), cmap = gray)
where hist2d is a matrix of histogram values. This works fine except for elements in hist2d that are zero. In particular, I obtain the following image
but would like the white patches to be black.
Thank you!
Here's an alternative method that does not require you to muck with your data by setting a rgb value for bad pixels.
import copy
data = np.arange(25).reshape((5,5))
my_cmap = copy.copy(matplotlib.cm.get_cmap('gray')) # copy the default cmap
my_cmap.set_bad((0,0,0))
plt.imshow(data,
norm=matplotlib.colors.LogNorm(),
interpolation='nearest',
cmap=my_cmap)
The problem is that bins with 0 can not be properly log normalized so they are flagged as 'bad', which are mapped to differently. The default behavior is to not draw anything on those pixels. You can also specify what color to draw pixels that are over or under the limits of the color map (the default is to draw them as the highest/lowest color).
If you're happy with the colour scaling as is, and simply want the 0 values to be black, I'd simply change the input matrix so that the 0s are replaced by the next smallest value:
import matplotlib.pyplot as plt
import matplotlib.cm, matplotlib.colors
import numpy
hist2d = numpy.arange(9).reshape(3,3)
plt.imshow(numpy.maximum(hist2d, sorted(hist2d.flat)[1]),
interpolation='nearest',
norm = matplotlib.colors.LogNorm(),
cmap = matplotlib.cm.gray)
produces
Was this generated with the matplotlib hist2d function?
All you need to do is go through the matrix and set some arbitrary floor value, then make sure to plot this with fixed limits
for f in hist2d:
f += 1e-3
then when you show the figure, all of the whitespace will now be at the floor value, and will show up on the lognormal plot . However, if you are letting hist2d automatically pick the scaling for you, it will want to use the 1e-3 floor value as the minimum. To avoid this, you need to set vmin and vmax values in hist2d
hist2d(x,y,bins=40, norm=LogNorm(), vmin=1, vmax=1e4)