Repository: I2PC/scipion-em-xmipp (language: python)

Issue:
subtract_projection: fixes and improvements
including:
- Modifications in subtract_projection protocol according to modifications in correspondent xmipp_program (https://github.com/I2PC/xmipp/pull/652 and https://github.com/I2PC/xmippCore/pull/144)
- New protocol for boosting particles (based on subtract_projection and thus, the protocol inherits from it)
- Add protocol to wizard XmippParticleMaskRadiusWizard as now the protocol uses it
- Modify test according to new changes and extend it for protocol boost particles


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index e57cc8df7..35419949c 100644
--- a/README.md
+++ b/README.md
@@ -5,10 +5,10 @@
 Plugin to use Xmipp programs within the Scipion framework.
 
 You can install it either, using the Scipion software following 
-the [Scipion's installation guide](https://scipion-em.github.io/docs/docs/scipion-modes/install-from-sources.html#installing-scipion-v3-0)
-(**strongly recommended for non-developers**) or cloning it on your own and 
-[linking it to Scipion](https://scipion-em.github.io/docs/docs/scipion-modes/install-plugins-command-line.html#devel-mode).
-The latter is only for those who intended to develop the Xmipp plugin but **not the whole Xmipp software**.
+the [Scipion's installation guide](https://scipion-em.github.io/docs/release-3.0.0/docs/scipion-modes/how-to-install.html)
+, (remember to install the scipion-em-xmipp plugin or remove the -noXmipp flag in the Scipion installation)**strongly recommended for non-developers** or cloning it on your own and 
+[linking it to Scipion](https://github.com/I2PC/xmipp#linking-standalone-version-to-scipion).
+The latter is only for those who intended to develop the Xmipp plugin.
 
 For **developers**, we strongly recommend to follow the guide on 
 [how to install xmipp-bundle](https://github.com/i2pc/xmipp#xmipp-as-a-standalone-bundle-for-developers) 
diff --git a/requirements.txt b/requirements.txt
index 3f316c576..bea124a39 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,8 @@
 scipion-em
 scikit-learn==0.22.1
+scipy<=1.8.1
 joblib
 peppercorn
 plotter
 scons
-PyWavelets
\ No newline at end of file
+PyWavelets
diff --git a/xmipp3/__init__.py b/xmipp3/__init__.py
index 865cef3cb..07f95e99e 100644
--- a/xmipp3/__init__.py
+++ b/xmipp3/__init__.py
@@ -39,9 +39,9 @@
 
 _logo = "xmipp_logo.png"
 _references = ['delaRosaTrevin2013', 'Sorzano2013']
-_currentBinVersion = '3.22.07.0'
+_currentBinVersion = '3.22.11.0'
 # This will allow scipion to detect the plugin version and print it in the log.
-__version__ = _currentBinVersion[2:] + ".1"  # Set this to ".0" on each xmipp binary release, otherwise increase it --> ".1", ".2", ...
+__version__ = _currentBinVersion[2:] + ".0"  # Set this to ".0" on each xmipp binary release, otherwise increase it --> ".1", ".2", ...
 
 
 class Plugin(pwem.Plugin):
diff --git a/xmipp3/protocols.conf b/xmipp3/protocols.conf
index 3239c5b24..a4ba17f31 100644
--- a/xmipp3/protocols.conf
+++ b/xmipp3/protocols.conf
@@ -10,7 +10,8 @@ Protocols SPA = [
 	]},
 	{"tag": "section", "text": "Micrographs", "openItem": "False", "children": [
 		{"tag": "protocol_group", "text": "Preprocess", "openItem": "False", "children": [
-		    {"tag": "protocol", "value": "XmippProtPreprocessMicrographs", "text": "default"}
+		    {"tag": "protocol", "value": "XmippProtPreprocessMicrographs", "text": "default"},
+		    {"tag": "protocol", "value": "XmippProtTiltAnalysis", "text": "default"}
 		]},
 		{"tag": "protocol_group", "text": "CTF estimation", "openItem": "False", "children": [
             {"tag": "protocol", "value": "XmippProtCTFMicrographs", "text": "default"},
@@ -109,7 +110,8 @@ Protocols SPA = [
 	]},
     {"tag": "protocol_group", "text": "Postprocess", "openItem": "False", "children": [
 		{"tag": "protocol", "value": "XmippProtLocSharp",  "text": "default"},
-		{"tag": "protocol", "value": "XmippProtDeepVolPostProc",  "text": "default"}
+		{"tag": "protocol", "value": "XmippProtDeepVolPostProc",  "text": "default"},
+		{"tag": "protocol", "value": "XmippProtBoostParticles",  "text": "default"}
 	]},
 	{"tag": "protocol_group", "text": "Analysis", "openItem": "False", "children": [
 		{"tag": "section", "text": "Heterogeneity", "openItem": "False", "children": [
@@ -148,7 +150,9 @@ Protocols SPA = [
 	]}
 	]},
 	{"tag": "section", "text": "Tools", "openItem": "False", "children": [
-		{"tag": "protocol_group", "text": "Sets", "openItem": "False", "children": []},
+		{"tag": "protocol_group", "text": "Sets", "openItem": "False", "children": [
+		    {"tag": "protocol", "value" : "XmippProtTriggerData","text": "default"}
+		]},
 		{"tag": "protocol_group", "text": "Calculators", "openItem": "False", "children": [
 			{"tag": "protocol", "value": "XmippProtImageOperateParticles", "text": "default"},
 			{"tag": "protocol", "value": "XmippProtImageOperateVolumes", "text": "default"},
diff --git a/xmipp3/protocols/__init__.py b/xmipp3/protocols/__init__.py
index c5d5815fb..0d01b5cbe 100644
--- a/xmipp3/protocols/__init__.py
+++ b/xmipp3/protocols/__init__.py
@@ -108,6 +108,8 @@
 from .protocol_solid_angles import XmippProtSolidAngles
 from .protocol_split_volume import XmippProtSplitvolume
 from .protocol_subtract_projection import XmippProtSubtractProjection
+from .protocol_subtract_projection import XmippProtBoostParticles
+from .protocol_tilt_analysis import XmippProtTiltAnalysis
 from .protocol_trigger_data import XmippProtTriggerData
 from .protocol_validate_nontilt import XmippProtValidateNonTilt
 from .protocol_validate_overfitting import XmippProtValidateOverfitting
@@ -123,7 +125,6 @@
 from .protocol_metaprotocol_create_subset import XmippMetaProtCreateSubset
 from .protocol_metaprotocol_create_output import XmippMetaProtCreateOutput
 from .protocol_metaprotocol_discrete_heterogeneity_scheduler import XmippMetaProtDiscreteHeterogeneityScheduler # keep this after XmippMetaProtCreateSubset and XmippMetaProtCreateOutput
-from .protocol_angular_alignment_zernike3d import XmippProtAngularAlignmentZernike3D
 from .protocol_volume_deform_zernike3d import XmippProtVolumeDeformZernike3D
 from .protocol_structure_map_zernike3d import XmippProtStructureMapZernike3D
 from .protocol_align_volume_and_particles import XmippProtAlignVolumeParticles
@@ -135,7 +136,7 @@
 # from .protocol_apply_deformation_zernike3d import XmippProtApplyZernike3D
 # from .protocol_kmeans_clustering import XmippProtKmeansSPH
 from .protocol_structure_map import XmippProtStructureMap
-from .protocol_pdb_deform_zernike3d import XmippProtDeformPDBZernike3D
+from .protocol_apply_zernike3d import XmippApplyZernike3D
 from .protocol_volume_adjust_sub import XmippProtVolAdjust, XmippProtVolSubtraction
 from .protocol_volume_consensus import XmippProtVolConsensus
 from .protocol_classes_2d_mapping import XmippProtCL2DMap
diff --git a/xmipp3/protocols/protocol_angular_alignment_zernike3d.py b/xmipp3/protocols/protocol_angular_alignment_zernike3d.py
deleted file mode 100644
index 345d7a353..000000000
--- a/xmipp3/protocols/protocol_angular_alignment_zernike3d.py
+++ /dev/null
@@ -1,251 +0,0 @@
-
-# **************************************************************************
-# *
-# * Authors:     Amaya Jimenez Moreno (ajimenez@cnb.csic.es)
-# *              David Herreros Calero (dherreos@cnb.csic.es)
-# *
-# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
-# *
-# * This program is free software; you can redistribute it and/or modify
-# * it under the terms of the GNU General Public License as published by
-# * the Free Software Foundation; either version 2 of the License, or
-# * (at your option) any later version.
-# *
-# * This program is distributed in the hope that it will be useful,
-# * but WITHOUT ANY WARRANTY; without even the implied warranty of
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# * GNU General Public License for more details.
-# *
-# * You should have received a copy of the GNU General Public License
-# * along with this program; if not, write to the Free Software
-# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
-# * 02111-1307  USA
-# *
-# *  All comments concerning this program package may be sent to the
-# *  e-mail address 'scipion@cnb.csic.es'
-# *
-# **************************************************************************
-
-
-import numpy as np
-
-import pyworkflow.protocol.params as params
-from pyworkflow.object import Integer
-from pyworkflow.utils.path import moveFile
-from pyworkflow import VERSION_2_0
-
-from pwem.protocols import ProtAnalysis3D
-import pwem.emlib.metadata as md
-from pwem.emlib.image import ImageHandler
-from pwem.constants import ALIGN_PROJ
-
-from xmipp3.convert import (writeSetOfParticles, createItemMatrix,
-                            setXmippAttributes)
-from xmipp3.base import writeInfoField, readInfoField
-
-
-class XmippProtAngularAlignmentZernike3D(ProtAnalysis3D):
-    """ Protocol for flexible angular alignment based on Zernike3D basis. """
-    _label = 'angular align - Zernike3D'
-    _lastUpdateVersion = VERSION_2_0
-
-    # --------------------------- DEFINE param functions --------------------------------------------
-    def _defineParams(self, form):
-        form.addSection(label='Input')
-        form.addHidden(params.USE_GPU, params.BooleanParam, default=True,
-                       label="Use GPU for execution",
-                       help="This protocol has both CPU and GPU implementation.\
-                           Select the one you want to use.")
-        form.addHidden(params.GPU_LIST, params.StringParam, default='0',
-                       expertLevel=params.LEVEL_ADVANCED,
-                       label="Choose GPU IDs",
-                       help="Add a list of GPU devices that can be used")
-        form.addParam('inputParticles', params.PointerParam, label="Input particles", pointerClass='SetOfParticles')
-        form.addParam('inputVolume', params.PointerParam, label="Input volume", pointerClass='Volume')
-        form.addParam('inputVolumeMask', params.PointerParam, label="Input volume mask", pointerClass='VolumeMask',
-                      allowsNull=True)
-        form.addParam('targetResolution', params.FloatParam, label="Target resolution (A)", default=8.0,
-                      help="In Angstroms, the images and the volume are rescaled so that this resolution is at "
-                           "2/3 of the Fourier spectrum.")
-        form.addParam('l1', params.IntParam, default=3,
-                      label='Zernike Degree',
-                      expertLevel=params.LEVEL_ADVANCED,
-                      help='Degree Zernike Polynomials of the deformation=1,2,3,...')
-        form.addParam('l2', params.IntParam, default=2,
-                      label='Harmonical Degree',
-                      expertLevel=params.LEVEL_ADVANCED,
-                      help='Degree Spherical Harmonics of the deformation=1,2,3,...')
-        form.addParam('maxShift', params.FloatParam, default=-1,
-                      label='Maximum shift (px)', expertLevel=params.LEVEL_ADVANCED,
-                      help='Maximum shift allowed in pixels')
-        form.addParam('maxAngular', params.FloatParam, default=5,
-                      label='Maximum angular change (degrees)', expertLevel=params.LEVEL_ADVANCED,
-                      help='Maximum angular change allowed (in degrees)')
-        form.addParam('maxResolution', params.FloatParam, default=4.0,
-                      label='Maximum resolution (A)', expertLevel=params.LEVEL_ADVANCED,
-                      help='Maximum resolution (A)')
-        form.addParam('regularization', params.FloatParam, default=0.005, label='Regularization',
-                      expertLevel=params.LEVEL_ADVANCED,
-                      help='Penalization to deformations (higher values penalize more the deformation).')
-        form.addParam('ignoreCTF', params.BooleanParam, default=False, label='Ignore CTF?',
-                      expertLevel=params.LEVEL_ADVANCED,
-                      help="If true, volume projection won't be subjected to CTF corrections")
-        form.addParam('optimizeAlignment', params.BooleanParam, default=True, label='Optimize alignment?',
-                     expertLevel=params.LEVEL_ADVANCED)
-        form.addParallelSection(threads=1, mpi=8)
-
-    def _createFilenameTemplates(self):
-        """ Centralize how files are called """
-        myDict = {
-            'imgsFn': self._getExtraPath('input_particles.xmd'),
-            'fnVol': self._getExtraPath('input_volume.vol'),
-            'fnVolMask': self._getExtraPath('input_volume_mask.vol'),
-            'fnOut': self._getExtraPath('output_particles.xmd'),
-            'fnOutDir': self._getExtraPath()
-                 }
-        self._updateFilenamesDict(myDict)
-
-    # --------------------------- INSERT steps functions --------------------------------------------
-    def _insertAllSteps(self):
-        self._createFilenameTemplates()
-        self._insertFunctionStep("convertStep")
-        self._insertFunctionStep("alignmentStep")
-        self._insertFunctionStep("createOutputStep")
-
-    # --------------------------- STEPS functions ---------------------------------------------------
-    def convertStep(self):
-        imgsFn = self._getFileName('imgsFn')
-        fnVol = self._getFileName('fnVol')
-        fnVolMask = self._getFileName('fnVolMask')
-
-        inputParticles = self.inputParticles.get()
-        writeSetOfParticles(inputParticles, imgsFn)
-        Xdim = inputParticles.getXDim()
-        self.Ts = inputParticles.getSamplingRate()
-        newTs = self.targetResolution.get() * 1.0 /3.0
-        self.newTs = max(self.Ts, newTs)
-        self.newXdim = int(Xdim * self.Ts / newTs)
-        writeInfoField(self._getExtraPath(), "sampling", md.MDL_SAMPLINGRATE, newTs)
-        writeInfoField(self._getExtraPath(), "size", md.MDL_XSIZE, self.newXdim)
-        if self.newXdim != Xdim:
-            self.runJob("xmipp_image_resize",
-                        "-i %s -o %s --save_metadata_stack %s --fourier %d" %
-                        (imgsFn,
-                         self._getExtraPath('scaled_particles.stk'),
-                         self._getExtraPath('scaled_particles.xmd'),
-                         self.newXdim), numberOfMpi=1)
-            moveFile(self._getExtraPath('scaled_particles.xmd'), imgsFn)
-
-        ih = ImageHandler()
-        ih.convert(self.inputVolume.get(), fnVol)
-        Xdim = self.inputVolume.get().getDim()[0]
-        if Xdim != self.newXdim:
-            self.runJob("xmipp_image_resize",
-                        "-i %s --dim %d " % (fnVol, self.newXdim), numberOfMpi=1)
-        if self.inputVolumeMask.get():
-            ih.convert(self.inputVolumeMask.get(), fnVolMask)
-            if Xdim != self.newXdim:
-                self.runJob("xmipp_image_resize",
-                            "-i %s --dim %d --interp nearest" % (fnVolMask, self.newXdim), numberOfMpi=1)
-
-    def alignmentStep(self):
-        imgsFn = self._getFileName('imgsFn')
-        fnVol = self._getFileName('fnVol')
-        fnOut = self._getFileName('fnOut')
-        fnVolMask = self._getFileName('fnVolMask')
-        fnOutDir = self._getFileName('fnOutDir')
-        Ts = readInfoField(self._getExtraPath(), "sampling", md.MDL_SAMPLINGRATE)
-        params = ' -i %s --ref %s -o %s --optimizeDeformation --optimizeDefocus ' \
-                 '--l1 %d --l2 %d --max_shift %f --max_angular_change %f --sampling %f ' \
-                 ' --max_resolution %f --odir %s --resume --regularization %f' %\
-                 (imgsFn, fnVol, fnOut, self.l1.get(), self.l2.get(), self.maxShift,
-                  self.maxAngular, Ts, self.maxResolution, fnOutDir, self.regularization.get())
-        if self.optimizeAlignment.get():
-            params += ' --optimizeAlignment'
-        if self.ignoreCTF.get():
-            params += ' --ignoreCTF'
-        if self.inputParticles.get().isPhaseFlipped():
-            params += ' --phaseFlipped'
-        if self.inputVolumeMask.get():
-            params += ' --mask %s' % fnVolMask
-
-        if self.useGpu.get():
-            params += ' --device %d' % self.getGpuList()[0]
-            program = 'xmipp_cuda_angular_sph_alignment'
-            self.runJob(program, params)
-        else:
-            program = 'xmipp_angular_sph_alignment'
-            self.runJob(program, params, numberOfMpi=self.numberOfMpi.get())
-
-
-    def createOutputStep(self):
-        from sklearn.manifold import TSNE
-        Xdim = self.inputParticles.get().getXDim()
-        self.Ts = self.inputParticles.get().getSamplingRate()
-        newTs = self.targetResolution.get() * 1.0 /3.0
-        self.newTs = max(self.Ts, newTs)
-        self.newXdim = int(Xdim * self.Ts / newTs)
-        fnOut = self._getFileName('fnOut')
-        mdOut = md.MetaData(fnOut)
-        coeffMatrix = np.vstack(mdOut.getColumnValues(md.MDL_SPH_COEFFICIENTS))
-        X_tsne_1d = TSNE(n_components=1).fit_transform(coeffMatrix)
-        X_tsne_2d = TSNE(n_components=2).fit_transform(coeffMatrix)
-
-        newMdOut = md.MetaData()
-        i = 0
-        for row in md.iterRows(mdOut):
-            newRow = row
-            newRow.setValue(md.MDL_SPH_TSNE_COEFF1D, float(X_tsne_1d[i, 0]))
-            newRow.setValue(md.MDL_SPH_TSNE_COEFF2D, [float(X_tsne_2d[i, 0]),  float(X_tsne_2d[i, 1])])
-            if self.newTs != self.Ts:
-                coeffs = mdOut.getValue(md.MDL_SPH_COEFFICIENTS, row.getObjId())
-                correctionFactor = self.inputVolume.get().getDim()[0] / self.newXdim
-                coeffs = [correctionFactor*coeff for coeff in coeffs]
-                newRow.setValue(md.MDL_SPH_COEFFICIENTS, coeffs)
-            newRow.addToMd(newMdOut)
-            i += 1
-        newMdOut.write(fnOut)
-
-        inputSet = self.inputParticles.get()
-        partSet = self._createSetOfParticles()
-
-        partSet.copyInfo(inputSet)
-        partSet.setAlignmentProj()
-        partSet.copyItems(inputSet,
-                          updateItemCallback=self._updateParticle,
-                          itemDataIterator=md.iterRows(fnOut, sortByLabel=md.MDL_ITEM_ID))
-        partSet.L1 = Integer(self.l1.get())
-        partSet.L2 = Integer(self.l2.get())
-        partSet.Rmax = Integer(self.inputVolume.get().getDim()[0] / 2)
-
-        self._defineOutputs(outputParticles=partSet)
-        self._defineTransformRelation(self.inputParticles, partSet)
-
-
-# --------------------------- UTILS functions --------------------------------------------
-    def _updateParticle(self, item, row):
-        setXmippAttributes(item, row, md.MDL_ANGLE_ROT, md.MDL_ANGLE_TILT,
-                           md.MDL_ANGLE_PSI, md.MDL_SHIFT_X, md.MDL_SHIFT_Y,
-                           md.MDL_FLIP, md.MDL_SPH_DEFORMATION,
-                           md.MDL_SPH_COEFFICIENTS, md.MDL_SPH_TSNE_COEFF1D,
-                           md.MDL_SPH_TSNE_COEFF2D)
-        createItemMatrix(item, row, align=ALIGN_PROJ)
-
-    def getInputParticles(self):
-        return self.inputParticles.get()
-
-    # ----------------------- VALIDATE functions ----------------------------------------
-    def validate(self):
-        """ Try to find errors on define params. """
-        errors = []
-        l1 = self.l1.get()
-        l2 = self.l2.get()
-        if (l1 - l2) < 0:
-            errors.append('Zernike degree must be higher than '
-                          'SPH degree.')
-        return errors
-
-
-
-
-
diff --git a/xmipp3/protocols/protocol_apply_zernike3d.py b/xmipp3/protocols/protocol_apply_zernike3d.py
new file mode 100644
index 000000000..f5f45056d
--- /dev/null
+++ b/xmipp3/protocols/protocol_apply_zernike3d.py
@@ -0,0 +1,216 @@
+
+# **************************************************************************
+# *
+# * Authors:     David Herreros Calero (dherreros@cnb.csic.es)
+# *              James Krieger (jmkrieger@cnb.csic.es)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+
+from pwem.protocols import ProtAnalysis3D
+from pwem.objects import AtomStruct, SetOfAtomStructs
+from pwem.objects import Volume, SetOfVolumes
+
+import pyworkflow.protocol.params as params
+import pyworkflow.utils as pwutils
+from pyworkflow.object import Float, Integer
+
+
+class XmippApplyZernike3D(ProtAnalysis3D):
+    """ Protocol for PDB deformation based on Zernike3D basis. """
+    _label = 'apply deformation field - Zernike3D'
+
+    # --------------------------- DEFINE param functions --------------------------------------------
+    def _defineParams(self, form):
+        form.addSection(label='Input')
+        form.addParam('volume', params.PointerParam, label="Zernike3D volume(s)",
+                      important=True, pointerClass="SetOfVolumes,Volume",
+                      help='Volume(s) with Zernike3D coefficients assigned.')
+        form.addParam('inputVolumeMask', params.PointerParam, label="Input volume mask", pointerClass='VolumeMask',
+                      condition="volume and not hasattr(volume,'refMask')")
+        form.addParam('L1', params.IntParam,
+                      label='Zernike Degree',
+                      condition="volume and not hasattr(volume,'L1')",
+                      help='Degree Zernike Polynomials of the deformation=1,2,3,...')
+        form.addParam('L2', params.IntParam,
+                      label='Harmonical Degree',
+                      condition="volume and not hasattr(volume,'L2')",
+                      help='Degree Spherical Harmonics of the deformation=1,2,3,...')
+        form.addParam('applyPDB', params.BooleanParam, label="Apply to structure?",
+                      default=False,
+                      help="If True, you will be able to provide an atomic structure to be deformed "
+                           "based on the Zernike3D coefficients associated to the input volume(s). "
+                           "If False, the coefficients will be applied to the volume(s) directly.")
+        form.addParam('inputPDB', params.PointerParam, label="Input PDB",
+                      pointerClass='AtomStruct', allowsNull=True, condition="applyPDB==True",
+                      help='Atomic structure to apply the deformation fields defined by the '
+                           'Zernike3D coefficients associated to the input volume. '
+                           'For better results, the volume(s) and structure should be aligned')
+        form.addParam('moveBoxOrigin', params.BooleanParam, default=False, condition="applyPDB==True",
+                      label="Move structure to box origin?",
+                      help="If PDB has been aligned inside Scipion, set to False. Otherwise, this option will "
+                           "correctly place the PDB in the origin of the volume.")
+
+    # --------------------------- INSERT steps functions -----------------------
+    def _insertAllSteps(self):
+        self._insertFunctionStep("deformStep")
+        self._insertFunctionStep("createOutputStep")
+
+    # --------------------------- STEPS functions ------------------------------
+    def deformStep(self):
+        if isinstance(self.volume.get(), Volume):
+            self.volumes = [self.volume.get()]
+        else:
+            self.volumes = self.volume.get()
+
+        num_vols = len(self.volumes)
+        self.len_num_vols = len(str(num_vols))
+
+        for i, volume in enumerate(self.volumes):
+            i_pad = str(i).zfill(self.len_num_vols)
+
+            # Write coefficients to file
+            z_clnm_file = self._getExtraPath("z_clnm_{0}.txt".format(i_pad))
+            z_clnm = volume._xmipp_sphCoefficients.get()
+            self.writeZernikeFile(z_clnm, z_clnm_file)
+
+            if self.applyPDB.get():
+                boxSize = self.volume.get().getXDim()
+                samplingRate = self.volume.get().getSamplingRate()
+                outFile = pwutils.removeBaseExt(self.inputPDB.get().getFileName()) + '_deformed_{0}.pdb'.format(i_pad)
+                params = ' --pdb %s --clnm %s -o %s --sr %f' % \
+                         (self.inputPDB.get().getFileName(), z_clnm_file, self._getExtraPath(outFile),
+                          samplingRate)
+                if self.moveBoxOrigin.get():
+                    params += " --boxsize %d" % boxSize
+                self.runJob("xmipp_pdb_sph_deform", params)
+            else:
+                outFile = self._getExtraPath("deformed_volume_{0}.mrc".format(i_pad))
+                volume_file = volume.getFileName()
+                if pwutils.getExt(volume_file) == ".mrc":
+                    volume_file += ":mrc"
+                params = "-i %s  --step 1 --blobr 2 -o %s --clnm %s" % \
+                         (volume_file, outFile, z_clnm_file)
+                if volume.refMask:
+                    mask_file = volume.refMask.get() if hasattr(volume, 'refMask') \
+                                else self.inputVolumeMask.get()
+                    if pwutils.getExt(mask_file) == ".mrc":
+                        volume_file += ":mrc"
+                    params += " --mask %s" % mask_file
+                self.runJob("xmipp_volume_apply_coefficient_zernike3d", params)
+
+    def createOutputStep(self):
+        L1 = self.volume.get().L1 if hasattr(self.volume.get(), 'L1') \
+                                     else Integer(self.L1.get())
+        L2 = self.volume.get().L2 if hasattr(self.volume.get(), 'L2') \
+            else Integer(self.L2.get())
+        Rmax = Float(int(0.5 * self.volume.get().getXDim()))
+        if isinstance(self.volumes, list):
+            volume = self.volume.get()
+
+            refMap = volume.refMap
+            refMask = volume.refMask
+            z_clnm = volume._xmipp_sphCoefficients
+
+            if self.applyPDB.get():
+                outFile = pwutils.removeBaseExt(self.inputPDB.get().getFileName()) + '_deformed.pdb'
+                pdb = AtomStruct(self._getExtraPath(outFile))
+                pdb.L1 = L1
+                pdb.L2 = L2
+                pdb.Rmax = Float(volume.getSamplingRate() * Rmax.get())
+                pdb.refMap = refMap
+                pdb.refMask = refMask
+                pdb._xmipp_sphCoefficients = z_clnm
+                self._defineOutputs(deformed=pdb)
+                self._defineSourceRelation(self.inputPDB, pdb)
+                self._defineSourceRelation(volume, pdb)
+            else:
+                vol = Volume()
+                vol.setSamplingRate(volume.getSamplingRate())
+                vol.setFileName(self._getExtraPath("deformed_volume.mrc"))
+                vol.L1 = L1
+                vol.L2 = L2
+                vol.Rmax = Rmax
+                vol.refMap = refMap
+                vol.refMask = refMask
+                vol._xmipp_sphCoefficients = z_clnm
+                self._defineOutputs(deformed=vol)
+                self._defineSourceRelation(volume, vol)
+        else:
+            if self.applyPDB.get():
+                pdbs = SetOfAtomStructs().create(self._getPath())
+            else:
+                vols = self._createSetOfVolumes()
+                vols.setSamplingRate(self.volumes.getSamplingRate())
+
+            for i, volume in enumerate(self.volumes):
+                i_pad = str(i).zfill(self.len_num_vols)
+
+                refMap = volume.refMap
+                refMask = volume.refMask
+                z_clnm = volume._xmipp_sphCoefficients
+
+                if self.applyPDB.get():
+                    outFile = pwutils.removeBaseExt(self.inputPDB.get().getFileName()) + '_deformed_{0}.pdb'.format(i_pad)
+                    pdb = AtomStruct(self._getExtraPath(outFile))
+                    pdb.L1 = L1
+                    pdb.L2 = L2
+                    pdb.Rmax = Float(volume.getSamplingRate() * Rmax.get())
+                    pdb.refMap = refMap
+                    pdb.refMask = refMask
+                    pdb._xmipp_sphCoefficients = z_clnm
+
+                    pdbs.append(pdb)
+                else:
+                    vol = Volume()
+                    vol.setSamplingRate(volume.getSamplingRate())
+                    vol.setFileName(self._getExtraPath("deformed_volume_{0}.mrc".format(i_pad)))
+                    vol.L1 = L1
+                    vol.L2 = L2
+                    vol.Rmax = Rmax
+                    vol.refMap = refMap
+                    vol.refMask = refMask
+                    vol._xmipp_sphCoefficients = z_clnm
+
+                    vols.append(vol)
+
+            if self.applyPDB.get():
+                self._defineOutputs(deformed=pdbs)
+                self._defineSourceRelation(self.inputPDB, pdbs)
+                self._defineSourceRelation(self.volume, pdbs)
+            else:
+                self._defineOutputs(deformed=vols)
+                self._defineSourceRelation(self.volume, vols)
+
+    # --------------------------- UTILS functions ------------------------------
+    def writeZernikeFile(self, z_clnm, file):
+        volume = self.volume.get()
+        L1 = volume.L1.get() if hasattr(volume, 'L1') \
+             else self.L1.get()
+        L2 = volume.L2.get() if hasattr(volume, 'L2') \
+             else self.L2.get()
+        Rmax = int(0.5 * volume.getXDim())
+        Rmax = volume.getSamplingRate() * Rmax if self.applyPDB.get() else Rmax
+        with open(file, 'w') as fid:
+            fid.write(' '.join(map(str, [L1, L2, Rmax])) + "\n")
+            fid.write(z_clnm.replace(",", " ") + "\n")
diff --git a/xmipp3/protocols/protocol_center_particles.py b/xmipp3/protocols/protocol_center_particles.py
index 61df8db3f..713e1f28a 100644
--- a/xmipp3/protocols/protocol_center_particles.py
+++ b/xmipp3/protocols/protocol_center_particles.py
@@ -51,7 +51,7 @@ def _defineParams(self, form):
                       pointerClass='SetOfClasses2D',
                       important=True,
                       label="Input Classes",
-                      help='Set of classes to be realing')
+                      help='Set of classes to be read')
         form.addParam('inputMics', params.PointerParam,
                       pointerClass='SetOfMicrographs',
                       important=True,
@@ -120,7 +120,7 @@ def realignStep(self):
                 mdClass = md.MetaData(block + "@" + inputMdName)
                 mdNewClass = md.MetaData()
                 i += 1
-                j = 0
+                flag_psi = True
                 for rowIn in md.iterRows(mdClass):
                     #To create the transformation matrix (and its parameters)
                     #  for the realigned particles
@@ -156,7 +156,6 @@ def realignStep(self):
                         md.MDL_YCOOR)+int(centerPoint[1]))
 
                     rowOut.addToMd(mdNewClass)
-                    j += 1
 
                 mdNewClass.write(block + "@" + self._getExtraPath(
                     'final_classes.xmd'), MD_APPEND)
@@ -175,7 +174,7 @@ def realignStep(self):
 
     def createOutputStep(self):
         inputParticles = self.inputClasses.get().getImages()
-        outputClasses = self._createSetOfClasses2D(inputParticles)
+        outputClasses = self._createSetOfClasses2D(self.inputClasses.get().getImagesPointer())
         self._fillClasses(outputClasses)
         result = {'outputClasses': outputClasses}
         self._defineOutputs(**result)
@@ -253,9 +252,9 @@ def _summary(self):
     def _validate(self):
         errors = []
         try:
-            self.inputClasses.get().getImages().getAcquisition()
+            self.inputClasses.get().getImages()
         except AttributeError:
-            errors.append('InputClasses has not clases')
+            errors.append('Try and catch InputClasses has no particles.')
 
         return errors
 
diff --git a/xmipp3/protocols/protocol_cl2d_align.py b/xmipp3/protocols/protocol_cl2d_align.py
index fccf9a7e8..0821cd3a2 100644
--- a/xmipp3/protocols/protocol_cl2d_align.py
+++ b/xmipp3/protocols/protocol_cl2d_align.py
@@ -26,7 +26,7 @@
 import pyworkflow.protocol.params as params
 
 import pwem.emlib.metadata as md
-from pwem.objects import Particle
+from pwem.objects import Particle, SetOfAverages, SetOfClasses2D
 from pwem.protocols import ProtAlign2D
 from pwem import ALIGN_NONE, ALIGN_2D
 
@@ -47,9 +47,10 @@ def _defineAlignParams(self, form):
                            'averaging subsets of the input images.')
         form.addParam('referenceImage', params.PointerParam,
                       condition='useReferenceImage',
-                      pointerClass='Particle', allowsNull=True,
+                      pointerClass='Particle, SetOfAverages, SetOfClasses2D', allowsNull=True,
                       label="Reference image",
-                      help='Image that will serve as class reference')
+                      help='Image that will serve as class reference. If the input is a set, then the first image '
+                           'will be used as reference.')
         form.addParam('maximumShift', params.IntParam, default=10,
                       label='Maximum shift (px):')
         form.addParam('numberOfIterations', params.IntParam, default=10,
@@ -76,7 +77,12 @@ def _insertAllSteps(self):
                 '--maxShift %(maxshift)d')
         
         if self.useReferenceImage:
-            args += " --ref0 " + getImageLocation(self.referenceImage.get())
+            if isinstance(self.referenceImage.get(),Particle):
+                args += " --ref0 " + getImageLocation(self.referenceImage.get())
+            elif isinstance(self.referenceImage.get(), SetOfClasses2D):
+                args += " --ref0 " + getImageLocation(self.referenceImage.get().getFirstItem().getRepresentative())
+            else:
+                args += " --ref0 " + getImageLocation(self.referenceImage.get().getFirstItem())
         else:
             args += " --nref0 1"
         self._insertRunJobStep("xmipp_classify_CL2D", args % self._params)
@@ -121,7 +127,10 @@ def _validate(self):
         if self.useReferenceImage:
             if self.referenceImage.hasValue():
                 refImage = self.referenceImage.get()
-                [x1, y1, z1] = refImage.getDim()
+                if isinstance(refImage,Particle):
+                    [x1, y1, z1] = refImage.getDim()
+                else:
+                    [x1, y1, z1] = refImage.getFirstItem().getDim()
                 [x2, y2, z2] = self.inputParticles.get().getDim()
                 if x1 != x2 or y1 != y2 or z1 != z2:
                     errors.append('The input images and the reference image '
diff --git a/xmipp3/protocols/protocol_classification_gpuCorr.py b/xmipp3/protocols/protocol_classification_gpuCorr.py
index 121b0672a..f8dc8d0b6 100644
--- a/xmipp3/protocols/protocol_classification_gpuCorr.py
+++ b/xmipp3/protocols/protocol_classification_gpuCorr.py
@@ -201,9 +201,7 @@ def classifyStep(self):
 
 
     def createOutputStep(self):
-
-        inputParticles = self.inputParticles.get()
-        classes2DSet = self._createSetOfClasses2D(inputParticles)
+        classes2DSet = self._createSetOfClasses2D(self.inputParticles)
         self._fillClassesFromLevel(classes2DSet)
         result = {'outputClasses': classes2DSet}
         self._defineOutputs(**result)
diff --git a/xmipp3/protocols/protocol_classification_gpuCorr_full.py b/xmipp3/protocols/protocol_classification_gpuCorr_full.py
index 131df0f9a..e14831091 100644
--- a/xmipp3/protocols/protocol_classification_gpuCorr_full.py
+++ b/xmipp3/protocols/protocol_classification_gpuCorr_full.py
@@ -753,10 +753,8 @@ def _getFirstJoinStep(self):
 
 
     def _createFinalClasses(self):
-
-        inputs = self.inputParticles.get()
         # Here the defineOutputs function will call the write() method
-        outputSet = self._createSetOfClasses2D(inputs)
+        outputSet = self._createSetOfClasses2D(self.inputParticles)
         self._fillClasses(outputSet)
         self._defineOutputs(**{'outputClasses': outputSet})
         self._defineSourceRelation(self.inputParticles, outputSet)
diff --git a/xmipp3/protocols/protocol_classification_gpuCorr_semi.py b/xmipp3/protocols/protocol_classification_gpuCorr_semi.py
index 3c3c4ba99..c1ec7c6d1 100644
--- a/xmipp3/protocols/protocol_classification_gpuCorr_semi.py
+++ b/xmipp3/protocols/protocol_classification_gpuCorr_semi.py
@@ -340,12 +340,12 @@ def _updateOutputSetOfClasses(self, outFnDone, streamMode):
         firstTime = True
 
         if outputClasses is None:
-            outputClasses=self._createSetOfClasses2D(self.inputParticles.get())
+            outputClasses = self._createSetOfClasses2D(self.inputParticles)
         else:
             firstTime = False
             outputClasses = SetOfClasses2D(filename=outputClasses.getFileName())
             outputClasses.setStreamState(streamMode)
-            outputClasses.setImages(self.inputParticles.get())
+            outputClasses.setImages(self.inputParticles)
 
         self._fillClassesFromMd(outFnDone, outputClasses, firstTime, streamMode)
         self._updateOutputSet(outputName, outputClasses, streamMode)
@@ -406,27 +406,7 @@ def _fillClassesFromMd(self, outFnDone, outputClasses, firstTime,
                         newClass.setStreamState(streamMode)
                         outputClasses.append(newClass)
 
-                    #Fill the output set with the previous particles
-                    # of the classes
-                    for cls in cls2d:
-                        repId = cls.getObjId()
-                        newClass = outputClasses[repId]
-                        for img in cls:
-                            # if not self.flag_relion \
-                            #         and img.hasAttribute('_rlnGroupName'):
-                            #     self.flag_relion=True
-                            newClass.append(img)
-                            #We store the last id just in case we found any
-                            # problem with repeated ids that requires to
-                            # change them, this must not happen
-                            #if img.getObjId()>self.lastId:
-                            #    self.lastId = img.getObjId()
-
-                    outputClasses.update(newClass)
-
             for imgRow in md.iterRows(mdImages, sortByLabel=md.MDL_REF):
-                #Just in case of having repeated ids, this must not happen
-                #self.lastId+=1
                 imgClassId = imgRow.getValue(md.MDL_REF)
                 imgId = imgRow.getValue(md.MDL_ITEM_ID)
 
@@ -440,8 +420,6 @@ def _fillClassesFromMd(self, outFnDone, outputClasses, firstTime,
 
                 part = inputSet[imgId]
                 self._updateParticle(part, imgRow)
-                # Just in case of having repeated ids, this must not happen
-                #part.setObjId(self.lastId)
                 newClass.append(part)
 
             # this is to update the last class into the set.
diff --git a/xmipp3/protocols/protocol_deep_hand.py b/xmipp3/protocols/protocol_deep_hand.py
index b981952c8..b784a779a 100644
--- a/xmipp3/protocols/protocol_deep_hand.py
+++ b/xmipp3/protocols/protocol_deep_hand.py
@@ -25,10 +25,9 @@
 # **************************************************************************
 
 from pwem.protocols import EMProtocol
-from pwem.emlib.image import ImageHandler
 from pwem.objects import Volume
 
-from pyworkflow.protocol.params import PointerParam, FloatParam, IntParam
+from pyworkflow.protocol.params import PointerParam, FloatParam
 from pyworkflow.object import Float
 from pyworkflow.utils.path import cleanPath
 
@@ -45,9 +44,11 @@ class XmippProtDeepHand(EMProtocol, XmippProtocol):
     def __init__(self, *args, **kwargs):
         EMProtocol.__init__(self, *args, **kwargs)
         XmippProtocol.__init__(self)
+        self.vResizedVolFile = 'resizedVol.mrc'
+        self.vMaskFile = 'mask.mrc'
+        self.vFilteredVolFile = 'filteredVol.mrc'
 
     def _defineParams(self, form):
-
         form.addSection('Input')
         form.addParam('inputVolume', PointerParam, pointerClass="Volume",
                       label='Input Volume', allowsNull=False,
@@ -67,16 +68,15 @@ def _insertAllSteps(self):
         self._insertFunctionStep('createOutputStep')
 
     def preprocessStep(self):
-
         # Get volume information
         volume = self.inputVolume.get()
         fn_vol = getImageLocation(volume)
         T_s = volume.getSamplingRate()
 
         # Paths to new files created
-        self.resizedVolFile = self._getPath('resizedVol.mrc')
-        self.maskFile = self._getPath('mask.mrc')
-        self.filteredVolFile = self._getPath('filteredVol.mrc')
+        self.resizedVolFile = self._getPath(self.vResizedVolFile)
+        self.maskFile = self._getPath(self.vMaskFile)
+        self.filteredVolFile = self._getPath(self.vFilteredVolFile)
 
         # Resize to 1A/px
         self.runJob("xmipp_image_resize", "-i %s -o %s --factor %f" %
@@ -102,8 +102,8 @@ def predictStep(self):
         args = "--alphaModel %s --handModel %s -o %s " \
                "--alphaThr %f --pathVf %s --pathVmask %s" % (
                 alpha_model, hand_model, self._getExtraPath(),
-                self.thresholdAlpha.get(), self._getPath('filteredVol.mrc'),
-                self._getPath('mask.mrc'))
+                self.thresholdAlpha.get(), self._getPath(self.vFilteredVolFile),
+                self._getPath(self.vMaskFile))
         self.runJob("xmipp_deep_hand", args, env=self.getCondaEnv())
 
         # Store hand value
@@ -135,9 +135,9 @@ def createOutputStep(self):
             self._defineOutputs(outputVol=self.inputVolume.get())
         self._defineSourceRelation(self.inputVolume, self.outputVol)
 
-        cleanPath(self._getPath('resizedVol.mrc'))
-        cleanPath(self._getPath('mask.mrc'))
-        cleanPath(self._getPath('filteredVol.mrc'))
+        cleanPath(self._getPath(self.vResizedVolFile))
+        cleanPath(self._getPath(self.vMaskFile))
+        cleanPath(self._getPath(self.vFilteredVolFile))
 
 # --------------------------- INFO functions -------------------------------
     def _summary(self):
@@ -151,7 +151,8 @@ def _summary(self):
                 summary.append('Volume was flipped as it was deemed to be left handed')
             else:
                 summary.append('Volume was not flipped as it was deemed to be right handed')
-
+        else:
+            summary.append("Output volume and handedness not ready yet.")
         return summary
 
     def _methods(self):
diff --git a/xmipp3/protocols/protocol_local_ctf.py b/xmipp3/protocols/protocol_local_ctf.py
index 1f72ea8cf..4780a7353 100644
--- a/xmipp3/protocols/protocol_local_ctf.py
+++ b/xmipp3/protocols/protocol_local_ctf.py
@@ -57,7 +57,7 @@ def _defineParams(self, form):
         form.addParam('inputVolume', PointerParam, label="Volume to compare images to",
                       pointerClass='Volume',
                       help='Volume to be used for class comparison')
-        form.addParam('maxDefocusChange', FloatParam, label="Maximum defocus change (A)", default=10000,
+        form.addParam('maxDefocusChange', FloatParam, label="Maximum defocus change (A)", default=500,
                       expertLevel=LEVEL_ADVANCED)
         form.addParam('maxGrayScaleChange', FloatParam, label="Maximum gray scale change", default=1,
                       expertLevel=LEVEL_ADVANCED, help="The reprojection is modified as a*P+b, a is restricted to the "
diff --git a/xmipp3/protocols/protocol_movie_gain.py b/xmipp3/protocols/protocol_movie_gain.py
index b88542133..1fe0a449b 100644
--- a/xmipp3/protocols/protocol_movie_gain.py
+++ b/xmipp3/protocols/protocol_movie_gain.py
@@ -208,7 +208,7 @@ def _processMovie(self, movie):
         if not self.doGainProcess(movieId):
             return
         inputGain = self.getInputGain()
-          
+
         if self.estimateGain.get() and not movieId in self.estimatedIds:
                 self.estimatedIds.append(movieId)
                 self.estimateGainFun(movie)
@@ -336,7 +336,7 @@ def _checkNewOutput(self):
                     estGainsSet = self._loadOutputSet(SetOfImages, self.estimatedDatabase)
                 if self.estimateResidualGain.get():
                     resGainsSet = self._loadOutputSet(SetOfImages, self.residualDatabase)
-                  
+
                 for movie in newDone:
                     movieId = movie.getObjId()
                     if not self.doGainProcess(movieId):
@@ -461,7 +461,7 @@ def invertImage(self, img, outFn):
 
     def getInputGain(self):
         return self.inputMovies.get().getGain()
-    
+
     def getEstimatedGainPath(self, movieId):
         return self._getExtraPath("movie_%06d_gain.xmp" % movieId)
 
diff --git a/xmipp3/protocols/protocol_pdb_deform_zernike3d.py b/xmipp3/protocols/protocol_pdb_deform_zernike3d.py
deleted file mode 100644
index f4896d539..000000000
--- a/xmipp3/protocols/protocol_pdb_deform_zernike3d.py
+++ /dev/null
@@ -1,64 +0,0 @@
-
-# **************************************************************************
-# *
-# * Authors:     David Herreros Calero (dherreros@cnb.csic.es)
-# *
-# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
-# *
-# * This program is free software; you can redistribute it and/or modify
-# * it under the terms of the GNU General Public License as published by
-# * the Free Software Foundation; either version 2 of the License, or
-# * (at your option) any later version.
-# *
-# * This program is distributed in the hope that it will be useful,
-# * but WITHOUT ANY WARRANTY; without even the implied warranty of
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# * GNU General Public License for more details.
-# *
-# * You should have received a copy of the GNU General Public License
-# * along with this program; if not, write to the Free Software
-# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
-# * 02111-1307  USA
-# *
-# *  All comments concerning this program package may be sent to the
-# *  e-mail address 'scipion@cnb.csic.es'
-# *
-# **************************************************************************
-
-from pwem.protocols import ProtAnalysis3D
-from pwem.objects import AtomStruct
-import pyworkflow.protocol.params as params
-import pyworkflow.utils as pwutils
-
-
-class XmippProtDeformPDBZernike3D(ProtAnalysis3D):
-    """ Protocol for PDB deformation based on Zernike3D basis. """
-    _label = 'deform pdb - Zernike3D'
-
-    # --------------------------- DEFINE param functions --------------------------------------------
-    def _defineParams(self, form):
-        form.addSection(label='Input')
-        form.addParam('inputPDB', params.PointerParam, label="Input PDB",
-                      pointerClass='AtomStruct', important=True,
-                      help='Select a PDB to be deformed.')
-        form.addParam('inputCoeff', params.PathParam, label="Input Coefficients",
-                      important=True,
-                      help='Specify a path to the deformation coefficients file.')
-
-    # --------------------------- INSERT steps functions -----------------------
-    def _insertAllSteps(self):
-        self._insertFunctionStep("deformStep")
-        self._insertFunctionStep("createOutputStep")
-
-    # --------------------------- STEPS functions ------------------------------
-    def deformStep(self):
-        outFile = pwutils.removeBaseExt(self.inputPDB.get().getFileName()) + '_deformed.pdb'
-        params = ' --pdb %s --clnm %s -o %s' % \
-                 (self.inputPDB.get().getFileName(), self.inputCoeff.get(), self._getExtraPath(outFile))
-        self.runJob("xmipp_pdb_sph_deform", params)
-
-    def createOutputStep(self):
-        outFile = pwutils.removeBaseExt(self.inputPDB.get().getFileName()) + '_deformed.pdb'
-        pdb = AtomStruct(self._getExtraPath(outFile))
-        self._defineOutputs(outputPDB=pdb)
-        self._defineSourceRelation(self.inputPDB, pdb)
diff --git a/xmipp3/protocols/protocol_pick_noise.py b/xmipp3/protocols/protocol_pick_noise.py
index 8a29628f0..97196bfd4 100644
--- a/xmipp3/protocols/protocol_pick_noise.py
+++ b/xmipp3/protocols/protocol_pick_noise.py
@@ -141,6 +141,8 @@ def _summary(self):
         summary = []
         if hasattr(self, 'outputCoordinates'):
             summary.append('%d noisy particles were picked'%self.outputCoordinates.getSize())
+        else:
+            summary.append("The output set of noisy particles has not finished yet")
         return summary
 
 
diff --git a/xmipp3/protocols/protocol_postProcessing_deepPostProcessing.py b/xmipp3/protocols/protocol_postProcessing_deepPostProcessing.py
index 73681fbf5..0297a8577 100644
--- a/xmipp3/protocols/protocol_postProcessing_deepPostProcessing.py
+++ b/xmipp3/protocols/protocol_postProcessing_deepPostProcessing.py
@@ -244,6 +244,7 @@ def deepVolPostProStep(self):
         else: #self.NORMALIZATION_MASK
           params+= " --checkpoint  %s "%self.getModel("deepEMhancer", "production_checkpoints/deepEMhancer_masked.hd5")
 
+        os.environ['TF_FORCE_GPU_ALLOW_GROWTH'] = 'true'
         self.runJob("xmipp_deep_volume_postprocessing", params, numberOfMpi=1)
 
                                                         
diff --git a/xmipp3/protocols/protocol_preprocess/protocol_create_mask3d.py b/xmipp3/protocols/protocol_preprocess/protocol_create_mask3d.py
index 63884f71a..34a732365 100644
--- a/xmipp3/protocols/protocol_preprocess/protocol_create_mask3d.py
+++ b/xmipp3/protocols/protocol_preprocess/protocol_create_mask3d.py
@@ -75,7 +75,7 @@ def _defineParams(self, form):
         # For volume sources
         isVolume = 'source==%d' % SOURCE_VOLUME
         form.addParam('inputVolume', PointerParam, pointerClass="Volume",
-                      label="Input volume",  condition=isVolume,
+                      label="Input volume", allowsNull=True, condition=isVolume,
                       help="Select the volume that will be used to create the mask")
         form.addParam('volumeOperation', EnumParam, default=OPERATION_THRESHOLD,
                       choices=['Threshold', 'Segment', 'Only postprocess'],
diff --git a/xmipp3/protocols/protocol_preprocess/protocol_crop_resize.py b/xmipp3/protocols/protocol_preprocess/protocol_crop_resize.py
index d244469bd..2eb92f3d1 100644
--- a/xmipp3/protocols/protocol_preprocess/protocol_crop_resize.py
+++ b/xmipp3/protocols/protocol_preprocess/protocol_crop_resize.py
@@ -28,6 +28,7 @@
 # **************************************************************************
 
 import pyworkflow.protocol.constants as const
+from pwem.convert.headers import setMRCSamplingRate
 from pyworkflow.protocol.params import (BooleanParam, EnumParam, FloatParam,
                                         IntParam)
 from pwem.objects import Volume
@@ -273,6 +274,7 @@ def _preprocessOutput(self, output):
         
         if self.doResize:
             output.setSamplingRate(self.samplingRate)
+            setMRCSamplingRate(self.outputStk, self.samplingRate)
             
     def _updateItem(self, item, row):
         """ Update also the sampling rate and 
@@ -398,31 +400,32 @@ def windowStep(self, isFirstStep, args):
         XmippResizeHelper.windowStep(self, self._ioArgs(isFirstStep)+args)
         
     def _preprocessOutput(self, volumes):
-        # We use the preprocess only whne input is a set
+        # We use the preprocess only when input is a set
         # we do not use postprocess to setup correctly
         # the samplingRate before each volume is added
         if not self._isSingleInput():
             if self.doResize:
                 volumes.setSamplingRate(self.samplingRate)
 
-    def _postprocessOutput(self, volumes):
+    def _postprocessOutput(self, volume:Volume):
         # We use the postprocess only when input is a volume
         if self._isSingleInput():
             if self.doResize:
-                volumes.setSamplingRate(self.samplingRate)
+                volume.setSamplingRate(self.samplingRate)
                 # we have a new sampling so origin need to be adjusted
                 iSampling = self.inputVolumes.get().getSamplingRate()
                 oSampling = self.samplingRate
                 xdim_i, ydim_i, zdim_i = self.inputVolumes.get().getDim()
-                xdim_o, ydim_o, zdim_o = volumes.getDim()
+                xdim_o, ydim_o, zdim_o = volume.getDim()
 
                 xOrig, yOrig , zOrig = \
                     self.inputVolumes.get().getShiftsFromOrigin()
                 xOrig += (xdim_i*iSampling-xdim_o*oSampling)/2.
                 yOrig += (ydim_i*iSampling-ydim_o*oSampling)/2.
                 zOrig += (zdim_i*iSampling-zdim_o*oSampling)/2.
-                volumes.setShiftsInOrigin(xOrig, yOrig, zOrig)
-                volumes.setSamplingRate(oSampling)
+                volume.setShiftsInOrigin(xOrig, yOrig, zOrig)
+                volume.setSamplingRate(oSampling)
+                setMRCSamplingRate(volume.getFileName(), oSampling)
 
     #--------------------------- INFO functions ----------------------------------------------------
     def _summary(self):
diff --git a/xmipp3/protocols/protocol_reconstruct_fourier.py b/xmipp3/protocols/protocol_reconstruct_fourier.py
index 4424f86f3..a9125b193 100644
--- a/xmipp3/protocols/protocol_reconstruct_fourier.py
+++ b/xmipp3/protocols/protocol_reconstruct_fourier.py
@@ -178,6 +178,10 @@ def reconstructStep(self, params):
                 self.runJob('xmipp_reconstruct_fourier', params)
             else:
                 self.runJob('xmipp_reconstruct_fourier_accel', params)
+
+        self.runJob("xmipp_image_header", "-i %s --sampling_rate %f"%\
+                    (self._getFileName('output_volume'), self.inputParticles.get().getSamplingRate()),
+                    numberOfMpi=1)
             
     def createOutputStep(self):
         imgSet = self.inputParticles.get()
diff --git a/xmipp3/protocols/protocol_resolution_deepres.py b/xmipp3/protocols/protocol_resolution_deepres.py
index 0898a6554..0607d3ea9 100644
--- a/xmipp3/protocols/protocol_resolution_deepres.py
+++ b/xmipp3/protocols/protocol_resolution_deepres.py
@@ -87,14 +87,14 @@ def _defineParams(self, form):
                             " First core index is 0, second 1 and so on.")               
 
         form.addParam('inputVolume', PointerParam, pointerClass='Volume',
-                      label="Input Volume", 
+                      label="Input Volume", important=True, 
                       help='Select a volume for determining its '
                       'local resolution.')
 
         form.addParam('Mask', PointerParam, pointerClass='VolumeMask', 
-                      allowsNull=True,
+                      important=True,
                       label="Mask", 
-                      help='The mask determines which points are specimen'
+                      help='Binary mask. The mask determines which points are specimen'
                       ' and which are not')
         
         form.addParam('range', EnumParam, choices=[u'2.5Å - 13.0Å', u'1.5Å - 6.0Å'],
@@ -363,5 +363,5 @@ def _validate(self):
         return error
     
     def _citations(self):
-        return ['Ramirez-Aportela-2019']
+        return ['Ramirez-Aportela et al., IUCrJ, 2019']
 
diff --git a/xmipp3/protocols/protocol_resolution_monogenic_signal.py b/xmipp3/protocols/protocol_resolution_monogenic_signal.py
index 1b0dd3be1..763652466 100644
--- a/xmipp3/protocols/protocol_resolution_monogenic_signal.py
+++ b/xmipp3/protocols/protocol_resolution_monogenic_signal.py
@@ -121,15 +121,14 @@ def _defineParams(self, form):
 
         group = form.addGroup('Extra parameters')
         line = group.addLine('Resolution Range (Å)',
-                             help="If the user knows the range of resolutions or"
-                                  " only a range of frequencies needs to be analysed."
-                                  "If Low is empty MonoRes will try to estimate the range. "
-                                  "it should be better if a range is provided")
+                             help="The range of resolutions to be analysed."
+                                  "The local resolution will be estimated from low to high in "
+                                  "steps given by the step box (advanced parameter)")
 
         group.addParam('significance', FloatParam, default=0.95,
                        expertLevel=LEVEL_ADVANCED,
                        label="Significance",
-                       help='Relution is computed using hypothesis tests, '
+                       help='Resolution is computed using hypothesis tests, '
                             'this value determines the significance of that test')
 
         self.maskthreshold = DeprecatedParam('maskThreshold', self)
diff --git a/xmipp3/protocols/protocol_subtract_projection.py b/xmipp3/protocols/protocol_subtract_projection.py
index 9eecea84a..d8212d6e5 100644
--- a/xmipp3/protocols/protocol_subtract_projection.py
+++ b/xmipp3/protocols/protocol_subtract_projection.py
@@ -34,42 +34,79 @@
 from xmipp3.convert import writeSetOfParticles, readSetOfParticles
 
 
-class XmippProtSubtractProjection(EMProtocol):
-    """ This protocol computes the subtraction between particles and a reference volume, by computing its projections with the same angles that input particles have. Then, each particle and the correspondent projection of the reference volume are numerically adjusted and subtracted using a mask which denotes the region to keep. """
-
-    _label = 'subtract projection'
-    INPUT_PARTICLES = "input_particles.xmd"
+class XmippProtSubtractProjectionBase(EMProtocol):
+    """ Helper class that contains some Protocol utilities methods
+    used by both  XmippProtSubtractProjection and XmippProtBoostParticles."""
 
     # --------------------------- DEFINE param functions --------------------------------------------
-    def _defineParams(self, form):
+    @classmethod
+    def _defineParams(cls, form):
         form.addSection(label='Input')
-        form.addParam('particles', PointerParam, pointerClass='SetOfParticles', label="Particles: ",
+        form.addParam('inputParticles', PointerParam, pointerClass='SetOfParticles', label="Particles: ",
                       help='Specify a SetOfParticles')
-        form.addParam('vol', PointerParam, pointerClass='Volume', label="Reference volume ", help='Specify a volume.')
-        form.addParam('mask', PointerParam, pointerClass='VolumeMask', label='Mask for region to keep', allowsNull=True,
-                      help='Specify a 3D mask for the region of the input volume that you want to keep. '
-                           'If no mask is given, the subtraction is performed in whole images.')
-        form.addParam('mwidth', FloatParam, label="Extra width in final mask: ", default=40, expertLevel=LEVEL_ADVANCED,
-                      help='Length (in A) to add for each side to the final result mask. -1 means no mask.')
-        form.addParam('resol', FloatParam, label="Maximum resolution: ", default=3,  expertLevel=LEVEL_ADVANCED,
+        form.addParam('vol', PointerParam, pointerClass='Volume', label="Reference volume ",
+                      help='Specify a volume.')
+        form.addParam('cirmaskrad', FloatParam, label="Circular mask radius: ", default=-1, expertLevel=LEVEL_ADVANCED,
+                      help='Radius of the circular mask to avoid edge artifacts. '
+                           'If -1 it is half the X dimension of the input particles')
+        form.addParam('resol', FloatParam, label="Maximum resolution: ", default=3, expertLevel=LEVEL_ADVANCED,
                       help='Maximum resolution (in A) of the data ')
-        form.addParam('limit_freq', BooleanParam, label="Limit frequency?: ", default=False, expertLevel=LEVEL_ADVANCED,
+        form.addParam('nonNegative', BooleanParam, label="Ignore particles with negative beta0 or R2?: ",
+                      default=True,
+                      expertLevel=LEVEL_ADVANCED,
+                      help='Particles with negative beta0 or R2 will not appear in the output set as they are '
+                           'considered bad particles. Moreover, negative betas will not contribute to mean beta if '
+                           '"mean" option is selected')
+        form.addParam('limit_freq', BooleanParam, label="Limit frequency?: ", default=False,
+                      expertLevel=LEVEL_ADVANCED,
                       help='Limit frequency in the adjustment process to the frequency correspondent to the resolution'
                            ' indicated in "Maximum resolution" field above')
-        form.addParam('sigma', FloatParam, label="Decay of the filter (sigma): ", default=3, expertLevel=LEVEL_ADVANCED,
+        form.addParam('sigma', FloatParam, label="Decay of the filter (sigma): ", default=3,
+                      expertLevel=LEVEL_ADVANCED,
                       help='Decay of the filter (sigma) to smooth the mask transition')
         form.addParam('pad', IntParam, label="Fourier padding factor: ", default=2, expertLevel=LEVEL_ADVANCED,
                       help='The volume is zero padded by this factor to produce projections')
 
     # --------------------------- INSERT steps functions --------------------------------------------
     def _insertAllSteps(self):
+        self._insertSubSteps()
+        self._insertFunctionStep('createOutputStep')
+
+    # --------------------------- STEPS functions --------------------------------------------
+    def createOutputStep(self):
+        inputSet = self.inputParticles.get()
+        outputSet = self._createSetOfParticles()
+        outputSet.copyInfo(inputSet)
+        readSetOfParticles(self._getExtraPath("output_particles.xmd"), outputSet,
+                           extraLabels=[emlib.MDL_SUBTRACTION_R2, emlib.MDL_SUBTRACTION_BETA0,
+                                        emlib.MDL_SUBTRACTION_BETA1])
+        self._defineOutputs(outputParticles=outputSet)
+        self._defineSourceRelation(inputSet, outputSet)
+
+
+class XmippProtSubtractProjection(XmippProtSubtractProjectionBase):
+    """ This protocol computes the subtraction between particles and a reference volume, by computing its projections with the same angles that input particles have. Then, each particle and the correspondent projection of the reference volume are numerically adjusted and subtracted using a mask which denotes the region to keep. """
+
+    _label = 'subtract projection'
+    INPUT_PARTICLES = "input_particles.xmd"
+
+    # --------------------------- DEFINE param functions --------------------------------------------
+    def _defineParams(self, form):
+        XmippProtSubtractProjectionBase._defineParams(form)
+        form.addParam('mask', PointerParam, pointerClass='VolumeMask', label='Mask for region to keep', allowsNull=True,
+                      help='Specify a 3D mask for the region of the input volume that you want to keep. '
+                           'If no mask is given, the subtraction is performed in whole images.')
+        form.addParam('mwidth', FloatParam, label="Extra width in final mask: ", default=40, expertLevel=LEVEL_ADVANCED,
+                      help='Length (in A) to add for each side to the final mask. -1 means no mask.')
+
+    # --------------------------- INSERT steps functions --------------------------------------------
+    def _insertSubSteps(self):
         self._insertFunctionStep('convertStep')
         self._insertFunctionStep('subtractionStep')
-        self._insertFunctionStep('createOutputStep')
 
     # --------------------------- STEPS functions --------------------------------------------
     def convertStep(self):
-        writeSetOfParticles(self.particles.get(), self._getExtraPath(self.INPUT_PARTICLES))
+        writeSetOfParticles(self.inputParticles.get(), self._getExtraPath(self.INPUT_PARTICLES))
 
     def subtractionStep(self):
         vol = self.vol.get().clone()
@@ -77,28 +114,21 @@ def subtractionStep(self):
         if fnVol.endswith('.mrc'):
             fnVol += ':mrc'
         args = '-i %s --ref %s -o %s --sampling %f --max_resolution %f --fmask_width %f --padding %f ' \
-               '--sigma %d --limit_freq %d' % \
+               '--sigma %d --limit_freq %d --cirmaskrad %d --save %s' % \
                (self._getExtraPath(self.INPUT_PARTICLES), fnVol, self._getExtraPath("output_particles"),
                 vol.getSamplingRate(), self.resol.get(), self.mwidth.get(), self.pad.get(), self.sigma.get(),
-                int(self.limit_freq.get()))
+                int(self.limit_freq.get()), self.cirmaskrad.get(), self._getExtraPath())
         mask = self.mask.get()
         if mask is not None:
             args += ' --mask %s' % mask.getFileName()
-        args += ' --save %s' % self._getExtraPath()
+        if self.nonNegative.get():
+            args += ' --nonNegative'
         self.runJob("xmipp_subtract_projection", args)
 
-    def createOutputStep(self):
-        inputSet = self.particles.get()
-        outputSet = self._createSetOfParticles()
-        outputSet.copyInfo(inputSet)
-        readSetOfParticles(self._getExtraPath('input_particles.xmd'), outputSet, extraLabels=[emlib.MDL_SUBTRACTION_R2])
-        self._defineOutputs(outputParticles=outputSet)
-        self._defineSourceRelation(inputSet, outputSet)
-
     # --------------------------- INFO functions --------------------------------------------
     def _validate(self):
         errors = []
-        part = self.particles.get().getFirstItem()
+        part = self.inputParticles.get().getFirstItem()
         vol = self.vol.get()
         mask = self.mask.get()
         if part.getDim()[0] != vol.getDim()[0]:
@@ -116,7 +146,7 @@ def _validate(self):
 
     def _summary(self):
         summary = ["Volume: %s\nSet of particles: %s\nMask: %s" %
-                   (self.vol.get().getFileName(), self.particles.get(), self.mask.get().getFileName())]
+                   (self.vol.get().getFileName(), self.inputParticles.get(), self.mask.get().getFileName())]
         return summary
 
     def _methods(self):
@@ -127,3 +157,64 @@ def _methods(self):
             methods.append("Volume projections subtracted to particles keeping the region in %s"
                            % basename(self.mask.get().getFileName()))
         return methods
+
+
+class XmippProtBoostParticles(XmippProtSubtractProjectionBase):
+    """ This protocol tries to boost the frequencies of the particles to imporve them, based on an adjustment on its correspondent projections from a reference volume. """
+
+    _label = 'boost particles'
+    INPUT_PARTICLES = "input_particles.xmd"
+
+    # --------------------------- DEFINE param functions --------------------------------------------
+    def _defineParams(self, form):
+        XmippProtSubtractProjectionBase._defineParams(form)
+
+    # --------------------------- INSERT steps functions --------------------------------------------
+    def _insertSubSteps(self):
+        self._insertFunctionStep('convertStep')
+        self._insertFunctionStep('boostingStep')
+
+    # --------------------------- STEPS functions --------------------------------------------
+    def convertStep(self):
+        writeSetOfParticles(self.inputParticles.get(), self._getExtraPath(self.INPUT_PARTICLES))
+
+    def boostingStep(self):
+        vol = self.vol.get().clone()
+        fnVol = vol.getFileName()
+        if fnVol.endswith('.mrc'):
+            fnVol += ':mrc'
+        args = '-i %s --ref %s -o %s --sampling %f --max_resolution %f --padding %f --sigma %d --limit_freq %d ' \
+               '--cirmaskrad %d --boost --save %s'\
+               % (self._getExtraPath(self.INPUT_PARTICLES), fnVol, self._getExtraPath("output_particles"),
+                  vol.getSamplingRate(), self.resol.get(), self.pad.get(), self.sigma.get(), int(self.limit_freq.get()),
+                  self.cirmaskrad.get(), self._getExtraPath())
+
+        if self.nonNegative.get():
+            args += ' --nonNegative'
+        self.runJob("xmipp_subtract_projection", args)
+
+    # --------------------------- INFO functions --------------------------------------------
+    def _validate(self):
+        errors = []
+        part = self.inputParticles.get().getFirstItem()
+        vol = self.vol.get()
+        if part.getDim()[0] != vol.getDim()[0]:
+            errors.append("Input particles and volume should have same X and Y dimensions")
+        if part.getSamplingRate() != vol.getSamplingRate():
+            errors.append("Input particles and volume should have same sampling rate")
+        if self.resol.get() == 0:
+            errors.append("Resolution (angstroms) should be bigger than 0")
+        return errors
+
+    def _summary(self):
+        summary = ["Volume: %s\nSet of particles: %s\n" %
+                   (self.vol.get().getFileName(), self.inputParticles.get())]
+        return summary
+
+    def _methods(self):
+        methods = []
+        if not hasattr(self, 'outputParticles'):
+            methods.append("Output particles not ready yet.")
+        else:
+            methods.append("Particles boosted according to their equivalent projections from a reference volume.")
+        return methods
\ No newline at end of file
diff --git a/xmipp3/protocols/protocol_tilt_analysis.py b/xmipp3/protocols/protocol_tilt_analysis.py
new file mode 100644
index 000000000..e6827da35
--- /dev/null
+++ b/xmipp3/protocols/protocol_tilt_analysis.py
@@ -0,0 +1,644 @@
+# **************************************************************************
+# *
+# * Authors:     Carlos Oscar S. Sorzano (coss@cnb.csic.es)
+# *              Daniel Marchán Torres (da.marchan@cnb.csic.es)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+import numpy as np
+from itertools import combinations
+import os
+from os.path import join, basename, exists
+import math
+from datetime import datetime
+from collections import OrderedDict
+from pyworkflow import VERSION_3_0
+from pyworkflow.protocol import STEPS_PARALLEL
+from pyworkflow.protocol.params import (PointerParam, IntParam,
+                                        BooleanParam, LEVEL_ADVANCED, FloatParam, GE, GT, Range)
+import pyworkflow.utils as pwutils
+from pyworkflow.utils.properties import Message
+from pyworkflow.utils.path import moveFile, getFiles
+import pyworkflow.protocol.constants as cons
+from pwem.objects import SetOfMicrographs, Image, Micrograph, Acquisition, String, Set, Float
+from pwem.emlib.image import ImageHandler
+from pwem.protocols import ProtMicrographs
+from xmipp3 import emlib
+from xmipp3.convert import getScipionObj
+from matplotlib import pyplot as plt
+
+
+class XmippProtTiltAnalysis(ProtMicrographs):
+    """ Estimate the tilt of a micrograph, by analyzing the PSD correlations of different segments of the image.
+    """
+    _label = 'tilt analysis'
+    _lastUpdateVersion = VERSION_3_0
+    stats = {}
+
+    def __init__(self, **args):
+        ProtMicrographs.__init__(self, **args)
+        self.stepsExecutionMode = STEPS_PARALLEL
+
+    # -------------------------- DEFINE param functions ----------------------
+    def _defineParams(self, form):
+        form.addSection(label=Message.LABEL_INPUT)
+
+        form.addParam('inputMicrographs', PointerParam,
+                      pointerClass='SetOfMicrographs',
+                      label="Input micrographs", important=True,
+                      help='Select the SetOfMicrograph to be preprocessed.')
+
+        form.addParam('window_size', IntParam, label='Window size',
+                      default=1024, expertLevel=LEVEL_ADVANCED, validators=[GE(256, 'Error must be greater than 256')],
+                      help='''By default, the micrograph will be divided into windows of dimensions 512x512, the PSD '''
+                           '''its correlations will be computed in every segment.''')
+
+        form.addParam('objective_resolution', FloatParam, label='Objective resolution',
+                      default=3, expertLevel=LEVEL_ADVANCED, validators=[GT(0, 'Error must be Positive')],
+                      help='''By default, micrographs PSD will be cropped into a central windows of dimensions (xdim*'''
+                           '''(sampling rate/objective resolution)) x (ydim*(sampling rate/objective resolution)).''')
+
+        form.addParam('meanCorr_threshold', FloatParam, label='Mean correlation threshold',
+                      default=0.5, expertLevel=LEVEL_ADVANCED, validators=[Range(0, 1)],
+                      help='''By default, micrographs will be divided into an output set and a discarded set based'''
+                           ''' on the mean and std threshold''')
+
+        form.addParam('stdCorr_threshold', FloatParam, label='STD correlation threshold',
+                      default=0.1, expertLevel=LEVEL_ADVANCED, validators=[GT(0, 'Error must be greater than 0')],
+                      help='''By default, micrographs will be divided into an output set and a discarded set based'''
+                           ''' on the mean and std threshold.''')
+
+        form.addHidden('saveIntermediateResults', BooleanParam, default=False, label="Save intermediate results",
+                       help='''Save the micrograph segments, the PSD of those segments'''
+                            ''' and the correlation statistics of those segments.''')
+
+        form.addParallelSection(threads=4, mpi=1)
+
+    # -------------------------- STEPS functions ------------------------------
+    def _insertAllSteps(self):
+        """ Insert the steps to perform CTF estimation, or re-estimation,
+        on a set of micrographs.
+        """
+        self.initializeStep()
+        fDeps = self._insertNewMicrographSteps(self.insertedDict,
+                                               self.micsDict.values())
+        # For the streaming mode, the steps function have a 'wait' flag that can be turned on/off. For example, here we insert the
+        # createOutputStep but it wait=True, which means that can not be executed until it is set to False
+        # (when the input micrographs stream is closed)
+        waitCondition = self._getFirstJoinStepName() == 'createOutputStep'
+        finalSteps = self._insertFinalSteps(fDeps)
+
+        self._insertFunctionStep('createOutputStep',
+                                 prerequisites=finalSteps, wait=waitCondition)
+
+    def initializeStep(self):
+        self.insertedDict = OrderedDict()
+        self.samplingRate = self.inputMicrographs.get().getSamplingRate()
+        self.micsFn = self.inputMicrographs.get().getFileName()
+        self.stats = {}
+        self.streamClosed = self.inputMicrographs.get().isStreamClosed()
+        self.micsDict = {mic.getObjId(): mic.clone() for mic in self._loadInputList(self.micsFn).iterItems()}
+        pwutils.makePath(self._getExtraPath('DONE'))
+
+    def createOutputStep(self):
+        self._closeOutputSet()
+
+    def _loadInputList(self, micsFn):
+        """ Load the input set of mics and create a list. """
+        self.debug("Loading input db: %s" % micsFn)
+        micSet = SetOfMicrographs(filename=micsFn)
+        micSet.loadAllProperties()
+        self.streamClosed = micSet.isStreamClosed()
+        micSet.close()
+        self.debug("Closed db.")
+        return micSet
+
+    def _stepsCheck(self):
+        # Input micrograph set can be loaded or None when checked for new inputs
+        # If None, we load it
+        self._checkNewInput()
+        self._checkNewOutput()
+
+    def _getFirstJoinStepName(self):
+        # This function will be used for streaming, to check which is
+        # the first function that need to wait for all micrographs
+        # to have completed, this can be overriden in subclasses
+        # (e.g., in Xmipp 'sortPSDStep')
+        return 'createOutputStep'
+
+    def _getFirstJoinStep(self):
+        for s in self._steps:
+            if s.funcName == self._getFirstJoinStepName():
+                return s
+        return None
+
+    def _checkNewInput(self):
+        # Check if there are new micrographs to process from the input set
+        now = datetime.now()
+        self.lastCheck = getattr(self, 'lastCheck', now)
+        mTime = datetime.fromtimestamp(os.path.getmtime(self.micsFn))
+        self.debug('Last check: %s, modification: %s'
+                   % (pwutils.prettyTime(self.lastCheck),
+                      pwutils.prettyTime(mTime)))
+        # If the input micrographs.sqlite have not changed since our last check,
+        # it does not make sense to check for new input data
+        if self.lastCheck > mTime and hasattr(self, 'micsDict'):
+            return None
+
+        self.lastCheck = now
+        # Open input micrographs.sqlite and close it as soon as possible
+        micSet = self._loadInputList(self.micsFn)
+
+        newMicsDict = {mic.getObjId(): mic.clone() for mic in micSet.iterItems()
+                       if mic.getObjId() not in self.insertedDict}
+        self.micsDict.update(newMicsDict)
+        outputStep = self._getFirstJoinStep()
+
+        if len(newMicsDict) > 0:
+            fDeps = self._insertNewMicrographSteps(self.insertedDict, newMicsDict.values())
+
+            if outputStep is not None:
+                outputStep.addPrerequisites(*fDeps)
+
+            self.updateSteps()
+
+    def _checkNewOutput(self):
+        if getattr(self, 'finished', False):
+            return
+        # Load previously done items (from text file)
+        doneList = self._readDoneList()
+        # Check for newly done items
+        newDone = [m.clone() for m in self.micsDict.values() if
+                   int(m.getObjId()) not in doneList and self._isMicDone(m)]
+        allDone = len(doneList) + len(newDone)
+        # We have finished when there is not more input movies
+        # (stream closed) and the number of processed movies is
+        # equal to the number of inputs
+        self.finished = self.streamClosed and allDone == len(self.micsDict)
+        streamMode = Set.STREAM_CLOSED if self.finished else Set.STREAM_OPEN
+
+        if newDone:
+            self._writeDoneList(newDone)
+        elif not self.finished:
+            # If we are not finished and no new output have been produced
+            # it does not make sense to proceed and updated the outputs
+            # so we exit from the function here
+            return
+
+        micsAccepted = []
+        micsDiscarded = []
+        for mic in newDone:
+            micId = mic.getObjId()
+            corr_mean = Float(self.stats[micId]['mean'])
+            corr_std = Float(self.stats[micId]['std'])
+            corr_min = Float(self.stats[micId]['min'])
+            corr_max = Float(self.stats[micId]['max'])
+            psdImage = Image(location=self.getPSDs(self._getExtraPath(), micId))
+            new_Mic = mic.clone()
+            setAttribute(new_Mic, '_tilt_mean_corr', corr_mean)
+            setAttribute(new_Mic, '_tilt_std_corr', corr_std)
+            setAttribute(new_Mic, '_tilt_min_corr', corr_min)
+            setAttribute(new_Mic, '_tilt_max_corr', corr_max)
+            setAttribute(new_Mic, '_tilt_psds_image', psdImage)
+            # Double threshold
+            if corr_mean > self.meanCorr_threshold.get() and corr_std < self.stdCorr_threshold.get():
+                micsAccepted.append(new_Mic)
+            else:
+                micsDiscarded.append(new_Mic)
+
+        if len(micsAccepted) > 0:
+            micSet = self._loadOutputSet(SetOfMicrographs, 'micrograph.sqlite')
+            for mic in micsAccepted:
+                micSet.append(mic)
+            self._updateOutputSet('outputMicrographs', micSet, streamMode)
+
+        if len(micsDiscarded) > 0:
+            micSet_discarded = self._loadOutputSet(SetOfMicrographs, 'micrograph' + 'DISCARDED' + '.sqlite')
+            for mic in micsDiscarded:
+                micSet_discarded.append(mic)
+            self._updateOutputSet('discardedMicrographs', micSet_discarded, streamMode)
+
+        if self.finished:  # Unlock createOutputStep if finished all jobs
+            outputStep = self._getFirstJoinStep()
+            if outputStep and outputStep.isWaiting():
+                outputStep.setStatus(cons.STATUS_NEW)
+
+    def _closeOutputSet(self):
+        if self.hasAttribute('outputMicrographs'):
+            self.outputMicrographs.close()
+            self.outputMicrographs.setStreamState(Set.STREAM_CLOSED)
+        if self.hasAttribute('discardedMicrographs'):
+            self.discardedMicrographs.close()
+            self.discardedMicrographs.setStreamState(Set.STREAM_CLOSED)
+
+    def _insertNewMicrographSteps(self, insertedDict, inputMics):
+        """ Insert steps to process new micrographs (from streaming)
+        Params:
+            insertedDict: contains already processed micrographs
+            inputMics: input mics set to be check
+        """
+        deps = []
+        # For each micrograph insert the step to process it
+        for micrograph in inputMics:
+            if micrograph.getObjId() not in insertedDict:
+                tiltStepId = self._insertMicrographStep(micrograph)
+                deps.append(tiltStepId)
+                insertedDict[micrograph.getObjId()] = tiltStepId
+
+        return deps
+
+    def _insertMicrographStep(self, micrograph):
+        """ Insert the processMicStep for a given movie. """
+        # Note1: At this point is safe to pass the micrograph, since this
+        # is not executed in parallel, here we get the params
+        # to pass to the actual step that is gone to be executed later on
+        # Note2: We are serializing the Movie as a dict that can be passed
+        # as parameter for a functionStep
+        micDict = micrograph.getObjDict(includeBasic=True)
+        micStepId = self._insertFunctionStep('processMicrographStep', micDict, prerequisites=[])
+        return micStepId
+
+    def processMicrographStep(self, micDict):
+        micrograph = Micrograph()
+        micrograph.setAcquisition(Acquisition())
+        micrograph.setAttributesFromDict(micDict, setBasic=True, ignoreMissing=True)
+        micFolderTmp = self._getOutputMicFolder(micrograph)
+        micFn = micrograph.getFileName()
+        micName = basename(micFn)
+        micDoneFn = self._getMicrographDone(micrograph)
+
+        if self.isContinued() and os.path.exists(micDoneFn):
+            self.info("Skipping micrograph: %s, seems to be done" % micFn)
+            return
+
+        pwutils.cleanPath(micDoneFn)
+
+        pwutils.makePath(micFolderTmp)
+        pwutils.createLink(micFn, join(micFolderTmp, micName))
+        newMicName = self._correctFormat(micName, micFn, micFolderTmp)
+
+        # Just store the original name in case it is needed in _processMovie
+        micrograph._originalFileName = String(objDoStore=False)
+        micrograph._originalFileName.set(micrograph.getFileName())
+        # Now set the new filename (either linked or converted)
+        micrograph.setFileName(os.path.join(micFolderTmp, newMicName))
+        self.info("Processing micrograph: %s" % micrograph.getFileName())
+
+        self._processMicrograph(micrograph)
+
+        if self.saveIntermediateResults.get():
+            micOutputFn = self._getResultsMicFolder(micrograph)
+            pwutils.makePath(micOutputFn)
+            for file in getFiles(micFolderTmp):
+                moveFile(file, micOutputFn)
+
+        # Mark this movie as finished
+        open(micDoneFn, 'w').close()
+
+    def _processMicrograph(self, micrograph):
+        micrographId = micrograph.getObjId()
+        correlations = self.calculateTiltCorrelationStep(micrograph)
+        # Numpy array to compute all the
+        correlations = np.asarray(correlations)
+        # Calculate the mean, dev of the correlation
+        stats = computeStats(correlations)
+        self.stats[micrographId] = stats
+
+        fnMonitorSummary = self._getPath("summaryForMonitor.txt")
+
+        if not os.path.exists(fnMonitorSummary):
+            fnMonitorSummary = open(fnMonitorSummary, "w")
+        else:
+            fnMonitorSummary = open(fnMonitorSummary, "a")
+
+        self.info("\nmicrograph_%06d: mean=%f std=%f [min=%f,max=%f] \n" %
+                  (micrographId, stats['mean'], stats['std'], stats['min'], stats['max']))
+
+        fnMonitorSummary.write("micrograph_%06d: mean=%f std=%f [min=%f,max=%f] \n" %
+                               (micrographId, stats['mean'], stats['std'], stats['min'], stats['max']))
+        fnMonitorSummary.close()
+
+    def calculateTiltCorrelationStep(self, mic):
+        psds = []
+        correlations = []
+        autocorrelations = []
+        # Read image
+        micFolder = self._getOutputMicFolder(mic)
+        micImage = ImageHandler().read(mic.getLocation())
+        dimx, dimy, z, n = micImage.getDimensions()
+        wind_step = self.window_size.get()
+        x_steps, y_steps = window_coordinates2D(dimx, dimy, wind_step)
+        subWindStep = int(wind_step * (self.samplingRate / self.objective_resolution.get()))
+        x_steps_psd, y_steps_psd = window_coordinates2D(subWindStep * 2, subWindStep * 2, subWindStep)
+        # Extract windows
+        window_image = ImageHandler().createImage()
+        rotatedWind_psd = ImageHandler().createImage()
+        output_image = ImageHandler().createImage()
+        output_array = np.zeros((subWindStep * 2, subWindStep * 2))
+        ih = ImageHandler()
+
+        for i in range(0, 3, 2):
+            for j in range(0, 3, 2):
+                window = micImage.window2D(x_steps[i], y_steps[j], x_steps[i + 1], y_steps[j + 1])
+                x_dim, y_dim, z, n = window.getDimensions()
+                # NORMALIZED
+                mean, dev, minCorr, maxCorr = window.computeStats()
+                winMatrix = (window.getData() - mean) / dev
+                window_image.setData(winMatrix)
+                # Compute PSD
+                wind_psd = window_image.computePSD(0.4, x_dim, y_dim, 1)
+                wind_psd.convertPSD()
+                # Rotate PSD
+                psdMatrix = wind_psd.getData()
+                P = np.identity(3)
+                rotatedPSD_matrix, M = rotation(psdMatrix, 90, psdMatrix.shape, P)
+                rotatedWind_psd.setData(rotatedPSD_matrix)
+                # Window intro a sub window psd for the correlations
+                x0_sub = int((x_dim / 2) - (subWindStep / 2))
+                y0_sub = int((x_dim / 2) - (subWindStep / 2))
+
+                subWind_psd = wind_psd.window2D(x0_sub, y0_sub, int(x0_sub + subWindStep - 1),
+                                                int(y0_sub + subWindStep - 1))
+
+                subRotatedWind_psd = rotatedWind_psd.window2D(x0_sub, y0_sub, int(x0_sub + subWindStep - 1),
+                                                              int(y0_sub + subWindStep - 1))
+                # SAVE images
+                filename = "tmp" + str(i) + str(j) + '.mrc'
+                window_image.write(os.path.join(micFolder, filename))
+                filename_subwindPSD = os.path.join(micFolder,
+                                                   "tmp_psd" + str(i) + str(j) + '.mrc')
+                filename_rotatedPSD = os.path.join(micFolder,
+                                                   "tmp_psd_rotated" + str(i) + str(j) + '.mrc')
+                subWind_psd.write(filename_subwindPSD)
+                subRotatedWind_psd.write(filename_rotatedPSD)
+                # Filter this window using runJob
+                filename_subwindPSD_filt = os.path.join(micFolder,
+                                                        "tmp_psd_filtered" + str(i) + str(j) + '.mrc')
+                filename_subwindRotatedPSD_filt = os.path.join(micFolder,
+                                                               "tmp_psd_rot_filtered" + str(i) + str(j) + '.mrc')
+
+                args1 = '-i %s -o %s --fourier low_pass 0.1' % (filename_subwindPSD, filename_subwindPSD_filt)
+                args2 = '-i %s -o %s --fourier low_pass 0.1' % (filename_rotatedPSD, filename_subwindRotatedPSD_filt)
+
+                self.runJob("xmipp_transform_filter", args1)
+                self.runJob("xmipp_transform_filter", args2)
+                # Calculate autocorrelation 90 degrees
+                subWind_psd_filt = ih.read(filename_subwindPSD_filt)
+                subRotatedWind_psd_filt = ih.read(filename_subwindRotatedPSD_filt)
+                autocorrelation = subWind_psd_filt.correlation(subRotatedWind_psd_filt)
+                # Paint the output array
+                output_array[y_steps_psd[j]:y_steps_psd[j] + subWindStep, x_steps_psd[i]:x_steps_psd[i] + subWindStep] = \
+                    subWind_psd_filt.getData()
+                # Append
+                autocorrelations.append(autocorrelation)
+                psds.append(subWind_psd_filt)
+
+        output_image.setData(output_array)
+        filename = "psd_outputs" + str(mic.getObjId()) + '.jpeg'
+        output_image.write(self._getExtraPath(filename))
+
+        correlation_pairs = list(combinations(psds, 2))
+        for m1, m2 in correlation_pairs:
+            correlation = m1.correlation(m2)
+            correlations.append(correlation)
+
+        correlations.extend(autocorrelations)
+        return correlations
+
+    def _loadOutputSet(self, SetClass, baseName):
+        """
+        Load the output set if it exists or create a new one.
+        """
+        setFile = self._getPath(baseName)
+        # -----------------Si no lo pones asi no funciona
+        if os.path.exists(setFile):
+            outputSet = SetClass(filename=setFile)
+            if outputSet.__len__() == 0:
+                pwutils.path.cleanPath(setFile)
+        # ----------------
+        if os.path.exists(setFile):
+            outputSet = SetClass(filename=setFile)
+            outputSet.loadAllProperties()
+            outputSet.enableAppend()
+        else:
+            outputSet = SetClass(filename=setFile)
+            outputSet.setStreamState(outputSet.STREAM_OPEN)
+
+        inputMicrographs = self.inputMicrographs.get()
+        outputSet.copyInfo(inputMicrographs)
+
+        return outputSet
+
+    def _updateOutputSet(self, outputName, outputSet, state=Set.STREAM_OPEN):
+        outputSet.setStreamState(state)
+        if self.hasAttribute(outputName):
+            outputSet.write()  # Write to commit changes
+            outputAttr = getattr(self, outputName)
+            # Copy the properties to the object contained in the protocol
+            outputAttr.copy(outputSet, copyId=False)
+            # Persist changes
+            self._store(outputAttr)
+        else:
+            # Here the defineOutputs function will call the write() method
+            self._defineOutputs(**{outputName: outputSet})
+            self._store(outputSet)
+        # Close set databaset to avoid locking it
+        outputSet.close()
+
+    # ------------------------- UTILS functions --------------------------------
+    def _correctFormat(self, micName, micFn, micFolderTmp):
+        if micName.endswith('bz2'):
+            newMicName = micName.replace('.bz2', '')
+            # We assume that if compressed the name ends with .mrc.bz2
+            if not exists(newMicName):
+                self.runJob('bzip2', '-d -f %s' % micName, cwd=micFolderTmp)
+
+        elif micName.endswith('tbz'):
+            newMicName = micName.replace('.tbz', '.mrc')
+            # We assume that if compressed the name ends with .tbz
+            if not exists(newMicName):
+                self.runJob('tar', 'jxf %s' % micName, cwd=micFolderTmp)
+
+        elif micName.endswith('.txt'):
+            # Support a list of frame as a simple .txt file containing
+            # all the frames in a raw list, we could use a xmd as well,
+            # but a plain text was choose to simply its generation
+            micTxt = os.path.join(micFolderTmp, micName)
+            with open(micTxt) as f:
+                micOrigin = os.path.basename(os.readlink(micFn))
+                newMicName = micName.replace('.txt', '.mrc')
+                ih = emlib.image.ImageHandler()
+                for i, line in enumerate(f):
+                    if line.strip():
+                        inputFrame = os.path.join(micOrigin, line.strip())
+                        ih.convert(inputFrame, (i + 1, os.path.join(micFolderTmp, newMicName)))
+        else:
+            newMicName = micName
+
+        return newMicName
+
+    def _insertFinalSteps(self, deps):
+        """ This should be implemented in subclasses"""
+        return deps
+
+    def _getOutputMicFolder(self, micrograph):
+        """ Create a Mic folder where to work with it. """
+        return self._getTmpPath('mic_%06d' % micrograph.getObjId())
+
+    def _getResultsMicFolder(self, micrograph):
+        """ Create a Mic folder where to work with it. """
+        return self._getExtraPath('mic_%06d' % micrograph.getObjId())
+
+    def getInputMicrographsPointer(self):
+        return self.inputMicrographs
+
+    def getInputMicrographs(self):
+        return self.getInputMicrographsPointer().get()
+
+    def _writeFailedList(self, micList):
+        """ Write to a text file the items that have failed. """
+        with open(self._getAllFailed(), 'a') as f:
+            for mic in micList:
+                f.write('%d\n' % mic.getObjId())
+
+    def _readDoneList(self):
+        """ Read from a text file the id's of the items that have been done. """
+        doneFile = self._getAllDone()
+        doneList = []
+        # Check what items have been previously done
+        if exists(doneFile):
+            with open(doneFile) as f:
+                doneList += [int(line.strip()) for line in f]
+        return doneList
+
+    def _getAllDone(self):
+        return self._getExtraPath('DONE_all.TXT')
+
+    def _writeDoneList(self, micList):
+        """ Write to a text file the items that have been done. """
+        with open(self._getAllDone(), 'a') as f:
+            for mic in micList:
+                f.write('%d\n' % mic.getObjId())
+
+    def _isMicDone(self, mic):
+        """ A mic is done if the marker file exists. """
+        return exists(self._getMicrographDone(mic))
+
+    def _getMicrographDone(self, mic):
+        """ Return the file that is used as a flag of termination. """
+        return self._getExtraPath('DONE', 'mic_%06d.TXT' % mic.getObjId())
+
+    @staticmethod
+    def getPSDs(micFolder, ID):
+        """ Return the Mic folder where find the PSDs in the tmp folder. """
+        filename = 'psd_outputs' + str(ID) + '.jpeg'
+        return os.path.join(micFolder, filename)
+
+    # --------------------------- INFO functions -------------------------------
+    def _summary(self):
+        fnSummary = self._getPath("summary.txt")
+        if not os.path.exists(fnSummary):
+            summary = ["No summary information yet."]
+        else:
+            fhSummary = open(fnSummary, "r")
+            summary = []
+            for line in fhSummary.readlines():
+                summary.append(line.rstrip())
+            fhSummary.close()
+        return summary
+
+
+# --------------------- WORKERS --------------------------------------
+def applyTransform(imag_array, M, shape):
+    '''Apply a transformation(M) to a np array(imag) and return it in a given shape'''
+    imag = emlib.Image()
+    imag.setData(imag_array)
+    imag = imag.applyWarpAffine(list(M.flatten()), shape, True)
+    return imag.getData()
+
+
+def rotation(imag, angle, shape, P):
+    '''Rotate a np.array and return also the transformation matrix
+    #imag: np.array
+    #angle: angle in degrees
+    #shape: output shape
+    #P: transform matrix (further transformation in addition to the rotation)'''
+    (hsrc, wsrc) = imag.shape
+    angle *= math.pi / 180
+    T = np.asarray([[1, 0, -wsrc / 2], [0, 1, -hsrc / 2], [0, 0, 1]])
+    R = np.asarray([[math.cos(angle), math.sin(angle), 0], [-math.sin(angle), math.cos(angle), 0], [0, 0, 1]])
+    M = np.matmul(np.matmul(np.linalg.inv(T), np.matmul(R, T)), P)
+
+    transformed = applyTransform(imag, M, shape)
+    return transformed, M
+
+
+def window_coordinates2D(x, y, wind_step):
+    x0 = 0
+    xF = x - 1
+    y0 = 0
+    yF = y - 1
+    x_coor = []
+    y_coor = []
+
+    if wind_step < x and wind_step < y:
+        x_coor.append(x0)
+        x_coor.append(x0 + wind_step - 1)
+        x_coor.append(xF - wind_step)
+        x_coor.append(xF)
+
+        y_coor.append(y0)
+        y_coor.append(y0 + wind_step - 1)
+        y_coor.append(yF - wind_step)
+        y_coor.append(yF)
+
+        return x_coor, y_coor
+    else:
+        print("Dimensions not correct")
+        return 0, 0
+
+
+def computeStats(correlations):
+    p = np.percentile(correlations, [25, 50, 75, 97.5])
+    mean = np.mean(correlations)
+    std = np.std(correlations)
+    var = np.var(correlations)
+    maxCorr = np.max(correlations)
+    minCorr = np.min(correlations)
+
+    stats = {'mean': mean,
+             'std': std,
+             'var': var,
+             'max': maxCorr,
+             'min': minCorr,
+             'per25': p[0],
+             'per50': p[1],
+             'per75': p[2],
+             'per97.5': p[3]
+             }
+    return stats
+
+
+def setAttribute(obj, label, value):
+    if value is None:
+        return
+    setattr(obj, label, getScipionObj(value))
diff --git a/xmipp3/protocols/protocol_trigger_data.py b/xmipp3/protocols/protocol_trigger_data.py
index 8eff545c0..20928398e 100644
--- a/xmipp3/protocols/protocol_trigger_data.py
+++ b/xmipp3/protocols/protocol_trigger_data.py
@@ -94,7 +94,7 @@ def _defineParams(self, form):
                            '\n For this option, select the option send all items to output.')
         form.addParam('triggerProt', PointerParam,
                       pointerClass=self.getClassName(),
-                      condition='triggerSignal', allowsNull=True,
+                      condition='triggerSignal',
                       label='Trigger data protocol',
                       help='Select the trigger data protocol that you will send a signal to stop the stream.')
         form.addParam('delay', IntParam, default=10, label="Delay (sec)",
@@ -173,16 +173,15 @@ def _checkNewOutput(self):
             self.finished = False
 
         # Send the signal to the connected protocol
-        if self.triggerSignal.get():
-            if len(self.images) >= self.outputSize:
-                print('Sending signal to stop the input trigger data protocol')
-                self.stopWait()
+        if self.triggerSignal.get() and len(self.images) >= self.outputSize:
+            self.info('Sending signal to stop the input trigger data protocol')
+            self.stopWait()
 
         # Wait for trigger data signal
         if self.triggerWait.get():
-            print('Waiting for signal to stop the stream')
+            self.info('Waiting for signal to stop the stream')
             if self.waitingHasFinished():
-                print('Stopped by received signal from a trigger data protocol')
+                self.info('Stopped by received signal from a trigger data protocol')
                 self.finished = True
 
         outputStep = self._getFirstJoinStep()
@@ -307,7 +306,10 @@ def _summary(self):
         return summary
 
     def _validate(self):
-        pass
+        errors = []
+        if self.triggerSignal.get():
+            if not isinstance(self.triggerProt.get(), XmippProtTriggerData):
+                errors.append("There is not a Trigger protocol connected to send a stop signal.")
 
     # --------------------------- UTILS functions -----------------------------
     def _getFirstJoinStepName(self):
diff --git a/xmipp3/viewers/__init__.py b/xmipp3/viewers/__init__.py
index 1c39faa8c..6e62d9437 100644
--- a/xmipp3/viewers/__init__.py
+++ b/xmipp3/viewers/__init__.py
@@ -67,5 +67,4 @@
 from .viewer_deep_align import XmippDeepAlignViewer
 
 # TODO: Import from continuousflex the modules needed to create clusters (soft dependency)
-from .viewer_angular_alignment_sph import XmippAngularAlignmentSphViewer
 from .viewer_analyze_local_ctf import XmippAnalyzeLocalCTFViewer
diff --git a/xmipp3/viewers/viewer.py b/xmipp3/viewers/viewer.py
index ca7ab84ba..7d1139702 100644
--- a/xmipp3/viewers/viewer.py
+++ b/xmipp3/viewers/viewer.py
@@ -52,6 +52,8 @@
 from xmipp3.protocols import XmippProtDeepDenoising
 from xmipp3.protocols import XmippProtParticleBoxsize
 from .plotter import XmippPlotter
+from xmipp3.protocols import XmippProtTiltAnalysis
+from pwem.viewers.viewers_data import MicrographsView
 
 
 class XmippViewer(DataViewer):
@@ -77,7 +79,8 @@ class XmippViewer(DataViewer):
                 XmippProtAngularGraphConsistency,
                 XmippProtMovieGain,
                 XmippProtDeepDenoising,
-                XmippProtParticleBoxsize
+                XmippProtParticleBoxsize,
+                XmippProtTiltAnalysis
                 ]
 
     def __createTemporaryCtfs(self, obj, setOfMics):
@@ -126,6 +129,9 @@ def _visualize(self, obj, **kwargs):
         #                                   viewParams={OBJCMDS: objCommands},
         #                                   **kwargs))
 
+
+
+
         elif (issubclass(cls, XmippProtExtractParticles) or
               issubclass(cls, XmippProtScreenParticles)):
             particles = obj.outputParticles
@@ -198,6 +204,38 @@ def _visualize(self, obj, **kwargs):
                                                            'labels': '_size',
                                                            'sortby': 'id'})
 
+        elif issubclass(cls, XmippProtTiltAnalysis):
+            if obj.hasAttribute('discardedMicrographs'):
+                fn = obj.discardedMicrographs.getFileName()
+                labels = ('id enabled psdCorr._filename _filename _tilt_mean_corr _tilt_std_corr '
+                          '_tilt_max_corr _tilt_min_corr _tilt_psds_image._filename ')
+                labelRender = (' psdCorr._filename _tilt_psds_image._filename')
+
+                self._views.append(ObjectView(self._project, obj.strId(), fn,
+                                              viewParams={ORDER: labels,
+                                                          VISIBLE: labels,
+                                                          RENDER: labelRender,
+                                                          ZOOM: 25,
+                                                          MODE: MODE_MD}))
+
+            if obj.hasAttribute('outputMicrographs'):
+                fn = obj.outputMicrographs.getFileName()
+                labels = ('id enabled psdCorr._filename _filename _tilt_mean_corr _tilt_std_corr '
+                          '_tilt_max_corr _tilt_min_corr _tilt_psds_image._filename ')
+                labelRender = (' psdCorr._filename _tilt_psds_image._filename')
+
+                self._views.append(ObjectView(self._project, obj.strId(), fn,
+                                                   viewParams={ORDER: labels,
+                                                   VISIBLE: labels,
+                                                   RENDER: labelRender,
+                                                   ZOOM: 25,
+                                                   MODE: MODE_MD}))
+
+            if not(obj.hasAttribute('discardedMicrographs')) and not(obj.hasAttribute('outputMicrographs')):
+                self._views.append(self.infoMessage("Output micrographs has "
+                                                    "not been produced yet."))
+
+
         elif issubclass(cls, XmippProtCompareReprojections):
                 fn = obj.reprojections.getFileName()
                 labels = ('id enabled _index _xmipp_image._filename _xmipp_imageRef._filename '
diff --git a/xmipp3/viewers/viewer_angular_alignment_sph.py b/xmipp3/viewers/viewer_angular_alignment_sph.py
deleted file mode 100644
index 6fc605c87..000000000
--- a/xmipp3/viewers/viewer_angular_alignment_sph.py
+++ /dev/null
@@ -1,216 +0,0 @@
-# **************************************************************************
-# *
-# * Authors:  Amaya Jimenez Moreno (ajimenez@cnb.csic.es)
-# *
-# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
-# *
-# * This program is free software; you can redistribute it and/or modify
-# * it under the terms of the GNU General Public License as published by
-# * the Free Software Foundation; either version 2 of the License, or
-# * (at your option) any later version.
-# *
-# * This program is distributed in the hope that it will be useful,
-# * but WITHOUT ANY WARRANTY; without even the implied warranty of
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# * GNU General Public License for more details.
-# *
-# * You should have received a copy of the GNU General Public License
-# * along with this program; if not, write to the Free Software
-# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
-# * 02111-1307  USA
-# *
-# *  All comments concerning this program package may be sent to the
-# *  e-mail address 'scipion@cnb.csic.es'
-# *
-# **************************************************************************
-
-from pyworkflow.viewer import DESKTOP_TKINTER, WEB_DJANGO, ProtocolViewer
-import pyworkflow.protocol.params as params
-import pwem.emlib.metadata as md
-import numpy as np
-import math
-import matplotlib.pyplot as plt
-from pyworkflow.utils.path import cleanPath
-from pwem.objects import SetOfParticles
-from xmipp3.protocols.protocol_angular_alignment_zernike3d import XmippProtAngularAlignmentZernike3D
-
-
-class XmippAngularAlignmentSphViewer(ProtocolViewer):
-    """ Visualize the output of protocol volume strain """
-    _label = 'viewer angular align sph'
-    _targets = [XmippProtAngularAlignmentZernike3D]
-    _environments = [DESKTOP_TKINTER, WEB_DJANGO]
-
-    def __init__(self, **kwargs):
-        ProtocolViewer.__init__(self, **kwargs)
-        self._data = None
-
-    def getData(self):
-        if self._data is None:
-            self._data = self.loadData()
-        return self._data
-
-
-    def _defineParams(self, form):
-        form.addSection(label='Show info for angular alignment')
-        form.addParam('doShowHist1D', params.LabelParam,
-                      label="Display the coefficients histogram 1D")
-        form.addParam('doShowHist2D', params.LabelParam,
-                      label="Display the coefficients histogram 2D")
-        # form.addParam('displayClustering', params.LabelParam,
-        #               label='Open clustering tool?',
-        #               help='Open a GUI to visualize the images as points'
-        #                    'and select some of them to create new clusters.')
-
-    def _getVisualizeDict(self):
-        self.protocol._createFilenameTemplates()
-        return {'doShowHist1D': self._doShowHist1D,
-                'doShowHist2D': self._doShowHist2D
-                # 'displayClustering': self._displayClustering
-                }
-
-    def _doShowHist1D(self, param=None):
-        tsne1D = self.loadData1D()
-        plt.hist(tsne1D, bins=50)
-        plt.title("SPH coefficients histogram in 1D")
-        plt.show()
-
-    def _doShowHist2D(self, param=None):
-        tsne2D = self.loadData2D()
-        # H, xedges, yedges = np.histogram2d(tsne2D[:,0], tsne2D[:,1], bins=(50,50))
-        # H = H.T
-        # fig = plt.figure()
-        # ax = fig.add_subplot(111, title='SPH coefficients histogram in 2D',
-        #                      aspect = 'equal', xlim = xedges[[0, -1]],
-        #                      ylim = yedges[[0, -1]])
-        # im = mpl.image.NonUniformImage(ax, interpolation='bilinear')
-        # xcenters = (xedges[:-1] + xedges[1:]) / 2
-        # ycenters = (yedges[:-1] + yedges[1:]) / 2
-        # im.set_data(xcenters, ycenters, H)
-        # ax.images.append(im)
-        # plt.show()
-
-        #AJ trying something new...
-        x = tsne2D[:,0]
-        y = tsne2D[:,1]
-
-        rangeX = np.max(x)-np.min(x)
-        rangeY = np.max(y)-np.min(y)
-        if rangeX>rangeY:
-            sigma = rangeX/50
-        else:
-            sigma = rangeY/50
-        print("sigma",sigma)
-
-        # define grid.
-        xi = np.linspace(min(x)-0.1, max(x)+0.1, 100)
-        yi = np.linspace(min(x)-0.1, max(x)+0.1, 100)
-
-        print(x.shape)
-        print(y.shape)
-        print(xi.shape)
-        print(yi.shape)
-
-        z = np.zeros((100, 100), float)
-        zSize = z.shape
-        N = len(x)
-        for c in range(zSize[1]):
-            for r in range(zSize[0]):
-                for d in range(N):
-                    z[r,c] = z[r,c] + (1.0/N) * (1.0/((2*math.pi)*sigma**2)) * math.exp(-((xi[c]-x[d])**2 + (yi[r]-y[d])**2)/(2*sigma**2))
-
-        # grid the data
-        #zi = griddata((x, y), z, (xi, yi), method='cubic')
-        zMax = np.max(z)
-        z = z/zMax
-
-        # contour the gridded data, plotting dots at the randomly spaced data points.
-        CS = plt.contour(xi, yi, z, 15, linewidths=0.5, colors='k')
-        CS = plt.contourf(xi, yi, z, 15, cmap=plt.cm.jet)
-        plt.colorbar()  # draw colorbar
-        # plot data points.
-        # plt.scatter(x, y, marker='o', c='b', s=0.5)
-        # plt.xlim(-2, 2)
-        # plt.ylim(-2, 2)
-        plt.title('griddata test')
-        plt.show()
-
-
-    # def _displayClustering(self, paramName):
-    #     self.clusterWindow = self.tkWindow(ClusteringWindow,
-    #                                        title='Clustering Tool',
-    #                                        dim=2,
-    #                                        data=self.getData(),
-    #                                        callback=self._createCluster
-    #                                        )
-    #     return [self.clusterWindow]
-
-    # def _createCluster(self):
-    #     """ Create the cluster with the selected particles
-    #     from the cluster. This method will be called when
-    #     the button 'Create Cluster' is pressed.
-    #     """
-    #     # Write the particles
-    #     prot = self.protocol
-    #     project = prot.getProject()
-    #     inputSet = prot.getInputParticles()
-    #     fnSqlite = prot._getExtraPath('cluster_particles.sqlite')
-    #     cleanPath(fnSqlite)
-    #     partSet = SetOfParticles(filename=fnSqlite)
-    #     partSet.copyInfo(inputSet)
-    #     for point in self.getData():
-    #         if point.getState() == Point.SELECTED:
-    #             particle = inputSet[point.getId()]
-    #             partSet.append(particle)
-    #     partSet.write()
-    #     partSet.close()
-    #
-    #     from xmipp3.protocols.nma.protocol_batch_cluster import BatchProtNMACluster
-    #     newProt = project.newProtocol(BatchProtNMACluster)
-    #     clusterName = self.clusterWindow.getClusterName()
-    #     if clusterName:
-    #         newProt.setObjLabel(clusterName)
-    #     newProt.inputNmaDimred.set(prot)
-    #     newProt.sqliteFile.set(fnSqlite)
-    #
-    #     project.launchProtocol(newProt)
-
-    def loadData1D(self):
-        fnOut = self.protocol._getFileName('fnOut')
-        mdOut = md.MetaData(fnOut)
-        i=0
-        for row in md.iterRows(mdOut):
-            coeff1D = mdOut.getValue(md.MDL_SPH_TSNE_COEFF1D, row.getObjId())
-            if i==0:
-                tsne1D = coeff1D
-            else:
-                tsne1D = np.vstack((tsne1D, coeff1D))
-            i+=1
-        return tsne1D
-
-    def loadData2D(self):
-        fnOut = self.protocol._getFileName('fnOut')
-        mdOut = md.MetaData(fnOut)
-        i=0
-        for row in md.iterRows(mdOut):
-            coeff2D = mdOut.getValue(md.MDL_SPH_TSNE_COEFF2D, row.getObjId())
-            if i==0:
-                tsne2D = coeff2D
-            else:
-                tsne2D = np.vstack((tsne2D, coeff2D))
-            i+=1
-        return tsne2D
-
-    # def loadData(self):
-    #     """ Iterate over the images and the output matrix txt file
-    #     and create a Data object with theirs Points.
-    #     """
-    #     matrix = self.loadData2D()
-    #     particles = self.protocol.getInputParticles()
-    #     data = Data()
-    #     for i, particle in enumerate(particles):
-    #         data.addPoint(Point(pointId=particle.getObjId(),
-    #                             data=matrix[i, :],
-    #                             weight=1))
-    #     return data
-
diff --git a/xmipp3/viewers/viewer_cl2d.py b/xmipp3/viewers/viewer_cl2d.py
index db48c903d..9d2049c8e 100644
--- a/xmipp3/viewers/viewer_cl2d.py
+++ b/xmipp3/viewers/viewer_cl2d.py
@@ -32,6 +32,7 @@
 from pyworkflow.viewer import ProtocolViewer, DESKTOP_TKINTER, WEB_DJANGO
 from pyworkflow.protocol.params import LabelParam, LEVEL_ADVANCED
 from pyworkflow.protocol.params import EnumParam, StringParam
+import pyworkflow.utils as pwutils
 
 from xmipp3.protocols.protocol_cl2d import XmippProtCL2D
 from pwem.viewers import DataView, ClassesView
@@ -125,7 +126,7 @@ def _viewLevelFiles(self, e=None):
                 viewFinalClasses = True
             levList = [self.protocol._lastLevel()]
         else:
-            levList = self._getListFromRangeString(self.showSeveralLevels.get())
+            levList = pwutils.getListFromRangeString(self.showSeveralLevels.get())
 
         for level in levList:
             if viewFinalClasses:
@@ -154,7 +155,7 @@ def _viewConvergence(self, e=None):
                                             sub='')
             views.append(self._viewInfo(fn))
         else:
-            levList = self._getListFromRangeString(self.showSeveralLevels.get())
+            levList = pwutils.getListFromRangeString(self.showSeveralLevels.get())
             if len(levList) > 0:
                 for level in levList:
                     if level <= self.protocol._lastLevel():
diff --git a/xmipp3/viewers/viewer_ml2d.py b/xmipp3/viewers/viewer_ml2d.py
index 1c009b94a..6f124e37a 100644
--- a/xmipp3/viewers/viewer_ml2d.py
+++ b/xmipp3/viewers/viewer_ml2d.py
@@ -33,6 +33,7 @@
 from pyworkflow.viewer import ProtocolViewer, DESKTOP_TKINTER, WEB_DJANGO
 from pyworkflow.protocol.params import LabelParam
 from pyworkflow.protocol.params import EnumParam, StringParam
+import pyworkflow.utils as pwutils
 from pwem.viewers import ClassesView
 from xmipp3.protocols.protocol_ml2d import XmippProtML2D
 
@@ -106,7 +107,7 @@ def _viewIterRefs(self, e=None):
                 viewFinalClasses = True
             iterations = [self.protocol._lastIteration()]
         else:
-            iterations = self._getListFromRangeString(self.iterSelection.get())
+            iterations = pwutils.getListFromRangeString(self.iterSelection.get())
         
         views = []
         
diff --git a/xmipp3/viewers/viewer_mltomo.py b/xmipp3/viewers/viewer_mltomo.py
index ba6491df5..88ab6dc6a 100644
--- a/xmipp3/viewers/viewer_mltomo.py
+++ b/xmipp3/viewers/viewer_mltomo.py
@@ -37,6 +37,7 @@
 import pwem.emlib.metadata as md
 from pyworkflow.protocol.params import EnumParam, NumericRangeParam, LabelParam
 from pyworkflow.viewer import ProtocolViewer, DESKTOP_TKINTER, WEB_DJANGO
+import pyworkflow.utils as pwutils
 
 from xmipp3.protocols.protocol_mltomo import XmippProtMLTomo
 
@@ -264,7 +265,7 @@ def _getRange(self, var, label):
             self.formWindow.showError('Provide %s selection.' % label)
             result = []
         else:
-            result = self._getListFromRangeString(value)
+            result = pwutils.getListFromRangeString(value)
         return result
 
     def _getVolumeNames(self):
diff --git a/xmipp3/viewers/viewer_pdb_deform_zernike3d.py b/xmipp3/viewers/viewer_pdb_deform_zernike3d.py
index 6834be13a..01b8cfada 100644
--- a/xmipp3/viewers/viewer_pdb_deform_zernike3d.py
+++ b/xmipp3/viewers/viewer_pdb_deform_zernike3d.py
@@ -30,60 +30,82 @@
 import pwem.viewers.views as vi
 import pyworkflow.protocol.params as params
 from pwem.viewers import ChimeraView
-from xmipp3.protocols.protocol_pdb_deform_zernike3d import XmippProtDeformPDBZernike3D
+from pyworkflow.object import Set
+
+from xmipp3.protocols.protocol_apply_zernike3d import XmippApplyZernike3D
 
 
 class XmippPDBDeformViewer(pwviewer.ProtocolViewer):
     """ Visualize the deformation applied to the PDB file """
     _label = 'viewer pdb deform sph'
-    _targets = [XmippProtDeformPDBZernike3D]
+    _targets = [XmippApplyZernike3D]
     _environments = [pwviewer.DESKTOP_TKINTER, pwviewer.WEB_DJANGO]
     OPEN_FILE = "open %s\n"
 
     def _defineParams(self, form):
+        self.deformed = self.protocol.deformed.get()
+        self.have_set = isinstance(self.deformed, Set)
+
         form.addSection(label='Show deformation')
+        form.addParam('pdbIdChoice', params.EnumParam,
+                      condition='self.have_set==True',
+                      choices=list(self.deformed.getIdSet()),
+                      default=0,
+                      label='Structure to display', display=params.EnumParam.DISPLAY_COMBO,
+                      help='Select which structure to display from the IDs of the set')
         form.addParam('doShowPDB', params.LabelParam,
-                      label="Display original and deformed PDB")
+                      label="Display original and deformed PDB or volume")
         form.addParam('doShowMorph', params.LabelParam,
-                      label="Display a morphing between the original and deformed PDB")
+                      label="Display a morphing between the original and deformed PDB or volume")
 
     def _getVisualizeDict(self):
+        if self.have_set==True:
+            self.chosen = self.deformed[list(self.deformed.getIdSet())[self.pdbIdChoice]]
+        else:
+            self.chosen = self.deformed
+
         return {'doShowPDB': self._doShowPDB,
                 'doShowMorph': self._doShowMorph}
 
     def _doShowPDB(self, obj, **kwargs):
-        scriptFile = self.protocol._getPath('pdb_deform_chimera.cxc')
-        fhCmd = open(scriptFile, 'w')
-        inputFile = os.path.abspath(self.protocol.inputPDB.get().getFileName())
-        outputFile = os.path.abspath(self.protocol.outputPDB.getFileName())
-
-        fhCmd.write(self.OPEN_FILE % inputFile)
-        fhCmd.write(self.OPEN_FILE % outputFile)
-        # fhCmd.write("start Model Panel\n")
-        fhCmd.write("show cartoons\n")
-        fhCmd.write("cartoon style width 1.5 thick 1.5\n")
-        fhCmd.write("style stick\n")
-        fhCmd.write("color bymodel\n")
-        fhCmd.close()
-
-        view = ChimeraView(scriptFile)
-        return [view]
+        if self.protocol.applyPDB.get() == True:
+            scriptFile = self.protocol._getPath('pdb_deform_chimera.cxc')
+            fhCmd = open(scriptFile, 'w')
+            inputFile = os.path.abspath(self.protocol.inputPDB.get().getFileName())
+            outputFile = os.path.abspath(self.chosen.getFileName())
+
+            fhCmd.write(self.OPEN_FILE % inputFile)
+            fhCmd.write(self.OPEN_FILE % outputFile)
+            # fhCmd.write("start Model Panel\n")
+            fhCmd.write("show cartoons\n")
+            fhCmd.write("cartoon style width 1.5 thick 1.5\n")
+            fhCmd.write("style stick\n")
+            fhCmd.write("color bymodel\n")
+            fhCmd.close()
+
+            view = ChimeraView(scriptFile)
+            return [view]
+        else:
+            raise ValueError("This viewer is only for atomic structures")
 
     def _doShowMorph(self, obj, **kwargs):
-        scriptFile = self.protocol._getPath('pdb_deform_chimera.cxc')
-        fhCmd = open(scriptFile, 'w')
-        inputFile = os.path.abspath(self.protocol.inputPDB.get().getFileName())
-        outputFile = os.path.abspath(self.protocol.outputPDB.getFileName())
-
-        fhCmd.write(self.OPEN_FILE % inputFile)
-        fhCmd.write(self.OPEN_FILE % outputFile)
-        fhCmd.write("hide models\n")
-        fhCmd.write("morph #1,2 frames 50 play false\n")
-        fhCmd.write("coordset #3 1,\n")
-        fhCmd.write("wait 50\n")
-        fhCmd.write("coordset #3 50,1\n")
-        fhCmd.close()
-
-        view = ChimeraView(scriptFile)
-        return [view]
+        if self.protocol.applyPDB.get() == True:
+            scriptFile = self.protocol._getPath('pdb_deform_chimera.cxc')
+            fhCmd = open(scriptFile, 'w')
+            inputFile = os.path.abspath(self.protocol.inputPDB.get().getFileName())
+            outputFile = os.path.abspath(self.protocol.chosen.getFileName())
+
+            fhCmd.write(self.OPEN_FILE % inputFile)
+            fhCmd.write(self.OPEN_FILE % outputFile)
+            fhCmd.write("hide models\n")
+            fhCmd.write("morph #1,2 frames 50 play false\n")
+            fhCmd.write("coordset #3 1,\n")
+            fhCmd.write("wait 50\n")
+            fhCmd.write("coordset #3 50,1\n")
+            fhCmd.close()
+
+            view = ChimeraView(scriptFile)
+            return [view]
+        else:
+            raise ValueError("This viewer is only for atomic structures")
 
diff --git a/xmipp3/viewers/viewer_projmatch.py b/xmipp3/viewers/viewer_projmatch.py
index 68f8ed48d..4505deb10 100644
--- a/xmipp3/viewers/viewer_projmatch.py
+++ b/xmipp3/viewers/viewer_projmatch.py
@@ -40,6 +40,7 @@
 from pyworkflow.protocol.params import (LabelParam, IntParam, FloatParam,
                                         StringParam, EnumParam,
                                         NumericRangeParam, BooleanParam)
+import pyworkflow.utils as pwutils
 from xmipp3.convert import *
 from xmipp3.viewers.plotter import XmippPlotter
 from xmipp3.protocols import XmippProtProjMatch
@@ -221,7 +222,7 @@ def _load(self):
         if self.showRef3DNo == REF_ALL:
             self._refsList = range(1, self.protocol.numberOfReferences+1)
         else:
-            self._refsList = self._getListFromRangeString(self.ref3DSelection.get())
+            self._refsList = pwutils.getListFromRangeString(self.ref3DSelection.get())
         # ToDo: enhance this
         self.firstIter = 1
         #self.lastIter = self.protocol.numberOfIterations.get()
@@ -230,7 +231,7 @@ def _load(self):
         if self.viewIter.get() == ITER_LAST:
             self._iterations = [self.lastIter]
         else:
-            self._iterations = self._getListFromRangeString(self.iterSelection.get())
+            self._iterations = pwutils.getListFromRangeString(self.iterSelection.get())
             
         from matplotlib.ticker import FuncFormatter
         self._plotFormatter = FuncFormatter(self._formatFreq) 
diff --git a/xmipp3/viewers/viewer_reconstruct_highres.py b/xmipp3/viewers/viewer_reconstruct_highres.py
index 123e73748..30b3dab29 100644
--- a/xmipp3/viewers/viewer_reconstruct_highres.py
+++ b/xmipp3/viewers/viewer_reconstruct_highres.py
@@ -33,6 +33,7 @@
                                         LabelParam, IntParam, FloatParam)
 from pyworkflow.protocol.constants import LEVEL_ADVANCED
 from pyworkflow.viewer import DESKTOP_TKINTER, WEB_DJANGO
+import pyworkflow.utils as pwutils
 from pwem.viewers import (ObjectView, showj, EmProtocolViewer, ChimeraAngDist)
 
 from pwem.emlib import (MDL_SAMPLINGRATE, MDL_ANGLE_ROT, MDL_ANGLE_TILT,
@@ -122,7 +123,7 @@ def _load(self):
         if self.viewIter.get() == ITER_LAST:
             self._iterations = [self.lastIter]
         else:
-            self._iterations = self._getListFromRangeString(self.iterSelection.get())
+            self._iterations = pwutils.getListFromRangeString(self.iterSelection.get())
             
         from matplotlib.ticker import FuncFormatter
         self._plotFormatter = FuncFormatter(self._formatFreq) 
diff --git a/xmipp3/viewers/viewer_volume_deform_zernike3d.py b/xmipp3/viewers/viewer_volume_deform_zernike3d.py
index e1da88de2..46ffb6622 100644
--- a/xmipp3/viewers/viewer_volume_deform_zernike3d.py
+++ b/xmipp3/viewers/viewer_volume_deform_zernike3d.py
@@ -24,20 +24,20 @@
 # *  e-mail address 'scipion@cnb.csic.es'
 # *
 # **************************************************************************
-
-
+from pyworkflow.object import Set
 from pyworkflow.viewer import DESKTOP_TKINTER, WEB_DJANGO, ProtocolViewer
 from pwem.viewers import ChimeraView
 import pyworkflow.protocol.params as params
 from pyworkflow.utils import getExt, removeExt
 from os.path import abspath
 from xmipp3.protocols.protocol_volume_deform_zernike3d import XmippProtVolumeDeformZernike3D
+from xmipp3.protocols.protocol_apply_zernike3d import XmippApplyZernike3D
 
 
 class XmippVolumeDeformZernike3DViewer(ProtocolViewer):
     """ Visualize the output of protocol volume strain """
     _label = 'viewer volume deform sph'
-    _targets = [XmippProtVolumeDeformZernike3D]
+    _targets = [XmippProtVolumeDeformZernike3D, XmippApplyZernike3D]
     _environments = [DESKTOP_TKINTER, WEB_DJANGO]
     OPEN_FILE = "open %s\n"
     VOXEL_SIZE = "volume #%d voxelSize %s\n"
@@ -45,17 +45,46 @@ class XmippVolumeDeformZernike3DViewer(ProtocolViewer):
     VIEW = "view\n"
 
     def _defineParams(self, form):
+        if isinstance(self.protocol, XmippApplyZernike3D):
+            self.only_apply = True
+            self.deformed = self.protocol.deformed.get()
+            self.have_set = isinstance(self.deformed, Set)
+        else:
+            self.have_set = False
+
         form.addSection(label='Show deformation')
+        form.addParam('idChoice', params.EnumParam,
+                      condition='self.have_set==True',
+                      choices=list(self.deformed.getIdSet()), default=0,
+                      label='Structure to display', display=params.EnumParam.DISPLAY_COMBO,
+                      help='Select which volume to display from the IDs of the set')
         form.addParam('doShowStrain', params.LabelParam,
+                      condition='self.only_apply==False',
                       label="Display the strain deformation")
         form.addParam('doShowRotation', params.LabelParam,
+                      condition='self.only_apply==False',
                       label="Display the rotation deformation")
         form.addParam('doShowMorphOrigRef', params.LabelParam,
+                      condition='self.only_apply==False',
                       label="Display the morphing between original and reference volumes")
         form.addParam('doShowMorphDeformedRef', params.LabelParam,
                       label="Display the morphing between deformed and reference volumes")
 
     def _getVisualizeDict(self):
+        if isinstance(self.protocol, XmippApplyZernike3D):
+            if self.have_set==True:
+                self.inputVol = self.protocol.volume[list(self.deformed.getIdSet())[self.pdbIdChoice]]
+                self.chosen = self.deformed[list(self.deformed.getIdSet())[self.pdbIdChoice]]
+            else:
+                self.inputVol = self.protocol.volume
+                self.chosen = self.deformed
+
+            myDict = {
+                'fnRefVol': self.inputVol.getFileName(),
+                'fnOutVol': self.chosen.getFileName()
+                    }
+            self.protocol._updateFilenamesDict(myDict)
+
         self.protocol._createFilenameTemplates()
         return {'doShowStrain': self._doShowStrain,
                 'doShowRotation': self._doShowRotation,
@@ -147,6 +176,8 @@ def _doShowMorphOrigRef(self, param=None):
         return [view]
 
     def _doShowDeformedOrigRef(self, param=None):
+        if self.apply_only == True and self.protocol.applyPDB.get() == True:
+            raise ValueError("This viewer is only for volumes, not atomic structures")
 
         scriptFile = self.protocol._getPath('morph_deformed_ref_chimera.cxc')
         fhCmd = open(scriptFile, 'w')
diff --git a/xmipp3/wizards.py b/xmipp3/wizards.py
index 7105e259b..5bb040088 100644
--- a/xmipp3/wizards.py
+++ b/xmipp3/wizards.py
@@ -43,7 +43,8 @@
     XmippProtHelicalParameters, XmippProtConsensusPicking, XmippProtMonoRes,
     XmippProtRotSpectra, XmippProtReconstructHighRes, XmippProtExtractUnit,
     XmippProtReconstructHeterogeneous, XmippMetaProtDiscreteHeterogeneityScheduler,
-    XmippProtShiftParticles, XmippProtVolumeDeformZernike3D, XmippProtStructureMapZernike3D)
+    XmippProtShiftParticles, XmippProtVolumeDeformZernike3D, XmippProtStructureMapZernike3D,
+    XmippProtSubtractProjection, XmippProtBoostParticles)
 
 
 #===============================================================================
@@ -196,7 +197,9 @@ class XmippParticleMaskRadiusWizard(ParticleMaskRadiusWizard):
                 (XmippProtPreprocessParticles, ['backRadius']),
                 (XmippProtReconstructHighRes, ['particleRadius']),
                 (XmippProtReconstructHeterogeneous, ['particleRadius']),
-                (XmippMetaProtDiscreteHeterogeneityScheduler, ['particleRadius'])]
+                (XmippMetaProtDiscreteHeterogeneityScheduler, ['particleRadius']),
+                (XmippProtSubtractProjection, ['cirmaskrad']),
+                (XmippProtBoostParticles, ['cirmaskrad'])]
 
     def _getParameters(self, protocol):
 
diff --git a/xmipp3/xmipp_logo_devel.png b/xmipp3/xmipp_logo_devel.png
index 0845cac12..5d5c60c3f 100644
Binary files a/xmipp3/xmipp_logo_devel.png and b/xmipp3/xmipp_logo_devel.png differ


Test patch:
diff --git a/xmipp3/tests/test_protocols_xmipp_2d.py b/xmipp3/tests/test_protocols_xmipp_2d.py
index 340d36c2c..3222f16e1 100644
--- a/xmipp3/tests/test_protocols_xmipp_2d.py
+++ b/xmipp3/tests/test_protocols_xmipp_2d.py
@@ -43,6 +43,12 @@
 from xmipp3.protocols import XmippResizeHelper as xrh
 from xmipp3.protocols import OP_DOTPRODUCT, OP_MULTIPLY, OP_SQRT
 
+MSG_WRONG_SIZE = "There was a problem with the size of the output "
+MSG_WRONG_OUTPUT = "There was a problem with the output "
+MSG_WRONG_IMPORT = "There was a problem with the import of the "
+MSG_WRONG_PROTOCOL = "There was a problem with the protocol: "
+MSG_WRONG_DIM = "There was a problem with the dimensions of the output "
+MSG_WRONG_SAMPLING = "There was a problem with the sampling rate value of the output "
 
 # Some utility functions to import particles that are used
 # in several tests.
@@ -1381,6 +1387,96 @@ def test_CorrectWiener(self):
         self.launchProtocol(protCorrect)
         self.assertIsNotNone(protCorrect.outputParticles, "There was a problem with Wiener Correction")
 
+class TestXmippPickNoise(TestXmippBase):
+    """This class checks if the protocol pick noise in Xmipp works properly."""
+    @classmethod
+    def setUpClass(cls):
+        setupTestProject(cls)
+        cls.dataset = DataSet.getDataSet('xmipp_tutorial')
+        cls.micsFn = cls.dataset.getFile('micrographs/BPV_1386.mrc')
+        cls.coordFn = cls.dataset.getFile('pickingXmipp/pickedAll/BPV_1386.pos')
+
+    def testXmippPickNoise(self):
+        # Import micrographs
+        protImportMics = self.newProtocol(emprot.ProtImportMicrographs,
+                                          filesPath=self.micsFn,
+                                          samplingRate=1.237,
+                                          voltage=300)
+        self.launchProtocol(protImportMics)
+        self.assertIsNotNone(protImportMics.outputMicrographs, (MSG_WRONG_IMPORT, "micrographs"))
+        # Import coordinates
+        protImportCoords = self.newProtocol(emprot.ProtImportCoordinates,
+                                            filesPath=self.coordFn,
+                                            inputMicrographs=protImportMics.outputMicrographs,
+                                            boxSize=110)
+        self.launchProtocol(protImportCoords)
+        self.assertIsNotNone(protImportCoords.outputCoordinates, (MSG_WRONG_IMPORT, "coordinates"))
+        # Protocol Pick Noise (default values)
+        protPickNoise1 = self.newProtocol(XmippProtPickNoise,
+                                         inputCoordinates=protImportCoords.outputCoordinates)
+        self.launchProtocol(protPickNoise1)
+        self.assertIsNotNone(protPickNoise1.getFiles(), (MSG_WRONG_PROTOCOL, "pick noise"))
+        self.assertIsNotNone(protPickNoise1.outputCoordinates, (MSG_WRONG_OUTPUT, "coordinates"))
+        # Protocol Pick Noise (extract noise number)
+        protPickNoise2 = self.newProtocol(XmippProtPickNoise,
+                                         inputCoordinates=protImportCoords.outputCoordinates,
+                                         extractNoiseNumber=140)
+        self.launchProtocol(protPickNoise2)
+        self.assertIsNotNone(protPickNoise2.getFiles(), (MSG_WRONG_PROTOCOL, "pick noise"))
+        self.assertIsNotNone(protPickNoise2.outputCoordinates, (MSG_WRONG_OUTPUT, "coordinates"))
+        # Check if the number of noisy particles is right
+        self.assertEquals(protPickNoise1.outputCoordinates.getSize(), 143, (MSG_WRONG_SIZE, "noisy particles"))
+        self.assertEquals(protPickNoise2.outputCoordinates.getSize(), 140, (MSG_WRONG_SIZE, "noisy particles"))
+
+class TestXmippScreenDeepLearning(TestXmippBase):
+    """This class checks if the protocol screen deep learning in Xmipp works properly."""
+    @classmethod
+    def setUpClass(cls):
+        setupTestProject(cls)
+        cls.dataset = DataSet.getDataSet('xmipp_tutorial')
+
+    def testXmippScreenDeepLearning(self):
+        protImportParts1 = self.newProtocol(emprot.ProtImportParticles,
+                                            objLabel='First Set of Particles',
+                                            importFrom=emprot.ProtImportParticles.IMPORT_FROM_SCIPION,
+                                            sqliteFile=self.dataset.getFile('particles/BPV_particles.sqlite'),
+                                            magnification=50000,
+                                            samplingRate=7.08,
+                                            haveDataBeenPhaseFlipped=False)
+        self.launchProtocol(protImportParts1)
+        self.assertIsNotNone(protImportParts1.getFiles(), (MSG_WRONG_IMPORT, "the first set of particles"))
+
+        protImportParts2 = self.newProtocol(emprot.ProtImportParticles,
+                                            objLabel='Second Set of Particles',
+                                            importFrom=emprot.ProtImportParticles.IMPORT_FROM_SCIPION,
+                                            sqliteFile=self.dataset.getFile('particles/BPV_particles_aligned.sqlite'),
+                                            magnification=50000,
+                                            samplingRate=7.08,
+                                            haveDataBeenPhaseFlipped=False)
+        self.launchProtocol(protImportParts2)
+        self.assertIsNotNone(protImportParts2.getFiles(), (MSG_WRONG_IMPORT, "the second set of particles"))
+
+        protAddNoise = self.newProtocol(XmippProtAddNoiseParticles,
+                                        input=protImportParts1.outputParticles,
+                                        gaussianStd=15.0)
+        self.launchProtocol(protAddNoise)
+        self.assertIsNotNone(protAddNoise.outputParticles, (MSG_WRONG_PROTOCOL, "add noise"))
+
+        protScreenDeepLearning = self.newProtocol(XmippProtScreenDeepLearning,
+                                                  inTrueSetOfParticles=protImportParts1.outputParticles,
+                                                  numberOfNegativeSets=1,
+                                                  negativeSet_1=protAddNoise.outputParticles,
+                                                  predictSetOfParticles=protImportParts2.outputParticles)
+        self.launchProtocol(protScreenDeepLearning)
+        self.assertIsNotNone(protScreenDeepLearning.getFiles(), (MSG_WRONG_PROTOCOL, "screen deep learning"))
+        self.assertIsNotNone(protScreenDeepLearning.outputParticles, (MSG_WRONG_OUTPUT, "particles"))
+        # Check the size of the output particles
+        self.assertEquals(protScreenDeepLearning.outputParticles.getSize(), 373, (MSG_WRONG_SIZE, "particles"))
+        # Check the dimensions of the first particle
+        self.assertEquals(protScreenDeepLearning.outputParticles.getFirstItem().getDim(), (140, 140, 1), (MSG_WRONG_DIM, "particles"))
+        # Check the sampling rate of the first particle
+        self.assertEqual(protScreenDeepLearning.outputParticles.getFirstItem().getSamplingRate(), 7.08, (MSG_WRONG_SAMPLING, "particles"))
+
 if __name__ == "__main__":
     if len(sys.argv) > 1:
         className = sys.argv[1]
diff --git a/xmipp3/tests/test_protocols_xmipp_3d.py b/xmipp3/tests/test_protocols_xmipp_3d.py
index 78223d91a..9c1d67ef8 100644
--- a/xmipp3/tests/test_protocols_xmipp_3d.py
+++ b/xmipp3/tests/test_protocols_xmipp_3d.py
@@ -48,6 +48,18 @@
                                                ALIGN_ALGORITHM_EXHAUSTIVE_LOCAL,
                                                ALIGN_ALGORITHM_LOCAL)
 
+# Global variables
+db_xmipp_tutorial = 'xmipp_tutorial'
+db_general = 'general'
+db_model_building_tutorial = 'model_building_tutorial'
+vol_coot1 = 'volumes/coot1.mrc'
+vol1_iter2 = 'volumes/volume_1_iter_002.mrc'
+vol2_iter2 = 'volumes/volume_2_iter_002.mrc'
+helix = 'volumes/helix_59_4__6_7.vol'
+pdb_coot1 = 'PDBx_mmCIF/coot1.pdb'
+
+
+# Output error messages
 MSG_WRONG_SAMPLING = "There was a problem with the sampling rate value of the output "
 MSG_WRONG_SIZE = "There was a problem with the size of the output "
 MSG_WRONG_DIM = "There was a problem with the dimensions of the output "
@@ -56,16 +68,15 @@
 MSG_WRONG_SHIFT = "There was a problem with output shift "
 MSG_WRONG_GALLERY = "There was a problem with the gallery creation"
 MSG_WRONG_ROTATION = "There was a problem with the rotation"
-
-
-
-
+MSG_WRONG_IMPORT = "There was a problem with the import of "
+MSG_WRONG_PROTOCOL = "There was a problem with the protocol: "
+MSG_WRONG_MAP = "There was a problem with the map creation"
 
 class TestXmippBase(BaseTest):
     """ Some utility functions to import volumes that are used in several tests."""
 
     @classmethod
-    def setData(cls, dataProject='xmipp_tutorial'):
+    def setData(cls, dataProject=db_xmipp_tutorial):
         cls.dataset = DataSet.getDataSet(dataProject)
         cls.volumes = cls.dataset.getFile('volumes')
         cls.vol1 = cls.dataset.getFile('vol1')
@@ -717,7 +728,7 @@ def testSqrt2(self):
 
 class TestXmippProtAlignVolume(TestXmippBase):
     @classmethod
-    def setData(cls, dataProject='xmipp_tutorial'):
+    def setData(cls, dataProject=db_xmipp_tutorial):
         cls.dataset = DataSet.getDataSet(dataProject)
         cls.volumes = cls.dataset.getFile('volumes')
         cls.vol1 = cls.dataset.getFile('vol1')
@@ -996,7 +1007,7 @@ class TestXmippRotationalSymmetry(TestXmippBase):
     @classmethod
     def setUpClass(cls):
         setupTestProject(cls)
-        cls.dataset = DataSet.getDataSet('xmipp_tutorial')
+        cls.dataset = DataSet.getDataSet(db_xmipp_tutorial)
         cls.vol = cls.dataset.getFile('vol110')
 
     def test_rotsym(self):
@@ -1211,9 +1222,9 @@ class TestXmippVolSubtraction(TestXmippBase):
     @classmethod
     def setUpClass(cls):
         setupTestProject(cls)
-        cls.dataset = DataSet.getDataSet('xmipp_tutorial')
-        cls.vol1 = cls.dataset.getFile('volumes/volume_1_iter_002.mrc')
-        cls.vol2 = cls.dataset.getFile('volumes/volume_2_iter_002.mrc')
+        cls.dataset = DataSet.getDataSet(db_xmipp_tutorial)
+        cls.vol1 = cls.dataset.getFile(vol1_iter2)
+        cls.vol2 = cls.dataset.getFile(vol2_iter2)
 
     def testXmippVolSub(self):
         print("Import Volume 1")
@@ -1368,8 +1379,8 @@ class TestXmippShiftParticlesAndVolume(TestXmippBase):
     @classmethod
     def setUpClass(cls):
         setupTestProject(cls)
-        cls.dataset = DataSet.getDataSet('xmipp_tutorial')
-        cls.vol1 = cls.dataset.getFile('volumes/volume_1_iter_002.mrc')
+        cls.dataset = DataSet.getDataSet(db_xmipp_tutorial)
+        cls.vol1 = cls.dataset.getFile(vol1_iter2)
 
     def testXmippShiftParticlesAndVolume(self):
         protImportVol = self.newProtocol(ProtImportVolumes,
@@ -1467,7 +1478,7 @@ def testXmippShiftParticlesAndVolume(self):
         self.assertEqual(protShiftVolPad.shiftZ.get(), 5.0, (MSG_WRONG_SHIFT, "z"))
 
 
-class TestXmippProjSubtraction(TestXmippBase):
+class TestXmippProjSubtractionAndBoostParticles(TestXmippBase):
 
     @classmethod
     def setUpClass(cls):
@@ -1504,7 +1515,7 @@ def testXmippProjSub(self):
 
         # Subtraction of particles - reference volume with and without mask
         protSubtractProj = self.newProtocol(XmippProtSubtractProjection,
-                                            particles=protCreateGallery.outputReprojections,
+                                            inputParticles=protCreateGallery.outputReprojections,
                                             vol=protCreatePhantom1item.outputVolume)
         self.launchProtocol(protSubtractProj)
         self.assertIsNotNone(protSubtractProj.outputParticles,
@@ -1514,8 +1525,8 @@ def testXmippProjSub(self):
                          (MSG_WRONG_DIM, "particles"))
         self.assertEqual(protSubtractProj.outputParticles.getSize(), 181, (MSG_WRONG_SIZE, "particles"))
         protSubtractProjMask = self.newProtocol(XmippProtSubtractProjection,
-                                                particles=protCreateGallery.outputReprojections,
-                                                vol=protCreatePhantom1item.outputVolume,
+                                                inputParticles=protCreateGallery.outputReprojections,
+                                                vol=protCreatePhantom2items.outputVolume,
                                                 mask=protCreateMaskKeep.outputMask)
         self.launchProtocol(protSubtractProjMask)
         self.assertIsNotNone(protSubtractProjMask.outputParticles,
@@ -1532,7 +1543,7 @@ def testXmippProjSub(self):
         self.assertIsNotNone(protSimulateCTF.outputParticles,
                              "There was a problem with CTF simulation")
         protSubtractProjCTF = self.newProtocol(XmippProtSubtractProjection,
-                                               particles=protSimulateCTF.outputParticles,
+                                               inputParticles=protSimulateCTF.outputParticles,
                                                vol=protCreatePhantom1item.outputVolume)
         self.launchProtocol(protSubtractProjCTF)
         self.assertIsNotNone(protSubtractProjCTF.outputParticles,
@@ -1548,7 +1559,7 @@ def testXmippProjSub(self):
         self.assertIsNotNone(protAddNoise.outputParticles,
                              "There was a problem with add noise protocol")
         protSubtractProjNoise = self.newProtocol(XmippProtSubtractProjection,
-                                                 particles=protAddNoise.outputParticles,
+                                                 inputParticles=protAddNoise.outputParticles,
                                                  vol=protCreatePhantom1item.outputVolume)
         self.launchProtocol(protSubtractProjNoise)
         self.assertIsNotNone(protSubtractProjNoise.outputParticles,
@@ -1564,7 +1575,7 @@ def testXmippProjSub(self):
         self.assertIsNotNone(protAddNoiseCTF.outputParticles,
                              "There was a problem with add noise to ctf particles protocol")
         protSubtractProjNoiseCTF = self.newProtocol(XmippProtSubtractProjection,
-                                                    particles=protAddNoiseCTF.outputParticles,
+                                                    inputParticles=protAddNoiseCTF.outputParticles,
                                                     vol=protCreatePhantom1item.outputVolume)
         self.launchProtocol(protSubtractProjNoiseCTF)
         self.assertIsNotNone(protSubtractProjNoiseCTF.outputParticles,
@@ -1603,8 +1614,8 @@ def testXmippProjSub(self):
 
         # Perform subtraction of overlapping particles with and without mask, noise and CTF
         protSubtractProjOver = self.newProtocol(XmippProtSubtractProjection,
-                                                particles=protCreateGalleryOver.outputReprojections,
-                                                vol=protCreatePhantom1Over.outputVolume,
+                                                inputParticles=protCreateGalleryOver.outputReprojections,
+                                                vol=protCreatePhantom2Over.outputVolume,
                                                 mask=protCreateMaskKeepOver.outputMask)
         self.launchProtocol(protSubtractProjOver)
         self.assertIsNotNone(protSubtractProjOver.outputParticles,
@@ -1614,7 +1625,7 @@ def testXmippProjSub(self):
                          (MSG_WRONG_DIM, "particles"))
         self.assertEqual(protSubtractProjOver.outputParticles.getSize(), 1647, (MSG_WRONG_SIZE, "particles"))
         protSubtractProjOverNoMask = self.newProtocol(XmippProtSubtractProjection,
-                                                      particles=protCreateGalleryOver.outputReprojections,
+                                                      inputParticles=protCreateGalleryOver.outputReprojections,
                                                       vol=protCreatePhantom1Over.outputVolume)
         self.launchProtocol(protSubtractProjOverNoMask)
         self.assertIsNotNone(protSubtractProjOverNoMask.outputParticles,
@@ -1635,7 +1646,7 @@ def testXmippProjSub(self):
         self.assertIsNotNone(protAddNoiseCTFOver.outputParticles,
                              "There was a problem with add noise to ctf overlap particles protocol")
         protSubtractProjNoiseCTFOver = self.newProtocol(XmippProtSubtractProjection,
-                                                        particles=protAddNoiseCTFOver.outputParticles,
+                                                        inputParticles=protAddNoiseCTFOver.outputParticles,
                                                         vol=protCreatePhantom1Over.outputVolume)
         self.launchProtocol(protSubtractProjNoiseCTFOver)
         self.assertIsNotNone(protSubtractProjNoiseCTFOver.outputParticles,
@@ -1645,6 +1656,19 @@ def testXmippProjSub(self):
                          (MSG_WRONG_DIM, "particles"))
         self.assertEqual(protSubtractProjNoiseCTFOver.outputParticles.getSize(), 1647, (MSG_WRONG_SIZE, "particles"))
 
+        # Boost particles with reference volume
+        protBoostPart = self.newProtocol(XmippProtBoostParticles,
+                                         inputParticles=protCreateGallery.outputReprojections,
+                                         vol=protCreatePhantom1item.outputVolume)
+        self.launchProtocol(protBoostPart)
+        self.assertIsNotNone(protBoostPart.outputParticles,
+                             "There was a problem with projection subtraction")
+        self.assertEqual(protBoostPart.outputParticles.getSamplingRate(), 1.0, (MSG_WRONG_SAMPLING, "particles"))
+        self.assertEqual(protBoostPart.outputParticles.getFirstItem().getDim(), (80, 80, 1),
+                         (MSG_WRONG_DIM, "particles"))
+        self.assertEqual(protBoostPart.outputParticles.getSize(), 181, (MSG_WRONG_SIZE, "particles"))
+
+
 class TestXmippAlignVolumeAndParticles(TestXmippBase):
 
     @classmethod
@@ -1760,6 +1784,109 @@ def testXmippAlignVolumeAndParticles(self):
                          protCreatePhantomReference.outputVolume.getSamplingRate(),
                          (MSG_WRONG_SAMPLING, "rotated phantom"))
 
+class TestXmippDeepHand(TestXmippBase):
+    """This class checks if the protocol deep hand in Xmipp works properly."""
+    @classmethod
+    def setUpClass(cls):
+        setupTestProject(cls)
+        cls.dataset = DataSet.getDataSet(db_general)
+        cls.vol1 = cls.dataset.getFile(helix)
+
+    def testXmippDeepHand(self):
+        # Import input data
+        protImportVol = self.newProtocol(ProtImportVolumes,
+                                         objLabel='Volume',
+                                         filesPath=self.vol1,
+                                         samplingRate=7.08)
+        self.launchProtocol(protImportVol)
+        # Check if there is an output
+        self.assertIsNotNone(protImportVol.getFiles(),
+                             "There was a problem with the volume import")
+
+        # Creation of the mask
+        protDeepHand = self.newProtocol(XmippProtDeepHand,
+                                        inputVolume=protImportVol.outputVolume,
+                                        threshold=0.05)
+        self.launchProtocol(protDeepHand)
+        # Check if there is an output
+        self.assertIsNotNone(protDeepHand.getFiles(), "There was a problem with the mask creation")
+
+        # Check if the sampling rate is right
+        self.assertEqual(protDeepHand.outputVol.getSamplingRate(), 7.08, (MSG_WRONG_SAMPLING, "volume"))
+        # Check if the input threshold is the same as the density of the volume
+        self.assertEqual(protDeepHand.threshold.get(), 0.05, "There was a problem with the density value")
+        # Check if the thresholdAlpha and thresholdHand match the default values
+        self.assertEqual(protDeepHand.thresholdAlpha.get(), 0.7, "There was a problem with the thresholdAlpha value")
+        self.assertEqual(protDeepHand.thresholdHand.get(), 0.6, "There was a problem with the thresholdHand value")
+        # Check if the dimension of the volume does not vary
+        self.assertEqual(protDeepHand.outputVol.getDim(), protImportVol.outputVolume.getDim(),
+                         (MSG_WRONG_DIM, "volume"))
+        # Check if the hand value is right
+        self.assertAlmostEquals(protDeepHand.outputHand.get(), 0.380511, 6,"There was a problem with the hand value")
+        # Check if the flip is right
+        self.assertTrue(protDeepHand.outputHand.get()<protDeepHand.thresholdHand.get(), "There was a problem with the flip")
+
+class TestXmippResolutionBfactor(TestXmippBase):
+    """This class checks if the protocol resolution b factor in Xmipp works properly."""
+    @classmethod
+    def setUpClass(cls):
+        setupTestProject(cls)
+        cls.dataset = DataSet.getDataSet(db_model_building_tutorial)
+        cls.vol1 = cls.dataset.getFile(vol_coot1)
+        cls.pdb = cls.dataset.getFile(pdb_coot1)
+
+    def testXmippResolutionBfactor(self):
+        # Import input volume
+        print("Import input volume")
+        protImportVol = self.newProtocol(ProtImportVolumes,
+                                         objLabel='Volume',
+                                         filesPath=self.vol1,
+                                         samplingRate=4)
+        self.launchProtocol(protImportVol)
+        # Check if there is an output volume
+        self.assertIsNotNone(protImportVol.getFiles(),
+                             (MSG_WRONG_IMPORT, "volume"))
+
+        # Create a mask from the volume
+        print("Create a mask from the volume")
+        protCreateMask = self.newProtocol(XmippProtCreateMask3D,
+                                          inputVolume=protImportVol.outputVolume,
+                                          threshold=0.09)
+        self.launchProtocol(protCreateMask)
+        # Check if there is an output 3D mask
+        self.assertIsNotNone(protCreateMask.getFiles(),
+                             MSG_WRONG_MASK)
+
+        # Create a map
+        print("Create a map")
+        protCreateMap = self.newProtocol(XmippProtMonoRes,
+                                         fullMap=protImportVol.outputVolume,
+                                         mask=protCreateMask.outputMask,
+                                         maxRes=10)
+        self.launchProtocol(protCreateMap)
+        # Check if there is an output map
+        self.assertIsNotNone(protCreateMap.getFiles(),
+                             MSG_WRONG_MAP)
+
+        # Import atomic structure
+        print("Import atomic structure")
+        protImportPdb = self.newProtocol(ProtImportPdb,
+                                         inputPdbData=ProtImportPdb.IMPORT_FROM_FILES,
+                                         pdbFile=self.pdb,
+                                         inputVolume=protImportVol.outputVolume)
+        self.launchProtocol(protImportPdb)
+        # Check if there is an output atomic structure
+        self.assertIsNotNone(protImportPdb.getFiles(),
+                             (MSG_WRONG_IMPORT, "atomic structure"))
+
+        # Protocol local resolution/local bfactor
+        print("Protocol local resolution/local bfactor")
+        protbfactorResolution = self.newProtocol(XmippProtbfactorResolution,
+                                                 pdbfile=protImportPdb.outputPdb,
+                                                 localResolutionMap=protCreateMap.resolution_Volume,
+                                                 fscResolution=8.35)
+        self.launchProtocol(protbfactorResolution)
+
 
 if __name__ == "__main__":
     if len(sys.argv) > 1:
diff --git a/xmipp3/tests/test_protocols_xmipp_mics.py b/xmipp3/tests/test_protocols_xmipp_mics.py
index 7f9e95b84..5db01d0ff 100644
--- a/xmipp3/tests/test_protocols_xmipp_mics.py
+++ b/xmipp3/tests/test_protocols_xmipp_mics.py
@@ -1192,3 +1192,39 @@ def testStreamingAndNonStreaming(self):
     #     self.assertAlmostEqual(outputParts.getSamplingRate() / samplingMics,
     #                            downFactor, 1)
     #     self._checkSamplingConsistency(outputParts)
+
+
+class TestXmippProtTiltAnalysis(TestXmippBase):
+    """This class check if the preprocessing micrographs protocol in Xmipp works properly."""
+    @classmethod
+    def setUpClass(cls):
+        setupTestProject(cls)
+        TestXmippBase.setData()
+        fileName = DataSet.getDataSet('relion_tutorial').getFile('allMics')
+        cls.protImport = cls.runImportMicrograph(fileName,
+                                                 samplingRate=3.54,
+                                                 voltage=300,
+                                                 sphericalAberration=2,
+                                                 scannedPixelSize=None,
+                                                 magnification=56000)
+
+    def testTiltAnalysis(self):
+        protTilt = XmippProtTiltAnalysis(window_size=512, objective_resolution=8, objLabel=
+                                        'Tilt analysis window 512 obj resolution 8A')
+        protTilt.inputMicrographs.set(self.protImport.outputMicrographs)
+        self.proj.launchProtocol(protTilt, wait=True)
+        # check that output micrographs have double sampling rate than input micrographs
+        self.assertEquals(len(protTilt.outputMicrographs), 20, "Incorrect number of accepted micrographs")
+        self.assertTrue(protTilt.isFinished(), "Tilt analysis failed")
+
+
+    def testTiltAnalysis2(self):
+        protTilt2 = XmippProtTiltAnalysis(window_size=600, objective_resolution=4,
+                                          meanCorr_threshold=0.9,
+                                          objLabel='Tilt analysis window 600 obj resolution 4A and threshold 0.9')
+        protTilt2.inputMicrographs.set(self.protImport.outputMicrographs)
+        self.proj.launchProtocol(protTilt2, wait=True)
+        # check that output micrographs have double sampling rate than input micrographs
+        self.assertEquals(len(protTilt2.outputMicrographs), 17, "Incorrect number of accepted micrographs")
+        self.assertEquals(len(protTilt2.discardedMicrographs), 3, "Incorrect number of discarded micrographs")
+        self.assertTrue(protTilt2.isFinished(), "Tilt analysis failed")
\ No newline at end of file