SlicerMorph paper

@stevenagl12 a couple of questions:

We are adding file export directly from the subject hierarchy tree (for example in Data module). The first iteration will probably only export a single file at a time, but soon we will add multiple item (and/or entire folder) export, which will write content of all the selected markup nodes into a single json file. From that file you can use a few lines of Python code, R code, or the Excel GUI to create a spreadsheet. Would that fulfill your needs?

We also have standard csv export in Markups module (Export/import table), which creates tsv/csv files like this:

image

We could easily implement multiple node export. Could this be improved/customized to fit your needs?

I agree that supporting multiple file formats for data storage is a huge liability and we ideally we would use a single format. However, supporting multiple file formats for data export would be probably fine, it should be just clear for the users that it is not for data storage, only for interoperability. These export options would not need to be added to the ā€œSave dataā€ window, they could be in separate module(s) or subject hierarchy right-click menu actions.


Among all discussed options however, this one sounds by far the most promising to me:

You can explore the available linear model tools in Python, and use them within Slicer/SlicerMorph. We can possibly work with you, if you want to go down that route

MorphoJ looks like a closed-source project, with no development community around it, based on 15-20 year old technologies and with a look and feel of about the same era. It still has about 500 mentions on Google Scholar per year, so there are users. This indicates that there is a huge opportunity here for an alternative software, which is similarly easy to use, but based on current technology, and it is open and reproducible. Maybe SlicerMorph could fulfill this need.

Probably all the algorithms that MorphoJ offers are available in various Python packages and Slicer/SlicerMorph has all the GUI components you may need, so what is missing is really just putting together convenient modules for specific analysis tasks that you are interested in. @muratmaga even offered to help you with this, so you would probably not need to invest a lot effort. Would you consider this option?

My specific concern is that people will do the landmarking, then they will export the data in a non-Slicer format. And because they exported it, they will think they are done (and implicitly assume they can import what they exported), and close their session without ever saving the markups in appropriate format, causing a data loss or a major headache to re-convert those back. I would rather avoid that. Most of those formats people need to do geometric morphometric analysis are very simple flat files, and can be easily compiled via scripts, so I donā€™t really see a value in direct export options for those.

For whatever reason, python is not very popular among evolutionary biologists (as far as I can tell), and there is a very well developed toolkits for statistical analysis on coordinate data in R (geomorph is the most popular one, but not the only one). So we always suggested people to continue doing their inferential statistics in R. At the moment, we have no bandwidth to recreate all those specific functions (nor have any dedicated funding for it). But if someone wants to take a crack at developing a commonly used subset of those (e.g., allometric regression, analysis of variance on procrustes distances, etc), we will be happy to help and potentially turn them into individual modules.

This is somewhat off-topic, but I have to agree with this, and I donā€™t understand it. Python is very powerful and continues to grow in the number of packages for bio, including packages where you can run R from Python in Jupyter notebooks. You get all the benefits of Python but can still use R analyses that may not yet be implemented in Python.

Anyway, sorry to derail the conversation a bit but this is a specific topic weā€™ve discussed here at AMNH several times.

It is a cultural thing. Biologists exposure to computational things is mostly through biostat courses, which utilize R often. To be frank, while Python is powerful, R is way more straighforward statistical analyses and modeling then Python as it is designed for that. You can use python in R (there are now multiple ways of bridging them). Most straighforward one is reticulate.

Hereā€™s an example python function that you could use in a simple Python module to run the conversion on a folder of .mrk.json files. It can be modified to work with .json or .fcsv. also, since itā€™s using the Slicer readers instead of parsing the text. Note that it also applies an LPS transform to the output, as by default the coordinates within Slicer are RAS and output as LPS.

The function requires that the landmarks be saved to a folder first, which is probably a good idea if you need to go back to the landmarks in Slicer.

import os, glob, numpy

def convertMorphoJLM(inputFileDirectory, outputLandmarkFile)
  extensionInput = ".mrk.json"
  fileList = glob.glob(inputDirectory+"*.json")
  success, currentLMNode = slicer.util.loadMarkupsFiducialList(fileList[0])
  subjectNumber = len(fileList)
  landmarkNumber = currentLMNode.GetNumberOfFiducials()
  LMArray=numpy.zeros(shape=(subjectNumber,landmarkNumber,3))
  fileIndex=0
  fileStems=[];
  headerLM = ["Subject"]
  for pointIndex in range(currentLMNode.GetNumberOfMarkups()):     
    headerLM.append(currentLMNode.GetNthControlPointLabel(pointIndex)+'_X')
    headerLM.append(currentLMNode.GetNthControlPointLabel(pointIndex)+'_Y')
    headerLM.append(currentLMNode.GetNthControlPointLabel(pointIndex)+'_Z')

  LPS = [-1,-1,1]
  point = [0,0,0]
  for file in fileList:
    if fileIndex>0:
      success, currentLMNode = slicer.util.loadMarkupsFiducialList(file)     
    fileStems.append(os.path.basename(file.split(extensionInput)[0]))
    for pointIndex in range(currentLMNode.GetNumberOfMarkups()):
      point=currentLMNode.GetNthControlPointPositionVector(pointIndex)
      point.Set(point[0]*LPS[0],point[1]*LPS[1],point[2]*LPS[2])
      LMArray[fileIndex, pointIndex,:]=point
    slicer.mrmlScene.RemoveNode(currentLMNode)
    fileIndex+=1

  temp = numpy.column_stack((numpy.array(fileStems), LMArray.reshape(subjectNumber, int(3 * landmarkNumber))))
  temp = numpy.vstack((numpy.array(headerLM), temp))
  numpy.savetxt(outputLandmarkFile, temp, fmt="%s", delimiter=",")

I apologize for taking so long to respond.

We are adding file export directly from the subject hierarchy tree (for example in Data module). The first iteration will probably only export a single file at a time, but soon we will add multiple item (and/or entire folder) export, which will write content of all the selected markup nodes into a single json file. From that file you can use a few lines of Python code, R code, or the Excel GUI to create a spreadsheet. Would that fulfill your needs?

This might be a good option, however, if itā€™s possible for said code to be available, I can modify it so that I can make it only save the json with the title of the sample, x, y, and z coordinates of each point in the same line on the sheet. Ultimately, I cannot use Python code or R code as the students would need to use said code for their assignment, which again they are neither accepting nor willing to do. The Excel GUI just adds extra steps/complexity to the assignment (admittedly not much), but I would rather just create a module myself that makes it simplified and does the process for them with extrememly limited input from the students to be able to do the saving.

We could easily implement multiple node export. Could this be improved/customized to fit your needs?

The multiple node export again suffers from the same problem as the Excel problem in that it would add extra steps. Currently, the fcsv files are obviously saved like this:

image

MorphoJ requires files like this:
image

Where the first column just has the sample name, followed by each point pithout identifiers as x,y,z. In order to get this form with the methods described, I either need to modify the way that they are being saved (which would require access to the script used to save fiducials, giving students unnecessary instructions for loading up each file and creating a new file with said format, using a multi-fiducial saving option, but it would still require students to go in and edit the files for the extraneous information, or doing a post-processing step for creating the format necessary (this could be a module where I could direct it to a folder that contains a series of .fcsv files and run an extraction to get the sample name from the file name, and each coordinate from the list of the fiducials saved, which I can code out no problem if I have an example of a module that creates a button to select said directory (the only part I am missing)).

Probably all the algorithms that MorphoJ offers are available in various Python packages and Slicer/SlicerMorph has all the GUI components you may need, so what is missing is really just putting together convenient modules for specific analysis tasks that you are interested in. @muratmaga even offered to help you with this, so you would probably not need to invest a lot effort. Would you consider this option?

I would love to actually be able to edit MorphoJ to be more amenable to landmark file formats as the file format necessary was based on outdated softwares, some of which arenā€™t even available anymore. In addition, I would love for MorphoJ to be able to import ids for the landmarks as it makes sense to be able to tag the plots with the landmark names and such. Finally, it would be awesome to use landmarked files created from scalismo that saves a bunch of extra information such as point uncertainty values (something that isnā€™t saved in slicer, which I think also should be) from the models, and be able to factor that in to the GMM analysis as wel, but alas as you mentioned MorphoJ is a closed off software, so all I can do is work with modifying things to import into it. I would love for us to be able to expand the capabilities of SlicerMorph with extra modules to encompass all of these tasks, but that would be a long-term goal as I am sure @muratmaga has enough on his plate already without me adding things like this to his to do list. I could do most of this myself if I had access to some examples of creating modules, pulling the PC data from slicerMorph without exporting it to separate files, and how to display results from analysis to slicer. I am just not familiar enough with building my own modules for data analysis. However, this is in general a longer-term project anyways as I need to have the protocol for the students ready by next semester, so for now I am trying to make do with using MorphoJ.

The RAS transformation to LPS is unnecessary as long as the coordinates are consistent between samples, and the coordinates are reasonable for calculating things like euclidean or procrustes distances. The code you have available requires the students to run a python script. I am trying to create a module for either saving the fiducials immediately as one batch csv/tsv/txt file, or as pulling a folder of fcsv files in and saving the batch file.

Yes, the Python function I provided could be wrapped in a very simple Slicer module with the input/output directories selected in the UI and sent to this function to run the batch conversion.

I think you can build onto the script @smrolfe provided to make it a self-sufficient module to read a directory of markups and turn them into a format MorphoJ understands. You may find developerā€™s tutorial useful to get started, and we can answer your questions.

https://github.com/PerkLab/PerkLabBootcamp/blob/master/Doc/day3_2_SlicerProgramming.pptx?raw=true

As an example of this sort of conversion tool, you can see the MorphologikaLMConverter in SlicerMorph. You can replace the input file selector with the input directory selector, and the run function with the convertMorphoJLM function I posted in this thread.

Ok, so thank you @smrolfe for all your help thus far. I have tried to use the MorphologikaLMConverter script and your previous script as templates for creating the new module. However, when I open 3D Slicer, while the module exists in the list of modules, when I click on it, the module does not display any of the parameter tools I have put into the script such as the input Directory selector, and such. Here is a screen shot:

The script is supposed to have an input directory selection, output directory selection, and output file name selection to run the conversion. Here is the code:

import os, glob
import unittest
import vtk, qt, ctk, slicer
from slicer.ScriptedLoadableModule import *
import logging

#
# ConvertMorphologikaLandmarks
#

class ExportMorphoJLandmarkFile(ScriptedLoadableModule):
  """Uses ScriptedLoadableModule base class, available at:
  https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  """

  def __init__(self, parent):
    ScriptedLoadableModule.__init__(self, parent)
    self.parent.title = "ExportMorphoJLandmarkFile"
    self.parent.categories = ["User_Specifications"]
    self.parent.dependencies = []
    self.parent.contributors = ["Steven Lewis (UB), Sara Rolfe (UW)"] # replace with "Firstname Lastname (Organization)"
    self.parent.helpText = """
This module parses fcsv landmark files for landmark coordinates and compiles them into a new csv file where each sample name is followed by the x,y or x,y,& z coordinates
for all landmarks tied to the file.
"""
    self.parent.helpText += self.getDefaultModuleDocumentationLink()
    self.parent.acknowledgementText = """
This module was developed by Steven Lewis with the assistance of Sara Rolfe.
""" # replace with organization, grant and thanks.

class ExportMorphoJLandmarkFileWidget(ScriptedLoadableModuleWidget):
  """Uses ScriptedLoadableModuleWidget base class, available at:
  https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  """

  def setup(self):
    ScriptedLoadableModuleWidget.setup(self)

    # Instantiate and connect widgets ...

    #
    # Parameters Area
    #
    parametersCollapsibleButton = ctk.ctkCollapsibleButton()
    parametersCollapsibleButton.text = "Parameters"
    self.layout.addWidget(parametersCollapsibleButton)

    # Layout within the dummy collapsible button
    parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)

    #
    # Select landmark folder to import
    #
    
    self.inputDirectorySelector = ctk.ctkDirectoryButton()
    self.inputDirectory.directory = gt.GDir.homePath()
    self.inputDirectory.setToolTip('Select Directory with fcsv lanmark files')
    parametersFormLayout.addRow("Input Directory:", self.inputDirectory)
       
    #
    # output directory selector
    #
    self.outputDirectorySelector = ctk.ctkDirectoryButton()
    self.outputDirectory.directory = qt.QDir.homePath()
    parametersFormLayout.addRow("Output Directory:", self.outputDirectory)
    self.outputFileName = ctk.ctkPathLineEdit()
    parametersFormLayout.addRow("Name of output file:", self.outputFileName)

    #
    # Apply Button
    #
    self.applyButton = qt.QPushButton("Apply")
    self.applyButton.toolTip = "Run the compiling."
    self.applyButton.enabled = False
    parametersFormLayout.addRow(self.applyButton)

    # connections
    self.applyButton.connect('clicked(bool)', self.onApplyButton)
    self.inputDirectory.connect('validInputChanged(bool)', self.onSelectInput)
    self.outputDirectory.connect('validOutputChanged(bool)', self.onSelectOutput)
 
    # Add vertical spacer
    self.layout.addStretch(1)

    # Refresh Apply button state
    self.onSelectInput()
    #self.onSelectOutput()

  def cleanup(self):
    pass

  def onSelectInput(self):
    self.applyButton.enabled = bool(self.inputDirectory.directory) 


  def onApplyButton(self):
    logic = ExportMorphoJLandmarkFileLogic()
    logic.run(self.inputDirectory.directory, self.outputDirectory.directory, self. outputFileName)

#
# ExportMorphoJLandmarkFileLogic
#

class ExportMorphoJLandmarkFileLogic(ScriptedLoadableModuleLogic):
  """This class should implement all the actual
  computation done by your module.  The interface
  should be such that other python code can import
  this class and make use of the functionality without
  requiring an instance of the Widget.
  Uses ScriptedLoadableModuleLogic base class, available at:
  https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  """

  def run(self, LandmarkfolderName, outputDirectory, outputFileName):
    """
    Run the actual conversion
    """
    assert os.path.isfile(LandmarkfolderName), 'Path specified is file not folder'
        
    directory = glob.glob(LandmarkfolderName+'/.*.fcsv')
    
    samples = []
    LMMatrix = directory
    
    for i,file in enumerate(directory):
        success, currentLMNode = slicer.util.loadMarkupsFiducialList(file)
        print('{}/{}'.format(i, i/(len(directory)+1)))
        print(file)
        for i,s in enumerate(directory):
            s = s.split('\\')
            s = s[-1].split('.')[0]
            samples[i] = s
        landmarkNumber = currentLMNode.GetNumberOfFiducials()
        LMArray = np.zeros(len(directory), landmarkNumber*3)
        for pointIndex in range(currentLMNode.GetNumberOfMarkups()):
            point=currentLMNode.GetNthControlPointPositionVector(pointIndex)
            point.Set(point[0],point[1],point[2])
            LMArray[i, pointIndex:pointIndex+2]=point
            
        LMMatrix = np.hstack(LMMatrix, LMArray)
            
    slicer.mrmlScene.RemoveNode(currentLMNode)
    np.savetxt(os.path.join(outputDirectory, outputFieName), LMArray, fmt="%s", delimiter=",")
    
    assert os.path.isfile(os.path.join(outputDirectory, outputFileName)), 'File Not Created'
    logging.info('Processing Completed')
        
    return True

There is an unrecognized package ā€œgtā€ being used thatā€™s not imported. You can see this is in the Slicer error output:

Traceback (most recent call last):
  File "/Users/sararolfe/SlicerTemp/ExportMorphoJLandmarkFile/ExportMorphoJLandmarkFile.py", line 56, in setup
    self.inputDirectory.directory = gt.GDir.homePath()
NameError: name 'gt' is not defined

I tried this replacing gt.GDir.homePath() with qt.QDir.homePath(). I have also added fixes to correct some I/O errors below:

import os, glob
import unittest
import vtk, qt, ctk, slicer
from slicer.ScriptedLoadableModule import *
import logging

#
# ConvertMorphologikaLandmarks
#

class ExportMorphoJLandmarkFile(ScriptedLoadableModule):
  """Uses ScriptedLoadableModule base class, available at:
  https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  """

  def __init__(self, parent):
    ScriptedLoadableModule.__init__(self, parent)
    self.parent.title = "ExportMorphoJLandmarkFile"
    self.parent.categories = ["User_Specifications"]
    self.parent.dependencies = []
    self.parent.contributors = ["Steven Lewis (UB), Sara Rolfe (UW)"] # replace with "Firstname Lastname (Organization)"
    self.parent.helpText = """
This module parses fcsv landmark files for landmark coordinates and compiles them into a new csv file where each sample name is followed by the x,y or x,y,& z coordinates
for all landmarks tied to the file.
"""
    self.parent.helpText += self.getDefaultModuleDocumentationLink()
    self.parent.acknowledgementText = """
This module was developed by Steven Lewis with the assistance of Sara Rolfe.
""" # replace with organization, grant and thanks.

class ExportMorphoJLandmarkFileWidget(ScriptedLoadableModuleWidget):
  """Uses ScriptedLoadableModuleWidget base class, available at:
  https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  """

  def setup(self):
    ScriptedLoadableModuleWidget.setup(self)

    # Instantiate and connect widgets ...

    #
    # Parameters Area
    #
    parametersCollapsibleButton = ctk.ctkCollapsibleButton()
    parametersCollapsibleButton.text = "Parameters"
    self.layout.addWidget(parametersCollapsibleButton)

    # Layout within the dummy collapsible button
    parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)

    #
    # Select landmark folder to import
    #
    
    self.inputDirectory = ctk.ctkDirectoryButton()
    self.inputDirectory.directory = qt.QDir.homePath()
    self.inputDirectory.setToolTip('Select Directory with fcsv lanmark files')
    parametersFormLayout.addRow("Input Directory:", self.inputDirectory)
       
    #
    # output directory selector
    #
    self.outputDirectory = ctk.ctkDirectoryButton()
    self.outputDirectory.directory = qt.QDir.homePath()
    parametersFormLayout.addRow("Output Directory:", self.outputDirectory)
    
    #
    # enter filename for output
    #
    self.outputFileName = qt.QLineEdit()
    self.outputFileName.text = "morphoJOutput"
    parametersFormLayout.addRow("Name of output file:", self.outputFileName)

    #
    # Apply Button
    #
    self.applyButton = qt.QPushButton("Apply")
    self.applyButton.toolTip = "Run the compiling."
    self.applyButton.enabled = False
    parametersFormLayout.addRow(self.applyButton)

    # connections
    self.applyButton.connect('clicked(bool)', self.onApplyButton)
    self.inputDirectory.connect('validInputChanged(bool)', self.onSelectInput)
 
    # Add vertical spacer
    self.layout.addStretch(1)

    # Refresh Apply button state
    self.onSelectInput()
    #self.onSelectOutput()

  def cleanup(self):
    pass

  def onSelectInput(self):
    self.applyButton.enabled = bool(self.inputDirectory.directory) 


  def onApplyButton(self):
    logic = ExportMorphoJLandmarkFileLogic()
    outputFile = self.outputFileName.text + ".csv"
    logic.run(self.inputDirectory.directory, self.outputDirectory.directory, outputFile)

#
# ExportMorphoJLandmarkFileLogic
#

class ExportMorphoJLandmarkFileLogic(ScriptedLoadableModuleLogic):
  """This class should implement all the actual
  computation done by your module.  The interface
  should be such that other python code can import
  this class and make use of the functionality without
  requiring an instance of the Widget.
  Uses ScriptedLoadableModuleLogic base class, available at:
  https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  """

  def run(self, LandmarkfolderName, outputDirectory, outputFileName):
    """
    Run the actual conversion
    """
    assert os.path.isdir(LandmarkfolderName), 'Path specified is file not folder'      
    directory = glob.glob(LandmarkfolderName+'/.*.fcsv')   
    samples = []
    LMMatrix = directory
    if(directory==[]):
      logging.info('No FCSV files found in input directory.')
      return False
    for i,file in enumerate(directory):
      success, currentLMNode = slicer.util.loadMarkupsFiducialList(file)
      print('{}/{}'.format(i, i/(len(directory)+1)))
      print(file)
      for i,s in enumerate(directory):
        s = s.split('\\')
        s = s[-1].split('.')[0]
        samples[i] = s
      landmarkNumber = currentLMNode.GetNumberOfFiducials()
      LMArray = np.zeros(len(directory), landmarkNumber*3)
      for pointIndex in range(currentLMNode.GetNumberOfMarkups()):
        point=currentLMNode.GetNthControlPointPositionVector(pointIndex)
        point.Set(point[0],point[1],point[2])
        LMArray[i, pointIndex:pointIndex+2]=point
            
      LMMatrix = np.hstack(LMMatrix, LMArray)
      slicer.mrmlScene.RemoveNode(currentLMNode)
      np.savetxt(os.path.join(outputDirectory, outputFileName), LMArray, fmt="%s", delimiter=",")
    
    assert os.path.isfile(os.path.join(outputDirectory, outputFileName)), 'File Not Created'
    logging.info('Processing Completed')
        
    return True

Side note: Whenever you are sharing/updating more than 10-20 lines of source code, you could consider uploading it to a gist. It is easier to view copy, and edit the code and the change history is stored, so you can see what fixes/improvements were made over time.

Thanks @lassoan this is a great tool!

Iā€™ve created a gist with my changes tracked here

1 Like

So I reviewed the gist you created and edited my own copy, as well as tried just downloading your python file, but Slicer is still not creating any of the parameter fields just like before.

@stevenagl12 Have you checked the Slicer error log? Could you post the output here?

It was a stupid mistake. Both when I downloaded the file from your account and when I went to adit my own script I accidentally entered an extra letter, albeit at different points, but it is fixed. The only problem I have now is when I actually run the script it says:

File ā€œC:/Users/Steven Lewis/AppData/Roaming/NA-MIC/Extensions-28438/user_specifications/lib/Slicer-4.11/qt-scripted-modules/ExportMorphoJLandmarkFile.pyā€, line 133, in run
success, currentLMNode = slicer.util.loadMarkupsFiducialList(file)
TypeError: ā€˜vtkSlicerMarkupsModuleMRMLPython.vtkMRMLMarkupsFiducialNodeā€™ object is not iterable

as the error in the log.

@stevenagl12 I replaced your run function with the one I shared previously itā€™s working for me. Iā€™ve made that change to the gist. If you need to change the formatting of the output, you might want to start from here.

So, I copied and pasted your script from github directly into a new Slicer module and only changed the module name, filename, and names of the classes to make sure that I wasnā€™t choosing the wrong module (ExportMorphoJFile instead of ExportMorphoJLandmarkFile). As you can see: this is the run function where nothing was changed: image
However, when I run this function it still gives me:
image