Skip to content

Conversation

@JostMigenda
Copy link
Collaborator

@JostMigenda JostMigenda commented Jun 25, 2024

This is step 1 of the larger project to remove the dependency on MIRTK; an alternate implementation of CemrgCommandLine::ExecuteSurf that uses CemrgCommonUtils::ExtractSurfaceFromSegmentation() instead of MIRTK’s extract-surface and smooth-surface binaries.

This is a draft PR right now, because there are a number of to-do items:

  • For now, there’s a boolean to easily switch between the old and new implementation during development and testing; that (plus the old implementation and its helper functions) needs to be deleted eventually
  • It still uses the MIRTK close-image binary at the start
  • Need to check that the semantics of the threshold/blur/smoothing parameters are identical and there aren’t any subtle differences between numerical values
  • Optional: Check whether it makes sense to input and return pointers to an mitk::Image/mitk::Surface directly, instead of writing them to disk and passing around the file path. Updating the calling code may be more work, but this could reduce overhead quite a bit.

@github-actions
Copy link

github-actions bot commented Jun 25, 2024

⚡ Code Analysis Results ⚡

🔴 Cppcheck found 22 issues! Click here to see details.

scalars = vtkFloatArray::SafeDownCast(surface->GetVtkPolyData()->GetCellData()->GetScalars());
numObjects = surface->GetVtkPolyData()->GetNumberOfCells();
} else{
// surface->GetVtkPolyData()->GetCellData()->SetActiveScalars(fieldName.toStdString().c_str());
scalars = vtkFloatArray::SafeDownCast(surface->GetVtkPolyData()->GetCellData()->GetScalars());
numObjects = surface->GetVtkPolyData()->GetNumberOfCells();

!Line: 1813 - style: Redundant initialization for 'scalars'. The initialized value is overwritten before it is read. [redundantInitialization]

vtkFloatArray *scalars = vtkFloatArray::New();
vtkIdType numObjects;
std::cout << "fieldName: " << fieldName.toStdString() << '\n';
if (isElem){

!Line: 1806 - note: scalars is initialized

scalars = vtkFloatArray::SafeDownCast(surface->GetVtkPolyData()->GetCellData()->GetScalars());
numObjects = surface->GetVtkPolyData()->GetNumberOfCells();
} else{
// surface->GetVtkPolyData()->GetCellData()->SetActiveScalars(fieldName.toStdString().c_str());
scalars = vtkFloatArray::SafeDownCast(surface->GetVtkPolyData()->GetCellData()->GetScalars());
numObjects = surface->GetVtkPolyData()->GetNumberOfCells();

!Line: 1813 - note: scalars is overwritten

class AtrialFibresClipperView : public QmitkAbstractView {
// this is needed for all Qt objects that should have a Qt meta-object
// (everything that derives from QObject and wants to have signal/slots)
Q_OBJECT

!Line: 55 - style: The class 'AtrialFibresClipperView' does not have a constructor although it has private member variables. [noConstructor]

class AtrialFibresView : public QmitkAbstractView {
// this is needed for all Qt objects that should have a Qt meta-object
// (everything that derives from QObject and wants to have signal/slots)
Q_OBJECT

!Line: 66 - style: The class 'AtrialFibresView' does not have a constructor although it has private member variables. [noConstructor]

radii = vtkDoubleArray::SafeDownCast(line->GetPointData()->GetArray("MaximumInscribedSphereRadius"));
double adjust = ctrPlane->GetRadius() / radii->GetValue(position);
//Adjust highlighted clipper
for (unsigned int i=0; i<clipperActors.size(); i++)
clipperActors.at(i)->GetProperty()->SetOpacity(i==(unsigned)index ? 1.0 : 0.1);

!Line: 451 - style: Redundant initialization for 'radii'. The initialized value is overwritten before it is read. [redundantInitialization]

vtkSmartPointer<vtkDoubleArray> radii = vtkSmartPointer<vtkDoubleArray>::New();
radii = vtkDoubleArray::SafeDownCast(line->GetPointData()->GetArray("MaximumInscribedSphereRadius"));
double adjust = ctrPlane->GetRadius() / radii->GetValue(position);
//Adjust highlighted clipper
for (unsigned int i=0; i<clipperActors.size(); i++)

!Line: 450 - note: radii is initialized

radii = vtkDoubleArray::SafeDownCast(line->GetPointData()->GetArray("MaximumInscribedSphereRadius"));
double adjust = ctrPlane->GetRadius() / radii->GetValue(position);
//Adjust highlighted clipper
for (unsigned int i=0; i<clipperActors.size(); i++)
clipperActors.at(i)->GetProperty()->SetOpacity(i==(unsigned)index ? 1.0 : 0.1);

!Line: 451 - note: radii is overwritten

class AtrialFibresLandmarksView : public QmitkAbstractView {
// this is needed for all Qt objects that should have a Qt meta-object
// (everything that derives from QObject and wants to have signal/slots)
Q_OBJECT

!Line: 53 - style: The class 'AtrialFibresLandmarksView' does not have a constructor although it has private member variables. [noConstructor]

double distance;
int pickedSeedId = -1;
double minDistance = 1E10;
for (int i=0; i<pickedCellPointIds->GetNumberOfIds(); i++) {
distance = vtkMath::Distance2BetweenPoints(
pickPosition, surface->GetVtkPolyData()->GetPoint(pickedCellPointIds->GetId(i)));

!Line: 392 - style: The scope of the variable 'distance' can be reduced. [variableScope]

std::ofstream fileRefined, fileRough, fileRefinedLabels;
MITK_INFO << "[SaveRefinedPoints] Saving TXT file.";
fileRefined.open((prodPath + outname + ".txt").toStdString());
fileRefinedLabels.open((prodPath + outname + "-Labels.txt").toStdString());

!Line: 248 - style: Unused variable: fileRough [unusedVariable]

class AtrialFibresVisualiseView : public QmitkAbstractView {
// this is needed for all Qt objects that should have a Qt meta-object
// (everything that derives from QObject and wants to have signal/slots)
Q_OBJECT

!Line: 50 - style: The class 'AtrialFibresVisualiseView' does not have a constructor although it has private member variables. [noConstructor]

if(!userInputAccepted){
QDialog* inputs = new QDialog(0, Qt::WindowFlags());
m_UIEditLabels.setupUi(inputs);
connect(m_UIEditLabels.buttonBox, SIGNAL(accepted()), inputs, SLOT(accept()));
connect(m_UIEditLabels.buttonBox, SIGNAL(rejected()), inputs, SLOT(reject()));
std::cout << "WHICH ATRIUM: " << uac_whichAtrium.toStdString() << '\n';

!Line: 1794 - style: Condition '!userInputAccepted' is always true [knownConditionTrueFalse]

bool userInputAccepted=false;
if(!userInputAccepted){
QDialog* inputs = new QDialog(0, Qt::WindowFlags());
m_UIEditLabels.setupUi(inputs);
connect(m_UIEditLabels.buttonBox, SIGNAL(accepted()), inputs, SLOT(accept()));

!Line: 1792 - note: Assignment 'userInputAccepted=false', assigned value is 0

if(!userInputAccepted){
QDialog* inputs = new QDialog(0, Qt::WindowFlags());
m_UIEditLabels.setupUi(inputs);
connect(m_UIEditLabels.buttonBox, SIGNAL(accepted()), inputs, SLOT(accept()));
connect(m_UIEditLabels.buttonBox, SIGNAL(rejected()), inputs, SLOT(reject()));
std::cout << "WHICH ATRIUM: " << uac_whichAtrium.toStdString() << '\n';

!Line: 1794 - note: Condition '!userInputAccepted' is always true

segImage = atrium->LoadImage(segRegPath);
resultString = segRegPath;
tagName += "-reg";
SetLgeAnalysis(true);

!Line: 2336 - style: Variable 'segImage' is assigned a value that is never used. [unreadVariable]

reply1 = Ask("Question", "Use alternative DICOM reader?");
#endif
if (reply1 == QMessageBox::Yes) {
QString dicomFolder = QFileDialog::getExistingDirectory(NULL, "Open folder with DICOMs.", mitk::IOUtil::GetProgramPath().c_str(), QFileDialog::ShowDirsOnly|QFileDialog::DontUseNativeDialog);

!Line: 192 - style: Redundant initialization for 'reply1'. The initialized value is overwritten before it is read. [redundantInitialization]

int reply1 = QMessageBox::No;
#if defined(__APPLE__)
MITK_INFO << "Ask user about alternative DICOM reader";
reply1 = Ask("Question", "Use alternative DICOM reader?");
#endif

!Line: 189 - note: reply1 is initialized

reply1 = Ask("Question", "Use alternative DICOM reader?");
#endif
if (reply1 == QMessageBox::Yes) {
QString dicomFolder = QFileDialog::getExistingDirectory(NULL, "Open folder with DICOMs.", mitk::IOUtil::GetProgramPath().c_str(), QFileDialog::ShowDirsOnly|QFileDialog::DontUseNativeDialog);

!Line: 192 - note: reply1 is overwritten

if (dcm_path_fix) {
MITK_INFO << "[ATTENTION] Saving imgTimes.lst file to project directory.";
time = directory + "/imgTimes.lst";
file.open(time.toStdString(), std::ofstream::binary);
file << "dcm- .nii\n";
}

!Line: 721 - style: Condition 'dcm_path_fix' is always true [knownConditionTrueFalse]

bool dcm_path_fix = true;
if (dcm_path_fix) {
MITK_INFO << "[ATTENTION] Saving imgTimes.lst file to project directory.";
time = directory + "/imgTimes.lst";
file.open(time.toStdString(), std::ofstream::binary);
file << "dcm- .nii\n";

!Line: 720 - note: Assignment 'dcm_path_fix=true', assigned value is 1

if (dcm_path_fix) {
MITK_INFO << "[ATTENTION] Saving imgTimes.lst file to project directory.";
time = directory + "/imgTimes.lst";
file.open(time.toStdString(), std::ofstream::binary);
file << "dcm- .nii\n";
}

!Line: 721 - note: Condition 'dcm_path_fix' is always true


also moves shared code to top of function to fix compilation error
Decimation creates irregular triangle sizes in the surface mesh, which may become a problem later in the workflow when mapping voxels onto the mesh
Resulting files look nearly identical (as discussed on Slack)
so remove unnecessary step that depends on MIRTK tool
@JostMigenda JostMigenda force-pushed the feature/replaceMIRTK branch from 08a6726 to 0b85f89 Compare July 10, 2024 16:27
Also deletes three (now unused) helper functions
Delete superfluous test case (which only differed in the morphOperation from the first test case)
@JostMigenda JostMigenda force-pushed the feature/replaceMIRTK branch 2 times, most recently from 21870a6 to d2df9c5 Compare July 11, 2024 17:17
@JostMigenda JostMigenda force-pushed the feature/replaceMIRTK branch from d2df9c5 to 2d54320 Compare July 15, 2024 10:54
@JostMigenda
Copy link
Collaborator Author

Reverted this commit after discussing with @OrodRazeghi on Slack. Writing the mitk::Surface to disk between steps is useful, e.g. if clinicians want to look at the mesh and check its quality before moving on to the next steps.

Copy link
Collaborator

@LouiseABowler LouiseABowler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR isn't quite ready for a full review as the surface generation parameters still need to be checked, but I have done a preliminary review to get the process started.

Overall, this is looking good 👍 I was able to run the example with no problems on Linux, and the changes to the code are clear.

I did spot one potential issue which @JostMigenda is going to pick up on. If invalid values are supplied for the threshold, smoothing or blur parameters, CemrgApp will crash as there isn't currently a way for it to gracefully exit or return an error message to the user. We've had a couple of messages about this and have identified two options for where to make these checks.

Once that's taken care of and the default parameters are finalised, I'll take another look and finish the review off.

@JostMigenda
Copy link
Collaborator Author

It looks like there are some subtle differences between the surfaces generated by MIRTK (on the development branch) and by CemrgCommonUtils::ExtractSurfaceFromSegmentation (our designated replacement). Note in particular the top left image in the attached screenshots, where the white line generated by MIRTK seems like a better fit.

I’m not sure whether that’s caused (at least in part) by the default parameters used for MIRTK being less well suited for the new implementation; so as discussed during previous meeetings, let’s wait for @alonsoJASL to experiment with that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants