Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalization in annotator, helps and use of the plugin #34

Open
wants to merge 4 commits into
base: devel
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
v3.2.0
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we bump the version in the init and release this?

- 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
Expand Down
85 changes: 78 additions & 7 deletions tomosegmemtv/protocols/protocol_annotate_membranes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):

Expand All @@ -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()
Expand Down Expand Up @@ -94,17 +151,31 @@ 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')
import time
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we do not want this delay?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do not, solved

time.sleep(12)
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()}
pconesa marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand Down
10 changes: 9 additions & 1 deletion tomosegmemtv/protocols/protocol_resize_tomomask.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 52 additions & 15 deletions tomosegmemtv/protocols/protocol_tomosegmentv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

Wow! Very detailed.

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
Expand All @@ -67,31 +95,40 @@ 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,
default=1,
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)
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 -----------------------------------

Expand Down Expand Up @@ -257,4 +295,3 @@ def _getSalCmd(self, inputFile, outputFile, Nthreads):
outputCmd += '%s ' % outputFile
outputCmd += ' -t %i' % Nthreads
return outputCmd

Original file line number Diff line number Diff line change
Expand Up @@ -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)
13 changes: 7 additions & 6 deletions tomosegmemtv/viewers_interactive/memb_annotator_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down