Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Userfolders can now also be set via environment variable `MATRAD_USERDATA`
- Documentation: Documented the userfolder feature and its usage as well as other datastructures more clearly
- Added helper to create ring VOIs from margins around existing structures

### Fixed
- possible negative doses in finesampling engine due to extrapolation in kernel interpolation
Expand Down
92 changes: 92 additions & 0 deletions matRad/geometry/matRad_createRing.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
function [cst, ixRing] = matRad_createRing(ixBase, ixLimit, cst, ct, vOuterMargin, vInnerMargin, metadata)
% matRad function to create an isotropic ring VOI clipped to a limiting VOI
%
% call:
% [cst, ixRing] = matRad_createRing(ixBase, ixLimit, cst, ct, vOuterMargin, vInnerMargin, metadata)
%
% input:
% ixBase: row index of the base VOI in the cst struct
% ixLimit: row index of the limiting VOI in the cst struct
% cst: matRad cst struct
% ct: matRad ct struct
% vOuterMargin: outer margin in mm, with fields x, y and z
% vInnerMargin: inner margin in mm, with fields x, y and z
% metadata: struct with fields name, type and visibleColor for the
% created ring VOI
%
% output:
% cst: updated matRad cst struct
% ixRing: row index of the created ring VOI
%
% References
% -
%
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Copyright 2026 the matRad development team.
%
% This file is part of the matRad project. It is subject to the license
% terms in the LICENSE file found in the top-level directory of this
% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part
% of the matRad project, including this file, may be copied, modified,
% propagated, or distributed except according to the terms contained in the
% LICENSE file.
%
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

matRadCfg = MatRad_Config.instance();

if nargin < 7
matRadCfg.dispError('Not enough input arguments specified for matRad_createRing.');
end

if ixBase < 1 || ixBase > size(cst, 1) || ixLimit < 1 || ixLimit > size(cst, 1)
matRadCfg.dispError('Base and limiting VOI indices must refer to existing cst rows.');
end

if ~isfield(ct, 'numOfCtScen') || ct.numOfCtScen < 1
matRadCfg.dispError('ct.numOfCtScen must be available and positive.');
end

if ~isfield(ct, 'cubeDim') || ~isfield(ct, 'resolution')
matRadCfg.dispError('ct.cubeDim and ct.resolution are required to create a ring VOI.');
end

if ~all(isfield(vOuterMargin, {'x', 'y', 'z'})) || ~all(isfield(vInnerMargin, {'x', 'y', 'z'}))
matRadCfg.dispError('Ring margins must contain x, y and z fields.');
end

if ~all(isfield(metadata, {'name', 'type', 'visibleColor'}))
matRadCfg.dispError('Ring metadata must contain name, type and visibleColor fields.');
end

if numel(cst{ixBase, 4}) < ct.numOfCtScen || numel(cst{ixLimit, 4}) < ct.numOfCtScen
matRadCfg.dispError('Base and limiting VOIs must contain all CT scenarios.');
end

voiRing = cell(1, ct.numOfCtScen);
useDiagonalConnectivity = false;

for scenIx = 1:ct.numOfCtScen
baseVoxels = cst{ixBase, 4}{scenIx};
limitVoxels = cst{ixLimit, 4}{scenIx};

baseMask = zeros(ct.cubeDim);
baseMask(baseVoxels) = 1;

outerVoxels = find(matRad_addMargin(baseMask, cst, ct.resolution, vOuterMargin, useDiagonalConnectivity));
innerVoxels = find(matRad_addMargin(baseMask, cst, ct.resolution, vInnerMargin, useDiagonalConnectivity));

voiRing{scenIx} = intersect(setdiff(outerVoxels, innerVoxels), limitVoxels);
end

ixRing = size(cst, 1) + 1;

cst{ixRing, 1} = cst{end, 1} + 1;
cst{ixRing, 2} = metadata.name;
cst{ixRing, 3} = metadata.type;
cst{ixRing, 4} = voiRing;
cst{ixRing, 5} = cst{ixBase, 5};
cst{ixRing, 5}.visibleColor = metadata.visibleColor;

end
70 changes: 70 additions & 0 deletions test/geometry/test_createRing.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
function test_suite = test_createRing

test_functions = localfunctions();

initTestSuite;

function test_createRingBuildsOuterMinusInnerVoi
[ct, cst] = helper_createRingFixture();
[outerMargin, innerMargin, metadata] = helper_ringArguments();

[cst, ixRing] = matRad_createRing(1, 2, cst, ct, outerMargin, innerMargin, metadata);

targetVoxel = sub2ind(ct.cubeDim, 3, 3, 2);
expectedRing = sort([ ...
sub2ind(ct.cubeDim, 2, 3, 2); ...
sub2ind(ct.cubeDim, 4, 3, 2); ...
sub2ind(ct.cubeDim, 3, 2, 2); ...
sub2ind(ct.cubeDim, 3, 4, 2); ...
sub2ind(ct.cubeDim, 3, 3, 1); ...
sub2ind(ct.cubeDim, 3, 3, 3)]);

assertEqual(ixRing, 3);
assertEqual(cst{ixRing, 2}, 'PTV_RING');
assertEqual(cst{ixRing, 3}, 'OAR');
assertEqual(cst{ixRing, 5}.visibleColor, [0 1 0]);
assertFalse(any(cst{ixRing, 4}{1} == targetVoxel));
assertEqual(sort(cst{ixRing, 4}{1}), expectedRing);

function test_createRingClipsToLimitVoi
[ct, cst] = helper_createRingFixture();
cst{2, 4}{1} = sub2ind(ct.cubeDim, [2 3 4], [3 3 3], [2 2 2])';
[outerMargin, innerMargin, metadata] = helper_ringArguments();

[cst, ixRing] = matRad_createRing(1, 2, cst, ct, outerMargin, innerMargin, metadata);

expectedRing = sort([ ...
sub2ind(ct.cubeDim, 2, 3, 2); ...
sub2ind(ct.cubeDim, 4, 3, 2)]);

assertEqual(sort(cst{ixRing, 4}{1}), expectedRing);

function [ct, cst] = helper_createRingFixture
ct.cubeDim = [5 5 3];
ct.numOfCtScen = 1;
ct.resolution = struct('x', 1, 'y', 1, 'z', 1);

targetVoxel = sub2ind(ct.cubeDim, 3, 3, 2);
bodyVoxels = (1:prod(ct.cubeDim))';

cst = cell(2, 6);
cst{1, 1} = 0;
cst{1, 2} = 'PTV';
cst{1, 3} = 'TARGET';
cst{1, 4} = {targetVoxel};
cst{1, 5} = struct('visibleColor', [1 0 0]);
cst{1, 6} = {};

cst{2, 1} = 1;
cst{2, 2} = 'BODY';
cst{2, 3} = 'OAR';
cst{2, 4} = {bodyVoxels};
cst{2, 5} = struct('visibleColor', [0 0 1]);
cst{2, 6} = {};

function [outerMargin, innerMargin, metadata] = helper_ringArguments
metadata.name = 'PTV_RING';
metadata.type = 'OAR';
metadata.visibleColor = [0 1 0];
outerMargin = struct('x', 1, 'y', 1, 'z', 1);
innerMargin = struct('x', 0, 'y', 0, 'z', 0);
Loading