6

There is an array containing 3D data of shape e.g. (64,64,64), how do you plot a plane given by a point and a normal (similar to hkl planes in crystallography), through this dataset? Similar to what can be done in MayaVi by rotating a plane through the data.

The resulting plot will contain non-square planes in most cases. Can those be done with matplotlib (some sort of non-rectangular patch)?

Edit: I almost solved this myself (see below) but still wonder how non-rectangular patches can be plotted in matplotlib...?

Edit: Due to discussions below I restated the question.

BandGap
  • 1,745
  • 4
  • 19
  • 26
  • You should go with a general solution that does interpolation through an arbitrary plane, as Thorsten suggested. When an interpolated value falls outside your cube of data, it will be assigned a `nan`, because it would have to be extrapolated, not interpolated. This will make the area outside the actual cross section of your cube to look different when you plot it, without having to worry about determining the exact boundaries. – Jaime Jan 03 '13 at 18:52
  • Interesting point about the `nan`. This would't tell matplotlib about the borders of the patch though. This is important to me because if you want to publish something like that it's nicer to have a plot with correct boundaries. – BandGap Jan 03 '13 at 20:13

5 Answers5

3

I have the penultimate solution for this problem. Partially solved by using the second answer to Plot a plane based on a normal vector and a point in Matlab or matplotlib :

# coding: utf-8
import numpy as np
from matplotlib.pyplot import imshow,show

A=np.empty((64,64,64)) #This is the data array
def f(x,y):
    return np.sin(x/(2*np.pi))+np.cos(y/(2*np.pi))
xx,yy= np.meshgrid(range(64), range(64))
for x in range(64):
    A[:,:,x]=f(xx,yy)*np.cos(x/np.pi)

N=np.zeros((64,64)) 
"""This is the plane we cut from A. 
It should be larger than 64, due to diagonal planes being larger. 
Will be fixed."""

normal=np.array([-1,-1,1]) #Define cut plane here. Normal vector components restricted to integers
point=np.array([0,0,0])
d = -np.sum(point*normal)

def plane(x,y): # Get plane's z values
    return (-normal[0]*x-normal[1]*y-d)/normal[2]

def getZZ(x,y): #Get z for all values x,y. If z>64 it's out of range
    for i in x:
        for j in y:
            if plane(i,j)<64:
                N[i,j]=A[i,j,plane(i,j)]

getZZ(range(64),range(64))
imshow(N, interpolation="Nearest")
show()

It's not the ultimate solution since the plot is not restricted to points having a z value, planes larger than 64 * 64 are not accounted for and the planes have to be defined at (0,0,0).

Community
  • 1
  • 1
BandGap
  • 1,745
  • 4
  • 19
  • 26
  • 1
    There are lots of suboptimal things in your code, maybe even some plain errors. The way to go is what Thorsten pointed out, if he doesn't get around to completing it during the day I will give it a shot. – Jaime Jan 03 '13 at 15:46
  • @Jaime: Could you be more specific on what is a 'plain error' in my code? Also see my last comment to Thorsten's answer concerning the interpolation. – BandGap Jan 03 '13 at 15:49
  • Seems like you are really just interested in the limited solution. If so, things become easier, sure. – Thorsten Kranz Jan 03 '13 at 16:15
  • Indeed. See comment above. – BandGap Jan 03 '13 at 16:19
  • The "plain error" is in the line "from matplotlib.pyploy import imshow,show" – Thorsten Kranz Jan 03 '13 at 16:31
  • I would call this a typo. Especially since we all know how it's should spell. – BandGap Jan 03 '13 at 20:15
2

This is funny, a similar question I replied to just today. The way to go is: interpolation. You can use griddata from scipy.interpolate:

Griddata

This page features a very nice example, and the signature of the function is really close to your data.

You still have to somehow define the points on you plane for which you want to interpolate the data. I will have a look at this, my linear algebra lessons where a couple of years ago

Thorsten Kranz
  • 12,492
  • 2
  • 39
  • 56
  • 1
    I don't think this is how MayaVi does it. As far as I understand the functioin you linked to, you would extract those points from my dataset which lie roughly on a specified plane and interpolate between them? – BandGap Jan 03 '13 at 13:28
  • No, I would interpolate in three dimensions. You give the information you have about your scalar field and the 3d-points where you want to interpolate. – Thorsten Kranz Jan 03 '13 at 13:39
  • I don't think interpolation is necessary here at all. You could use interpolationt if you needed more datapoints than already supplied by the dataset. In the other question you answered you are correct but here the interpolation will be done by imshow, if you don't use 'Nearest' ... – BandGap Jan 03 '13 at 15:47
  • You definitely will need to interpolate. You have limited data (e.g. 64**3) but unlimited possible orientations of your plane. As long as you keep the "normal" parallel to one of the dimensions and move the "point" only in 64 steps, you're fine without interpolation. But as soon as these become arbitrary, you most likely won't hit any of your data points with your plane. So how can you determine the values on the plane? Interpolation. – Thorsten Kranz Jan 03 '13 at 16:03
  • Ok I understand what you mean. I didn't think of arbitrary planes when I stated the question but more of hkl planes like they are used in crystallography... Not that arbitrary in the end :) Sorry for not making that clear enough. – BandGap Jan 03 '13 at 16:17
1

For the reduced requirements, I prepared a simple example

import numpy as np
import pylab as plt

data = np.arange((64**3))
data.resize((64,64,64))

def get_slice(volume, orientation, index):
    orientation2slicefunc = {
        "x" : lambda ar:ar[index,:,:], 
        "y" : lambda ar:ar[:,index,:],  
        "z" : lambda ar:ar[:,:,index]
    }
    return orientation2slicefunc[orientation](volume)

plt.subplot(221)
plt.imshow(get_slice(data, "x", 10), vmin=0, vmax=64**3)

plt.subplot(222)
plt.imshow(get_slice(data, "x", 39), vmin=0, vmax=64**3)

plt.subplot(223)
plt.imshow(get_slice(data, "y", 15), vmin=0, vmax=64**3)
plt.subplot(224)
plt.imshow(get_slice(data, "z", 25), vmin=0, vmax=64**3)  

plt.show()  

This leads to the following plot:

Four example slices

The main trick is dictionary mapping orienations to lambda-methods, which saves us from writing annoying if-then-else-blocks. Of course you can decide to give different names, e.g., numbers, for the orientations.

Maybe this helps you.

Thorsten

P.S.: I didn't care about "IndexOutOfRange", for me it's o.k. to let this exception pop out since it is perfectly understandable in this context.

Thorsten Kranz
  • 12,492
  • 2
  • 39
  • 56
  • +1 For the nice sleek code, but that is less than the OP wants, see http://en.wikipedia.org/wiki/Miller_index and http://en.wikipedia.org/wiki/Reciprocal_lattice#Simple_cubic_lattice, so I think he is after planes with normal vectors of the form `[i/64, j/64, k/64]` where `i`, `j` and `k` are all integers, and going through a point `[x, y, z]` where all values are integers in `[0, 64)`. – Jaime Jan 03 '13 at 18:58
  • -1 This is not even remotely what was asked. And why would I write a function as you did in the first place? plt.imshow(data[index,:,:] ... ) would do the exact same thing without introducing illegible code. – BandGap Jan 03 '13 at 20:07
  • The reason for my function is: orientation is meant as a minimal "normal-vector" - which is only o.k. when the normals have to be parallel to one of the edges of the lattice. Now I understood that this is not what you want. I used lambdas as your suggestion (data[index,:,:]) only works if you now which axis is to be indexed - but we do not know in my example, it depends on the "orientation" – Thorsten Kranz Jan 03 '13 at 21:19
  • But why wouldn't we know which axis we want? Why would we rather know it's x,y or z? – BandGap Jan 03 '13 at 22:11
  • When we execute the funtion, we know: n=[0,0,1] -> "z"; n=[1,0,0] -> "x" But when we define the function, we don't. – Thorsten Kranz Jan 03 '13 at 23:16
1

I had to do something similar for a MRI data enhancement:

Probably the code can be optimized but it works as it is.
My data is 3 dimension numpy array representing an MRI scanner. It has size [128,128,128] but the code can be modified to accept any dimensions. Also when the plane is outside the cube boundary you have to give the default values to the variable fill in the main function, in my case I choose: data_cube[0:5,0:5,0:5].mean()

def create_normal_vector(x, y,z):

    normal = np.asarray([x,y,z])
    normal = normal/np.sqrt(sum(normal**2))
    return normal



def get_plane_equation_parameters(normal,point):
    a,b,c = normal
    d = np.dot(normal,point)
    return a,b,c,d        #ax+by+cz=d  

def get_point_plane_proximity(plane,point):
    #just aproximation
    return np.dot(plane[0:-1],point) - plane[-1]

def get_corner_interesections(plane, cube_dim = 128): #to reduce the search space
    #dimension is 128,128,128
    corners_list = []
    only_x = np.zeros(4)
    min_prox_x = 9999
    min_prox_y = 9999
    min_prox_z = 9999
    min_prox_yz = 9999
    for i in range(cube_dim):
        temp_min_prox_x=abs(get_point_plane_proximity(plane,np.asarray([i,0,0])))
       # print("pseudo distance x: {0}, point: [{1},0,0]".format(temp_min_prox_x,i))
        if temp_min_prox_x <  min_prox_x:
            min_prox_x = temp_min_prox_x
            corner_intersection_x = np.asarray([i,0,0])
            only_x[0]= i

        temp_min_prox_y=abs(get_point_plane_proximity(plane,np.asarray([i,cube_dim,0])))
       # print("pseudo distance y: {0}, point: [{1},{2},0]".format(temp_min_prox_y,i,cube_dim))

        if temp_min_prox_y <  min_prox_y:
            min_prox_y = temp_min_prox_y
            corner_intersection_y = np.asarray([i,cube_dim,0]) 
            only_x[1]= i

        temp_min_prox_z=abs(get_point_plane_proximity(plane,np.asarray([i,0,cube_dim])))
        #print("pseudo distance z: {0}, point: [{1},0,{2}]".format(temp_min_prox_z,i,cube_dim))

        if temp_min_prox_z <  min_prox_z:
            min_prox_z = temp_min_prox_z
            corner_intersection_z = np.asarray([i,0,cube_dim])
            only_x[2]= i

        temp_min_prox_yz=abs(get_point_plane_proximity(plane,np.asarray([i,cube_dim,cube_dim])))
        #print("pseudo distance z: {0}, point: [{1},{2},{2}]".format(temp_min_prox_yz,i,cube_dim))

        if temp_min_prox_yz <  min_prox_yz:
            min_prox_yz = temp_min_prox_yz
            corner_intersection_yz = np.asarray([i,cube_dim,cube_dim])
            only_x[3]= i

    corners_list.append(corner_intersection_x)      
    corners_list.append(corner_intersection_y)            
    corners_list.append(corner_intersection_z)            
    corners_list.append(corner_intersection_yz)
    corners_list.append(only_x.min()) 
    corners_list.append(only_x.max())           

    return corners_list       

def get_points_intersection(plane,min_x,max_x,data_cube,shape=128):

    fill = data_cube[0:5,0:5,0:5].mean() #this can be a parameter
    extended_data_cube = np.ones([shape+2,shape,shape])*fill
    extended_data_cube[1:shape+1,:,:] = data_cube 
    diag_image = np.zeros([shape,shape])
    min_x_value = 999999

    for i in range(shape):

        for j in range(shape):

            for k in range(int(min_x),int(max_x)+1):


                current_value = abs(get_point_plane_proximity(plane,np.asarray([k,i,j])))
                #print("current_value:{0}, val: [{1},{2},{3}]".format(current_value,k,i,j))
                if current_value < min_x_value:
                    diag_image[i,j] = extended_data_cube[k,i,j]
                    min_x_value = current_value

            min_x_value = 999999

    return diag_image   

The way it works is the following:

you create a normal vector: for example [5,0,3]

normal1=create_normal_vector(5, 0,3) #this is only to normalize

then you create a point: (my cube data shape is [128,128,128])

point = [64,64,64]

You calculate the plane equation parameters, [a,b,c,d] where ax+by+cz=d

plane1=get_plane_equation_parameters(normal1,point)

then to reduce the search space you can calculate the intersection of the plane with the cube:

corners1 = get_corner_interesections(plane1,128)

where corners1 = [intersection [x,0,0],intersection [x,128,0],intersection [x,0,128],intersection [x,128,128], min intersection [x,y,z], max intersection [x,y,z]]

With all these you can calculate the intersection between the cube and the plane:

image1 = get_points_intersection(plane1,corners1[-2],corners1[-1],data_cube)

Some examples:

normal is [1,0,0] point is [64,64,64]

enter image description here

normal is [5,1,0],[5,1,1],[5,0,1] point is [64,64,64]:

enter image description here

normal is [5,3,0],[5,3,3],[5,0,3] point is [64,64,64]:

enter image description here

normal is [5,-5,0],[5,-5,-5],[5,0,-5] point is [64,64,64]:

enter image description here

Thank you.

Diego Orellana
  • 994
  • 1
  • 9
  • 20
1

The other answers here do not appear to be very efficient with explicit loops over pixels or using scipy.interpolate.griddata, which is designed for unstructured input data. Here is an efficient (vectorized) and generic solution.

There is a pure numpy implementation (for nearest-neighbor "interpolation") and one for linear interpolation, which delegates the interpolation to scipy.ndimage.map_coordinates. (The latter function probably didn't exist in 2013, when this question was asked.)

import numpy as np
from scipy.ndimage import map_coordinates
     
def slice_datacube(cube, center, eXY, mXY, fill=np.nan, interp=True):
    """Get a 2D slice from a 3-D array.
    
    Copyright: Han-Kwang Nienhuys, 2020.
    License: any of CC-BY-SA, CC-BY, BSD, GPL, LGPL
    Reference: https://stackoverflow.com/a/62733930/6228891
    
    Parameters:
    
    - cube: 3D array, assumed shape (nx, ny, nz).
    - center: shape (3,) with coordinates of center.
      can be float. 
    - eXY: unit vectors, shape (2, 3) - for X and Y axes of the slice.
      (unit vectors must be orthogonal; normalization is optional).
    - mXY: size tuple of output array (mX, mY) - int.
    - fill: value to use for out-of-range points.
    - interp: whether to interpolate (rather than using 'nearest')
    
    Return:
        
    - slice: array, shape (mX, mY).
    """
    
    center = np.array(center, dtype=float)
    assert center.shape == (3,)
    
    eXY = np.array(eXY)/np.linalg.norm(eXY, axis=1)[:, np.newaxis]
    if not np.isclose(eXY[0] @ eXY[1], 0, atol=1e-6):
        raise ValueError(f'eX and eY not orthogonal.')

    # R: rotation matrix: data_coords = center + R @ slice_coords
    eZ = np.cross(eXY[0], eXY[1])
    R = np.array([eXY[0], eXY[1], eZ], dtype=np.float32).T
    
    # setup slice points P with coordinates (X, Y, 0)
    mX, mY = int(mXY[0]), int(mXY[1])    
    Xs = np.arange(0.5-mX/2, 0.5+mX/2)
    Ys = np.arange(0.5-mY/2, 0.5+mY/2)
    PP = np.zeros((3, mX, mY), dtype=np.float32)
    PP[0, :, :] = Xs.reshape(mX, 1)
    PP[1, :, :] = Ys.reshape(1, mY)
        
    # Transform to data coordinates (x, y, z) - idx.shape == (3, mX, mY)
    if interp:
        idx = np.einsum('il,ljk->ijk', R, PP) + center.reshape(3, 1, 1)
        slice = map_coordinates(cube, idx, order=1, mode='constant', cval=fill)
    else:
        idx = np.einsum('il,ljk->ijk', R, PP) + (0.5 + center.reshape(3, 1, 1))
        idx = idx.astype(np.int16)
        # Find out which coordinates are out of range - shape (mX, mY)
        badpoints = np.any([
            idx[0, :, :] < 0,
            idx[0, :, :] >= cube.shape[0], 
            idx[1, :, :] < 0,
            idx[1, :, :] >= cube.shape[1], 
            idx[2, :, :] < 0,
            idx[2, :, :] >= cube.shape[2], 
            ], axis=0)
        
        idx[:, badpoints] = 0
        slice = cube[idx[0], idx[1], idx[2]]
        slice[badpoints] = fill
        
    return slice
    
# Demonstration
nx, ny, nz = 50, 70, 100
cube = np.full((nx, ny, nz), np.float32(1))

cube[nx//4:nx*3//4, :, :] += 1
cube[:, ny//2:ny*3//4, :] += 3
cube[:, :, nz//4:nz//2] += 7
cube[nx//3-2:nx//3+2, ny//2-2:ny//2+2, :] = 0 # black dot
     
Rz, Rx = np.pi/6, np.pi/4 # rotation angles around z and x
cz, sz = np.cos(Rz), np.sin(Rz)
cx, sx = np.cos(Rx), np.sin(Rx)

Rmz = np.array([[cz, -sz, 0], [sz, cz, 0], [0, 0, 1]])
Rmx = np.array([[1, 0, 0], [0, cx, -sx], [0, sx, cx]])
eXY = (Rmx @ Rmz).T[:2]
  
slice = slice_datacube(
    cube, 
    center=[nx/3, ny/2, nz*0.7], 
    eXY=eXY,
    mXY=[80, 90],
    fill=np.nan,
    interp=False
    )

import matplotlib.pyplot as plt
plt.close('all')
plt.imshow(slice.T) # imshow expects shape (mY, mX)
plt.colorbar()

Output (for interp=False):

Test case: 2D slice of 3D dataset

For this test case (50x70x100 datacube, 80x90 slice size) the run time is 376 µs (interp=False) and 550 µs (interp=True) on my laptop.

Han-Kwang Nienhuys
  • 3,084
  • 2
  • 12
  • 31