diff --git a/CHANGES.txt b/CHANGES.txt index 52332ae..a566844 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +v3.2.0 + - The annotator was generalized to make use of tomomask created by other softwares + - The helps were enhanced this affects to protocols and parameters v3.1.2: - Place correctly the protocols on the left panel. - Fix execution in CentOS or other distros diff --git a/tomosegmemtv/protocols/protocol_annotate_membranes.py b/tomosegmemtv/protocols/protocol_annotate_membranes.py index 4714138..c5db0bd 100644 --- a/tomosegmemtv/protocols/protocol_annotate_membranes.py +++ b/tomosegmemtv/protocols/protocol_annotate_membranes.py @@ -24,23 +24,36 @@ # ************************************************************************** import glob from enum import Enum +from os.path import join, basename from pwem.protocols import EMProtocol from pyworkflow.object import Integer from pyworkflow.protocol import PointerParam -from pyworkflow.utils import removeBaseExt +from pyworkflow.utils import removeBaseExt, makePath, createLink, replaceBaseExt from tomo.objects import SetOfTomoMasks, TomoMask from tomosegmemtv.viewers_interactive.memb_annotator_tomo_viewer import MembAnnotatorDialog from tomosegmemtv.viewers_interactive.memb_annotator_tree import MembAnnotatorProvider +EXT_MRC = '.mrc' +FLT_SUFFIX = '_flt' class outputObjects(Enum): tomoMasks = SetOfTomoMasks class ProtAnnotateMembranes(EMProtocol): - """ Manual annotation tool for segmented membranes + """ Manual annotation tool for segmented membranes\n + + The annotation tool will open a graphical interface that will allow to manually + label the set of tomo mask. The graphical interface will call the function membseg2 + for supervising the segmentation. This graphical interface was slightly modified + in collaboration with the autor for simplifying its use. + + A complete tutorial about the use of this tool can be seen in: + + https://scipion-em.github.io/docs/release-3.0.0/docs/user/denoising_mbSegmentation_pysegDirPicking/tomosegmemTV-pySeg-workflow.html#membrane-annotation + """ _label = 'annotate segmented membranes' _possibleOutputs = outputObjects @@ -49,7 +62,7 @@ def __init__(self, **kwargs): EMProtocol.__init__(self, **kwargs) self._objectsToGo = Integer() self._provider = None - self._tomoList = None + self._tomoMaskDict = None def _defineParams(self, form): @@ -61,12 +74,56 @@ def _defineParams(self, form): allowsNull=False, help='Select the Tomogram Masks (segmented tomograms) for the membrane annotation.') + form.addParam('inputTomos', PointerParam, + label="Tomograms (Optional, only used for visualization)", + pointerClass='SetOfTomograms', + allowsNull=True, + help='Select the the set of tomogram used for obtaining the Tomo Masks. This set will' + 'only be used for visualization purpose in order to simplify the annotation. Of the ' + 'tomo masks.') + # --------------------------- INSERT steps functions ---------------------- def _insertAllSteps(self): + self._initialize() + self._insertFunctionStep(self.convertInputStep) self._insertFunctionStep(self.runMembraneAnnotator, interactive=True) # --------------------------- STEPS functions ----------------------------- + def convertInputStep(self): + ''' + In this convert we perform two things: + 1) A folder per tomogram is created. The name of the folder is the TsId + 2) Symbolic link are created: The annotator expects two files the tomogram and the tomomask. + The files must have .mrc extension. The tomomask should also contain the suffix _flt. + The function will create two symbolic links, one for the tomo mask and one for the tomogram. + Due to the tomograms are not mandatory in the form. If the tomogram is not provided the second + symbolic link will also point to the tomomask. + ''' + + for tsId, tomoMask in self._tomoMaskDict.items(): + tsIdPath = self._getExtraPath(tsId) + makePath(tsIdPath) + + createLink(tomoMask.getFileName(), self.getfltFile(tomoMask, FLT_SUFFIX + EXT_MRC)) + if self.inputTomos.get(): + tomo = self._tomoDict.get(tsId, None) + if tomo: + createLink(tomo.getFileName(), self.getTomoMaskFile(tomo)) + else: + createLink(tomoMask.getFileName(), self.getTomoMaskFile(tomoMask)) + else: + createLink(tomoMask.getFileName(), self.getfltFile(tomoMask) + EXT_MRC) + + def getfltFile(self, tomoMask, suffix=''): + tsId = tomoMask.getTsId() + return self._getExtraPath(tsId, removeBaseExt(tomoMask.getFileName().replace('_flt', '')) + suffix) + + def getTomoMaskFile(self, tomoMask): + tsId = tomoMask.getTsId() + return self._getExtraPath(tsId, basename(tomoMask.getFileName())) + + def runMembraneAnnotator(self): # There are still some objects which haven't been annotated --> launch GUI self._getAnnotationStatus() @@ -94,17 +151,29 @@ def _summary(self): summary.append('All segmentations have been already annotated.') return summary + def _validate(self): + error = [] + # This is a tolerance in the sampling rate to ensure that tomoMask and tomograms have similar pixel size + tolerance = 0.001 + if self.inputTomos.get(): + if abs(self.inputTomos.get().getSamplingRate() - self.inputTomoMasks.get().getSamplingRate()) > tolerance: + error.append('The sampling rate of the tomograms does not match the sampling rate of the input masks') + + return error + # --------------------------- UTIL functions ----------------------------------- def _initialize(self): - self._tomoList = [tomo.clone() for tomo in self.inputTomoMasks.get().iterItems()] - self._provider = MembAnnotatorProvider(self._tomoList, self._getExtraPath(), 'membAnnotator') + self._tomoMaskDict = {tomoMask.getTsId(): tomoMask.clone() for tomoMask in self.inputTomoMasks.get().iterItems()} + if self.inputTomos.get(): + self._tomoDict = {tomo.getTsId(): tomo.clone() for tomo in self.inputTomos.get().iterItems()} + self._provider = MembAnnotatorProvider(list(self._tomoMaskDict.values()), self._getExtraPath(), 'membAnnotator') self._getAnnotationStatus() def _getAnnotationStatus(self): """Check if all the tomo masks have been annotated and store current status in a text file""" - doneTomes = [self._provider.getObjectInfo(tomo)['tags'] == 'done' for tomo in self._tomoList] - self._objectsToGo.set(len(self._tomoList) - sum(doneTomes)) + doneTomes = [self._provider.getObjectInfo(tomo)['tags'] == 'done' for tomo in list(self._tomoMaskDict.values())] + self._objectsToGo.set(len(self._tomoMaskDict) - sum(doneTomes)) def _getCurrentTomoMaskFile(self, inTomoFile): baseName = removeBaseExt(inTomoFile) diff --git a/tomosegmemtv/protocols/protocol_resize_tomomask.py b/tomosegmemtv/protocols/protocol_resize_tomomask.py index 9d22109..a472bd6 100644 --- a/tomosegmemtv/protocols/protocol_resize_tomomask.py +++ b/tomosegmemtv/protocols/protocol_resize_tomomask.py @@ -41,7 +41,15 @@ class outputObjects(Enum): class ProtResizeSegmentedVolume(EMProtocol): - """Resize segmented volumes or annotated (TomoMasks).""" + """Resize segmented volumes or annotated (TomoMasks). + + Given a TomoMask and a Tomogram the tomoMask will be upsampled or downsampled to + according to the sampling rate of the input tomograms. The outpu tomoMasks will + have the same sampling rate than the Tomograms. + + The used algorithm for scaling is based on splines, see: + https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.zoom.html + """ _label = 'Resize segmented or annotated volume' _possibleOutputs = outputObjects diff --git a/tomosegmemtv/protocols/protocol_tomosegmentv.py b/tomosegmemtv/protocols/protocol_tomosegmentv.py index 1ae3ac1..2fcff48 100644 --- a/tomosegmemtv/protocols/protocol_tomosegmentv.py +++ b/tomosegmemtv/protocols/protocol_tomosegmentv.py @@ -51,7 +51,35 @@ class outputObjects(Enum): class ProtTomoSegmenTV(EMProtocol): - """Segment membranes in tomograms""" + """TomoSegMemTV is a software suite for segmenting membranes in tomograms. The method + is based on (1) a Gaussian-like model of membrane profile, (2) a local differential structure + approach and (3) anisotropic propagation of the local structural information using the tensor + voting algorithm. In particular, it makes use of the next steps\n + + _1 Scale-space_: This stage allows isolation of the information according to the spatial + scale by filtering out features with a size smaller than the given scale. It basically + consists of a Gaussian filtering.\n + _2 Dense Tensor voting_: In this stage, the voxels of the input tomogram communicate among + themselves by propagating local structural information between each other. The local + information is encoded in a second order tensor, called vote. The local properties at each + voxel are then refined according to the information received from the neighbors. + Voxels belonging to the same geometric feature will have strengthened each other and their + tensors will have been modified to enhance the underlying global structure.\n + + _3 Surfaceness/Saliency_: This stage applies a local detector based on the + Gaussian membrane model. The local detector relies on differential information, + as it has to analyze local structure. In order to make it invariant to the membrane + direction, the detector is established along the normal to the membrane at the local + scale. An eigen-analysis of the Hessian tensor is well suited to determine such + direction and provide the membrane-strength (M) for each voxel. Only voxels with + membrane-strength higher than a threshold are considered and subjected to a non-maximum + supression (NMS) operation so as to give a 1-voxel-thick surface. The final output map + consists in planarity descriptors that represent the actual probability of + belonging to a true surface (hence surfaceness).\n + + Once these protocols ends, it is neccesary to threshold the output map. + + """ _label = 'tomogram segmentation' _possibleOutputs = outputObjects @@ -67,7 +95,8 @@ def _defineParams(self, form): form.addParam('inTomograms', PointerParam, pointerClass='SetOfTomograms', allowsNull=False, - label='Input tomograms') + label='Input tomograms', + help='This is the set of tomograms to be segmented obtaining tomo Masks') form.addParam('mbThkPix', IntParam, allowsNull=False, @@ -75,23 +104,31 @@ def _defineParams(self, form): validators=[GT(0)], label='Membrane thickness (voxels)', help='It basically represents the standard deviation of a Gaussian filtering. ' + 'This parameter should represent the thickness (in pixels) of the membranes sought.' + 'So, visual inspection of the tomogram helps the user to find out a proper value.' 'In general, any value in a range around that thickness works well. Too low ' 'values may make spurious details produce false positives at the local membrane ' 'detector while too high values may excessively smear out the membranes, which ' - 'in turn may produce discontinuities in the segmentation results.' - ) + 'in turn may produce discontinuities in the segmentation results. ' + 'This parameter is used in the scale-space step') + form.addParam('mbScaleFactor', IntParam, allowsNull=False, default=10, validators=[GT(0)], label='Membrane scale factor (voxels)', - help='This defines the effective neighborhood involved in the voting process. ' - 'Depending on the thickness of the membranes in the tomogram, lower (for ' - 'thinner membranes) or higher values (for thicker ones) may be more appropriate.' + help='This parameter is used for tensor voting. This defines the effective neighborhood ' + 'involved in the voting process. Depending on the thickness of the membranes in ' + 'the tomogram, lower (for thinner membranes) or higher values (for thicker ones)' + ' may be more appropriate.' ) + form.addParam('blackOverWhite', BooleanParam, label='Is black over white?', - default=True + default=True, + help = 'By default, the program assumes that the features to detect (foreground/membranes) ' + 'are black (darker) over white (lighter) background. This is normally the case ' + 'in cryo-tomography.' ) group = form.addGroup('Membrane delineation', expertLevel=LEVEL_ADVANCED) @@ -101,13 +138,14 @@ def _defineParams(self, form): validators=[GT(0)], expertLevel=LEVEL_ADVANCED, label='Membrane-strength threshold', - help='Allow the user tune the amount of output membrane points and remove false positives. ' - 'Only voxels with values of membrane-strength threshold higher than this value ' + help='Allows the user to specify a threshold for the membrane-strength. ' + 'Only voxels with values than the membrane-strength threshold ' 'will be considered as potential membrane points, and planarity descriptors will ' 'be calculated for them. Higher values will generate less membrane points, at the ' 'risk of producing gaps in the membranes. Lower values will provide more membrane ' - 'points, at the risk of generating false positives.' - ) + 'points, at the risk of generating false positives.\n' + 'Check the gray level of the membranes of the input images to introduce a proper ' + 'value.') group.addParam('sigmaS', FloatParam, label='Sigma for the initial gaussian filtering', default=1, @@ -143,7 +181,7 @@ def _defineParams(self, form): ' - Saliency --> *filename%s.mrc*' % (S2, TV, SURF, TV2, FLT) ) - form.addParallelSection(threads=8, mpi =1) + form.addParallelSection(threads=8, mpi=1) def _insertAllSteps(self): self._insertFunctionStep(self.convertInputStep) @@ -220,7 +258,7 @@ def _summary(self): def _validate(self): if not os.path.exists(Plugin.getProgram(SCALE_SPACE)): return ["%s is not at %s. Review installation. Please go to %s for instructions." % - (SCALE_SPACE, Plugin.getProgram(SCALE_SPACE),Plugin.getUrl())] + (SCALE_SPACE, Plugin.getProgram(SCALE_SPACE), Plugin.getUrl())] # --------------------------- UTIL functions ----------------------------------- @@ -257,4 +295,3 @@ def _getSalCmd(self, inputFile, outputFile, Nthreads): outputCmd += '%s ' % outputFile outputCmd += ' -t %i' % Nthreads return outputCmd - diff --git a/tomosegmemtv/viewers_interactive/memb_annotator_tomo_viewer.py b/tomosegmemtv/viewers_interactive/memb_annotator_tomo_viewer.py index 0111855..9e7fea9 100644 --- a/tomosegmemtv/viewers_interactive/memb_annotator_tomo_viewer.py +++ b/tomosegmemtv/viewers_interactive/memb_annotator_tomo_viewer.py @@ -68,10 +68,9 @@ def launchMembAnnotatorForTomogram(self, tomoMask): # different protocols. MembraneAnnotator expects both to be in the same location, so a symbolic link is # is generated in the extra dir of the segmentation protocol pointing to the selected tomogram print("\n==> Running Membrane Annotator:") - tomoNameSrc = abspath(tomoMask.getVolName()) - tomoName = abspath(join(getParentFolder(tomoMask.getFileName()), basename(tomoNameSrc))) - if not exists(tomoName): - symlink(tomoNameSrc, tomoName) + tomoName = abspath(self.prot.getfltFile(tomoMask, '.mrc')) + + tsId = tomoMask.getTsId() arguments = "inTomoFile '%s' " % tomoName - arguments += "outFilename '%s'" % abspath(join(self.path, removeBaseExt(tomoName))) + arguments += "outFilename '%s'" % abspath(self.prot._getExtraPath(tsId, tsId)) Plugin.runMembraneAnnotator(self.prot, arguments, env=Plugin.getMembSegEnviron(), cwd=self.path) diff --git a/tomosegmemtv/viewers_interactive/memb_annotator_tree.py b/tomosegmemtv/viewers_interactive/memb_annotator_tree.py index 6a1a4cd..1ccc8e2 100644 --- a/tomosegmemtv/viewers_interactive/memb_annotator_tree.py +++ b/tomosegmemtv/viewers_interactive/memb_annotator_tree.py @@ -33,16 +33,17 @@ class MembAnnotatorProvider(TomogramsTreeProvider): def getObjectInfo(self, inTomo): - tomogramName = removeBaseExt(inTomo.getVolName()) - filePath = join(self._path, tomogramName + "_materials.mrc") + + tsId = inTomo.getTsId() + filePath = join(self._path, tsId, tsId + "_materials.mrc") if not isfile(filePath): - return {'key': tomogramName, 'parent': None, - 'text': tomogramName, 'values': "PENDING", + return {'key': tsId, 'parent': None, + 'text': tsId, 'values': "PENDING", 'tags': "pending"} else: - return {'key': tomogramName, 'parent': None, - 'text': tomogramName, 'values': "DONE", + return {'key': tsId, 'parent': None, + 'text': tsId, 'values': "DONE", 'tags': "done"} def getColumns(self):