Efficiently calculating the pixels crossed by a ray bundle
5 views (last 30 days)
Show older comments
Dominik Rhiem
on 24 Oct 2023
Commented: Dominik Rhiem
on 25 Oct 2023
I have created a script that calculates which pixels on a regular square grid are crossed by which ray(s) of a bundle. Up to the line "B(idx_lin) = 1;", the code is quite efficient, I would say. It's calculated on the GPU for the simple reason that there are easily thousands of rays, and these calculations are sped up quite a lot on the GPU.
Recently, however, I have tried to go from square pixels to circles (with the circle diameter being the pixel edge length and the circles lying in the pixels' centers), and I have unfortunately not found a solution for vectorisation yet, instead solving it with a loop, which is highly inefficient, especially on a GPU. I have tested it and the results are correct, i.e. the solution works, its performance is just bad. The basis for the function min_dist2_ray_pix_GPU can be found here, for the interested reader. The function simply calculates the minimum distance between all rays and all pixels supplied to it.
Without a preselection of pixels, this operation is on the order of n_rays * n_pix_x * n_pix_y. The code shown here performs an operation on the order n_rays * (n_pix_x + n_pix_y), which is far quicker, creating a preselection of pixels which is then supplied to the minimum distance calculation, making that calculation far quicker in theory, if it was vectorised.
In principle, the issue is that each ray now needs a different selection of pixels it passes through, in most cases not even the same number of pixels, which I don't know how to supply to the function all at once. This is quite easily solved in a loop, as shown, but it makes the code slow. So how can this be vectorised? Or is there maybe a wholly different approach yielding the same results which I overlooked?
%all of this is usually supplied to the function, but I explicitly write it
%here so that calculations are possible
x_r = 0;
y_r = -0.3;
x_r = repmat(x_r, 1, 1001);
y_r = repmat(y_r, 1, 1001);
vectors_dummy = [0;1];
vectors = zeros(2,1001);
angles = linspace(-30,30,1001) * pi/180;
vectors = rot_matrix_2d(vectors_dummy,angles);
vectors = squeeze(vectors).';
vox.dx = 0.002;
vox.dy = 0.002;
vox.xstart = -0.2;
vox.xend = 0.2;
vox.ystart = -0.2;
vox.yend = 0.2;
vox.Nx = 201;
vox.Ny = 201;
coordaxis.x = linspace(vox.xstart, vox.xend, vox.Nx);
coordaxis.y = linspace(vox.ystart, vox.yend, vox.Ny);
coordaxis.z = 0;
[array.X, array.Y, array.Z] = ndgrid(coordaxis.x,coordaxis.y,coordaxis.z);
vox_pos = zeros(3, length(array.X(:)));
vox_pos(1,:) = array.X(:);
vox_pos(2,:) = array.Y(:);
N_rays = size(vectors, 1);
v = gpuArray(vectors.');
x_r = gpuArray(x_r);
y_r = gpuArray(y_r);
%define outer grid around pixels
x2 = gpuArray(vox.xstart - vox.dx/2:vox.dx:vox.xend + vox.dx/2);
y2 = gpuArray(vox.ystart - vox.dy/2:vox.dy:vox.yend + vox.dy/2);
% Calculate t-values for the intersection points with all the grid lines
tx = ( x2(:) - x_r ) ./ v(1,:);
ty = ( y2(:) - y_r ) ./ v(2,:);
% Sort the t-values for each ray
t = sort( [tx; ty], 1);
M = size(t,1);
if exist('t_hit_min', 'var')
t(t>t_hit_min.') = NaN;
end
% Calculate the distance between subsequent intersection points
dist_t = diff(t);
% Define mid point between two intersection points
p = reshape([x_r; y_r], 2, 1, N_rays) + ...
reshape(t(1:end-1,:) + 1/2*dist_t, 1, M - 1, N_rays) .* reshape(v, 2, 1, N_rays);
% Define nearest grid points to mid points between two intersection points
idx_x = (round((p(1,:,:) - vox.xstart)/ vox.dx) + 1);
idx_y = (round((p(2,:,:) - vox.ystart)/ vox.dy) + 1);
idx_x(idx_x < 1 | idx_x > vox.Nx) = NaN;
idx_y(idx_y < 1 | idx_y > vox.Ny) = NaN;
%% Calculate matrix
mask = isfinite(idx_x) & isfinite(idx_y);
idx_ray = repmat(reshape(1:N_rays, 1, 1, []), 1, size(idx_x, 2), 1);
idx_lin = sub2ind([vox.Nx, vox.Ny, N_rays], idx_x(mask), idx_y(mask), idx_ray(mask));
%this operation is necessary, as in some cases, the same linear index can
%occur multiple times (which I don't fully understand)
idx_lin = unique(idx_lin);
%boolean is more efficient and also sufficient
B = false(vox.Nx*vox.Ny, N_rays, 'gpuArray');
B(idx_lin) = 1;
for i = 1:N_rays
idx_dummy = (B(:,i) == 1);
d2 = min_dist2_ray_pix_GPU(vox_pos(1:2,B(:,i)), vectors(i,:).', [x_r(i); y_r(i)]);
B(idx_dummy,i) = (d2 < (vox.dx/2)^2);
end
function rot_vec = rot_matrix_2d(vec, theta)
if length(theta)>1
theta = reshape(theta, 1, 1, length(theta));
end
R = [cos(theta) sin(theta) ; -sin(theta) cos(theta)];
rot_vec = pagemtimes(R, vec);
end
6 Comments
Accepted Answer
Bruno Luong
on 24 Oct 2023
Edited: Bruno Luong
on 25 Oct 2023
I only add an altervative to the for-loop at the end.
On my PC the runtime if about 10 time faster.
%all of this is usually supplied to the function, but I explicitly write it
%here so that calculations are possible
x_r = 0;
y_r = -0.3;
x_r = repmat(x_r, 1, 1001);
y_r = repmat(y_r, 1, 1001);
vectors_dummy = [0;1];
% vectors = zeros(2,1001);
angles = linspace(-30,30,1001) * pi/180;
vectors = rot_matrix_2d(vectors_dummy,angles);
vectors = squeeze(vectors).'; % 1001 x 2
vox.dx = 0.002;
vox.dy = 0.002;
vox.xstart = -0.2;
vox.xend = 0.2;
vox.ystart = -0.2;
vox.yend = 0.2;
vox.Nx = 201;
vox.Ny = 201;
coordaxis.x = linspace(vox.xstart, vox.xend, vox.Nx);
coordaxis.y = linspace(vox.ystart, vox.yend, vox.Ny);
coordaxis.z = 0;
[array.X, array.Y, array.Z] = ndgrid(coordaxis.x,coordaxis.y,coordaxis.z);
vox_pos = zeros(3, length(array.X(:)));
vox_pos(1,:) = array.X(:);
vox_pos(2,:) = array.Y(:); % 3 x nvoxels
N_rays = size(vectors, 1);
v = gpuArray(vectors.'); % 2 x 1001
x_r = gpuArray(x_r); % 1 x 1001
y_r = gpuArray(y_r); % 1 x 1001
tic
%define outer grid around pixels
x2 = gpuArray(vox.xstart - vox.dx/2:vox.dx:vox.xend + vox.dx/2); % 1 x 202
y2 = gpuArray(vox.ystart - vox.dy/2:vox.dy:vox.yend + vox.dy/2); % 1 x 202
% Calculate t-values for the intersection points with all the grid lines
tx = ( x2(:) - x_r ) ./ v(1,:); % 202 x 1001
ty = ( y2(:) - y_r ) ./ v(2,:); % 202 x 1001
% Sort the t-values for each ray
t = sort( [tx; ty], 1); % 404 x 1001
M = size(t,1); % == 404
if exist('t_hit_min', 'var')
t(t>t_hit_min.') = NaN;
end
% Calculate the distance between subsequent intersection points
dist_t = diff(t); % 403 x 1001
% Define mid point between two intersection points
p = reshape([x_r; y_r], 2, 1, N_rays) + ...
reshape(t(1:end-1,:) + 1/2*dist_t, 1, M - 1, N_rays) .* reshape(v, 2, 1, N_rays); % 3 x 403 x 1001
% Define nearest grid points to mid points between two intersection points
idx_x = (round((p(1,:,:) - vox.xstart)/ vox.dx) + 1); % 1 x 403 x 1001
idx_y = (round((p(2,:,:) - vox.ystart)/ vox.dy) + 1); % 1 x 403 x 1001
idx_x(idx_x < 1 | idx_x > vox.Nx) = NaN;
idx_y(idx_y < 1 | idx_y > vox.Ny) = NaN;
%% Calculate matrix
mask = isfinite(idx_x) & isfinite(idx_y);
idx_ray = repmat(reshape(1:N_rays, 1, 1, []), 1, size(idx_x, 2), 1); % 1 x 403 x 1001
idx_lin = sub2ind([vox.Nx, vox.Ny, N_rays], idx_x(mask), idx_y(mask), idx_ray(mask)); %long column vector
%this operation is necessary, as in some cases, the same linear index can
%occur multiple times (which I don't fully understand)
idx_lin = unique(idx_lin);
%boolean is more efficient and also sufficient
B = false(vox.Nx*vox.Ny, N_rays, 'gpuArray');
B(idx_lin) = 1;
toc % Elapsed time is 0.086350 seconds.
BORG = B; % Just for debug
tic
for i = 1:N_rays
idx_dummy = (B(:,i) == 1);
d2 = min_dist2_ray_pix_GPU(vox_pos(1:2,B(:,i)), vectors(i,:).', [x_r(i); y_r(i)]);
B(idx_dummy,i) = (d2 < (vox.dx/2)^2);
end
toc % Elapsed time is 0.907424 seconds.
B1 = B; % Save result, Just for debug
B = BORG; % restore original array, Just for debug
tic
[idx_v,idx_r] = find(B);
VOX_POS = vox_pos(1:2, idx_v);
R_VEC = vectors(idx_r,:).';
R_POS = [x_r(idx_r); y_r(idx_r)];
d2 = nc_min_dist2_ray_pix_GPU(VOX_POS, R_VEC, R_POS);
B(sub2ind(size(B),idx_v,idx_r)) = (d2 < (vox.dx/2)^2);
toc % Elapsed time is 0.091884 seconds.
B2 = B; % Save result, Just for debug
% Check correctness
isequal(B1, B2)
%%
function rot_vec = rot_matrix_2d(vec, theta)
if length(theta)>1
theta = reshape(theta, 1, 1, length(theta));
end
R = [cos(theta) sin(theta) ; -sin(theta) cos(theta)];
rot_vec = pagemtimes(R, vec);
end
%%
% (Non cross) distance
function d2 = nc_min_dist2_ray_pix_GPU(VOX_POS, R_VEC, R_POS, t_hit_min)
% this function calculates the distance between a bundle of
% vectors/rays, which start at some point(s) ray_pos, and a bunch of
% pixels/voxels given by their positions vox_pos
% vectors: 2 x n
% vox_pos: 2 x n
% ray_pos: 2 x n
% if vectors which hit an object's edge are considered, the parameter t is
% given in t_hit_min, where edge = ray_pos + t * vector.
vox_pos = gpuArray(VOX_POS);
vectors = gpuArray(R_VEC);
ray_pos = gpuArray(R_POS);
vox_vec = vox_pos - ray_pos;
the_norm2 = sum(vectors.*vectors,1);
projections = sum(vectors./the_norm2 .* vox_vec,1); % divide on 2D array rather than3D array
%include safeguard: we only want rays in the positive directions
projections(projections<0) = Inf;
% this is a 2 x n_vec x n_pix array
dummy = vox_vec - projections .* vectors;
d2 = sum(dummy.*dummy,1);
%now we test whether a ray that hits an object hits the object or the pixel
%first
if nargin >= 4
t_hit_min = gpuArray(t_hit_min);
idx_dummy = projections < t_hit_min;
d2(~idx_dummy) = Inf;
end
end
%%
function d2 = min_dist2_ray_pix_GPU(vox_pos, vectors, ray_pos, t_hit_min)
% this function calculates the distance between a bundle of
% vectors/rays, which start at some point(s) ray_pos, and a bunch of
% pixels/voxels given by their positions vox_pos
% vectors: 2 x n_vec
% vox_pos: 2 x n_pix
% ray_pos: 2 x n_vec
% if vectors which hit an object's edge are considered, the parameter t is
% given in t_hit_min, where edge = ray_pos + t * vector.
n_vec = size(vectors,2);
n_pix = size(vox_pos,2);
vox_pos = gpuArray(vox_pos);
vectors = gpuArray(vectors);
ray_pos = gpuArray(ray_pos);
vox_vec = reshape(vox_pos, 2, 1, n_pix) - ray_pos;
the_norm2 = sum(vectors.*vectors,1);
projections = sum(vectors./the_norm2 .* vox_vec,1); % divide on 2D array rather than3D array
%include safeguard: we only want rays in the positive directions
projections(projections<0) = Inf;
% this is a 2 x n_vec x n_pix array
dummy = vox_vec - projections .* vectors;
d2 = sum(dummy.*dummy,1);
d2 = reshape(d2, n_vec, n_pix);
%now we test whether a ray that hits an object hits the object or the pixel
%first
if nargin >= 4
t_hit_min = gpuArray(t_hit_min);
projections = reshape(projections, n_vec, n_pix);
idx_dummy = projections < t_hit_min;
d2(~idx_dummy) = Inf;
end
end
2 Comments
More Answers (1)
Matt J
on 24 Oct 2023
Edited: Matt J
on 24 Oct 2023
I have tried to go from square pixels to circles,
I'm assuming the circles are inscribed inside the square pixels? If so, I would first use the code you already have to detect intersections with the square pixels. In order for a line to intersect a circle, it must first intersect the square that circumscribes it, which narrows down the list of candidate intersections.
Once you've narrowed the list (or even if you haven't), it is easy to test intersections of lines with a circles in a vectorized manner. Given a line whose equation is in the form A*x+B*y=C where norm([A,B])=1 and C>=0 and given a circle of radius R and centered at (P,Q), the line intersects the circle if,
abs(A*P+B*Q-C)<=R
which can easily be vectorized over multiple lines and circles.
5 Comments
Bruno Luong
on 25 Oct 2023
Edited: Bruno Luong
on 25 Oct 2023
@Dominik Rhiem Can you please post the final version of all the modifs integrated? I lost track of what ideas you use, and curious to see them.
See Also
Categories
Find more on Matrices and Arrays in Help Center and File Exchange
Community Treasure Hunt
Find the treasures in MATLAB Central and discover how the community can help you!
Start Hunting!