Skip to content

Commit

Permalink
Move to use namedtuple as outputs in PyDIP.
Browse files Browse the repository at this point in the history
  • Loading branch information
crisluengo committed Dec 7, 2024
1 parent 2bfea34 commit 008c186
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 289 deletions.
25 changes: 25 additions & 0 deletions changelogs/diplib_next.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,31 @@ date: 2020-00-00

### Changed functionality

- The function `dip.MaximumAndMinimum()` now returns a `namedtuple` of type `MinMaxValues` instead of a plain `tuple`.
It behaves in the same way as the previous tuple, but the two values can additionally (and preferably) be accessed
using the dot notation: `mm.maximum` instead of `mm[1]`.
See [issue #184](https://github.com/DIPlib/diplib/issues/184).

- Likewise, the functions `dip.MandersColocalizationCoefficients()` and `dip.CostesColocalizationCoefficients()`
now return a `namedtuple` of type `ColocalizationCoefficients` instead of a plain `tuple`.

- Likewise, the functions `dip.ChainCode.BoundingBox()` and `dip.Polygon.BoundingBox()` now return a `namedtuple`
of type `BoundingBoxInteger` and `BoundingBoxFloat` respectively, which contain two `namedtuple`s of type
`VertexInteger` or `VertexFloat`. Again, these types mimic the old `tuple` outputs, but are self-documenting and
easier to use.

- Likewise, other `dip.Polygon` functions such as `dip.Polygon.Centroid()` now return a `namedtuple` of type `VertexFloat`
instead of a plain `tuple`.

- Likewise, `dip.ChainCode.start` is now a `namedtuple` of type `VertexInteger` instead of a plain `tuple`.

- The types `SubpixelLocationResult`, `RadonCircleParameters`, `RegressionParameters`, `GaussianParameters`,
`FeatureInformation`, `ValueInformation`, `EllipseParameters`, `FeretValues`, `RadiusValues`, `QuartilesResult`,
`StatisticsValues`, `CovarianceValues`, `MomentValues` and `SpatialOverlapMetrics`, all used only as outputs to
functions, and all simply emulating a C++ `struct`, are no longer special types in the `diplib.PyDIP_bin` namespace,
but `namedtuple`s. They new types behave identically, but can additionally be unpacked, for example:
`_, q1, _, q3, _ = dip.Quartiles(img)`.

(See also changes to *DIPlib*.)

### Bug fixes
Expand Down
49 changes: 16 additions & 33 deletions pydip/src/analysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@
#include "diplib/microscopy.h"
#include "diplib/neighborlist.h"

namespace pybind11 {
namespace detail {

DIP_OUTPUT_TYPE_CASTER( SubpixelLocationResult, "SubpixelLocationResult", "coordinates value", src.coordinates, src.value )
DIP_OUTPUT_TYPE_CASTER( ColocalizationCoefficients, "ColocalizationCoefficients", "M1 M2", src.M1, src.M2 )
DIP_OUTPUT_TYPE_CASTER( RadonCircleParameters, "RadonCircleParameters", "origin radius", src.origin, src.radius )

} // namespace detail
} // namespace pybind11


void init_analysis( py::module& m ) {

// diplib/distribution.h
Expand Down Expand Up @@ -68,15 +79,6 @@ void init_analysis( py::module& m ) {
distr.def( "MaximumLikelihood", &dip::Distribution::MaximumLikelihood, doc_strings::dip·Distribution·MaximumLikelihood );

// diplib/analysis.h
auto loc = py::class_< dip::SubpixelLocationResult >( m, "SubpixelLocationResult", doc_strings::dip·SubpixelLocationResult );
loc.def_readonly( "coordinates", &dip::SubpixelLocationResult::coordinates, doc_strings::dip·SubpixelLocationResult·coordinates );
loc.def_readonly( "value", &dip::SubpixelLocationResult::value, doc_strings::dip·SubpixelLocationResult·value );
loc.def( "__repr__", []( dip::SubpixelLocationResult const& self ) {
std::ostringstream os;
os << "<SubpixelLocationResult: coordinates=" << self.coordinates << ", value=" << self.value << '>';
return os.str();
} );

m.def( "Find", &dip::Find, "in"_a, "mask"_a = dip::Image{}, doc_strings::dip·Find·Image·CL·Image·CL );
m.def( "SubpixelLocation", &dip::SubpixelLocation,
"in"_a, "position"_a, "polarity"_a = dip::S::MAXIMUM, "method"_a = dip::S::PARABOLIC_SEPARABLE, doc_strings::dip·SubpixelLocation·Image·CL·UnsignedArray·CL·String·CL·String·CL );
Expand Down Expand Up @@ -202,15 +204,6 @@ void init_analysis( py::module& m ) {
"marker"_a, "condition"_a, py::kw_only(), "out"_a, doc_strings::dip·GeodesicDistanceTransform·Image·CL·Image·CL·Image·L );

// diplib/detection.h
auto rcp = py::class_< dip::RadonCircleParameters >( m, "RadonCircleParameters", doc_strings::dip·RadonCircleParameters );
rcp.def_readonly( "origin", &dip::RadonCircleParameters::origin, doc_strings::dip·RadonCircleParameters·origin );
rcp.def_readonly( "radius", &dip::RadonCircleParameters::radius, doc_strings::dip·RadonCircleParameters·radius );
rcp.def( "__repr__", []( dip::RadonCircleParameters const& self ) {
std::ostringstream os;
os << "<RadonCircleParameters: origin=" << self.origin << ", radius=" << self.radius << '>';
return os.str();
} );

m.def( "HoughTransformCircleCenters", py::overload_cast< dip::Image const&, dip::Image const&, dip::UnsignedArray const& >( &dip::HoughTransformCircleCenters ),
"in"_a, "gv"_a, "range"_a = dip::UnsignedArray{}, doc_strings::dip·HoughTransformCircleCenters·Image·CL·Image·CL·Image·L·UnsignedArray·CL );
m.def( "HoughTransformCircleCenters", py::overload_cast< dip::Image const&, dip::Image const&, dip::Image&, dip::UnsignedArray const& >( &dip::HoughTransformCircleCenters ),
Expand All @@ -227,7 +220,7 @@ void init_analysis( py::module& m ) {
dip::RadonCircleParametersArray params = dip::RadonTransformCircles( in, out, radii, sigma, threshold, mode, options );
return py::make_tuple( out, params );
}, "in"_a, "radii"_a = dip::Range{ 10, 30 }, "sigma"_a = 1.0, "threshold"_a = 1.0, "mode"_a = dip::S::FULL, "options"_a = dip::StringSet{ dip::S::NORMALIZE, dip::S::CORRECT },
"Detects hyperspheres (circles, spheres) using the generalized Radon transform."
"Detects hyperspheres (circles, spheres) using the generalized Radon transform.\n"
"Returns a tuple, the first element is the parameter space (the `out` image),\n"
"the second element is a list of `dip.RadonCircleParameters` containing the\n"
"parameters of the detected circles." );
Expand Down Expand Up @@ -291,20 +284,10 @@ void init_analysis( py::module& m ) {
"channel1"_a, "channel2"_a, "mask"_a = dip::Image{}, doc_strings::dip·MandersOverlapCoefficient·Image·CL·Image·CL·Image·CL );
m.def( "IntensityCorrelationQuotient", py::overload_cast< dip::Image const&, dip::Image const&, dip::Image const& >( &dip::IntensityCorrelationQuotient ),
"channel1"_a, "channel2"_a, "mask"_a = dip::Image{}, doc_strings::dip·IntensityCorrelationQuotient·Image·CL·Image·CL·Image·CL );
m.def( "MandersColocalizationCoefficients", []( dip::Image const& channel1, dip::Image const& channel2, dip::Image const& mask, dip::dfloat threshold1, dip::dfloat threshold2 ) {
auto out = dip::MandersColocalizationCoefficients( channel1, channel2, mask, threshold1, threshold2 );
return py::make_tuple( out.M1, out.M2 );
}, "channel1"_a, "channel2"_a, "mask"_a = dip::Image{}, "threshold1"_a = 0.0, "threshold2"_a = 0.0,
"Computes Manders' Colocalization Coefficients.\n"
"Instead of a `dip::ColocalizationCoefficients` object, returns a tuple with\n"
"the `M1` and `M2` values." );
m.def( "CostesColocalizationCoefficients", []( dip::Image const& channel1, dip::Image const& channel2, dip::Image const& mask ) {
auto out = dip::CostesColocalizationCoefficients( channel1, channel2, mask );
return py::make_tuple( out.M1, out.M2 );
}, "channel1"_a, "channel2"_a, "mask"_a = dip::Image{},
"Computes Costes' colocalization coefficients.\n"
"Instead of a `dip::ColocalizationCoefficients` object, returns a tuple with\n"
"the `M1` and `M2` values." );
m.def( "MandersColocalizationCoefficients", py::overload_cast< dip::Image const&, dip::Image const&, dip::Image const&, dip::dfloat, dip::dfloat >( &dip::MandersColocalizationCoefficients ),
"channel1"_a, "channel2"_a, "mask"_a = dip::Image{}, "threshold1"_a = 0.0, "threshold2"_a = 0.0, doc_strings::dip·MandersColocalizationCoefficients·Image·CL·Image·CL·Image·CL·dfloat··dfloat· );
m.def( "CostesColocalizationCoefficients", py::overload_cast< dip::Image const&, dip::Image const&, dip::Image const& >( &dip::CostesColocalizationCoefficients ),
"channel1"_a, "channel2"_a, "mask"_a = dip::Image{}, doc_strings::dip·CostesColocalizationCoefficients·Image·CL·Image·CL·Image·CL );
m.def( "CostesSignificanceTest", []( dip::Image const& channel1, dip::Image const& channel2, dip::Image const& mask, dip::UnsignedArray blockSizes, dip::uint repetitions ) {
return dip::CostesSignificanceTest( channel1, channel2, mask, RandomNumberGenerator(), std::move( blockSizes ), repetitions );
},
Expand Down
26 changes: 3 additions & 23 deletions pydip/src/histogram.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ class type_caster< dip::Histogram::Configuration::Mode > {
PYBIND11_TYPE_CASTER( type, _( "Mode" ));
};

DIP_OUTPUT_TYPE_CASTER( RegressionParameters, "RegressionParameters", "intercept slope", src.intercept, src.slope )
DIP_OUTPUT_TYPE_CASTER( GaussianParameters, "GaussianParameters", "position amplitude sigma", src.position, src.amplitude, src.sigma )

} // namespace detail
} // namespace pybind11

Expand Down Expand Up @@ -277,29 +280,6 @@ void init_histogram( py::module& m ) {
m.def( "PerObjectHistogram", &dip::PerObjectHistogram,
"grey"_a, "label"_a, "mask"_a = dip::Image{}, "configuration"_a = dip::Histogram::Configuration{}, "mode"_a = dip::S::FRACTION, "background"_a = dip::S::EXCLUDE, doc_strings::dip·PerObjectHistogram·Image·CL·Image·CL·Image·CL·Histogram·Configuration··String·CL·String·CL );

auto regParams = py::class_< dip::RegressionParameters >( m, "RegressionParameters", doc_strings::dip·RegressionParameters );
regParams.def( "__repr__", []( dip::RegressionParameters const& s ) {
std::ostringstream os;
os << "<RegressionParameters: intercept=" << s.intercept << ", slope=" << s.slope << '>';
return os.str();
} );
regParams.def_readonly( "intercept", &dip::RegressionParameters::intercept, doc_strings::dip·RegressionParameters·intercept );
regParams.def_readonly( "slope", &dip::RegressionParameters::slope, doc_strings::dip·RegressionParameters·slope );

auto gaussParams = py::class_< dip::GaussianParameters >( m, "GaussianParameters", doc_strings::dip·GaussianParameters );
gaussParams.def( "__repr__", []( dip::GaussianParameters const& s ) {
std::ostringstream os;
os << "<GaussianParameters: "
<< "position=" << s.position
<< ", amplitude=" << s.amplitude
<< ", sigma=" << s.sigma
<< '>';
return os.str();
} );
gaussParams.def_readonly( "position", &dip::GaussianParameters::position, doc_strings::dip·GaussianParameters·position );
gaussParams.def_readonly( "amplitude", &dip::GaussianParameters::amplitude, doc_strings::dip·GaussianParameters·amplitude );
gaussParams.def_readonly( "sigma", &dip::GaussianParameters::sigma, doc_strings::dip·GaussianParameters·sigma );

// These next two functions are the old implementation of `dip.Histogram`, which we keep
// here for backwards compatibility. Setting `dip.Histogram = dip.Histogram_old` in Python
// will allow old programs that use these functions to continue working.
Expand Down
Loading

0 comments on commit 008c186

Please sign in to comment.