Skip to content

Commit c747a27

Browse files
authored
Connected components (#54)
* Add connected component analysis - `ConnectedComponentAlaysis` implementation of connected component analysis - `UnionFind` implementation of union find (required for cca) - `ConnectedComponentAnalaysisTest` tests for 2D and 3D * Switch to imglib2 release version. Thanks to @tpietzsch for a prompt release! * Remove explicit imglib2 version Version specified by pom-scijava is the one we should use * Fix javadoc error * Improve connected components - union find interface to allow for sparse labels - add interface for caller to specify id for each voxel (instead of using IntervalIndexer) Avoid use of streams Add IntArrayUnionFind Add union find for sparse labels with TLongMap as store Add test with offset Expose mapping from set root to id to caller and add IdFromIntervalIndexerWithInterval Restructure CCA * Format connected component analaysis sources Move test into appropriate package * Fix JavaDoc * Add note that mask and labeling should have same mind and max to JavaDoc
1 parent 4dbf7a5 commit c747a27

File tree

6 files changed

+1291
-0
lines changed

6 files changed

+1291
-0
lines changed
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
/*
2+
* #%L
3+
* ImgLib2: a general-purpose, multidimensional image processing library.
4+
* %%
5+
* Copyright (C) 2009 - 2016 Tobias Pietzsch, Stephan Preibisch, Stephan Saalfeld,
6+
* John Bogovic, Albert Cardona, Barry DeZonia, Christian Dietz, Jan Funke,
7+
* Aivar Grislis, Jonathan Hale, Grant Harris, Stefan Helfrich, Mark Hiner,
8+
* Martin Horn, Steffen Jaensch, Lee Kamentsky, Larry Lindsey, Melissa Linkert,
9+
* Mark Longair, Brian Northan, Nick Perry, Curtis Rueden, Johannes Schindelin,
10+
* Jean-Yves Tinevez and Michael Zinsmaier.
11+
* %%
12+
* Redistribution and use in source and binary forms, with or without
13+
* modification, are permitted provided that the following conditions are met:
14+
*
15+
* 1. Redistributions of source code must retain the above copyright notice,
16+
* this list of conditions and the following disclaimer.
17+
* 2. Redistributions in binary form must reproduce the above copyright notice,
18+
* this list of conditions and the following disclaimer in the documentation
19+
* and/or other materials provided with the distribution.
20+
*
21+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
25+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31+
* POSSIBILITY OF SUCH DAMAGE.
32+
* #L%
33+
*/
34+
35+
package net.imglib2.algorithm.labeling;
36+
37+
import java.util.function.LongFunction;
38+
import java.util.function.LongUnaryOperator;
39+
import java.util.function.ToLongBiFunction;
40+
41+
import gnu.trove.map.hash.TLongLongHashMap;
42+
import net.imglib2.Cursor;
43+
import net.imglib2.FinalInterval;
44+
import net.imglib2.Interval;
45+
import net.imglib2.Localizable;
46+
import net.imglib2.RandomAccessible;
47+
import net.imglib2.RandomAccessibleInterval;
48+
import net.imglib2.algorithm.neighborhood.DiamondShape;
49+
import net.imglib2.algorithm.neighborhood.Neighborhood;
50+
import net.imglib2.algorithm.neighborhood.RectangleNeighborhood;
51+
import net.imglib2.algorithm.neighborhood.Shape;
52+
import net.imglib2.algorithm.util.unionfind.IntArrayRankedUnionFind;
53+
import net.imglib2.algorithm.util.unionfind.UnionFind;
54+
import net.imglib2.type.BooleanType;
55+
import net.imglib2.type.numeric.IntegerType;
56+
import net.imglib2.util.IntervalIndexer;
57+
import net.imglib2.util.Intervals;
58+
import net.imglib2.view.Views;
59+
60+
/**
61+
*
62+
* @author Philipp Hanslovsky
63+
*
64+
*/
65+
public class ConnectedComponentAnalysis
66+
{
67+
68+
private static class StartAtOneIdForNextSet implements LongUnaryOperator
69+
{
70+
71+
private final TLongLongHashMap setMappings = new TLongLongHashMap();
72+
73+
@Override
74+
public long applyAsLong( final long root )
75+
{
76+
77+
if ( !setMappings.containsKey( root ) )
78+
{
79+
setMappings.put( root, setMappings.size() + 1 );
80+
}
81+
return setMappings.get( root );
82+
}
83+
84+
}
85+
86+
private static final class IdFromIntervalIndexerWithInterval< T > implements ToLongBiFunction< Localizable, T >
87+
{
88+
89+
private final Interval interval;
90+
91+
private IdFromIntervalIndexerWithInterval( final Interval interval )
92+
{
93+
this.interval = interval;
94+
}
95+
96+
@Override
97+
public long applyAsLong( final Localizable l, final T t )
98+
{
99+
return IntervalIndexer.positionToIndexForInterval( l, interval );
100+
}
101+
}
102+
103+
public static < T > ToLongBiFunction< Localizable, T > idFromIntervalIndexer( final Interval interval )
104+
{
105+
return new IdFromIntervalIndexerWithInterval<>( interval );
106+
}
107+
108+
/**
109+
*
110+
* Implementation of connected component analysis that uses
111+
* {@link IntArrayRankedUnionFind} to find sets of pixels that are connected
112+
* with respect to a 4-neighborhood ({@link DiamondShape}) or the
113+
* generalization for higher dimenions over a binary mask. {@code mask} and
114+
* {@code labeling} are expected to have equal min and max.
115+
*
116+
* @param mask
117+
* Boolean mask to distinguish foreground ({@code true}) from
118+
* background ({@code false}).
119+
* @param labeling
120+
* Output parameter to store labeling: background pixels are
121+
* labeled zero, foreground pixels are greater than zero: 1, 2,
122+
* ..., N. Note that initially all pixels are expected to be zero
123+
* as background values will not be written.
124+
*/
125+
public static < B extends BooleanType< B >, L extends IntegerType< L > > void connectedComponents(
126+
final RandomAccessibleInterval< B > mask,
127+
final RandomAccessibleInterval< L > labeling )
128+
{
129+
connectedComponents( mask, labeling, new DiamondShape( 1 ) );
130+
}
131+
132+
/**
133+
*
134+
* Implementation of connected component analysis that uses
135+
* {@link IntArrayRankedUnionFind} to find sets of pixels that are connected
136+
* with respect to a neighborhood ({@code shape}) over a binary mask. {@code mask}
137+
* and {@code labeling} are expected to have equal min and max.
138+
*
139+
* @param mask
140+
* Boolean mask to distinguish foreground ({@code true}) from
141+
* background ({@code false}).
142+
* @param labeling
143+
* Output parameter to store labeling: background pixels are
144+
* labeled zero, foreground pixels are greater than zero: 1, 2,
145+
* ..., N. Note that initially all pixels are expected to be zero
146+
* as background values will not be written.
147+
* @param shape
148+
* Connectivity of connected components, e.g. 4-neighborhood
149+
* ({@link DiamondShape}), 8-neighborhood
150+
* ({@link RectangleNeighborhood}) and their generalisations for
151+
* higher dimensions.
152+
*/
153+
public static < B extends BooleanType< B >, L extends IntegerType< L > > void connectedComponents(
154+
final RandomAccessibleInterval< B > mask,
155+
final RandomAccessibleInterval< L > labeling,
156+
final Shape shape )
157+
{
158+
assert Intervals.numElements( labeling ) < Integer.MAX_VALUE: "Cannot Image Using array union find.";
159+
connectedComponents(
160+
mask,
161+
labeling,
162+
shape,
163+
n -> new IntArrayRankedUnionFind( ( int ) n ),
164+
idFromIntervalIndexer( labeling ),
165+
new StartAtOneIdForNextSet() );
166+
}
167+
168+
/**
169+
*
170+
* Implementation of connected component analysis that uses
171+
* {@link UnionFind} to find sets of pixels that are connected with respect
172+
* to a neighborhood ({@code shape}) over a binary mask. {@code mask} and
173+
* {@code labeling} are expected to have equal min and max.
174+
*
175+
* @param mask
176+
* Boolean mask to distinguish foreground ({@code true}) from
177+
* background ({@code false}).
178+
* @param labeling
179+
* Output parameter to store labeling: background pixels are
180+
* labeled zero, foreground pixels are greater than zero: 1, 2,
181+
* ..., N. Note that this is expected to be zero as background
182+
* values will not be written.
183+
* @param shape
184+
* Connectivity of connected components, e.g. 4-neighborhood
185+
* ({@link DiamondShape}), 8-neighborhood
186+
* ({@link RectangleNeighborhood}) and their generalisations for
187+
* higher dimensions.
188+
* @param unionFindFactory
189+
* Creates appropriate {@link UnionFind} data structure for size
190+
* of {@code labeling}, e.g. {@link IntArrayRankedUnionFind} of
191+
* appropriate size.
192+
* @param idForPixel
193+
* Create id from pixel location and value. Multiple calls with
194+
* the same argument should always return the same result.
195+
* @param idForSet
196+
* Create id for a set from the root id of a set. Multiple calls
197+
* with the same argument should always return the same result.
198+
*/
199+
public static < B extends BooleanType< B >, L extends IntegerType< L > > void connectedComponents(
200+
final RandomAccessibleInterval< B > mask,
201+
final RandomAccessibleInterval< L > labeling,
202+
final Shape shape,
203+
final LongFunction< UnionFind > unionFindFactory,
204+
final ToLongBiFunction< Localizable, L > idForPixel,
205+
final LongUnaryOperator idForSet )
206+
{
207+
assert Intervals.contains( mask, labeling ) && Intervals.contains( labeling, mask ): "Mask and labeling are not the same size.";
208+
assert Intervals.numElements( labeling ) <= Integer.MAX_VALUE: "Too many pixels for integer based union find.";
209+
210+
final UnionFind uf = makeUnion( mask, labeling, shape, unionFindFactory, idForPixel );
211+
UnionFind.relabel( mask, labeling, uf, idForPixel, idForSet );
212+
}
213+
214+
private static < B extends BooleanType< B >, L extends IntegerType< L > > UnionFind makeUnion(
215+
final RandomAccessibleInterval< B > mask,
216+
final RandomAccessibleInterval< L > labeling,
217+
final Shape shape,
218+
final LongFunction< UnionFind > unionFindFactory,
219+
final ToLongBiFunction< Localizable, L > idForPixel )
220+
{
221+
final UnionFind uf = unionFindFactory.apply( Intervals.numElements( labeling ) );
222+
if ( shape instanceof DiamondShape && ( ( DiamondShape ) shape ).getRadius() == 1 )
223+
{
224+
connectedComponentsDiamondShape( mask, labeling, uf, idForPixel );
225+
}
226+
else
227+
{
228+
connectedComponentsGeneralShape( mask, labeling, shape, uf, idForPixel );
229+
}
230+
return uf;
231+
}
232+
233+
private static < B extends BooleanType< B >, L extends IntegerType< L > > void connectedComponentsDiamondShape(
234+
final RandomAccessible< B > mask,
235+
final RandomAccessibleInterval< L > labeling,
236+
final UnionFind uf,
237+
final ToLongBiFunction< Localizable, L > id )
238+
{
239+
final int nDim = labeling.numDimensions();
240+
final long[] min = Intervals.minAsLongArray( labeling );
241+
final long[] max = Intervals.maxAsLongArray( labeling );
242+
243+
for ( int d = 0; d < nDim; ++d )
244+
{
245+
final long[] minPlusOne = min.clone();
246+
final long[] maxMinusOne = max.clone();
247+
minPlusOne[ d ] += 1;
248+
maxMinusOne[ d ] -= 1;
249+
final Cursor< B > lower = Views.interval( mask, new FinalInterval( min, maxMinusOne ) ).cursor();
250+
final Cursor< B > upper = Views.interval( mask, new FinalInterval( minPlusOne, max ) ).cursor();
251+
final Cursor< L > lowerL = Views.interval( labeling, new FinalInterval( min, maxMinusOne ) ).localizingCursor();
252+
final Cursor< L > upperL = Views.interval( labeling, new FinalInterval( minPlusOne, max ) ).localizingCursor();
253+
254+
while ( lower.hasNext() )
255+
{
256+
final boolean l = lower.next().get();
257+
final boolean u = upper.next().get();
258+
lowerL.fwd();
259+
upperL.fwd();
260+
if ( l && u )
261+
{
262+
final long r1 = uf.findRoot( id.applyAsLong( lowerL, lowerL.get() ) );
263+
final long r2 = uf.findRoot( id.applyAsLong( upperL, upperL.get() ) );
264+
if ( r1 != r2 )
265+
{
266+
uf.join( r1, r2 );
267+
}
268+
}
269+
}
270+
}
271+
}
272+
273+
private static < B extends BooleanType< B >, L extends IntegerType< L > > void connectedComponentsGeneralShape(
274+
final RandomAccessibleInterval< B > mask,
275+
final RandomAccessibleInterval< L > labeling,
276+
final Shape shape,
277+
final UnionFind uf,
278+
final ToLongBiFunction< Localizable, L > id )
279+
{
280+
281+
final RandomAccessible< Neighborhood< B > > maskNeighborhood = shape.neighborhoodsRandomAccessible( Views.extendZero( mask ) );
282+
final RandomAccessible< Neighborhood< L > > labelingNeighborhood = shape.neighborhoodsRandomAccessible( Views.extendZero( labeling ) );
283+
284+
final Cursor< Neighborhood< B > > mnhCursor = Views.flatIterable( Views.interval( maskNeighborhood, labeling ) ).cursor();
285+
final Cursor< Neighborhood< L > > lnhCursor = Views.flatIterable( Views.interval( labelingNeighborhood, labeling ) ).cursor();
286+
final Cursor< B > maskCursor = Views.flatIterable( mask ).cursor();
287+
final Cursor< L > labelCursor = Views.flatIterable( labeling ).localizingCursor();
288+
289+
while ( maskCursor.hasNext() )
290+
{
291+
final B center = maskCursor.next();
292+
final L label = labelCursor.next();
293+
final Neighborhood< B > mnh = mnhCursor.next();
294+
final Neighborhood< L > lnh = lnhCursor.next();
295+
if ( center.get() )
296+
{
297+
final long index = id.applyAsLong( labelCursor, label );
298+
final Cursor< L > lnhc = lnh.localizingCursor();
299+
final Cursor< B > mnhc = mnh.cursor();
300+
while ( mnhc.hasNext() )
301+
{
302+
final L l = lnhc.next();
303+
final B m = mnhc.next();
304+
if ( m.get() )
305+
{
306+
final long r1 = uf.findRoot( index );
307+
final long r2 = uf.findRoot( id.applyAsLong( lnhc, l ) );
308+
if ( r1 != r2 )
309+
{
310+
uf.join( r1, r2 );
311+
}
312+
}
313+
}
314+
}
315+
}
316+
317+
}
318+
319+
}

0 commit comments

Comments
 (0)