diff --git a/changelogs/diplib_next.md b/changelogs/diplib_next.md index f666bd27..4d25e618 100644 --- a/changelogs/diplib_next.md +++ b/changelogs/diplib_next.md @@ -61,6 +61,9 @@ None, but see bugfixes to *DIPlib*. ### Changed functionality +- The `dip.viewer.SliceViewer` object now also maps the `complex` and `projection` properties + (matching the third and fourth columns of the "control panel"). + ### Bug fixes ### Build changes diff --git a/include/diplib/viewer/viewer.h b/include/diplib/viewer/viewer.h index cb29325c..217897a5 100644 --- a/include/diplib/viewer/viewer.h +++ b/include/diplib/viewer/viewer.h @@ -63,31 +63,30 @@ struct DIPVIEWER_NO_EXPORT ViewingOptions { enum class Diff : uint8 { None, Draw, Place, Mapping, Projection, Complex }; // Projection - dip::IntegerArray dims_; ///< Dimensions to visualize (MainX, MainY, LeftX, TopY). - dip::UnsignedArray operating_point_; ///< Value of non-visualized, non-projected dimensions. + dip::IntegerArray dims_; ///< Dimensions to visualize (MainX, MainY, LeftX, TopY). Use -1 to not map to any image dimension. + dip::UnsignedArray operating_point_; ///< Coordinates of selected point, which also determines which slice is shown. ComplexToReal complex_; ///< What to do with complex numbers. Projection projection_; ///< Type of projection. dip::UnsignedArray roi_origin_; ///< Origin of projection ROI. dip::UnsignedArray roi_sizes_; ///< Sizes of projection ROI. - dip::String labels_; ///< Labels to use for axes. + dip::String labels_; ///< Labels to use for axes, one character per axis. // Mapping FloatRange range_; ///< value range across image (histogram limits). FloatRangeArray tensor_range_; ///< value range per tensor. FloatRange mapping_range_; ///< mapped value range (colorbar limits). - Mapping mapping_; ///< from input to [0, 1], modifies mapping_range_. + Mapping mapping_; ///< Grey-value mapping options, sets mapping_range_. // Color dip::uint element_; ///< Tensor element to visualize. - LookupTable lut_; ///< from [0, 1] to [0, 0, 0]-[255, 255, 255]. + LookupTable lut_; ///< Grey-value to color mapping options. dip::IntegerArray color_elements_; ///< Which tensor element is R, G, and B. // Placement dip::IntegerArray split_; ///< Split point between projections (pixels). // Display - dip::FloatArray zoom_; ///< \brief Zoom factor per dimension (from physical dimensions + user). - ///< Also determines relative viewport sizes. + dip::FloatArray zoom_; ///< \brief Zoom factor per dimension (from physical dimensions + user). Also determines relative viewport sizes. dip::FloatArray origin_; ///< Display origin for moving the image around. // Status diff --git a/pydip/src/PyDIP_py.py b/pydip/src/PyDIP_py.py index dd91acd6..1becfa6b 100644 --- a/pydip/src/PyDIP_py.py +++ b/pydip/src/PyDIP_py.py @@ -61,11 +61,11 @@ def Show(img, range=(), complexMode='abs', projectionMode='mean', coordinates=() real values for display. One of `'abs'` or `'magnitude'`, `'phase'`, `'real'`, `'imag'`. The default is `'abs'`. projectionMode -- a string indicating how to extract a 2D slice from a - multi-dimensional image for display. One of `'slice'`, `'max'`, + multidimensional image for display. One of `'slice'`, `'max'`, `'mean'`. The default is `'mean'`. coordinates -- Coordinates of a pixel to be shown, as a tuple with as many elements as image dimensions. Determines which slice is shown - out of a multi-dimensional image. + out of a multidimensional image. dim1 -- Image dimension to be shown along x-axis of display. dim2 -- Image dimension to be shown along y-axis of display. colormap -- Name of a color map to use for display. If it is one of @@ -113,16 +113,12 @@ def Show(img, range=(), complexMode='abs', projectionMode='mean', coordinates=() pip3 install matplotlib or under Windows: python3 -m pip install matplotlib - Alternatively, use `diplib.viewer.ShowModal()`, or `diplib.Image.ShowSlice()` and - `diplib.viewer.Spin()`. + Alternatively, use `diplib.viewer.ShowModal()`, or `diplib.viewer.Show()`, + `diplib.Image.ShowSlice()` and `diplib.viewer.Spin()`. """, RuntimeWarning) _reportedPlotLib = True return - if dim1 == dim2: - # Note that we could handle this case, but we choose not to, it complicates things a bit - raise RuntimeError("dim1 and dim2 should be distinct") - import matplotlib import matplotlib.pyplot as pp import numpy as np @@ -159,9 +155,19 @@ def Show(img, range=(), complexMode='abs', projectionMode='mean', coordinates=() axes.set_xlim((x[0], x[-1])) axes.set_ylim((np.amin(data), np.amax(data))) else: + if dim1 == dim2: + # Note that we could handle this case, but we choose not to, it complicates things a bit + raise RuntimeError("dim1 and dim2 should be distinct") out = ImageDisplay(img, range, complexMode=complexMode, projectionMode=projectionMode, coordinates=coordinates, dim1=dim1, dim2=dim2) out = np.asarray(out) + colormap_aliases = { + 'divergent': 'diverging', + 'periodic': 'cyclic', + 'gray': 'grey', + 'labels': 'label', + 'sequential': 'linear' + } if colormap == '': if range == 'base' or range == 'based': colormap = 'diverging' @@ -171,6 +177,8 @@ def Show(img, range=(), complexMode='abs', projectionMode='mean', coordinates=() colormap = 'cyclic' else: colormap = 'grey' + elif colormap in colormap_aliases: + colormap = colormap_aliases[colormap] if colormap in {'grey', 'saturation', 'linear', 'diverging', 'cyclic', 'label'}: cmap = np.asarray(ApplyColorMap(np.arange(256), colormap)) / 255 cmap = matplotlib.colors.ListedColormap(cmap) diff --git a/pydip/src/viewer/viewer.cpp b/pydip/src/viewer/viewer.cpp index 06b086d3..113e9e3e 100644 --- a/pydip/src/viewer/viewer.cpp +++ b/pydip/src/viewer/viewer.cpp @@ -1,5 +1,6 @@ /* * (c)2017, Wouter Caarls + * (c)2024, Cris Luengo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +16,12 @@ */ #include -#include +#include #include "pydip.h" #include "dipviewer.h" #include "diplib/viewer/slice.h" +#include "diplib/private/robin_map.h" // IWYU pragma: no_include "pythonrun.h" @@ -30,21 +32,36 @@ bool AreDimensionsReversed() { return static_cast< py::object >( py::module_::import( "diplib" ).attr( "AreDimensionsReversed" ))().cast< bool >(); } -dip::String toString( dip::uint idx, dip::String const* options, dip::uint n ) { +// std::to_array<>() from C++20 +template +constexpr auto to_array(T&&... t) -> std::array < V, sizeof...(T) > { + return {{ std::forward(t)... }}; +} + +template< dip::uint n > +dip::String toString( dip::uint idx, std::array< dip::String, n > options ) { DIP_THROW_IF( idx >= n, dip::E::INDEX_OUT_OF_RANGE ); return options[ idx ]; } -dip::uint toIndex( dip::String const& str, dip::String const* options, dip::uint n ) { +template< dip::uint n > +dip::uint toIndex( dip::String const& str, std::array< dip::String, n > options ) { for( dip::uint idx = 0; idx < n; ++idx ) { if( options[ idx ] == str ) { return idx; } } - DIP_THROW_INVALID_FLAG( str ); } +dip::String const& lookupAlias( dip::String const& str, tsl::robin_map< dip::String, dip::String > const& map ) { + auto it = map.find( str ); + if( it == map.end() ) { + return str; + } + return it->second; +} + int drawHook() { dip::viewer::Draw(); return 0; @@ -60,7 +77,7 @@ PYBIND11_MODULE( PyDIPviewer, m ) { if( PyOS_InputHook == &drawHook ) { PyOS_InputHook = nullptr; } - } ) ); + } )); auto sv = py::class_< dip::viewer::SliceViewer, std::shared_ptr< dip::viewer::SliceViewer > >( m, "SliceViewer" ); sv.def( "SetImage", &dip::viewer::SliceViewer::setImage, "Sets the image to be visualized." ); @@ -93,19 +110,7 @@ PYBIND11_MODULE( PyDIPviewer, m ) { newdims[ 3 ] = dims[ 2 ]; } self.options().dims_ = newdims; - } ); - - sv.def_property( - "labels", - []( dip::viewer::SliceViewer& self ) { - dip::viewer::SliceViewer::Guard guard( self ); - return self.options().labels_; - }, - []( dip::viewer::SliceViewer& self, dip::String const& labels ) { - dip::viewer::SliceViewer::Guard guard( self ); - DIP_THROW_IF( labels.empty(), dip::E::INVALID_PARAMETER ); - self.options().labels_ = labels; - } ); + }, "Dimensions to visualize (MainX, MainY, LeftX, TopY). Use -1 to not map to any image dimension." ); sv.def_property( "operating_point", @@ -118,46 +123,43 @@ PYBIND11_MODULE( PyDIPviewer, m ) { DIP_THROW_IF( !( point < self.image().Sizes() ), dip::E::COORDINATES_OUT_OF_RANGE ); self.options().operating_point_ = point; self.updateLinkedViewers(); - } ); + }, "Coordinates of selected point, which also determines which slice is shown." ); + auto complexToRealOpts = to_array< dip::String >( "real", "imag", "magnitude", "phase" ); sv.def_property( - "element", - []( dip::viewer::SliceViewer& self ) { - dip::viewer::SliceViewer::Guard guard( self ); - return self.options().element_; + "complex", + [=]( dip::viewer::SliceViewer& self ) { + return toString( static_cast< dip::uint >( self.options().complex_ ), complexToRealOpts ); }, - []( dip::viewer::SliceViewer& self, dip::uint element ) { + [=]( dip::viewer::SliceViewer& self, dip::String const& complex ) { dip::viewer::SliceViewer::Guard guard( self ); - DIP_THROW_IF( element >= self.image().TensorElements(), dip::E::INDEX_OUT_OF_RANGE ); - self.options().element_ = element; - } ); + self.options().complex_ = static_cast< dip::viewer::ViewingOptions::ComplexToReal >( toIndex( complex, complexToRealOpts )); + }, "What to do with complex numbers. One of: 'real', 'imag', 'magnitude', 'phase'." ); + auto projectionOpts = to_array< dip::String >( "none", "min", "mean", "max" ); sv.def_property( - "zoom", - []( dip::viewer::SliceViewer& self ) { - dip::viewer::SliceViewer::Guard guard( self ); - return self.options().zoom_; + "projection", + [=]( dip::viewer::SliceViewer& self ) { + return toString( static_cast< dip::uint >( self.options().projection_ ), projectionOpts ); }, - []( dip::viewer::SliceViewer& self, dip::FloatArray const& zoom ) { + [=]( dip::viewer::SliceViewer& self, dip::String const& projection ) { dip::viewer::SliceViewer::Guard guard( self ); - DIP_THROW_IF( zoom.size() != self.image().Dimensionality(), dip::E::DIMENSIONALITIES_DONT_MATCH ); - DIP_THROW_IF( ( zoom <= 0. ).any(), dip::E::PARAMETER_OUT_OF_RANGE ); - self.options().zoom_ = zoom; - self.updateLinkedViewers(); - } ); + self.options().projection_ = ( projection == "slice" ) + ? dip::viewer::ViewingOptions::Projection::None + : static_cast< dip::viewer::ViewingOptions::Projection >( toIndex( projection, projectionOpts )); + }, "Type of projection. One of: 'none', 'min', 'mean', 'max'." ); sv.def_property( - "origin", - []( dip::viewer::SliceViewer& self ) { + "labels", + []( dip::viewer::SliceViewer &self ) { dip::viewer::SliceViewer::Guard guard( self ); - return self.options().origin_; + return self.options().labels_; }, - []( dip::viewer::SliceViewer& self, dip::FloatArray const& origin ) { + []( dip::viewer::SliceViewer &self, dip::String const &labels ) { dip::viewer::SliceViewer::Guard guard( self ); - DIP_THROW_IF( origin.size() != self.image().Dimensionality(), dip::E::DIMENSIONALITIES_DONT_MATCH ); - self.options().origin_ = origin; - self.updateLinkedViewers(); - } ); + DIP_THROW_IF( labels.empty(), dip::E::INVALID_PARAMETER ); + self.options().labels_ = labels; + }, "Labels to use for axes. A string, one character per axis." ); sv.def_property( "mapping_range", @@ -169,30 +171,85 @@ PYBIND11_MODULE( PyDIPviewer, m ) { dip::viewer::SliceViewer::Guard guard( self ); DIP_THROW_IF( range.size() != 2, dip::E::ARRAY_PARAMETER_WRONG_LENGTH ); self.options().mapping_range_ = dip::viewer::FloatRange( range[ 0 ], range[ 1 ] ); - } ); + }, "Mapped value range (colorbar limits)." ); - dip::String mappings[ ] = { "unit", "angle", "8bit", "lin", "base", "log" }; + auto mappingOpts = to_array< dip::String >( "unit", "angle", "8bit", "lin", "base", "log" ); + tsl::robin_map< dip::String, dip::String > mappingAliases{ + {"normal", "8bit"}, + {"linear", "lin"}, + {"all", "lin"}, + {"based", "base"}, + // Not possible: "12bit", "16bit", "s8bit", "s12bit", "s16bit", "orientation", "percentile", "modulo", "labels" + }; sv.def_property( "mapping", [=]( dip::viewer::SliceViewer& self ) { - return toString( static_cast< dip::uint >( self.options().mapping_ ), mappings, 6 ); + return toString( static_cast< dip::uint >( self.options().mapping_ ), mappingOpts ); }, [=]( dip::viewer::SliceViewer& self, dip::String const& mapping ) { dip::viewer::SliceViewer::Guard guard( self ); - self.options().mapping_ = static_cast< dip::viewer::ViewingOptions::Mapping >( toIndex( mapping, mappings, 6 )); + dip::String const& newMapping = lookupAlias( mapping, mappingAliases ); + self.options().mapping_ = static_cast< dip::viewer::ViewingOptions::Mapping >( toIndex( newMapping, mappingOpts )); self.options().setMappingRange( self.options().mapping_ ); - } ); + }, "Grey-value mapping options, sets mapping_range." ); + + sv.def_property( + "element", + []( dip::viewer::SliceViewer& self ) { + dip::viewer::SliceViewer::Guard guard( self ); + return self.options().element_; + }, + []( dip::viewer::SliceViewer& self, dip::uint element ) { + dip::viewer::SliceViewer::Guard guard( self ); + DIP_THROW_IF( element >= self.image().TensorElements(), dip::E::INDEX_OUT_OF_RANGE ); + self.options().element_ = element; + }, "Tensor element to visualize." ); - dip::String luts[ ] = { "original", "ternary", "grey", "sequential", "divergent", "periodic", "labels" }; + auto lutOpts = to_array< dip::String >( "original", "ternary", "grey", "sequential", "divergent", "periodic", "labels" ); + tsl::robin_map< dip::String, dip::String > lutAliases{ + {"linear", "sequential"}, + {"diverging", "divergent"}, + {"cyclic", "periodic"}, + {"label", "labels"}, + {"gray", "grey"}, + }; sv.def_property( "lut", [=]( dip::viewer::SliceViewer& self ) { - return toString( static_cast< dip::uint >( self.options().lut_ ), luts, 7 ); + return toString( static_cast< dip::uint >( self.options().lut_ ), lutOpts ); }, [=]( dip::viewer::SliceViewer& self, dip::String const& lut ) { dip::viewer::SliceViewer::Guard guard( self ); - self.options().lut_ = static_cast< dip::viewer::ViewingOptions::LookupTable >( toIndex( lut, luts, 7 )); - } ); + dip::String const& newLut = lookupAlias( lut, lutAliases ); + self.options().lut_ = static_cast< dip::viewer::ViewingOptions::LookupTable >( toIndex( newLut, lutOpts )); + }, "Grey-value to color mapping options. One of: 'original, 'ternary', 'grey', 'sequential', 'divergent', 'periodic', 'labels'." ); + + sv.def_property( + "zoom", + []( dip::viewer::SliceViewer& self ) { + dip::viewer::SliceViewer::Guard guard( self ); + return self.options().zoom_; + }, + []( dip::viewer::SliceViewer& self, dip::FloatArray const& zoom ) { + dip::viewer::SliceViewer::Guard guard( self ); + DIP_THROW_IF( zoom.size() != self.image().Dimensionality(), dip::E::DIMENSIONALITIES_DONT_MATCH ); + DIP_THROW_IF( ( zoom <= 0. ).any(), dip::E::PARAMETER_OUT_OF_RANGE ); + self.options().zoom_ = zoom; + self.updateLinkedViewers(); + }, "Zoom factor per dimension. Also determines relative viewport sizes." ); + + sv.def_property( + "origin", + []( dip::viewer::SliceViewer& self ) { + dip::viewer::SliceViewer::Guard guard( self ); + return self.options().origin_; + }, + []( dip::viewer::SliceViewer& self, dip::FloatArray const& origin ) { + dip::viewer::SliceViewer::Guard guard( self ); + DIP_THROW_IF( origin.size() != self.image().Dimensionality(), dip::E::DIMENSIONALITIES_DONT_MATCH ); + self.options().origin_ = origin; + self.updateLinkedViewers(); + }, "Display origin for moving the image around." ); m.def( "Show", []( dip::Image const& image, dip::String const& title ) { if( PyOS_InputHook == nullptr ) {