Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

WIP: Generic binding for nanvar and nanstd #158

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions docs/image_tool.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ IMAGE TOOL
The *ImageTool* window is the second control window which provides various image-related
information and controls.

.. image:: images/ImageTool.png
:width: 800

Image control
-------------

.. image:: images/ImageTool.png
:width: 800

+----------------------------+--------------------------------------------------------------------+
| Input | Description |
+============================+====================================================================+
Expand All @@ -40,24 +39,45 @@ Image control
+----------------------------+--------------------------------------------------------------------+


Mask image
Mask panel
""""""""""

The action bar provides several actions for real-time masking operation. The pixel values in the
masked region will be set to 0.
.. image:: images/mask_panel.png

Besides the nan pixels inherited from the calibration pipeline, users are allowed to mask additional
pixels to nan in this panel with threshold mask, tile edge mask and image mask.

It should be noted that image mask is treated differently in **EXtra-foam**. One can draw and erase
image mask at run time as well as save/load it as an assembled image or in modules if the detector
has a geometry. Nan pixels outside the masked region of the image mask will not be saved and thus
will also not be overwritten after loading an image mask from file.

+----------------------------+--------------------------------------------------------------------+
| Input | Description |
+============================+====================================================================+
| ``Mask`` | Mask a rectangular region. |
| ``Threshold mask`` | An interval that pixels with values outside the interval will be |
| | masked. Please distinguish *threshold mask* from clipping_. |
+----------------------------+--------------------------------------------------------------------+
| ``Unmask`` | Remove mask in a rectangular region. |
| ``Mask tile edges`` | Mask the edge pixel of each tile. *Only applicable for AGIPD, LPD |
| | and DSSC if EXtra-foam is selected as the* ``Assembler`` *in* |
| | :ref:`Geometry`. |
+----------------------------+--------------------------------------------------------------------+
| ``Draw`` | Draw mask in a rectangular region. |
+----------------------------+--------------------------------------------------------------------+
| ``Erase`` | Erase mask in a rectangular region. |
+----------------------------+--------------------------------------------------------------------+
| ``Trash mask`` | Remove all the mask. |
| ``Remove mask`` | Remove the image mask. |
+----------------------------+--------------------------------------------------------------------+
| ``Save image mask`` | Save the current image mask in `.npy` format. |
| ``Load mask`` | Load an image mask in `.npy` format. The dtype of the loaded |
| | numpy array will be casted into bool if it is not. For detectors |
| | with a geometry, it is allowed to load an image mask in modules, |
| | i.e., an array which has the shape (modules, ss, fs). |
+----------------------------+--------------------------------------------------------------------+
| ``Load image mask`` | Load a image mask in `.npy` format. |
| ``Save mask`` | Save the current image mask in `.npy` format. |
+----------------------------+--------------------------------------------------------------------+
| ``Save mask in modules`` | Save image mask in modules. *Only applicable for AGIPD, LPD |
| | and DSSC if EXtra-foam is selected as the* ``Assembler`` *in* |
| | :ref:`Geometry`. |
+----------------------------+--------------------------------------------------------------------+


Expand Down
Binary file added docs/images/mask_panel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions extra_foam/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ class _Config(dict):
"GUI_CORRELATION_COLORS": (('b', 'r'), ('g', 'p')),
# color of the image mask bounding box while drawing
"GUI_MASK_BOUNDING_BOX_COLOR": 'b',
# color of the masked area for MaskItem
"GUI_MASK_FILL_COLOR": 'g',
# -------------------------------------------------------------
# Misc
# -------------------------------------------------------------
Expand Down
32 changes: 30 additions & 2 deletions extra_foam/geometries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,24 @@

class _1MGeometryPyMixin:
def output_array_for_position_fast(self, extra_shape=(), dtype=_IMAGE_DTYPE):
"""Match the EXtra-geom signature."""
"""Make an array with the shape of assembled data filled with nan.

Match the EXtra-geom signature.
"""
shape = extra_shape + tuple(self.assembledShape())
if dtype == np.bool:
return np.full(shape, 0, dtype=dtype)
return np.full(shape, np.nan, dtype=dtype)

def position_all_modules(self, modules, out, *, ignore_tile_edge=False):
"""Match the EXtra-geom signature.
"""Assemble data in modules according to where the pixels are.

Match the EXtra-geom signature.

:param numpy.ndarray/list modules: data in modules.
Shape = (memory cells, modules, y x) / (modules, y, x)
:param numpy.ndarray out: assembled data.
Shape = (memory cells, y, x) / (y, x)
:param ignore_tile_edge: True for ignoring the pixels at the edges
of tiles. If 'out' is pre-filled with nan, it it equivalent to
masking the tile edges. This is an extra feature which does not
Expand All @@ -47,6 +58,23 @@ def position_all_modules(self, modules, out, *, ignore_tile_edge=False):
ml.append(modules[:, i, ...])
self.positionAllModules(ml, out, ignore_tile_edge)

def output_array_for_dismantle_fast(self, extra_shape=(), dtype=_IMAGE_DTYPE):
"""Make an array with the shape of data in modules filled with nan."""
shape = extra_shape + (self.n_modules, *self.module_shape)
if dtype == np.bool:
return np.full(shape, 0, dtype=dtype)
return np.full(shape, np.nan, dtype=dtype)

def dismantle_all_modules(self, assembled, out):
"""Dismantle assembled data into data in modules.

:param numpy.ndarray out: assembled data.
Shape = (memory cells, y, x) / (y, x)
:param numpy.ndarray out: data in modules.
Shape = (memory cells, modules, y x) / (modules, y, x)
"""
self.dismantleAllModules(assembled, out)


class DSSC_1MGeometryFast(_DSSC_1MGeometry, _1MGeometryPyMixin):
"""DSSC_1MGeometryFast.
Expand Down
18 changes: 15 additions & 3 deletions extra_foam/geometries/tests/test_1M_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@


class _Test1MGeometryMixin:
@pytest.mark.parametrize("dtype", [_IMAGE_DTYPE, _RAW_IMAGE_DTYPE])
@pytest.mark.parametrize("dtype", [_IMAGE_DTYPE, _RAW_IMAGE_DTYPE, bool])
def testAssemblingSinglePulse(self, dtype):
modules = np.ones((self.n_modules, *self.module_shape), dtype=dtype)

Expand All @@ -38,7 +38,13 @@ def testAssemblingSinglePulse(self, dtype):
assert out_gt.shape == out_fast.shape
np.testing.assert_array_equal(out_fast, out_gt)

@pytest.mark.parametrize("dtype", [_IMAGE_DTYPE, _RAW_IMAGE_DTYPE])
# test dismantle
dismantled_out = self.geom_fast.output_array_for_dismantle_fast(dtype=_IMAGE_DTYPE)
self.geom_fast.dismantle_all_modules(out_fast, dismantled_out)

np.testing.assert_array_equal(modules, dismantled_out)

@pytest.mark.parametrize("dtype", [_IMAGE_DTYPE, _RAW_IMAGE_DTYPE, bool])
def testAssemblingBridge(self, dtype):
modules = np.ones((self.n_pulses, self.n_modules, *self.module_shape), dtype=dtype)

Expand All @@ -56,7 +62,13 @@ def testAssemblingBridge(self, dtype):
assert out_gt.shape == out_fast.shape
np.testing.assert_array_equal(out_fast, out_gt)

@pytest.mark.parametrize("dtype", [_IMAGE_DTYPE, _RAW_IMAGE_DTYPE])
# test dismantle
dismantled_out = self.geom_fast.output_array_for_dismantle_fast((self.n_pulses,), dtype=_IMAGE_DTYPE)
self.geom_fast.dismantle_all_modules(out_fast, dismantled_out)

np.testing.assert_array_equal(modules, dismantled_out)

@pytest.mark.parametrize("dtype", [_IMAGE_DTYPE, _RAW_IMAGE_DTYPE, bool])
def testAssemblingFile(self, dtype):
modules = StackView(
{i: np.ones((self.n_pulses, *self.module_shape), dtype=dtype) for i in range(self.n_modules)},
Expand Down
21 changes: 15 additions & 6 deletions extra_foam/gui/ctrl_widgets/mask_ctrl_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@ def __init__(self, *args, **kwargs):
self._non_reconfigurable_widgets = [
self.save_btn,
self.load_btn,
self.mask_save_in_modules_cb
]

if not self._require_geometry:
self.mask_tile_cb.setDisabled(True)
self.mask_save_in_modules_cb.setDisabled(True)
else:
self._non_reconfigurable_widgets.append(
self.mask_save_in_modules_cb)

self.initUI()
self.initConnections()

Expand All @@ -62,9 +68,8 @@ def initUI(self):
layout.addWidget(QLabel("Threshold mask: "), row, 0, AR)
layout.addWidget(self.threshold_mask_le, row, 1)

if self._require_geometry:
row += 1
layout.addWidget(self.mask_tile_cb, row, 0, AR)
row += 1
layout.addWidget(self.mask_tile_cb, row, 0, AR)

row += 1
sub_layout = QHBoxLayout()
Expand Down Expand Up @@ -108,8 +113,12 @@ def _onAssemblerChange(self, assembler):
if assembler == GeomAssembler.EXTRA_GEOM:
self.mask_tile_cb.setChecked(False)
self.mask_tile_cb.setEnabled(False)

self.mask_save_in_modules_cb.setChecked(False)
self.mask_save_in_modules_cb.setEnabled(False)
else:
self.mask_tile_cb.setEnabled(True)
self.mask_save_in_modules_cb.setEnabled(True)

def updateMetaData(self):
"""Override."""
Expand All @@ -125,8 +134,8 @@ def loadMetaData(self):
self.threshold_mask_le.setText(cfg["threshold_mask"][1:-1])
if self._require_geometry:
self.mask_tile_cb.setChecked(cfg["mask_tile"] == 'True')
self.mask_save_in_modules_cb.setChecked(
cfg["mask_save_in_modules"] == 'True')
self.mask_save_in_modules_cb.setChecked(
cfg["mask_save_in_modules"] == 'True')

@pyqtSlot(bool)
def _updateExclusiveBtns(self, checked):
Expand Down
3 changes: 1 addition & 2 deletions extra_foam/gui/image_tool/corrected_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
QHBoxLayout, QSplitter, QVBoxLayout, QWidget
)

from .simple_image_data import _SimpleImageData
from .base_view import _AbstractImageToolView, create_imagetool_view
from ..plot_widgets import HistMixin, ImageAnalysis, PlotWidgetF
from ..ctrl_widgets import (
Expand Down Expand Up @@ -140,7 +139,7 @@ def initConnections(self):
def updateF(self, data, auto_update):
"""Override."""
if auto_update or self._corrected.image is None:
self._corrected.setImageData(_SimpleImageData(data.image))
self._corrected.setImage(data.image)
self._roi_proj_plot.updateF(data)
self._roi_hist.updateF(data)

Expand Down
85 changes: 0 additions & 85 deletions extra_foam/gui/image_tool/simple_image_data.py

This file was deleted.

19 changes: 14 additions & 5 deletions extra_foam/gui/image_tool/tests/test_image_tool.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import unittest
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, patch, PropertyMock
import math
import os
import tempfile
Expand Down Expand Up @@ -76,7 +76,6 @@ def setUp(self):
self.image_tool = self.gui._image_tool

self.view = self.image_tool._corrected_view.imageView
self.view.setImageData(None)
self.view._image = None

self.pulse_worker._image_proc = ImageProcessor()
Expand Down Expand Up @@ -247,9 +246,11 @@ def testMaskCtrlWidget(self):
self.assertEqual(False, widget.mask_tile_cb.isChecked())
self.assertEqual(False, widget.mask_save_in_modules_cb.isChecked())

@patch("extra_foam.pipeline.processors.ImageProcessor._require_geom",
new_callable=PropertyMock, create=True, return_value=False)
@patch("extra_foam.pipeline.processors.image_assembler.ImageAssemblerFactory.BaseAssembler.process",
side_effect=lambda x: x)
def testDrawMask(self, patched_process):
def testDrawMask(self, patched_process, require_geometry):
# TODO: test by really drawing something on ImageTool
from extra_foam.ipc import ImageMaskPub

Expand Down Expand Up @@ -777,6 +778,7 @@ def testGeometryCtrlWidget(self):
# prepare for the following test
widget._stack_only_cb.setChecked(True)
mask_ctrl_widget.mask_tile_cb.setChecked(True)
mask_ctrl_widget.mask_save_in_modules_cb.setChecked(True)
image_proc.update()
self.assertTrue(assembler._stack_only)
self.assertTrue(assembler._mask_tile)
Expand All @@ -788,6 +790,8 @@ def testGeometryCtrlWidget(self):
self.assertFalse(widget._stack_only_cb.isChecked())
self.assertFalse(mask_ctrl_widget.mask_tile_cb.isEnabled())
self.assertFalse(mask_ctrl_widget.mask_tile_cb.isChecked())
self.assertFalse(mask_ctrl_widget.mask_save_in_modules_cb.isEnabled())
self.assertFalse(mask_ctrl_widget.mask_save_in_modules_cb.isChecked())
widget._geom_file_le.setText("/geometry/file/")
for i in range(4):
for j in range(2):
Expand All @@ -803,6 +807,7 @@ def testGeometryCtrlWidget(self):
widget._assembler_cb.setCurrentText(assemblers_inv[GeomAssembler.OWN])
self.assertTrue(widget._stack_only_cb.isEnabled())
self.assertTrue(mask_ctrl_widget.mask_tile_cb.isEnabled())
self.assertTrue(mask_ctrl_widget.mask_save_in_modules_cb.isEnabled())

# test loading meta data
mediator = widget._mediator
Expand Down Expand Up @@ -895,14 +900,18 @@ def testGeometryCtrlWidget(self):

def testMaskCtrlWidget(self):
widget = self.image_tool._mask_ctrl_widget
self.assertEqual(-1, widget.layout().indexOf(widget.mask_tile_cb))

self.assertFalse(widget.mask_tile_cb.isEnabled())
self.assertFalse(widget.mask_save_in_modules_cb.isEnabled())

# test loading meta data
# test if the meta data is invalid
mediator = widget._mediator
mediator.onImageMaskTileEdgeChange(True)
mediator.onImageMaskSaveInModulesToggled(True)
widget.loadMetaData()
self.assertEqual(False, widget.mask_tile_cb.isChecked())
self.assertFalse(widget.mask_tile_cb.isChecked())
self.assertFalse(widget.mask_save_in_modules_cb.isChecked())

def testCalibrationCtrlWidget(self):
widget = self.image_tool._calibration_view._ctrl_widget
Expand Down
Loading