DIPlib / diplib

Quantitative Image Analysis in C++, MATLAB and Python
https://diplib.org
Apache License 2.0
211 stars 48 forks source link

Add functionality to select objects based on measurement features #160

Closed crisluengo closed 1 month ago

crisluengo commented 1 month ago

We can currently iterate over a dip::Measurement object and fish out object IDs for objects that satisfy certain conditions. It would be nice if we could do so without iterating (at least for the simpler conditions).

We also need a function that will erase labels in an image that were not selected. This currently is most easily done using dip::ObjectToMeasurement(), thresholding the resulting image according to the desired conditions, and then applying this mask to the label image. This is not very efficient.

Usage could look like this:

dip::Image img = ...;
dip::Image labels = dip::Label(img);
dip::MeasurementTool msrtool;
auto msr = msrtool.Measure( labels, {}, { "Size" } );
auto keep = ( msr[ "Size" ] > 10 ) & ( msr[ "Size" ] < 20 );
KeepLabels( labels, labels, keep );

The auto keep here would be a set of IDs (alternatively, it could be a map of ID to Boolean, but the advantage of that would be quite minor, only indicating which IDs exist but were not selected).

We'd need to define:

using ObjectSet = tsl::robin_set< dip::uint >; // Represents selected objects

ObjectSet operator==( Measurement::IteratorFeature const& featureValues, Measurement::ValueType value );
ObjectSet operator!=( Measurement::IteratorFeature const& featureValues, Measurement::ValueType value );
ObjectSet operator>( Measurement::IteratorFeature const& featureValues, Measurement::ValueType value );
ObjectSet operator>=( Measurement::IteratorFeature const& featureValues, Measurement::ValueType value );
ObjectSet operator<( Measurement::IteratorFeature const& featureValues, Measurement::ValueType value );
ObjectSet operator<=( Measurement::IteratorFeature const& featureValues, Measurement::ValueType value );

ObjectSet operator&( ObjectSet lhs, ObjectSet rhs ); // Logical AND is a set intersection
ObjectSet operator|( ObjectSet lhs, ObjectSet rhs ); // Logical OR is a set union
ObjectSet operator^( ObjectSet lhs, ObjectSet rhs ); // Logical XOR
ObjectSet operator~( ObjectSet rhs ); // Logical not -- not really necessary
                                      // To implement this one we'd need `ObjectSet` to be a map

// Remove the labels from the image that are not listed in `objects`. If `mode` is `"relabel"`, the output
// labeled image has consecutive labels starting at 1. If it's `"binary"` the output image is binary.
// Otherwise the objects will keep their ID (this is the default).
void KeepLabels( Image const& in, Image& out, ObjectSet const& objects, String const& mode = "" );

The function dip::SmallObjectsRemove() has some code that could be replaced with this functionality (and be more efficient).

The KeepLabels() function proposed here could be implemented in terms of a more generic function MapLabels(), which would map some object IDs to new IDs. So this would be a version of dip::LookupTable::Apply() that specifically maps IDs to IDs, and where pixels with a value that doesn't match any source ID is not changed. Maybe

using ObjectMap = tsl::robin_map< dip::uint, dip::uint >;
void MapLabels( Image const& in, Image& out, ObjectMap const& map );

KeepLabels() then would create the map using objects and mapping either to itself, to new values 1 to N, or to 1s (and create a binary output image), depending on mode.

crisluengo commented 1 month ago

I’ve implemented this, but comparing features yields a dip::LabelMap, which is a lookup table for labels. Initially these labels map to themselves or to 0 (background). You can modify what a label maps to. Many labels can map to the same value. dip::LabelMap::Apply() transforms a labeled image. a2bfbe9, ede0332.

Next, I’d like to add indexing operations to dip::Measurement and dip::measurement::IteratorFeature so that these can also be relabeled and subsetted (labels that map to 0 would be rows in the measurement table that are dropped). Maybe this is also a dip::LabelMap::Apply() with a measurement table or column as input? Indexing on a measurement seems quite natural to me though:

msr = msr[ msr['Size'] > 50 ];
crisluengo commented 1 month ago

Indexing into the dip::Measurement object implemented in 2a2f05cf.

I don't think we can easily index into dip::measurement::IteratorFeature. It would require a new object type, since the IteratorFeature references contiguous data in a dip::Measurement object.

I'm going to leave this as it is now.