Skip to content

Commit 97c4fc0

Browse files
authored
Segmentation Tool (#2332)
* Add skeleton segmentation tool * Display segmentation * Restyle * Working on segmentation painting tool * Working on segmentation tool * 2D circle cursor * Reformat * Segmentation working. * Segmentation working. * Segmentation working. * More functionality needed for segmentation support. * Cleanup * Fix up paint tool for constraints * Prioritize segmentation over contour tool * Improve segmentation cursor * Updates for projects with only images. * Fix crash with segmentation tools when no images are loaded Show message and disable tools when images are not present in project. * Fix default segmentation mode, fix layout.
1 parent 3458e8f commit 97c4fc0

26 files changed

+1146
-158
lines changed

Libs/Analyze/MeshGenerator.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ MeshHandle MeshGenerator::build_mesh_from_image(ImageType::Pointer image, float
9898
MeshHandle mesh(new StudioMesh);
9999

100100
try {
101+
// only interested in 1's and 0's
102+
Image itk_image = Image(image);
103+
itk_image.binarize(0, 1);
104+
image = itk_image.getITKImage();
105+
101106
// connect to VTK
102107
vtkSmartPointer<vtkImageImport> vtk_image = vtkSmartPointer<vtkImageImport>::New();
103108
itk::VTKImageExport<ImageType>::Pointer itk_exporter = itk::VTKImageExport<ImageType>::New();
@@ -177,7 +182,7 @@ MeshHandle MeshGenerator::build_mesh_from_file(std::string filename, float iso_v
177182
mesh = this->build_mesh_from_image(image, iso_value);
178183

179184
if (mesh->get_poly_data()->GetNumberOfPoints() == 0) {
180-
SW_ERROR("Mesh generated for {} with iso-level {} is empty",filename, iso_value);
185+
SW_ERROR("Mesh generated for {} with iso-level {} is empty", filename, iso_value);
181186
}
182187

183188
} catch (itk::ExceptionObject& excep) {

Libs/Analyze/Shape.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,55 @@ MeshGroup Shape::get_original_meshes(bool wait) {
107107
return original_meshes_;
108108
}
109109

110+
//---------------------------------------------------------------------------
111+
void Shape::recompute_original_surface() {
112+
auto seg = get_segmentation();
113+
if (!seg) {
114+
return;
115+
}
116+
if (original_meshes_.meshes().empty()) {
117+
return;
118+
}
119+
auto meshes = original_meshes_.meshes();
120+
Image copy = *seg;
121+
copy.binarize(0, 1);
122+
Mesh mesh = copy.toMesh(0.001);
123+
MeshHandle mesh_handle = std::make_shared<StudioMesh>();
124+
mesh_handle->set_poly_data(mesh.getVTKMesh());
125+
original_meshes_.set_mesh(0, mesh_handle);
126+
}
127+
128+
129+
//---------------------------------------------------------------------------
130+
void Shape::ensure_segmentation()
131+
{
132+
if (get_segmentation()) {
133+
return;
134+
}
135+
136+
if (!subject_) {
137+
return;
138+
}
139+
140+
if (subject_->get_feature_filenames().empty()) {
141+
return;
142+
}
143+
144+
// get image volume
145+
auto image_name = subject_->get_feature_filenames().begin()->first;
146+
auto base_image = get_image_volume(image_name);
147+
if (!base_image) {
148+
return;
149+
}
150+
151+
// deep copy
152+
Image blank = *base_image;
153+
blank.fill(0);
154+
segmentation_ = std::make_shared<Image>(blank);
155+
// remove extension and add "_seg.nrrd"
156+
segmentation_filename_ = StringUtils::removeExtension(image_volume_filename_) + "_seg.nrrd";
157+
}
158+
110159
//---------------------------------------------------------------------------
111160
MeshGroup Shape::get_groomed_meshes(bool wait) {
112161
if (!subject_) {
@@ -593,6 +642,33 @@ std::shared_ptr<Image> Shape::get_image_volume(std::string image_volume_name) {
593642
return nullptr;
594643
}
595644

645+
//---------------------------------------------------------------------------
646+
std::shared_ptr<Image> Shape::get_segmentation() {
647+
if (segmentation_) {
648+
return segmentation_;
649+
}
650+
if (!subject_) {
651+
return nullptr;
652+
}
653+
auto filenames = subject_->get_original_filenames();
654+
if (filenames.empty()) {
655+
return nullptr;
656+
}
657+
auto filename = filenames[0];
658+
659+
if (Image::isSupportedType(filename)) {
660+
try {
661+
std::shared_ptr<Image> image = std::make_shared<Image>(filename);
662+
segmentation_ = image;
663+
segmentation_filename_ = filename;
664+
} catch (std::exception& ex) {
665+
SW_ERROR("Unable to open file \"{}\": {}", filename, ex.what());
666+
}
667+
}
668+
669+
return segmentation_;
670+
}
671+
596672
//---------------------------------------------------------------------------
597673
void Shape::apply_feature_to_points(std::string feature, ImageType::Pointer image) {
598674
using LinearInterpolatorType = itk::LinearInterpolateImageFunction<ImageType, double>;

Libs/Analyze/Shape.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ class Shape {
167167

168168
std::shared_ptr<Image> get_image_volume(std::string image_volume_name);
169169

170+
std::shared_ptr<Image> get_segmentation();
171+
std::string get_segmentation_filename() { return segmentation_filename_; }
172+
170173
Eigen::VectorXd get_point_features(std::string feature);
171174

172175
void set_point_features(std::string feature, Eigen::VectorXd values);
@@ -186,6 +189,11 @@ class Shape {
186189

187190
std::vector<std::shared_ptr<MeshWrapper>> get_groomed_mesh_wrappers();
188191

192+
void recompute_original_surface();
193+
194+
//! If a segmentation doesn't exist, create a blank canvas
195+
void ensure_segmentation();
196+
189197
private:
190198
void generate_meshes(std::vector<std::string> filenames, MeshGroup& mesh_list, bool save_transform,
191199
bool wait = false);
@@ -225,6 +233,9 @@ class Shape {
225233
std::shared_ptr<Image> image_volume_;
226234
std::string image_volume_filename_;
227235

236+
std::shared_ptr<Image> segmentation_;
237+
std::string segmentation_filename_;
238+
228239
std::vector<Constraints> constraints_; // one set for each domain
229240
int alignment_type_;
230241
};

Libs/Image/Image.cpp

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "Image.h"
22

3+
#include <Logging.h>
34
#include <itkAntiAliasBinaryImageFilter.h>
45
#include <itkBinaryFillholeImageFilter.h>
56
#include <itkBinaryThresholdImageFilter.h>
@@ -905,6 +906,11 @@ Image& Image::isolate() {
905906
return *this;
906907
}
907908

909+
double Image::get_largest_dimension_size() const {
910+
auto our_size = size();
911+
return std::max(our_size[0], std::max(our_size[1], our_size[2]));
912+
}
913+
908914
double Image::get_minimum_spacing() const {
909915
auto spacing = this->spacing();
910916
return std::min(spacing[0], std::min(spacing[1], spacing[2]));
@@ -1038,12 +1044,129 @@ Image::PixelType Image::evaluate(Point p) {
10381044
return interpolator_->Evaluate(p);
10391045
}
10401046

1047+
//-------------------------------------------------------------------------
1048+
void Image::paintSphere(Point p, double radius, PixelType value) {
1049+
painted_ = true;
1050+
1051+
// Convert the center and radius to the index space
1052+
ImageType::IndexType centerIndex;
1053+
itk_image_->TransformPhysicalPointToIndex(p, centerIndex);
1054+
1055+
// Calculate the bounding box of the sphere in the index space
1056+
const double radiusSquared = radius * radius;
1057+
ImageType::RegionType region = itk_image_->GetLargestPossibleRegion();
1058+
1059+
ImageType::SizeType size = region.GetSize();
1060+
ImageType::IndexType start = region.GetIndex();
1061+
1062+
for (unsigned int i = 0; i < 3; ++i) {
1063+
start[i] = std::max<long>(start[i], centerIndex[i] - static_cast<long>(std::ceil(radius)));
1064+
size[i] = std::min<long>(size[i], centerIndex[i] + static_cast<long>(std::ceil(radius)) + 1);
1065+
}
1066+
1067+
// Iterate only within the bounding box of the sphere
1068+
ImageType::IndexType index;
1069+
for (index[2] = start[2]; index[2] < start[2] + size[2]; ++index[2]) {
1070+
for (index[1] = start[1]; index[1] < start[1] + size[1]; ++index[1]) {
1071+
for (index[0] = start[0]; index[0] < start[0] + size[0]; ++index[0]) {
1072+
// Check if the current index is within the radius
1073+
Point3 q = logicalToPhysical(index);
1074+
if ((p - q).GetSquaredNorm() <= radiusSquared) {
1075+
itk_image_->SetPixel(index, value);
1076+
}
1077+
}
1078+
}
1079+
}
1080+
}
1081+
1082+
//-------------------------------------------------------------------------
1083+
void Image::paintCircle(Point p, double radius, unsigned int axis, PixelType value) {
1084+
painted_ = true;
1085+
// Convert the center to index space
1086+
ImageType::IndexType centerIndex;
1087+
itk_image_->TransformPhysicalPointToIndex(p, centerIndex);
1088+
1089+
ImageType::SpacingType spacing = itk_image_->GetSpacing();
1090+
1091+
// Calculate the radius squared
1092+
const double radiusSquared = radius * radius;
1093+
1094+
// Define the plane's coordinate pairs based on the given VTK axis (dim1, dim2 are the in-plane dimensions)
1095+
unsigned int dim1, dim2;
1096+
switch (axis) {
1097+
case 2: // XY plane
1098+
dim1 = 0;
1099+
dim2 = 1;
1100+
break;
1101+
case 1: // XZ plane
1102+
dim1 = 0;
1103+
dim2 = 2;
1104+
break;
1105+
case 0: // YZ plane
1106+
dim1 = 1;
1107+
dim2 = 2;
1108+
break;
1109+
default:
1110+
throw std::invalid_argument("Invalid axis, must be 0 (YZ), 1 (XZ), or 2 (XY) according to VTK conventions");
1111+
}
1112+
1113+
// Get the image region
1114+
ImageType::RegionType region = itk_image_->GetLargestPossibleRegion();
1115+
ImageType::IndexType startIndex = region.GetIndex();
1116+
ImageType::SizeType size = region.GetSize();
1117+
ImageType::IndexType endIndex;
1118+
endIndex[0] = startIndex[0] + size[0] - 1;
1119+
endIndex[1] = startIndex[1] + size[1] - 1;
1120+
endIndex[2] = startIndex[2] + size[2] - 1;
1121+
1122+
// Calculate bounding box of the circle in the specified plane, considering spacing
1123+
startIndex[dim1] =
1124+
std::max<long>(startIndex[dim1], centerIndex[dim1] - static_cast<long>(std::ceil(radius / spacing[dim1])));
1125+
startIndex[dim2] =
1126+
std::max<long>(startIndex[dim2], centerIndex[dim2] - static_cast<long>(std::ceil(radius / spacing[dim2])));
1127+
endIndex[dim1] =
1128+
std::min<long>(endIndex[dim1], centerIndex[dim1] + static_cast<long>(std::ceil(radius / spacing[dim1])));
1129+
endIndex[dim2] =
1130+
std::min<long>(endIndex[dim2], centerIndex[dim2] + static_cast<long>(std::ceil(radius / spacing[dim2])));
1131+
1132+
// Set the orthogonal dimension index to the center slice
1133+
ImageType::IndexType index;
1134+
index[(axis == 2) ? 2 : (axis == 1) ? 1 : 0] = centerIndex[(axis == 2) ? 2 : (axis == 1) ? 1 : 0];
1135+
1136+
// Iterate only within the bounding box of the circle
1137+
for (index[dim2] = startIndex[dim2]; index[dim2] <= endIndex[dim2]; ++index[dim2]) {
1138+
for (index[dim1] = startIndex[dim1]; index[dim1] <= endIndex[dim1]; ++index[dim1]) {
1139+
// Calculate the 2D distance on the specified plane
1140+
Point3 q = logicalToPhysical(index);
1141+
double distanceSquared = std::pow((p[dim1] - q[dim1]), 2) + std::pow((p[dim2] - q[dim2]), 2);
1142+
1143+
// If it's within the radius, set the pixel value
1144+
if (distanceSquared <= radiusSquared) {
1145+
itk_image_->SetPixel(index, value);
1146+
}
1147+
}
1148+
}
1149+
}
1150+
1151+
//-------------------------------------------------------------------------
1152+
Image& Image::fill(PixelType value) {
1153+
itk::ImageRegionIterator<ImageType> imageIterator(itk_image_, itk_image_->GetLargestPossibleRegion());
1154+
while (!imageIterator.IsAtEnd()) {
1155+
imageIterator.Set(value);
1156+
++imageIterator;
1157+
}
1158+
painted_ = true;
1159+
return *this;
1160+
}
1161+
1162+
//-------------------------------------------------------------------------
10411163
TransformPtr Image::createCenterOfMassTransform() {
10421164
AffineTransformPtr xform(AffineTransform::New());
10431165
xform->Translate(-(center() - centerOfMass())); // ITK translations go in a counterintuitive direction
10441166
return xform;
10451167
}
10461168

1169+
//-------------------------------------------------------------------------
10471170
TransformPtr Image::createRigidRegistrationTransform(const Image& target_dt, float isoValue, unsigned iterations) {
10481171
if (!target_dt.itk_image_) {
10491172
throw std::invalid_argument("Invalid target. Expected distance transform image");
@@ -1067,6 +1190,7 @@ TransformPtr Image::createRigidRegistrationTransform(const Image& target_dt, flo
10671190
return AffineTransform::New();
10681191
}
10691192

1193+
//-------------------------------------------------------------------------
10701194
std::ostream& operator<<(std::ostream& os, const Image& img) {
10711195
return os << "{\n\tdims: " << img.dims() << ",\n\torigin: " << img.origin() << ",\n\tsize: " << img.size()
10721196
<< ",\n\tspacing: " << img.spacing() << "\n}";

Libs/Image/Image.h

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <StringUtils.h>
34
#include <itkImage.h>
45
#include <itkImageRegionIterator.h>
56
#include <itkLinearInterpolateImageFunction.h>
@@ -145,7 +146,7 @@ class Image {
145146
const ImageType::DirectionType direction, InterpolationType interp = NearestNeighbor);
146147

147148
/// applies the given transformation to the image by using resampling filter with reference image
148-
Image &applyTransform(const TransformPtr transform, const Image& referenceImage, InterpolationType interp = Linear);
149+
Image& applyTransform(const TransformPtr transform, const Image& referenceImage, InterpolationType interp = Linear);
149150

150151
/// extracts/isolates a specific voxel label from a given multi-label volume and outputs the corresponding binary
151152
/// image
@@ -216,6 +217,9 @@ class Image {
216217
/// physical dimensions of the image (dims * spacing)
217218
Point3 size() const { return toPoint(spacing()) * toPoint(dims()); }
218219

220+
/// largest dimension size
221+
double get_largest_dimension_size() const;
222+
219223
/// physical spacing of the image
220224
Vector spacing() const { return itk_image_->GetSpacing(); }
221225

@@ -290,11 +294,33 @@ class Image {
290294
//! Evaluates the image at a given position
291295
Image::PixelType evaluate(Point p);
292296

297+
//! Paints a sphere in the image
298+
void paintSphere(Point p, double radius, PixelType value);
299+
300+
//! Paints a circle in the image
301+
void paintCircle(Point p, double radius, unsigned int axis, PixelType value);
302+
303+
//! Returns if the image has been painted
304+
bool isPainted() const { return painted_; }
305+
306+
//! fill with value
307+
Image& fill(PixelType value);
308+
293309
//! Return supported file types
294310
static std::vector<std::string> getSupportedTypes() {
295311
return {"nrrd", "nii", "nii.gz", "mhd", "tiff", "jpeg", "jpg", "png", "dcm", "ima"};
296312
}
297313

314+
//! Return if the file type is supported
315+
static bool isSupportedType(const std::string& filename) {
316+
for (const auto& type : Image::getSupportedTypes()) {
317+
if (StringUtils::hasSuffix(filename, type)) {
318+
return true;
319+
}
320+
}
321+
return false;
322+
}
323+
298324
private:
299325
friend struct SharedCommandData;
300326
Image()
@@ -318,6 +344,8 @@ class Image {
318344

319345
ImageType::Pointer itk_image_;
320346

347+
bool painted_ = false;
348+
321349
InterpolatorType::Pointer interpolator_;
322350
};
323351

0 commit comments

Comments
 (0)