Skip to content

Commit

Permalink
dip.viewer.SliceViewer object also maps the complex and projection pr…
Browse files Browse the repository at this point in the history
…operties.
  • Loading branch information
crisluengo committed Jun 23, 2024
1 parent 6a5591c commit b9182bd
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 69 deletions.
3 changes: 3 additions & 0 deletions changelogs/diplib_next.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 6 additions & 7 deletions include/diplib/viewer/viewer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 16 additions & 8 deletions pydip/src/PyDIP_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand All @@ -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)
Expand Down
165 changes: 111 additions & 54 deletions pydip/src/viewer/viewer.cpp
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -15,11 +16,12 @@
*/

#include <algorithm>
#include <memory>
#include <array>

#include "pydip.h"
#include "dipviewer.h"
#include "diplib/viewer/slice.h"
#include "diplib/private/robin_map.h"

// IWYU pragma: no_include "pythonrun.h"

Expand All @@ -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 <typename V, typename... T>
constexpr auto to_array(T&&... t) -> std::array < V, sizeof...(T) > {
return {{ std::forward<T>(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;
Expand All @@ -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." );
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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 ) {
Expand Down

0 comments on commit b9182bd

Please sign in to comment.