From 008c18600010ea13ab63c8abc3fdf4b03e33a57e Mon Sep 17 00:00:00 2001 From: Cris Luengo Date: Sat, 7 Dec 2024 09:39:18 -0700 Subject: [PATCH] Move to use namedtuple as outputs in PyDIP. --- changelogs/diplib_next.md | 25 +++++ pydip/src/analysis.cpp | 49 +++------ pydip/src/histogram.cpp | 26 +---- pydip/src/measurement.cpp | 207 ++++++++++++++++++-------------------- pydip/src/pydip.cpp | 19 +--- pydip/src/pydip.h | 31 ++++++ pydip/src/statistics.cpp | 124 ++++------------------- 7 files changed, 192 insertions(+), 289 deletions(-) diff --git a/changelogs/diplib_next.md b/changelogs/diplib_next.md index 1165cb0d..f7436424 100644 --- a/changelogs/diplib_next.md +++ b/changelogs/diplib_next.md @@ -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 diff --git a/pydip/src/analysis.cpp b/pydip/src/analysis.cpp index 7867ac95..9a4e446c 100644 --- a/pydip/src/analysis.cpp +++ b/pydip/src/analysis.cpp @@ -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 @@ -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 << "( 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 << "( &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 ), @@ -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." ); @@ -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 ); }, diff --git a/pydip/src/histogram.cpp b/pydip/src/histogram.cpp index bc5754ea..c0252f7f 100644 --- a/pydip/src/histogram.cpp +++ b/pydip/src/histogram.cpp @@ -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 @@ -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 << "( m, "GaussianParameters", doc_strings::dip·GaussianParameters ); - gaussParams.def( "__repr__", []( dip::GaussianParameters const& s ) { - std::ostringstream os; - os << " #include #include +#include #include #include "pydip.h" @@ -26,6 +27,38 @@ #include "diplib/chain_code.h" #include "diplib/label_map.h" + +namespace pybind11 { +namespace detail { + +DIP_OUTPUT_TYPE_CASTER( Measurement::FeatureInformation, "FeatureInformation", "name startColumn numberValues", src.name, src.startColumn, src.numberValues ) +DIP_OUTPUT_TYPE_CASTER( Feature::ValueInformation, "ValueInformation", "name units", src.name, src.units ) +DIP_OUTPUT_TYPE_CASTER( CovarianceMatrix::EllipseParameters, "EllipseParameters", "majorAxis minorAxis orientation eccentricity", src.majorAxis, src.minorAxis, src.orientation, src.eccentricity ) +DIP_OUTPUT_TYPE_CASTER( FeretValues, "FeretValues", "maxDiameter minDiameter maxPerpendicular maxAngle minAngle", src.maxDiameter, src.minDiameter, src.maxPerpendicular, src.maxAngle, src.minAngle ) +DIP_OUTPUT_TYPE_CASTER( RadiusValues, "RadiusValues", "mean standardDev maximum minimum circularity", src.Mean(), src.StandardDeviation(), src.Maximum(), src.Minimum(), src.Circularity() ) + +} // namespace detail +} // namespace pybind11 + + +namespace { + +template< typename T > +py::object VertexTuple( dip::Vertex< T > const& v ) { + char const* vertexType = std::is_same< T, dip::dfloat >::value ? "VertexFloat" : "VertexInteger"; + return CreateNamedTuple( vertexType, "x y", v.x, v.y ); +} + +template< typename T > +py::object BoundingBoxTuple( dip::BoundingBox< T > const& bb ) { + char const* boundingBoxType = std::is_same< T, dip::dfloat >::value ? "BoundingBoxFloat" : "BoundingBoxInteger"; + auto topLeft = VertexTuple( bb.topLeft ); + auto bottomRight = VertexTuple( bb.bottomRight ); + return CreateNamedTuple( boundingBoxType, "topLeft bottomRight", topLeft, bottomRight ); +} + +} // namespace + namespace pybind11 { namespace detail { @@ -51,12 +84,71 @@ class type_caster< dip::VertexFloat > { } static handle cast( dip::VertexFloat const& src, return_value_policy /*policy*/, handle /*parent*/ ) { - return make_tuple( src.x, src.y ).release(); + return VertexTuple( src ).release(); } PYBIND11_TYPE_CASTER( type, _( "VertexFloat" )); }; +template<> +class type_caster< dip::VertexInteger > { + public: + using type = dip::VertexInteger; + + bool load( handle src, bool /*convert*/ ) { + if( !isinstance< sequence >( src )) { + return false; + } + auto const seq = reinterpret_borrow< sequence >( src ); + if( seq.size() != 2 ) { + return false; + } + if( !PyLong_Check( seq[ 0 ].ptr() ) || !PyLong_Check( seq[ 1 ].ptr() )) { + return false; + } + value = { seq[ 0 ].cast< dip::sint >(), seq[ 1 ].cast< dip::sint >() }; + return true; + } + + static handle cast( dip::VertexInteger const& src, return_value_policy /*policy*/, handle /*parent*/ ) { + return VertexTuple( src ).release(); + } + + PYBIND11_TYPE_CASTER( type, _( "VertexInteger" )); +}; + +template<> +class type_caster< dip::BoundingBoxFloat > { + public: + using type = dip::BoundingBoxFloat; + + bool load( handle /*src*/, bool /*convert*/ ) { + return false; // Disallow casting to the type, this is not an input argument anywhere + } + + static handle cast( dip::BoundingBoxFloat const& src, return_value_policy /*policy*/, handle /*parent*/ ) { + return BoundingBoxTuple( src ).release(); + } + + PYBIND11_TYPE_CASTER( type, _( "BoundingBoxFloat" )); +}; + +template<> +class type_caster< dip::BoundingBoxInteger > { + public: + using type = dip::BoundingBoxInteger; + + bool load( handle /*src*/, bool /*convert*/ ) { + return false; // Disallow casting to the type, this is not an input argument anywhere + } + + static handle cast( dip::BoundingBoxInteger const& src, return_value_policy /*policy*/, handle /*parent*/ ) { + return BoundingBoxTuple( src ).release(); + } + + PYBIND11_TYPE_CASTER( type, _( "BoundingBoxInteger" )); +}; + } // namespace detail } // namespace pybind11 @@ -243,29 +335,6 @@ void init_measurement( py::module& m ) { "and its description. If the description ends with a '*' character, a gray-value\n" "image is required for the feature." ); - // dip::Measurement::FeatureInformation - auto fInfo = py::class_< dip::Measurement::FeatureInformation >( tool, "FeatureInformation", doc_strings::dip·Measurement·FeatureInformation ); - fInfo.def( "__repr__", []( dip::Measurement::FeatureInformation const& self ) { - std::ostringstream os; - os << "( tool, "ValueInformation", doc_strings::dip·Feature·ValueInformation ); - vInfo.def( "__repr__", []( dip::Feature::ValueInformation const& self ) { - std::ostringstream os; - os << "( m, "Measurement", py::buffer_protocol(), doc_strings::dip·Measurement ); meas.def_buffer( []( dip::Measurement& self ) -> py::buffer_info { return MeasurementToBuffer( self ); } ); @@ -356,13 +425,8 @@ void init_measurement( py::module& m ) { m.def( "Percentile", py::overload_cast< dip::Measurement::IteratorFeature const&, dip::dfloat >( &dip::Percentile ), "featureValues"_a, "percentile"_a, doc_strings::dip·Percentile·Measurement·IteratorFeature·CL·dfloat· ); m.def( "Median", py::overload_cast< dip::Measurement::IteratorFeature const& >( &dip::Median ), "featureValues"_a, doc_strings::dip·Median·Measurement·IteratorFeature·CL ); m.def( "Mean", py::overload_cast< dip::Measurement::IteratorFeature const& >( &dip::Mean ), "featureValues"_a, doc_strings::dip·Mean·Measurement·IteratorFeature·CL ); - m.def( "MaximumAndMinimum", []( dip::Measurement::IteratorFeature const& featureValues ) { - dip::MinMaxAccumulator acc = dip::MaximumAndMinimum( featureValues ); - return py::make_tuple( acc.Minimum(), acc.Maximum() ); - }, "featureValues"_a, - "Returns the maximum and minimum feature values in the first column of\n`featureValues`.\n" - "Like the C++ function, but instead of returning a `dip::MinMaxAccumulator`\n" - "object, returns a tuple with the minimum and maximum values." ); + m.def( "MaximumAndMinimum", py::overload_cast< dip::Measurement::IteratorFeature const& >( &dip::MaximumAndMinimum ), + "featureValues"_a, doc_strings::dip·MaximumAndMinimum·Measurement·IteratorFeature·CL ); m.def( "Quartiles", &dip::Quartiles, "featureValues"_a, doc_strings::dip·Quartiles·Measurement·IteratorFeature·CL ); m.def( "SampleStatistics", &dip::SampleStatistics, "featureValues"_a, doc_strings::dip·SampleStatistics·Measurement·IteratorFeature·CL ); m.def( "ObjectMinimum", &dip::ObjectMinimum, "featureValues"_a, doc_strings::dip·ObjectMinimum·Measurement·IteratorFeature·CL ); @@ -389,17 +453,7 @@ void init_measurement( py::module& m ) { poly.def( "__iter__", []( dip::Polygon const& self ) { return py::make_iterator( self.vertices.begin(), self.vertices.end() ); }, py::keep_alive< 0, 1 >() ); - poly.def( "BoundingBox", []( dip::Polygon const& self ) { - auto bb = self.BoundingBox(); - auto topLeft = py::make_tuple( bb.topLeft.x, bb.topLeft.y ); - auto bottomRight = py::make_tuple( bb.bottomRight.x, bb.bottomRight.y ); - return py::make_tuple( topLeft, bottomRight ); - }, - "Returns the bounding box of the polygon.\n" - "Like the C++ function, but instead of returning a `dip::BoundingBoxFloat`\n" - "object, returns a tuple with two tuples. The first tuple are the (x, y)\n" - "coordinates for the top-left corner, the second one are the (x, y) coordinates\n" - "for the bottom-right corner." ); + poly.def( "BoundingBox", &dip::Polygon::BoundingBox, doc_strings::dip·Polygon·BoundingBox·C ); poly.def( "IsClockWise", &dip::Polygon::IsClockWise, doc_strings::dip·Polygon·IsClockWise·C ); poly.def( "Area", &dip::Polygon::Area, doc_strings::dip·Polygon·Area·C ); poly.def( "Centroid", &dip::Polygon::Centroid, doc_strings::dip·Polygon·Centroid·C ); @@ -463,7 +517,7 @@ void init_measurement( py::module& m ) { "cc.codes is the same as list(cc), and copies the chain code values to a list.\n" "To access individual code values, it's better to just index cc directly: cc[4],\n" "or use an iterator: iter(cc)." ); - chain.def_property_readonly( "start", []( dip::ChainCode const& self ) { return py::make_tuple( self.start.x, self.start.y ); }, doc_strings::dip·ChainCode·start ); + chain.def_readonly( "start", &dip::ChainCode::start, doc_strings::dip·ChainCode·start ); chain.def_readonly( "objectID", &dip::ChainCode::objectID, doc_strings::dip·ChainCode·objectID ); chain.def_readonly( "is8connected", &dip::ChainCode::is8connected, doc_strings::dip·ChainCode·is8connected ); chain.def( "ConvertTo8Connected", &dip::ChainCode::ConvertTo8Connected, doc_strings::dip·ChainCode·ConvertTo8Connected·C ); @@ -471,17 +525,7 @@ void init_measurement( py::module& m ) { chain.def( "Length", &dip::ChainCode::Length, "boundaryPixels"_a = dip::S::EXCLUDE, doc_strings::dip·ChainCode·Length·String·CL·C ); chain.def( "Feret", &dip::ChainCode::Feret, "angleStep"_a = 5.0 / 180.0 * dip::pi, doc_strings::dip·ChainCode·Feret·dfloat··C ); chain.def( "BendingEnergy", &dip::ChainCode::BendingEnergy, doc_strings::dip·ChainCode·BendingEnergy·C ); - chain.def( "BoundingBox", []( dip::ChainCode const& self ) { - auto bb = self.BoundingBox(); - auto topLeft = py::make_tuple( bb.topLeft.x, bb.topLeft.y ); - auto bottomRight = py::make_tuple( bb.bottomRight.x, bb.bottomRight.y ); - return py::make_tuple( topLeft, bottomRight ); - }, - "Finds the bounding box for the object described by the chain code.\n" - "Like the C++ function, but instead of returning a `dip::BoundingBoxInteger`\n" - "object, returns a tuple with two tuples. The first tuple are the (x, y)\n" - "coordinates for the top-left corner, the second one are the (x, y) coordinates\n" - "for the bottom-right corner." ); + chain.def( "BoundingBox", &dip::ChainCode::BoundingBox, doc_strings::dip·ChainCode·BoundingBox·C ); chain.def( "LongestRun", &dip::ChainCode::LongestRun, doc_strings::dip·ChainCode·LongestRun·C ); chain.def( "Polygon", &dip::ChainCode::Polygon, doc_strings::dip·ChainCode·Polygon·C ); chain.def( "Image", py::overload_cast<>( &dip::ChainCode::Image, py::const_ ), doc_strings::dip·ChainCode·Image·dip·Image·L·C ); @@ -494,59 +538,4 @@ void init_measurement( py::module& m ) { "labels"_a, "objectIDs"_a = dip::UnsignedArray{}, "connectivity"_a = 2, doc_strings::dip·GetImageChainCodes·Image·CL·std·vectorgtLabelTypelt·CL·dip·uint· ); m.def( "GetSingleChainCode", &dip::GetSingleChainCode, "labels"_a, "startCoord"_a, "connectivity"_a = 2, doc_strings::dip·GetSingleChainCode·Image·CL·UnsignedArray·CL·dip·uint· ); - // dip::CovarianceMatrix::EllipseParameters - auto ellipseParams = py::class_< dip::CovarianceMatrix::EllipseParameters >( m, "EllipseParameters", doc_strings::dip·CovarianceMatrix·EllipseParameters ); - ellipseParams.def( "__repr__", []( dip::CovarianceMatrix::EllipseParameters const& s ) { - std::ostringstream os; - os << "( m, "FeretValues", doc_strings::dip·FeretValues ); - feretVals.def( "__repr__", []( dip::FeretValues const& s ) { - std::ostringstream os; - os << "( m, "RadiusValues", doc_strings::dip·RadiusValues ); - radiusVals.def( "__repr__", []( dip::RadiusValues const& s ) { - std::ostringstream os; - os << " map; + PYBIND11_MODULE( PyDIP_bin, m ) { m.doc() = "The portion of the PyDIP module that contains the C++ DIPlib bindings."; @@ -106,23 +108,6 @@ PYBIND11_MODULE( PyDIP_bin, m ) { py::register_exception< dip::ParameterError >( m, "ParameterError", error ); py::register_exception< dip::RunTimeError >( m, "RunTimeError", error ); - // diplib/library/types.h - // dip::RegressionParameters defined in histogram.cpp - - auto quartiles = py::class_< dip::QuartilesResult >( m, "QuartilesResult", doc_strings::dip·QuartilesResult ); - quartiles.def( "__repr__", []( dip::QuartilesResult const& s ) { - std::ostringstream os; - os << "( m, "Tensor", doc_strings::dip·Tensor ); tensor.def( py::init<>(), doc_strings::dip·Tensor·Tensor ); diff --git a/pydip/src/pydip.h b/pydip/src/pydip.h index 9e34aab4..067b17af 100644 --- a/pydip/src/pydip.h +++ b/pydip/src/pydip.h @@ -21,6 +21,7 @@ #include "diplib.h" // IWYU pragma: export #include "diplib/random.h" #include "diplib/file_io.h" // for dip::FileInformation +#include "diplib/private/robin_map.h" // IWYU pragma: begin_exports #include @@ -77,6 +78,36 @@ inline void ReverseDimensions( dip::FileInformation& fi ) { fi.origin.reverse(); // let's hope this array has the right number of elements... } + +// Mapping namedtuple type name to type object. This avoids creating multiple namedtuple types with the same name. +// Multiple calls to the same function returning a namedtuple will return an object of the same type. +// It also probably speeds up stuff a tiny bit. +extern tsl::robin_map< std::string, py::object > map; + +// This function creates a named tuple. This is a type in Python that behaves like a tuple but also like +// a struct with member names. +template< typename... Args > +inline py::object CreateNamedTuple( char const* tuple_name, char const* member_names, Args&& ...values ) { + if( !map.contains( tuple_name )) { + py::object namedtuple = py::module::import( "collections" ).attr( "namedtuple" ); + py::object type = namedtuple( tuple_name, member_names ); + map[ tuple_name ] = type; + } + return map[ tuple_name ]( std::forward< Args >( values )... ); +} + +// Note that this macro must be used inside the pybind11::detail namespace +// `diptype` is the DIPlib type without the `dip::` in front. `name` is the name for the type in Python, +// which doesn't need to be the same. We call `CreateNamedTuple( name, values, ... )`. +#define DIP_OUTPUT_TYPE_CASTER( diptype, name, values, ... ) \ +template<> class type_caster< dip:: diptype > { public: \ +using type = dip:: diptype; \ +bool load( handle /*src*/, bool /*convert*/ ) { return false; } \ +static handle cast( dip:: diptype const& src, return_value_policy /*policy*/, handle /*parent*/ ) { \ +return CreateNamedTuple( name, values, __VA_ARGS__ ).release(); } \ +PYBIND11_TYPE_CASTER( type, _( name )); }; + + namespace pybind11 { // py::buffer type that implicitly casts to c-style dense double arrays diff --git a/pydip/src/statistics.cpp b/pydip/src/statistics.cpp index aa77df4a..f6e8f1d5 100644 --- a/pydip/src/statistics.cpp +++ b/pydip/src/statistics.cpp @@ -15,12 +15,25 @@ * limitations under the License. */ -#include - #include "pydip.h" #include "diplib/statistics.h" #include "diplib/accumulators.h" + +namespace pybind11 { +namespace detail { + +DIP_OUTPUT_TYPE_CASTER( QuartilesResult, "QuartilesResult", "minimum lowerQuartile median upperQuartile maximum", src.minimum, src.lowerQuartile, src.median, src.upperQuartile, src.maximum ) +DIP_OUTPUT_TYPE_CASTER( StatisticsAccumulator, "StatisticsValues", "mean standardDev variance skewness kurtosis number", src.Mean(), src.StandardDeviation(), src.Variance(), src.Skewness(), src.ExcessKurtosis(), src.Number() ) +DIP_OUTPUT_TYPE_CASTER( CovarianceAccumulator, "CovarianceValues", "Number MeanX MeanY VarianceX VarianceY StandardDeviationX StandardDeviationY Covariance Correlation Slope", src.Number(), src.MeanX(), src.MeanY(), src.VarianceX(), src.VarianceY(), src.StandardDeviationX(), src.StandardDeviationY(), src.Covariance(), src.Correlation(), src.Slope() ) +DIP_OUTPUT_TYPE_CASTER( MinMaxAccumulator, "MinMaxValues", "minimum maximum", src.Minimum(), src.Maximum() ) +DIP_OUTPUT_TYPE_CASTER( MomentAccumulator, "MomentValues", "zerothOrder firstOrder secondOrder plainSecondOrder", src.Sum(), src.FirstOrder(), src.SecondOrder(), src.PlainSecondOrder() ) +DIP_OUTPUT_TYPE_CASTER( SpatialOverlapMetrics, "SpatialOverlapMetrics", "truePositives trueNegatives falsePositives falseNegatives diceCoefficient jaccardIndex sensitivity specificity fallout accuracy precision", src.truePositives, src.trueNegatives, src.falsePositives, src.falseNegatives, src.diceCoefficient, src.jaccardIndex, src.sensitivity, src.specificity, src.fallout, src.accuracy, src.precision ) + +} // namespace detail +} // namespace pybind11 + + void init_statistics( py::module& m ) { m.def( "Count", py::overload_cast< dip::Image const&, dip::Image const& >( &dip::Count ), "in"_a, "mask"_a = dip::Image{}, doc_strings::dip·Count·Image·CL·Image·CL ); @@ -30,13 +43,8 @@ void init_statistics( py::module& m ) { "in"_a, "mask"_a = dip::Image{}, "process"_a = dip::BooleanArray{}, doc_strings::dip·CumulativeSum·Image·CL·Image·CL·Image·L·BooleanArray·CL ); m.def( "CumulativeSum", py::overload_cast< dip::Image const&, dip::Image const&, dip::Image&, dip::BooleanArray const& >( &dip::CumulativeSum ), "in"_a, "mask"_a = dip::Image{}, py::kw_only(), "out"_a, "process"_a = dip::BooleanArray{}, doc_strings::dip·CumulativeSum·Image·CL·Image·CL·Image·L·BooleanArray·CL ); - m.def( "MaximumAndMinimum", []( dip::Image const& in, dip::Image const& mask ) { - dip::MinMaxAccumulator acc = dip::MaximumAndMinimum( in, mask ); - return py::make_tuple( acc.Minimum(), acc.Maximum() ); - }, "in"_a, "mask"_a = dip::Image{}, - "Finds the largest and smallest value in the image, within an optional mask.\n" - "Like the C++ function, but instead of returning a `dip::MinMaxAccumulator`" - "object, returns a tuple with the minimum and maximum values." ); + m.def( "MaximumAndMinimum", py::overload_cast< dip::Image const&, dip::Image const& >( &dip::MaximumAndMinimum ), + "in"_a, "mask"_a = dip::Image{}, doc_strings::dip·MaximumAndMinimum·Image·CL·Image·CL ); m.def( "Quartiles", py::overload_cast< dip::Image const&, dip::Image const& >( &dip::Quartiles ), "in"_a, "mask"_a = dip::Image{}, doc_strings::dip·Quartiles·Image·CL·Image·CL ); m.def( "SampleStatistics", py::overload_cast< dip::Image const&, dip::Image const& >( &dip::SampleStatistics ), "in"_a, "mask"_a = dip::Image{}, doc_strings::dip·SampleStatistics·Image·CL·Image·CL ); @@ -208,102 +216,4 @@ void init_statistics( py::module& m ) { m.def( "EstimateNoiseVariance", py::overload_cast< dip::Image const&, dip::Image const& >( &dip::EstimateNoiseVariance ), "in"_a, "mask"_a = dip::Image{}, doc_strings::dip·EstimateNoiseVariance·Image·CL·Image·CL ); - // dip::StatisticsAccumulator - auto statsAcc = py::class_< dip::StatisticsAccumulator >( m, "StatisticsValues", doc_strings::dip·StatisticsAccumulator ); - statsAcc.def( "__repr__", []( dip::StatisticsAccumulator const& s ) { - std::ostringstream os; - os << "( m, "CovarianceValues", doc_strings::dip·CovarianceAccumulator ); - covAcc.def( "__repr__", []( dip::CovarianceAccumulator const& s ) { - std::ostringstream os; - os << "( m, "MomentValues", doc_strings::dip·MomentAccumulator ); - momentAcc.def( "__repr__", []( dip::MomentAccumulator const& s ) { - std::ostringstream os; - os << "( m, "SpatialOverlapMetrics", doc_strings::dip·SpatialOverlapMetrics ); - overlap.def( "__repr__", []( dip::SpatialOverlapMetrics const& s ) { - std::ostringstream os; - os << "