Skip to content

Commit

Permalink
BUG: Make Segment Editor effect names translatable
Browse files Browse the repository at this point in the history
Segment Editor effects have now separate "name" (for modules to refer to the effect) and "title" (displayed on the GUI).

This partially fixes these issues:
- Slicer#6177
- Slicer/SlicerLanguagePacks#12
  • Loading branch information
mhdiop authored and lassoan committed Oct 3, 2023
1 parent 0c6e9a9 commit 222213b
Show file tree
Hide file tree
Showing 38 changed files with 381 additions and 282 deletions.
26 changes: 23 additions & 3 deletions Base/Python/slicer/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ def translate(context, text):


def getContext(sourceFile):
"""Get the translation context based on the source file name."""
"""Get the translation context name.
The context name is constructed from the Python module name (name of the folder that contains `__init__.py` file - if exists)
and the source file name (without the .py extension).
Most Slicer modules do not have a __init__.py file in their folder, so the context name is simply the source file name
(for example, `DICOMEnhancedUSVolumePlugin`).
Most helper Python scripts in Slicer are Python modules (subfolders containing addition Python scripts and an __init__.py file)
and their name is constructed as PythonModuleName.SourceFileName (for example, `SegmentEditorEffects.SegmentEditorDrawEffect.`).
"""
if os.path.isfile(sourceFile):
parentFolder = os.path.dirname(sourceFile)
init_file_path = parentFolder + os.path.sep + '__init__.py'
Expand All @@ -28,8 +38,18 @@ def getContext(sourceFile):


def tr(text):
"""Translation function for python scripted modules that gets context name from filename.
Experimental, not used yet."""
"""Translation function for python scripted modules that automatically determines context name.
This is more convenient to use than `translate(context, text)` because the developer does not need to manually specify the context.
This function is typically imported as `_` function.
Example::
from slicer.i18n import tr as _
...
statusText = _("Idle") if idle else _("Running")
"""
filename = inspect.stack()[1][1]
contextName = getContext(filename)
return translate(contextName, text)
2 changes: 1 addition & 1 deletion Modules/Loadable/Models/qSlicerModelsModule.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ QString qSlicerModelsModule::helpText()const
//-----------------------------------------------------------------------------
QString qSlicerModelsModule::acknowledgementText()const
{
return "This work was partially funded by NIH grants 3P41RR013218-12S1 and R01CA184354.";
return tr("This work was partially funded by NIH grants 3P41RR013218-12S1 and R01CA184354.");
}

//-----------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import vtk

import slicer
from slicer.i18n import tr as _

from .AbstractScriptedSegmentEditorEffect import *

Expand Down Expand Up @@ -86,14 +87,14 @@ def isBackgroundLabelmap(labelmapOrientedImageData, label=None):
return False

def setupOptionsFrame(self):
self.autoUpdateCheckBox = qt.QCheckBox("Auto-update")
self.autoUpdateCheckBox.setToolTip("Auto-update results preview when input segments change.")
self.autoUpdateCheckBox = qt.QCheckBox(_("Auto-update"))
self.autoUpdateCheckBox.setToolTip(_("Auto-update results preview when input segments change."))
self.autoUpdateCheckBox.setChecked(True)
self.autoUpdateCheckBox.setEnabled(False)

self.previewButton = qt.QPushButton("Initialize")
self.previewButton = qt.QPushButton(_("Initialize"))
self.previewButton.objectName = self.__class__.__name__ + 'Preview'
self.previewButton.setToolTip("Preview complete segmentation")
self.previewButton.setToolTip(_("Preview complete segmentation"))
# qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
# fails on some systems, therefore set the policies using separate method calls
qSize = qt.QSizePolicy()
Expand All @@ -103,35 +104,35 @@ def setupOptionsFrame(self):
previewFrame = qt.QHBoxLayout()
previewFrame.addWidget(self.autoUpdateCheckBox)
previewFrame.addWidget(self.previewButton)
self.scriptedEffect.addLabeledOptionsWidget("Preview:", previewFrame)
self.scriptedEffect.addLabeledOptionsWidget(_("Preview:"), previewFrame)

self.previewOpacitySlider = ctk.ctkSliderWidget()
self.previewOpacitySlider.setToolTip("Adjust visibility of results preview.")
self.previewOpacitySlider.setToolTip(_("Adjust visibility of results preview."))
self.previewOpacitySlider.minimum = 0
self.previewOpacitySlider.maximum = 1.0
self.previewOpacitySlider.value = 0.0
self.previewOpacitySlider.singleStep = 0.05
self.previewOpacitySlider.pageStep = 0.1
self.previewOpacitySlider.spinBoxVisible = False

self.previewShow3DButton = qt.QPushButton("Show 3D")
self.previewShow3DButton.setToolTip("Preview results in 3D.")
self.previewShow3DButton = qt.QPushButton(_("Show 3D"))
self.previewShow3DButton.setToolTip(_("Preview results in 3D."))
self.previewShow3DButton.setCheckable(True)

displayFrame = qt.QHBoxLayout()
displayFrame.addWidget(qt.QLabel("inputs"))
displayFrame.addWidget(qt.QLabel(_("inputs")))
displayFrame.addWidget(self.previewOpacitySlider)
displayFrame.addWidget(qt.QLabel("results"))
displayFrame.addWidget(qt.QLabel(_("results")))
displayFrame.addWidget(self.previewShow3DButton)
self.scriptedEffect.addLabeledOptionsWidget("Display:", displayFrame)
self.scriptedEffect.addLabeledOptionsWidget(_("Display:"), displayFrame)

self.cancelButton = qt.QPushButton("Cancel")
self.cancelButton = qt.QPushButton(_("Cancel"))
self.cancelButton.objectName = self.__class__.__name__ + 'Cancel'
self.cancelButton.setToolTip("Clear preview and cancel auto-complete")
self.cancelButton.setToolTip(_("Clear preview and cancel auto-complete"))

self.applyButton = qt.QPushButton("Apply")
self.applyButton = qt.QPushButton(_("Apply"))
self.applyButton.objectName = self.__class__.__name__ + 'Apply'
self.applyButton.setToolTip("Replace segments by previewed result")
self.applyButton.setToolTip(_("Replace segments by previewed result"))

finishFrame = qt.QHBoxLayout()
finishFrame.addWidget(self.cancelButton)
Expand Down Expand Up @@ -244,13 +245,13 @@ def updateGUIFromMRML(self):
wasBlocked = self.previewOpacitySlider.blockSignals(True)
self.previewOpacitySlider.value = self.getPreviewOpacity()
self.previewOpacitySlider.blockSignals(wasBlocked)
self.previewButton.text = "Update"
self.previewButton.text = _("Update")
self.previewShow3DButton.setEnabled(True)
self.previewShow3DButton.setChecked(self.getPreviewShow3D())
self.autoUpdateCheckBox.setEnabled(True)
self.observeSegmentation(self.autoUpdateCheckBox.isChecked())
else:
self.previewButton.text = "Initialize"
self.previewButton.text = _("Initialize")
self.autoUpdateCheckBox.setEnabled(False)
self.previewShow3DButton.setEnabled(False)
self.delayedAutoUpdateTimer.stop()
Expand All @@ -276,7 +277,7 @@ def onPreview(self):
return
self.previewComputationInProgress = True

slicer.util.showStatusMessage(f"Running {self.scriptedEffect.name} auto-complete...", 2000)
slicer.util.showStatusMessage(_("Running {effectName} auto-complete...").format(effectName=self.scriptedEffect.name), 2000)
try:
# This can be a long operation - indicate it to the user
qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
Expand Down Expand Up @@ -404,7 +405,8 @@ def effectiveExtentChanged(self):
# Current extent used for auto-complete preview
currentLabelExtent = self.mergedLabelmapGeometryImage.GetExtent()

# Determine if the current merged labelmap extent has less than a 3 voxel margin around the effective segment extent (limited by the master image extent)
# Determine if the current merged labelmap extent has less than a 3 voxel margin around the effective segment extent
# (limited by the master image extent)
return ((masterImageExtent[0] != currentLabelExtent[0] and currentLabelExtent[0] > effectiveLabelExtent[0] - self.minimumExtentMargin) or
(masterImageExtent[1] != currentLabelExtent[1] and currentLabelExtent[1] < effectiveLabelExtent[1] + self.minimumExtentMargin) or
(masterImageExtent[2] != currentLabelExtent[2] and currentLabelExtent[2] > effectiveLabelExtent[2] - self.minimumExtentMargin) or
Expand Down Expand Up @@ -522,8 +524,9 @@ def preview(self):
# as the closed surfaces will be converted as necessary by the segmentation logic.

mergedImage = slicer.vtkOrientedImageData()
segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage,
vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.mergedLabelmapGeometryImage, self.selectedSegmentIds)
segmentationNode.GenerateMergedLabelmapForAllSegments(
mergedImage,
vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.mergedLabelmapGeometryImage, self.selectedSegmentIds)

outputLabelmap = slicer.vtkOrientedImageData()
self.computePreviewLabelmap(mergedImage, outputLabelmap)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
#-----------------------------------------------------------------------------
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/SegmentEditorEffects.__init__.py.in
${CMAKE_BINARY_DIR}/${Slicer_QTSCRIPTEDMODULES_LIB_DIR}/SegmentEditorEffects/__init__.py
)

#-----------------------------------------------------------------------------
set(SegmentEditorEffects_PYTHON_SCRIPTS
__init__
AbstractScriptedSegmentEditorEffect
AbstractScriptedSegmentEditorLabelEffect
AbstractScriptedSegmentEditorPaintEffect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import vtk

import slicer
from slicer.i18n import tr as _

from SegmentEditorEffects import *

Expand All @@ -15,7 +16,8 @@ class SegmentEditorDrawEffect(AbstractScriptedSegmentEditorLabelEffect):
"""

def __init__(self, scriptedEffect):
scriptedEffect.name = 'Draw'
scriptedEffect.name = 'Draw' # no tr (don't translate it because modules find effects by this name)
scriptedEffect.title = _('Draw')
self.drawPipelines = {}
AbstractScriptedSegmentEditorLabelEffect.__init__(self, scriptedEffect)

Expand All @@ -32,13 +34,13 @@ def icon(self):
return qt.QIcon()

def helpText(self):
return """<html>Draw segment outline in slice viewers<br>.
<p><ul style="margin: 0">
<li><b>Left-click:</b> add point.</li>
<li><b>Left-button drag-and-drop:</b> add multiple points.</li>
<li><b>x:</b> delete last point.</li>
<li><b>Double-left-click</b> or <b>right-click</b> or <b>a</b> or <b>enter</b>: apply outline.</li>
</ul><p></html>"""
return _("""<html>Draw segment outline in slice viewers<br>.
<p><ul style="margin: 0">
<li><b>Left-click:</b> add point.</li>
<li><b>Left-button drag-and-drop:</b> add multiple points.</li>
<li><b>x:</b> delete last point.</li>
<li><b>Double-left-click</b> or <b>right-click</b> or <b>a</b> or <b>enter</b>: apply outline.</li>
</ul><p></html>""")

def deactivate(self):
# Clear draw pipelines
Expand Down Expand Up @@ -92,7 +94,8 @@ def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
sliceNode = viewWidget.sliceLogic().GetSliceNode()
pipeline.lastInsertSliceNodeMTime = sliceNode.GetMTime()
abortEvent = True
elif (eventId == vtk.vtkCommand.RightButtonReleaseEvent and pipeline.actionState == "finishing") or (eventId == vtk.vtkCommand.LeftButtonDoubleClickEvent and not anyModifierKeyPressed):
elif ((eventId == vtk.vtkCommand.RightButtonReleaseEvent and pipeline.actionState == "finishing")
or (eventId == vtk.vtkCommand.LeftButtonDoubleClickEvent and not anyModifierKeyPressed)):
abortEvent = (pipeline.rasPoints.GetNumberOfPoints() > 1)
sliceNode = viewWidget.sliceLogic().GetSliceNode()
if abs(pipeline.lastInsertSliceNodeMTime - sliceNode.GetMTime()) < 2:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import qt
import vtk

from slicer.i18n import tr as _

from SegmentEditorEffects import *


Expand All @@ -14,7 +16,8 @@ class SegmentEditorFillBetweenSlicesEffect(AbstractScriptedSegmentEditorAutoComp

def __init__(self, scriptedEffect):
AbstractScriptedSegmentEditorAutoCompleteEffect.__init__(self, scriptedEffect)
scriptedEffect.name = 'Fill between slices'
scriptedEffect.name = 'Fill between slices' # no tr (don't translate it because modules find effects by this name)
scriptedEffect.title = _('Fill between slices')

def clone(self):
import qSlicerSegmentationsEditorEffectsPythonQt as effects
Expand All @@ -29,17 +32,17 @@ def icon(self):
return qt.QIcon()

def helpText(self):
return """<html>Interpolate segmentation between slices<br>. Instructions:
<p><ul>
<li>Create complete segmentation on selected slices using any editor effect.
Segmentation will only expanded if a slice is segmented but none of the direct neighbors are segmented, therefore
do not use sphere brush with Paint effect and always leave at least one empty slice between segmented slices.</li>
<li>All visible segments will be interpolated, not just the selected segment.</li>
<li>The complete segmentation will be created by interpolating segmentations in empty slices.</li>
</ul><p>
Masking settings are ignored. If segments overlap, segment higher in the segments table will have priority.
The effect uses <a href="https://insight-journal.org/browse/publication/977">morphological contour interpolation method</a>.
<p></html>"""
return _("""<html>Interpolate segmentation between slices<br>. Instructions:
<p><ul>
<li>Create complete segmentation on selected slices using any editor effect.
Segmentation will only expanded if a slice is segmented but none of the direct neighbors are segmented, therefore
do not use sphere brush with Paint effect and always leave at least one empty slice between segmented slices.</li>
<li>All visible segments will be interpolated, not just the selected segment.</li>
<li>The complete segmentation will be created by interpolating segmentations in empty slices.</li>
</ul><p>
Masking settings are ignored. If segments overlap, segment higher in the segments table will have priority.
The effect uses <a href="https://insight-journal.org/browse/publication/977">morphological contour interpolation method</a>.
<p></html>""")

def computePreviewLabelmap(self, mergedImage, outputLabelmap):
import vtkITK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import vtk

import slicer
from slicer.i18n import tr as _

from SegmentEditorEffects import *

Expand All @@ -18,7 +19,8 @@ class SegmentEditorGrowFromSeedsEffect(AbstractScriptedSegmentEditorAutoComplete

def __init__(self, scriptedEffect):
AbstractScriptedSegmentEditorAutoCompleteEffect.__init__(self, scriptedEffect)
scriptedEffect.name = 'Grow from seeds'
scriptedEffect.name = 'Grow from seeds' # no tr (don't translate it because modules find effects by this name)
scriptedEffect.title = _('Grow from seeds')
self.minimumNumberOfSegments = 2
self.clippedMasterImageDataRequired = True # source volume intensities are used by this effect
self.clippedMaskImageDataRequired = True # masking is used
Expand All @@ -37,21 +39,21 @@ def icon(self):
return qt.QIcon()

def helpText(self):
return """<html>Growing segments to create complete segmentation<br>.
Location, size, and shape of initial segments and content of source volume are taken into account.
Final segment boundaries will be placed where source volume brightness changes abruptly. Instructions:<p>
<ul style="margin: 0">
<li>Use Paint or other offects to draw seeds in each region that should belong to a separate segment.
Paint each seed with a different segment. Minimum two segments are required.</li>
<li>Click <dfn>Initialize</dfn> to compute preview of full segmentation.</li>
<li>Browse through image slices. If previewed segmentation result is not correct then switch to
Paint or other effects and add more seeds in the misclassified region. Full segmentation will be
updated automatically within a few seconds</li>
<li>Click <dfn>Apply</dfn> to update segmentation with the previewed result.</li>
</ul><p>
If segments overlap, segment higher in the segments table will have priority.
The effect uses <a href="http://interactivemedical.org/imic2014/CameraReadyPapers/Paper%204/IMIC_ID4_FastGrowCut.pdf">fast grow-cut method</a>.
<p></html>"""
return _("""<html>Growing segments to create complete segmentation<br>.
Location, size, and shape of initial segments and content of source volume are taken into account.
Final segment boundaries will be placed where source volume brightness changes abruptly. Instructions:<p>
<ul style="margin: 0">
<li>Use Paint or other offects to draw seeds in each region that should belong to a separate segment.
Paint each seed with a different segment. Minimum two segments are required.</li>
<li>Click <dfn>Initialize</dfn> to compute preview of full segmentation.</li>
<li>Browse through image slices. If previewed segmentation result is not correct then switch to
Paint or other effects and add more seeds in the misclassified region. Full segmentation will be
updated automatically within a few seconds</li>
<li>Click <dfn>Apply</dfn> to update segmentation with the previewed result.</li>
</ul><p>
If segments overlap, segment higher in the segments table will have priority.
The effect uses <a href="http://interactivemedical.org/imic2014/CameraReadyPapers/Paper%204/IMIC_ID4_FastGrowCut.pdf">fast grow-cut method</a>.
<p></html>""")

def reset(self):
self.growCutFilter = None
Expand All @@ -70,10 +72,10 @@ def setupOptionsFrame(self):
self.seedLocalityFactorSlider.decimals = 1
self.seedLocalityFactorSlider.singleStep = 0.1
self.seedLocalityFactorSlider.pageStep = 1.0
self.seedLocalityFactorSlider.setToolTip('Increasing this value makes the effect of seeds more localized,'
' thereby reducing leaks, but requires seed regions to be more evenly distributed in the image.'
' The value is specified as an additional "intensity level difference" per "unit distance."')
self.scriptedEffect.addLabeledOptionsWidget("Seed locality:", self.seedLocalityFactorSlider)
self.seedLocalityFactorSlider.setToolTip(_('Increasing this value makes the effect of seeds more localized,'
' thereby reducing leaks, but requires seed regions to be more evenly distributed in the image.'
' The value is specified as an additional "intensity level difference" per "unit distance."'))
self.scriptedEffect.addLabeledOptionsWidget(_("Seed locality:"), self.seedLocalityFactorSlider)
self.seedLocalityFactorSlider.connect('valueChanged(double)', self.updateAlgorithmParameterFromGUI)

def setMRMLDefaults(self):
Expand Down
Loading

0 comments on commit 222213b

Please sign in to comment.