Replace numbers below threshold # in numpy array with zeroes - python
So I have a very big Numpy array (2560x1920). Its actually from a grayscale picture where every pixel is given a number from 0-1 indicating its brightness.
I'm trying to replace all values below a threshold, say 0.5, with zeroes.
This is probably a simple task but I'm a beginner with Numpy and I've searched around and still can't figure it out.
This is what I've attempted and I know its wrong...
for x in np.nditer(Image):
if x < .5:
x == 0
plt.imshow(Image, cmap=plt.cm.gray)
plt.show()
It just outputs the normal image without changing anything.
Also the array looks something like this (abbreviated obviously):
[[ 0.24565263 0.24565263 0.24902149 ..., 0.27528678 0.27265316
0.27606536]
[ 0.24565263 0.24565263 0.24902149 ..., 0.27870309 0.27606536
0.27948296]
[ 0.24228902 0.24228902 0.24565263 ..., 0.28212482 0.27948296
0.282906 ]
...,
[ 0.29706944 0.29706944 0.29706944 ..., 0.17470162 0.17144636
0.17144636]
[ 0.29362457 0.29362457 0.29362457 ..., 0.17144636 0.16495056
0.16170998]
[ 0.2901852 0.2901852 0.2901852 ..., 0.16819602 0.16170998
0.15847427]]
There is numpy's builtin indexing which can be used for replacing elements. This is can be done as:
Image[Image<0.5] = 0
I have returned from the future with suggestions.
The above approaches work great for a simple global threshold.
I'm posting this answer to warn that non-adaptive thresholding, like this, may be too naïve depending on your application.
Without adaptation to an image's average brightness or other qualities, your output won't be consistent if you are analyzing multiple pictures taken in different conditions.
There are much more accurate approaches for this, but they are a tiny bit more complicated. Scikit-Image makes this easy.
One of the most popular approaches is Otsu's (I can't say which is the most accurate for each situation, I haven't researched enough).
https://en.wikipedia.org/wiki/Otsu%27s_method
Scikit-Image has this and a few other algorithms built into their modules.
The answer to the question above, using this approach, is as simple as this:
import matplotlib.pyplot as plt
from skimage.filters import threshold_otsu
thresh = threshold_otsu(Image)
binary = Image > thresh
plt.imshow(Image, cmap=plt.cm.gray)
plt.show()
Read an example here:
http://scikit-image.org/docs/dev/auto_examples/plot_otsu.html
And about usage here:
http://scikit-image.org/docs/dev/api/skimage.filters.html?highlight=local%20otsu
Related
How does one Fourier transform an array of 1's and 0's
I'm trying to Fourier transform a matrix of 0's with a solid circle (like a pinhole) of 1's using Python. I am trying to get an image of an Airy Function, which should look like concentric circular ripples viewed from above. I'm still a bit of a beginner with Python and coding more generally. import numpy as np dimension = 256 list1 = [] listpiece = [] for i in range(dimension): for j in range(dimension): listpiece.append(0) list1.append(listpiece) listpiece = [] k=128 for i in range(dimension): for j in range(dimension): if (i-k)*(i-k) + (j-k)*(j-k) <= 64*2: list1[i][j] = 1 import matplotlib.pylab as plt import scipy.sparse as sparse plt.spy(list1) plt.show() Which gave this image of a black circle on a white background. I then converted this list to a numpy array. singledimlist = [] for i in range(dimension): for j in range(dimension): singledimlist.append(list1[i][j]) prefourierline = np.array( singledimlist ) shape = ( dimension, dimension ) prefourier = prefourierline.reshape( shape ) print(prefourier) plt.spy(prefourier) plt.show() Which gave an identical image: Using np.fft.fft2 gave a blank image, even though the output had very large changes: from scipy.fftpack import fft, ifft fouriered = np.fft.fft2(prefourier) plt.spy(fouriered) plt.show() Output: [[ 405. +0.00000000e+00j -401.08038516-1.50697234e-16j 389.47420686-2.31615451e-15j ... -370.63201656-5.88988318e-15j 389.47420686+2.35778788e-15j -401.08038516+8.95615360e-15j] [-401.08038516-2.27306384e-15j 397.18553235-1.77932604e-15j -385.65292606-1.63119926e-15j ... 366.93100304+7.84568423e-15j -385.65292606-2.13934425e-15j 397.18553235-1.08069809e-14j] [ 389.47420686+8.66313300e-15j -385.65292606-1.67296339e-14j 374.33891021+6.30297134e-15j ... -355.97430091-1.40810576e-14j 374.33891021+1.25700186e-14j -385.65292606-1.24588719e-14j] ... [-370.63201656-4.69963986e-14j 366.93100304+4.87944288e-14j -355.97430091-4.69561772e-14j ... 338.1937218 +3.81585557e-14j -355.97430091-4.67444422e-14j 366.93100304+3.64531853e-14j] [ 389.47420686+3.34933421e-14j -385.65292606-2.70693599e-14j 374.33891021+3.08443590e-14j ... -355.97430091-3.30709228e-14j 374.33891021+2.07603249e-14j -385.65292606-2.63513116e-14j] [-401.08038516-5.83528175e-14j 397.18553235+7.09535468e-14j -385.65292606-5.72142574e-14j ... 366.93100304+7.01916155e-14j -385.65292606-6.12008707e-14j 397.18553235+6.47498390e-14j]] So, I tried using np.fft.fft, but fared little better, instead of a blank image, I output a black horizontal stripe with the same width as the radius of the original circle, bisecting the white background. from scipy.fftpack import fft, ifft fouriered = np.fft.fft(prefourier) plt.spy(fouriered) plt.show() I suspect the main problem lies between my computer screen and my chair. My question is, what am I doing wrong? How does one Fourier transform an array of this sort? Thanks, I'd be grateful for some help, ES
There are multiple things, so I just provided a working example, that uses numpy. The zeroes and ones question is not a problem, since those are legitimate floating point numbers too, so the physics is fine. There are two issues in finding the right answer in the output. One is to zoom in or, alternatively, make the circle very small. Play with that and calculate the expected ring sizes from the close form solution (Airy-Function). The other is contrast. Below I just used a log to visualize better. Alternatives would be to take a root. Also note that I didn't square the result (as physics would indicate, i.e. intensity vs electric field). import matplotlib.pyplot as p import numpy as np n=1000 aa=np.ones((n,n)) x=np.linspace(-1,1,n) y=np.linspace(-1,1,n) X,Y= np.meshgrid(x,y) #this allows us to use vectorized approach, no for loops R = np.sqrt(X**2+Y**2) aa[R<0.1]=0 p.figure(figsize=(20,6)) p.subplot(131) p.imshow(aa) p.colorbar() p.subplot(132) spec= np.fft.fftshift(np.fft.fft2(aa)) p.imshow( np.log(np.abs(spec))) p.colorbar() p.title('airy func too fine to see') p.subplot(133) p.imshow( np.log(np.abs(spec[450:550,450:550]))) p.colorbar() p.title('zoomed in');
Median filter produces unexpected result on FITS file
This is based on a couple of other questions that haven't quite been answered, so I've started a new post. I'm working on finding the median of a masked array in 50-pixel patches. The image and the mask are both 901x877 telescope images. import numpy as np import matplotlib.pyplot as plt from astropy.io import fits # Use the fits files as input image and mask hdulist = fits.open('xbulge-w1.fits') w1data = hdulist[0].data hdulist3 = fits.open('xbulge-mask.fits') mask = 1 - hdulist3[0].data w1masked = np.ma.array(w1data, mask = mask) # Use general arrays as input image and mask #w1data = np.arange(790177).reshape(901,877) #w1masked = np.ma.masked_inside(w1data, 30000, 60000) side = 50 w, h = w1data.shape width_index = np.array(range(w//side)) * side height_index = np.array(range(h//side)) * side def assign_patch(patch, median, side): """Break this loop out to prevent 4 nested 'for' loops""" for j in range(side): for i in range(side): patch[i,j] = median return patch for width in width_index: for height in height_index: patch = w1masked[width:width+side, height:height+side] median = np.median(patch) assign_patch(patch, median, side) plt.imshow(w1masked) plt.show() The problem is, when I use the general arrays as input image and mask (the commented out section), it works fine, but when I use the FITS files, it produces 'side'-sized patches on the output image. I can't figure out what's going on with this.
I don't know how your FITS files look like but there are several things standing out: np.median doesn't take the mask into account. In fact in recent NumPy releases this (correctly) prints a Warning if attempted. You should be using np.ma.median instead. If you would update your NumPy you'll likely see this: UserWarning: Warning: 'partition' will ignore the 'mask' of the MaskedArray. The assign_patch function is unnecessary when you know that you can use slice assignment: w1masked[width:width+side, height:height+side] = median # instead of "assign_patch(patch, median, side)" That's also much faster than doing a double loop to replace each value. I assume that the issue is in fact because you use np.median instead of np.ma.median. There are lots of values a masked pixel could have including nan, 0, inf, ... so if these are taken into account (when they should be ignored) could produce any kind of problems, especially if the median starts returning nans or similar. More generally if you really wanted a median filter you can't just calculate the median of a patch and replace all values in the patch with that median. You should be using a median filter that takes the mask into account. Unfortunately I've never seen such a filter implemented in any wide-spread Python package. But if you have numba you could checkout a (very experimental!) package of mine numbamisc which contains a median_filter that takes masks into account.
Discretize or bin LAB colorspace in 2 dimensions
I have a lab colorspace And I want to "bin" the colorspace in a grid of 10x10 squares. So the first bin might be (-110,-110) to (-100,-100) then the next one might be (-100,-110) to (-90,-100) and so on. These bins could be bin 1 and bin 2 I have seen np.digitize() but it appears that you have to pass it 1-dimensional bins. A rudimentary approach that I have tried is this: for fn in filenames: image = color.rgb2lab(io.imread(fn)) ab = image[:,:,1:] width,height,d = ab.shape reshaped_ab = np.reshape(ab,(width*height,d)) print reshaped_ab.shape images.append(reshaped_ab) all_abs = np.vstack(images) all_abs = shuffle(all_abs,random_state=0) sns df = pd.DataFrame(all_abs[:3000],columns=["a","b"]) top_a,top_b = df.max() bottom_a,bottom_b = df.min() range_a = top_a-bottom_a range_b = top_b-bottom_b corner_a = bottom_a corner_b = bottom_b bins = [] for i in xrange(int(range_a/10)): for j in xrange(int(range_b/10)): bins.append([corner_a,corner_b,corner_a+10,corner_b+10]) corner_b = bottom_b+10 corner_a = corner_a+10 but the "bins" that results seem kinda sketchy. For one thing there are many empty bins as the color space does have values in a square arrangement and that code pretty much just boxes off from the max and min values. Additionally, the rounding might cause issues. I am wondering if there is a better way to do this? I have heard of color histograms which count the values in each "bin". I don't need the values but the bins are I think what I am looking for here. Ideally the bins would be an object that each have a label. So I could do bins.indices[0] and it would return the bounding box I gave it. Then also I could bin each observation, like if a new color was color = [15.342,-6.534], color.bin would return 15 or the 15th bin. I realize this is a lot to ask for, but I think it must be a somewhat common need for people working with color spaces. So is there any python module or tool that can accomplish what I'm asking? How would you approach this? thanks!
Use the standard numpy 2D-histogram function: numpy.histogram2d: import numpy as np # a and b are arrays representing your color points H, a_edges, b_edges = np.histogram2d(a, b, bins=10) If you want to discard the empty bins, you'd have to do some work from here. But I don't see why you'd want that, because assigning future colors to existing nonempty bins will be much more work if they are not on a rectangular grid.
You are probably trying to repeat what Richard Zhang did in "Colorful Image Colorization" research: http://richzhang.github.io/colorization/ Here, author himself discuss this problem: https://github.com/richzhang/colorization/issues/23 Fortunately Zhang provides .npy file, that contains those quantized values. It is under: https://github.com/richzhang/colorization/blob/master/resources/pts_in_hull.npy The only thing, you have to do now, is to load this file in your python script: import numpy as np pts_in_hull = np.load("pts_in_hull.npy") It is numpy array of shape 313x2 containing values from your image. I know this answer comes few years too late, but maybe it will help someone else.
Python IndexError: Out of bounds
I've created a class of which I pass an image (2D array, 1280x720). It's suppose to iterate through, looking for the highest value: import bumpy as np class myCv: def maxIntLoc(self,image): intensity = image[0,0] #columns, rows coordinates = (0,0) for y in xrange(0,len(image)): for x in xrange(0,len(image[0])): if np.all(image[x,y] > intensity): intensity = image[x,y] coordinates = (x,y) return (intensity,coordinates) Yet when I run it I get the error: if np.all(image[x,y] > intensity): IndexError: index 720 is out of bounds for axis 0 with size 720 Any help would be great as I'm new to Python. Thanks, Shaun
Regardless of the index error that you are experience, which has been addressed by others, iterating through pixels/voxels is not a valid method for manipulating images. The issue becomes particularly evident in multi-dimensional images, where you face the curse of dimensionality. The correct way to do this is to use vectorisation in programming languages that support it (e.g. Python, Julia, MATLAB). Through this method, you will achieve the results you're looking for much more efficiently (and thousands of times faster). Click here to find out more about vectorisation (aka. array programming). In Python, this can be achieved either using generators, which are not suitable for images as they don't really produce the results until called; or using NumPy arrays. Here is an example: Masking image matrices by vectorisation from numpy.random import randint from matplotlib.pyplot import figure, imshow, title, grid, show def mask_img(img, thresh, replacement): # Copy of the image for masking. Use of |.copy()| is essential to # prevent memory mapping. masked = initial_image.copy() # Replacement is the value to replace anything that # (in this case) is bellow the threshold. masked[initial_image<thresh] = replacement # Mask using vectorisation methods. return masked # Initial image to be masked (arbitrary example here). # In this example, we assign a 100 x 100 matrix of random integers # between 1 and 256 as our sample image. initial_image = randint(0, 256, [100, 100]) threshold = 150 # Threshold # Masking process. masked_image = mask_img(initial_image, threshold, 0) # Plots. fig = figure(figsize=[16,9]) fig.add_subplot(121) imshow(initial_image, interpolation='None', cmap='gray') title('Initial image') grid('off') fig.add_subplot(122) imshow(masked_image, interpolation='None', cmap='gray') title('Masked image') grid('off') show() Which returns: Of course you can put the masking process (function) in a loop to do this on a batch of images. You can modify the indices and do it on 3D, 4D (e.g. MRI), or 5D (e.g. CAT scan) images too, without the need to iterate over each individual pixel or voxel. Hope this helps.
In python, like most programming languages, indexes start at 0. So you can access only pixels from 0 to 719. Check with a debug print that len(image) and len(image[0]) are indeed returning 1280 and 720.
Artifacts in a filled contour plot on 3D axes
I have a frustrating problem that only manifests itself when plotting filled contour plots on 3D axes and only in certain situations. Here is an example of the issue I am experiencing: and These are the same data at different contouring intervals. You'll notice on the left side of the domain there is mis-filling occurring. This is a plot with the Z points squished into the Z=0 plane, via a plotting command like ax3d.contourf(X, Y, dbz[z25,:,:], zdir='z', offset=0, levels=levels, cmap='pymeteo_radar', alpha=0.50) The miscontouring happens regardless of alpha level or colormap used, but is sensitive to the number of levels. The use of zdir and offset do not effect the mis-contouring (the artifact just occurs on the Z surface. If I do not fill the contour, there is no mis-contouring. I can also alter the domain to sometimes make the issue better (or worse), but I have many plots to make within the same domain so that is not a fix. This issue does not occur when the same data is plotted on 2D axes, e.g.: This plot has some extra data on it, but you can see that the filled contouring does not have the same artifact from mis-filling the contour that occurs on the 3d axes. Below is a script you can run to reproduce the issue. #!/usr/bin/env python import numpy as np import matplotlib.pyplot as plt import mpl_toolkits.mplot3d.axes3d as p3 data=np.array([[53.9751, 51.5681, 50.7119, 51.1049, 51.5339, 51.4977, 51.2387,50.761, 50.1732, 49.8218, 49.5442, 48.936, 47.4498, 46.6484, 45.8542, 45.136, 44.5268, 44.071, 43.7665, 43.5928, 43.5269, 43.5385, 43.6053, 45.565, 47.0071, 46.8664, 47.372, 47.8324, 48.295, 48.731, 49.0522, 49.4001, 49.7111, 49.9919, 50.2527, 50.4928, 50.7135, 50.8831, 51.0806, 51.2683 ], [55.6671, 52.53, 50.7764, 50.5632, 51.2095, 51.5659, 51.521, 51.2143, 50.653, 50.2371, 49.989, 49.8089, 49.6058, 47.8355, 47.3124, 46.7346, 46.1616, 45.6498, 45.2462, 44.967, 44.8005, 44.7284, 44.7295, 44.7869, 46.959, 45.0194, 46.73, 48.0766, 48.9395, 49.5325, 49.8498, 50.1887, 50.4798, 50.7406, 50.9808, 51.2003, 51.4074, 51.555, 51.7429, 51.9218 ], [56.6513, 53.5919, 51.2774, 50.3133, 50.7705, 51.533, 51.8287, 51.7083, 51.2816, 50.7933, 50.4806, 50.2671, 50.1009, 50.0096, 49.9052, 49.4698, 47.4655, 47.0717, 46.6849, 46.3583, 46.1122, 45.952, 45.8678, 45.8485, 45.8811, 45.956, 46.0634, 47.2225, 49.4363, 50.2482, 50.527, 50.8558, 51.1358, 51.3809, 51.607, 51.8179, 52.0161, 52.1454, 52.3263, 52.497 ], [57.078, 54.3224, 52.0759, 50.4679, 50.4677, 51.297, 52.0284, 52.1594, 51.9395, 51.5518, 51.1419, 50.8765, 50.6686, 50.5101, 50.4078, 50.3473, 50.3592, 50.3813, 49.7504, 47.55, 47.324, 47.1365, 46.9978, 46.9119, 46.8743, 46.8811, 46.9257, 47.0013, 50.0148, 50.9106, 51.1133, 51.4282, 51.7064, 51.943, 52.1587, 52.3597, 52.4789, 52.6631, 52.8359, 52.9966 ], [57.3835, 54.9025, 52.8571, 50.9842, 50.5197, 51.1494, 52.0599, 52.4732, 52.4716, 52.2656, 51.9535, 51.6068, 51.3466, 51.1513, 50.9708, 50.8321, 50.7639, 50.7944, 50.8817, 49.8122, 48.2038, 48.086, 47.9704, 47.8735, 47.8035, 47.7644, 47.7574, 47.7803, 50.8194, 51.5486, 51.6645, 51.9745, 52.2349, 52.4508, 52.6481, 52.8317, 52.9412, 53.1097, 53.2699, 53.4171 ], [57.9157, 55.6092, 53.6306, 51.8011, 50.9372, 51.2615, 52.1406, 52.7436, 52.8528, 52.7829, 52.6322, 52.403, 52.1149, 51.866, 51.6624, 51.4773, 51.317, 51.2183, 51.2153, 51.1367, 48.5913, 48.6216, 48.6218, 48.5951, 48.5589, 48.527, 48.5081, 50.5185, 51.6998, 51.905, 52.2258, 52.4891, 52.7062, 52.8926, 53.0655, 53.2251, 53.3262, 53.4755, 53.6169, 53.7471 ], [58.6093, 56.432, 54.307, 52.6277, 51.584, 51.6482, 52.3762, 53.0685, 53.2545, 53.217, 53.1356, 53.0351, 52.8481, 52.6154, 52.39, 52.177, 51.9977, 51.843, 51.7172, 51.4587, 48.7481, 48.7984, 48.864, 48.9291, 48.9843, 49.0228, 50.496, 51.8667, 52.3404, 52.4759, 52.6889, 52.8851, 53.0525, 53.2072, 53.354, 53.4576, 53.5925, 53.7217, 53.8432, 53.956 ], [58.9719, 56.9885, 54.8768, 53.3526, 52.3025, 52.2089, 52.7762, 53.4444, 53.6768, 53.6706, 53.5692, 53.5162, 53.4373, 53.2886, 53.1113, 52.9065, 52.6988, 52.5193, 52.3544, 52.0384, 48.9624, 48.9653, 49.0005, 49.0574, 49.1258, 50.692, 51.9726, 52.4309, 52.699, 52.8194, 52.9845, 53.1336, 53.2669, 53.393, 53.5118, 53.6086, 53.7213, 53.8293, 53.9308, 54.026 ], [58.5754, 56.945, 55.068, 53.7798, 52.9469, 52.854, 53.3136,53.8929, 54.1205, 54.1178, 54.0128, 53.9289, 53.8906, 53.8239,53.717, 53.5724, 53.3818, 53.1892, 53.009, 49.3078, 49.2524,49.2165, 49.2032, 49.2187, 50.463, 51.9497, 52.4487, 52.7041,52.8358, 52.9776, 53.1101, 53.2293, 53.3419, 53.4487, 53.5401,53.6365, 53.7301, 53.8205, 53.9062, 53.9869 ], [57.623, 56.547, 55.0117, 54.0512, 53.5372, 53.5246, 53.927,54.3868, 54.5828, 54.5811, 54.4501, 54.3235, 54.2626, 54.2334,54.1802, 54.1137, 53.9897, 53.8202, 49.796, 49.6864, 49.5946,49.5216, 49.4703, 49.4432, 51.8479, 52.5574, 52.8359, 52.9722,53.0827, 53.1826, 53.2747, 53.3597, 53.4405, 53.5138, 53.5944,53.6751, 53.7536, 53.829, 53.9019, 53.9721 ], [56.902, 56.0005, 54.9159, 54.3352, 54.123, 54.2014, 54.5659,54.8917, 55.0307, 55.0139, 54.8838, 54.7044, 54.5863, 54.5548,54.5258, 54.4957, 54.4633, 51.4821, 50.1897, 50.0758, 49.9683,49.8704, 49.7842, 51.5064, 52.7625, 53.0724, 53.1926, 53.2682,53.3404, 53.4119, 53.4831, 53.5517, 53.6169, 53.6763, 53.7383,53.8009, 53.8644, 53.9281, 53.9905, 54.0517 ], [56.3455, 55.5524, 54.9336, 54.6836, 54.703, 54.8657, 55.1749,55.3844, 55.4521, 55.4019, 55.2622, 55.0281, 54.8981, 54.6591,54.7866, 54.7678, 54.7654, 54.0436, 54.2302, 52.2533, 50.3305,50.2276, 50.1268, 52.9617, 53.4395, 53.5504, 53.5481, 53.5524,53.5699, 53.6014, 53.644, 53.6931, 53.7445, 53.7996, 53.8548,53.9097, 53.9655, 54.0229, 54.0813, 54.1393 ], [55.7493, 55.3019, 55.1012, 55.0906, 55.234, 55.4751, 55.7134,55.8462, 55.8461, 55.7425, 55.5725, 55.3535, 55.1612, 54.958,55.0193, 54.9584, 54.9531, 54.8886, 54.8256, 54.2211, 50.6477,50.5564, 53.0546, 53.8592, 54.08, 54.0288, 53.9509, 53.8796,53.8307, 53.8073, 53.8034, 53.8142, 53.8383, 53.8725, 53.9128,53.9558, 54.0013, 54.0497, 54.103, 54.1597 ], [55.2575, 55.1664, 55.3165, 55.5004, 55.7345, 55.9901, 56.1852,56.2599, 56.2027, 56.0454, 55.818, 55.5754, 55.302, 55.2083,55.0224, 55.1415, 55.0656, 55.0446, 55.0263, 54.7728, 50.8924,53.4671, 54.2587, 54.5146, 54.6171, 54.519, 54.3857, 54.2497,54.1355, 54.0509, 53.9932, 53.9584, 53.941, 53.939, 53.9527,53.9798, 54.0111, 54.0465, 54.0868, 54.1339 ], [54.8665, 55.1533, 55.5095, 55.8512, 56.1541, 56.3995, 56.5593,56.6009, 56.5079, 56.3001, 56.0178, 55.7187, 55.448, 55.063,55.2016, 55.2116, 55.1817, 55.112, 55.1099, 55.0299, 54.3358,54.6966, 54.9199, 55.0156, 55.0728, 54.975, 54.8299, 54.6609,54.493, 54.3475, 54.2349, 54.1517, 54.0928, 54.0516, 54.0245,54.013, 54.0206, 54.0404, 54.0667, 54.0989 ], [54.2676, 55.1132, 55.6112, 56.09, 56.428, 56.6661, 56.8056,56.8374, 56.7339, 56.4923, 56.1474, 55.7977, 55.4805, 55.2341,54.8999, 55.2662, 55.2927, 55.185, 55.1237, 55.1268, 54.9772,55.1418, 55.2612, 55.3333, 55.379, 55.3244, 55.2153, 55.0629,54.881, 54.6926, 54.523, 54.3866, 54.2855, 54.2118, 54.1583,54.1191, 54.0935, 54.0834, 54.0885, 54.1057 ], [54.1771, 55.0795, 55.7075, 56.1772, 56.5183, 56.7522, 56.8898,56.9315, 56.8427, 56.6056, 56.2317, 55.8095, 55.4436, 55.183,55.0284, 54.9504, 55.2833, 55.2563, 55.1498, 55.1342, 55.1331,55.259, 55.3705, 55.4452, 55.4955, 55.5087, 55.4697, 55.3766,55.2324, 55.049, 54.8485, 54.6578, 54.4995, 54.3822, 54.3002,54.2427, 54.2022, 54.1749, 54.1598, 54.1561 ], [53.9112, 54.85, 55.6641, 56.0844, 56.4062, 56.6232, 56.757,56.8149, 56.7669, 56.5754, 56.2311, 55.785, 55.366, 55.0104,54.812, 54.8845, 55.1273, 55.2339, 55.1976, 55.1049, 55.0913,55.1843, 55.3048, 55.4076, 55.4709, 55.518, 55.5455, 55.5329,55.4636, 55.3349, 55.1595, 54.9529, 54.7462, 54.5681, 54.4342,54.3439, 54.2848, 54.2446, 54.2222, 54.2135 ], [53.9368, 54.9196, 55.4408, 55.7999, 56.0652, 56.2423, 56.348,56.4106, 56.4114, 56.3028, 56.0519, 55.6779, 55.2493, 54.8836,54.6592, 54.6347, 54.8341, 55.0606, 55.1396, 55.0967, 55.0325,55.0501, 55.1451, 55.2627, 55.3559, 55.4216, 55.4789, 55.5183,55.5245, 55.4779, 55.3701, 55.2072, 55.0029, 54.7876, 54.5915,54.4378, 54.3368, 54.2787, 54.2415, 54.2271 ], [53.9325, 54.6506, 55.0421, 55.2926, 55.4603, 55.5679, 55.6285,55.6792, 55.7234, 55.731, 55.639, 55.3923, 55.043, 54.6845,54.4188, 54.3242, 54.4606, 54.7449, 54.9548, 55.0171, 55.0047,54.9454, 54.9666, 55.0651, 55.1828, 55.2677, 55.3308, 55.3914,55.438, 55.4544, 55.4277, 55.3385, 55.1907, 54.9981, 54.7786,54.5691, 54.4013, 54.2898, 54.233, 54.1994 ] ]) fig = plt.figure() ax = fig.add_subplot(111,projection='3d') X,Y = np.meshgrid(np.arange(-30.0,-20.0,0.25), np.arange(20.0,25,0.25)) ax.contourf(X,Y,data,zdir='z',offset=0, levels=np.arange(0,75,1)) ax.set_zlim(0.0,2.0) plt.savefig('testfig.png') plt.close() This code will produce the plot: In all of the cases I have observed this mis-contouring the bad triangle always has a vertex near the bottom left of the domain. My data is regularly gridded and for the domain in question is uniform in X and Y. In this case the mis-filling will go away if the number of contour levels is reduced. In some other cases this does not always help or just changes the visual appearance of the error. In any case, even at very coarse contouring I still get errors in a subset of my plots. Has anyone seen this before and found a fix for it? Am I overlooking something? I'm open to workarounds that don't involve lowering my contouring level (which does reduce the errors overall). If others are in agreement that this could be a bug in the mplot3d, I will file a bug report with them (Issue opened here). I have a feeling the problem lies with contouring very strong gradients when the levels option causes dense contours, but oddly only on 3d axes. Relevant version information: Python 3.4.1 matplotlib 1.4.3 numpy 1.9.0
This turned out to be a longstanding bug in matplotlib.mplot3d that ignores path information when taking 2D contourf sets and extending them into 3D. This causes, under certain circumstances, paths with holes to render improperly when a path segment intended as a "move" is instead "drawn". I contributed a fix for this issue to matplotlib and this bug is fixed in the matplotlib 1.5.0 stable release. The same test code as in the question produces a correct plot with matplotlib 1.5, as seen below:
The problem is most probably in matplotlib itself and you're not doing anything wrong. By experimenting a bit I found that if you multiply the input data by 1.01 or 0.999 the plot comes out right, but 1.001 or 0.9999 is not enough to fix the issue. Adding or subtracting a constant instead shifts the color but keeps the problem evident. As a wild guess some internal computation falls in a singularity (even if I cannot think what formula would be in danger in this case). You should submit a bug to their tracker. EDIT On a second thought may be matplotlib is trying to compute contour polygons instead of just computing a background texture on a texel-by-texel basis and this could result in annoying accuracy problems that depend on the value. Drawing contour lines is instead much easier because you can just compute the segments in a marching-square approach without worrying about rebuilding the full contour line topology (and for example if a very tiny segment is missing from the line contour plot you're not going to notice anyway). If this is indeed the bug then may be the fix is not easy because requires a full reimplementation of the plane drawing in a completely different (even if easier) way.