How to get correct pixel size in 2D after cut 3D voxel using obliqueslice

17 views (last 30 days)
Hi,
I have a 3D voxel created from a CT scanner. I cut it with obliqueslice to get the surface that I would like to calculate. As the cut was not orthogonal cut I guess my pixel size in space changed. There is a way to correct it? I think I should calculate the angle between each axis and apply it to each pixels (x, y,z)? Is it correct way to do it? Thank you for any respons.

Accepted Answer

Rik
Rik on 18 Dec 2024
Edited: Rik on 18 Dec 2024
If you use the second output format:
this function already returns the coordinates for each pixel in your oblique slice.
To compute the pixel size, you can simply take the difference and multiply that by the native voxel size. Pay attention to the difference between slice increment and slice thickness.
load mri,V = squeeze(D);
point = [73 50 15.5];
normal = [0 15 20];
[B,x,y,z] = obliqueslice(V,point,normal);
imshow(B)
corners=[...
x(1,1) x(2,2);
y(1,1) y(2,2);
z(1,1) z(2,2)];
disp(diff(corners,[],2))
1.0000 0.8000 -0.6000
For this example we don't know the native voxel size, but this is the distance traversed going from one corner to the other of a single pixel in the slice.
Just like every other parallelogram you can calculate the area with the cross product of two side vectors:
A = [x(2,1)-x(1,1), y(2,1)-y(1,1), z(2,1)-z(1,1)];
B = [x(1,2)-x(1,1), y(1,2)-y(1,1), z(1,2)-z(1,1)];
disp(A),disp(B)
0 0.8000 -0.6000 1 0 0
% Here you need to multiply by the size of the voxels before calling cross().
area = double(norm(cross(A,B)))
area = 1.0000
  7 Comments
William Rose
William Rose on 15 Jan 2025
Please explain what you mean by applying obliqueslice() to a 3D surface. Tell us the big picture: what are you really trying do? Are you trying to find the outline of an organ, on an oblique slice through an MRI data set?
Any slice (oblique or not), through a 3D surface will yield a curve, or a set of curves, or the empty set. You can't use obliqueslice() to slice a surface, because obliqueslice() operates on a 3D array, and a surface is represented by a 2D array (the Z values at a set of grid points), or by a set of vertices and faces (see isosurface, for example).
Kamil
Kamil on 16 Jan 2025
Edited: Kamil on 16 Jan 2025
Indeed, I was not precised. As I mentioned I use regionprops3 for 3D and regionprops for 2D to compute some data such as surface, perimeter, volume etc. As, these function does not enable me to put pixel dimensions it does not work correctly for uneven dimensions (x, y, z). I do not have any problem to calculate volume but still I cannot have correct values for surface. So far only solution I have is to interpolate volume to have even dimensions. I found as well one solution based on the isosurface calculation. Below I present you what I have so far.
format default
% dimensions
side = 100; % # of pixels of eqch side of the cuboid
pixel_size_x = 0.1; % pixel resolution in x
pixel_size_y = 0.2; % pixel resolution in y
pixel_size_z = 0.3; % pixel resolution in z
VoxelDimensions = [pixel_size_x pixel_size_y pixel_size_z];
% Sphere
r = 10; % Radius
W = r; % Volume edge half-length
N = 100; % Number of voxels along edge
% 3D meshgrid to get radii
[x3, y3, z3] = meshgrid(linspace(-W, W, N), linspace(-W, W, N), linspace(-W, W, N));
% Set dimensions
dsx = pixel_size_x; % mm
dsy = pixel_size_y; % mm
dsz = pixel_size_z; % mm
xlim = 10;
ylim = 10;
zlim = 10;
[x3, y3, z3] = meshgrid(-xlim:dsx:xlim,-ylim:dsy:ylim,-zlim:dsz:zlim);
V = sqrt(x3.^2 + y3.^2 + z3.^2);
rr = 5; % radius
% Set center axis to 1
V(V == 0) = 1;
% Anything outside of cylinder radius (10 cm) set to 0
V(abs(V) >= rr) = 0;
% Convert all non-zeros to 1 (all pts on and inside cylinder)
V(abs(V) > 0) = 1;
% visualise if needed
figure(1)
isosurface(V)
hold on
grid on
xlabel('x-axis')
ylabel('y-axis');
zlabel('z-axis');
% Sphere
Exact_surface_3D = 4*pi*(rr)^2
Exact_volume_3D = 4/3*pi*(rr)^3
% volume and surface in 3D regionprops3
stats = regionprops3(V,'SurfaceArea','Volume');
surface_regionprops3 = stats.SurfaceArea
volume_regionprops3 = stats.Volume*prod(VoxelDimensions)
% another possibility using isosurface
[F1,V1] = isosurface(V,0);
% Surface Area Calculation
SA = 0;
for i = 1:size(F1, 1)
v1 = V1(F1(i, 1), :);
v2 = V1(F1(i, 2), :);
v3 = V1(F1(i, 3), :);
edge1 = v2 - v1;
edge2 = v3 - v1;
area = 0.5 * norm(cross(edge1.*VoxelDimensions, edge2.*VoxelDimensions));
SA = SA + area;
end
Surface_case_2 = SA
Volume_3D = sum(V,"all")*prod(VoxelDimensions)
surface_proc_regionprops3 = (surface_regionprops3-Exact_surface_3D)/Exact_surface_3D*100
volume_proc_regionprops3 = (volume_regionprops3-Exact_volume_3D)/Exact_volume_3D*100
Surface_proc_case_2 = (Surface_case_2-Exact_surface_3D)/Exact_surface_3D*100

Sign in to comment.

More Answers (3)

Matt J
Matt J on 18 Dec 2024
Edited: Matt J on 19 Dec 2024
IMHO obliqueslice is a bit of a crude tool for slice sampling. One of the many things you can do with this FEX package,
is set up a planar grid XYZ of sample points at which to interpolate the image values. However, unlike obliqueslice, this gives you direct control not over the spacing between the sample points (i.e. the pixel size in your slice image), but also the orientation of the planar grid relative to the volume.
load mri,V = squeeze(single(D));
V=squeeze(single(D));
V=imresize3( V , round(size(V).*[1,1,2.5]) ); %make isotropic
[nx,ny,nz]=size(V);
F=griddedInterpolant(V,'linear','none');
origin=[nx,ny,nz]/2-0.5; %Origin
basis1=[0,0,nz]; %Plane basis vector 1
basis2=[nx,ny,0]; %Plane basis vector 2
pixelLength=0.8;
t1=(0:ceil(norm(basis1))/pixelLength)/norm(basis1); t1=t1-max(t1)/2; %step size 1
t2=(0:ceil(norm(basis2))/pixelLength)/norm(basis2); t2=t2-max(t2)/2; %step size 2
XYZ=planarFit.xyzsim(origin,basis1,basis2,t1,t2)';%Oblique sample points
Vs = F( XYZ ); %Oblique image values
Vs=reshape(Vs,numel(t1),numel(t2));
imshow(Vs,[0,max(Vs(:))]); axis xy

Catalytic
Catalytic on 19 Dec 2024
Edited: Catalytic on 19 Dec 2024
The pixels sizes in the slice images are always 1, as you can see below. The dimensions of the slice are a harder thing to predict as you also see below. The intersection of the slice plane with the image cube forms a polygon-shaped region. To form the final image, obliqueslice must pixelize either a sub-rectangle inside the polygon or a super-rectangle enclosing the polygon. I don't think there's any easy way to determine how it is done, or to predict the final image dimensions even if we could.
for i=1:5
normal=normalize(rand(1,3),'norm'); %random slice normal
[Slice,x,y,z] = obliqueslice(rand(300,300,300),[151,151,151],normal);
sliceDimensions = size(Slice)
pixelHeight=norm([x(2,1)-x(1,1), y(2,1)-y(1,1), z(2,1)-z(1,1)]) ,
pixelWidth =norm([x(1,2)-x(1,1), y(1,2)-y(1,1), z(1,2)-z(1,1)]) ,
disp ' '
end
sliceDimensions = 1×2
323 390
<mw-icon class=""></mw-icon>
<mw-icon class=""></mw-icon>
pixelHeight = 1.0000
pixelWidth = 1.0000
sliceDimensions = 1×2
369 341
<mw-icon class=""></mw-icon>
<mw-icon class=""></mw-icon>
pixelHeight = 1.0000
pixelWidth = 1.0000
sliceDimensions = 1×2
341 412
<mw-icon class=""></mw-icon>
<mw-icon class=""></mw-icon>
pixelHeight = 1.0000
pixelWidth = 1.0000
sliceDimensions = 1×2
426 442
<mw-icon class=""></mw-icon>
<mw-icon class=""></mw-icon>
pixelHeight = 1.0000
pixelWidth = 1.0000
sliceDimensions = 1×2
404 379
<mw-icon class=""></mw-icon>
<mw-icon class=""></mw-icon>
pixelHeight = 1.0000
pixelWidth = 1.0000

William Rose
William Rose on 21 Dec 2024
Edited: William Rose on 10 Jan 2025
[edit: fix spelling errors (twice), no changes to code.]
[Edit: Replace color images with new images that don't have an error in the right hand panel.]
@Catalytic is right that the pixel size in the new, oblique slice is 1, when measured in the units of the pixels in the original image (i.e. assuming each of the original pizels has size 1x1x1).
obliqueslice(V,point,normal)
It helps me to think of an oblique slice as a setting up a new reference frame that is oriented obliquely relative to the original frame. The original frame has axes Xo,Yo,Zo. The new frame's axes are Xn,Yn,Zn. Zn points along vector "normal". The new image is in the Xn,Yn plane. Let Zn=0 at point "point". Then all the points in the oblique slice have Zn=0. Now create a grid of points with spacing 1x1 in the Xn,Zn plane. Those are the pixel centers in the new image. We can transform those back to their location in the original volume reference frame. The pixel centers in the oblique slice will not exactly coincide with voxel centers in the original slice, so at each pixel center, obliqueslice() takes a weighted average of the image intensities at the nearest voxel centers of the original image.
Since the pixels in the oblique slice have size 1x1, and the plane is tilted relative to the original volume, the oblique plane is likely to have more pixels that the original volume, at least if it passes through the center of the volume. And this is in fact what you will observe.
One thing that is interesting about the change of coordinates way of thinking about obliqueslice() is that the direction of Xn and Yn is not specified by vector "normal". So obliqueslice() decides how to orient Xn and Yn. Sometimes I like obliqueslice()'s choice for Xn and Yn, and sometimes I do not.
I have attached a script, image3dslice.m, to illustrate some aspects of how obliqueslice() works.
image3dslice.m creates a mostly black volume with array dimensions 81x71x91, i.e 81 pixels along axis Yo, 71 along Xo, and 91 along Zo. There is a white plane at every 10th pixel, i.e. at 1, 11, 21,..., in all 3 directions.
The first part of image3dslice.m takes slices parallel to the original axes, and plots them. When normal=[0,0,1], i.e. normal points along the Zo axis, then the slice is in the X-Y plane. Point "point" is chosen to be in a black area, so the resulting slice is mostly black, with size 81 rows x 71 columns, and a grid of white lines up/down and across, at every 10th pixel. See left panel in figure below. I call imshow() with the "xy" option, which puts the X,Y origin at the bottom left. When normal=[0,1,0], i.e. normal points along Yo, then the slice is parallel to the X-Z plane, and the image slice has size 91 rows by 71 columns. It is mostly black, with white lines at every 10th pixel. See center panel in figure below. When normal =[1,0,0], the normal points along Xo, and the image slice is in the Y-Z plane, and it has the expected dimensions - see right panel in image below.
The second part of image3dslice.m take slices at oblique angles. For simplicity, the normal vector in the three examples below is rotated about just one of the orignal axes. This means the normal vector has one of its coordinates=0 in each of the three examples shown. The normal vector is rotated by 18.4 degrees. I chose this angle because the tangent of 18.4 degrees is 1/3, so the tilted plane goes 1 original voxel up for every 3 voxels over, and we will see this pattern in the results.
Note that the tilted planes have more pixels, in some directions, than the original volume dimensions. The left panel below shows the slice when normal=[ 0,.316,.949]. This normal vector is close to the Zo-axis, but it is tilted 18.4 degress toward the Yo-axis. The plane normal to "normal" has x-values identical to the original x values (because the x-component of normal=0), and the y-values are close to, but not identical to, the original y values. That is why I labeled the axes "X" and "Y*". There are 84 rows in the slice, compared to 81 rows in the orthogonal slice on the left in the previous figure. The extra pixels in the oblique slice reflect the fact that the tilted plane inside the volume is longer than the volume is tall.
The right hand panel in the image blow shows a case where obliqueslice() selected Xn and Yn axes in what I think is an unhelpful way. Since normal=[.316,.949,0], one could have chosen to let Yn (columns of the oblique slice) point in direction [0,0,1] (in the originial reference frame), and let Xn (rows of the slice) point in direction [.949,-.316,0]. Then the rows of the slice image would correspond to Zo in the volume, and the right hand slice below would look nice and rectangular, instead of being tilited.
There is a lot more one could say about the image slices above, including to evidence for 3 to 1 ratios, etc.
Matlb's obliqueslice() does not work with color volume images. Matlab's sliceViewer does display color volume images, but it does not allow oblique slicing. If you make a volume image like the one I made above, but with red, green, and blue planes in the different directions, instead of all white planes, then the oblique slices would be more revealing. With a small effort, you could write a function to apply obliqueslice() to the red, green, and blue planes of an image, then combine the slices to make a color oblique slice. So I did.
  1 Comment
Kamil
Kamil on 6 Jan 2025
Thank you all for your help and time. Big thanks to @William Rose for very detailled explanation. I will keep it in my mind. Sorry for late answer. Before I used the regionprops function which gives me area, perimeter and diameter of the object. This I wanted to calculate. Here below there is a script and the goal I wanted to achived. If you have any more suggestion, do not hesitate to add.
% dimensions
side = 100; % # of pixels of eqch side of the cuboid
pixel_size_x = 0.3; % pixel resolution in x
pixel_size_y = 0.5; % pixel resolution in y
pixel_size_z = 0.7; % pixel resolution in z
VoxelDimensions = [pixel_size_x pixel_size_y pixel_size_z];
% create cuboid (100x100x100 pixels)
V = ones(side,side,side);
% adding pad array
V = padarray(V,[10 10 10],'both');
% Case 1 simple cut plane parallel to the y axis
point = [60 60 60]; % point in the middle
normal = [1 0 0]; % normal vector to the point
% create cut plane
[BW,x,y,z] = obliqueslice(V,point,normal,'Method',"nearest");
A = [x(2,1)-x(1,1), y(2,1)-y(1,1), z(2,1)-z(1,1)];
B = [x(1,2)-x(1,1), y(1,2)-y(1,1), z(1,2)-z(1,1)];
pixels_area = double(norm(cross(A.*VoxelDimensions,B.*VoxelDimensions)));
Area_case1 = sum(BW,"all")*pixels_area
% plot to see it if it is correct
figure(1)
isosurface(V)
hold on
xlabel('x-axis')
ylabel('y-axis');
zlabel('z-axis');
alpha(0.25)
surf(x,y,z,BW,'EdgeColor','None','HandleVisibility','off');
% Case 2 adding some angle
point = [60 60 60]; % point in the middle
normal = [1 0 1]; % normal vector to the point
% create cut plane
[BW,x,y,z] = obliqueslice(V,point,normal,'Method',"nearest");
%plot to see it if it is correct
figure(2)
isosurface(V)
hold on
alpha(0.25)
xlabel('x-axis')
ylabel('y-axis');
zlabel('z-axis');
surf(x,y,z,BW,'EdgeColor','None','HandleVisibility','off');
A = [x(2,1)-x(1,1), y(2,1)-y(1,1), z(2,1)-z(1,1)];
B = [x(1,2)-x(1,1), y(1,2)-y(1,1), z(1,2)-z(1,1)];
pixels_area = double(norm(cross(A.*VoxelDimensions,B.*VoxelDimensions)));
Area_case2 = sum(BW,"all")*pixels_area

Sign in to comment.

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!