Skip to content

Commit 3fad443

Browse files
committed
WIP: do proper alpha-compositing of the labels and the image.
This works, but right now the alpha valus is hardcoded. I need to find a way to plug the alpha value to the slider we have in the side panel of the BDV.
1 parent d89957c commit 3fad443

2 files changed

Lines changed: 295 additions & 3 deletions

File tree

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
package fiji.plugin.trackmate.gui.editor.labkit.component;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.concurrent.ExecutorService;
6+
import java.util.stream.Collectors;
7+
8+
import bdv.viewer.SourceAndConverter;
9+
import bdv.viewer.render.AccumulateProjectorFactory;
10+
import bdv.viewer.render.ProjectorUtils;
11+
import bdv.viewer.render.ProjectorUtils.ArrayData;
12+
import bdv.viewer.render.VolatileProjector;
13+
import net.imglib2.Interval;
14+
import net.imglib2.RandomAccessible;
15+
import net.imglib2.RandomAccessibleInterval;
16+
import net.imglib2.type.numeric.ARGBType;
17+
import net.imglib2.util.Intervals;
18+
import net.imglib2.util.StopWatch;
19+
20+
public class SrcOverAccumulateProjectorARGB
21+
{
22+
23+
public static class Factory implements AccumulateProjectorFactory< ARGBType >
24+
{
25+
26+
private float[] sourceAlphas;
27+
28+
public Factory( final float[] sourceAlphas )
29+
{
30+
this.sourceAlphas = sourceAlphas;
31+
}
32+
33+
public void setSourceAlpha( final int sourceIndex, final float alpha )
34+
{
35+
if ( sourceIndex >= 0 && sourceIndex < sourceAlphas.length )
36+
{
37+
this.sourceAlphas[ sourceIndex ] = Math.max( 0, Math.min( 1, alpha ) );
38+
}
39+
}
40+
41+
public float getSourceAlpha( final int sourceIndex )
42+
{
43+
return ( sourceIndex >= 0 && sourceIndex < sourceAlphas.length )
44+
? sourceAlphas[ sourceIndex ]
45+
: 1.0f;
46+
}
47+
48+
@Override
49+
public VolatileProjector createProjector(
50+
final List< VolatileProjector > sourceProjectors,
51+
final List< SourceAndConverter< ? > > sources,
52+
final List< ? extends RandomAccessible< ? extends ARGBType > > sourceScreenImages,
53+
final RandomAccessibleInterval< ARGBType > targetScreenImage,
54+
final int numThreads,
55+
final ExecutorService executorService )
56+
{
57+
58+
final ProjectorData projectorData = getProjectorData( sourceScreenImages, targetScreenImage );
59+
if ( projectorData == null )
60+
{
61+
// Fallback to generic (not optimized)
62+
return null;
63+
}
64+
65+
return new SrcOverProjectorARGBArrayData( sourceProjectors, projectorData, sourceAlphas );
66+
}
67+
}
68+
69+
private static class ProjectorData
70+
{
71+
private final ArrayData targetData;
72+
73+
private final List< int[] > sourceData;
74+
75+
ProjectorData( final ArrayData targetData, final List< int[] > sourceData )
76+
{
77+
this.targetData = targetData;
78+
this.sourceData = sourceData;
79+
}
80+
81+
ArrayData targetData()
82+
{
83+
return targetData;
84+
}
85+
86+
List< int[] > sourceData()
87+
{
88+
return sourceData;
89+
}
90+
}
91+
92+
private static ProjectorData getProjectorData(
93+
final List< ? extends RandomAccessible< ? extends ARGBType > > sources,
94+
final RandomAccessibleInterval< ARGBType > target )
95+
{
96+
97+
final ArrayData targetData = ProjectorUtils.getARGBArrayData( target );
98+
if ( targetData == null )
99+
return null;
100+
101+
final int numSources = sources.size();
102+
final List< int[] > sourceData = new ArrayList<>( numSources );
103+
for ( int i = 0; i < numSources; ++i )
104+
{
105+
final RandomAccessible< ? extends ARGBType > source = sources.get( i );
106+
if ( !( source instanceof RandomAccessibleInterval ) )
107+
return null;
108+
if ( !Intervals.equals( target, ( Interval ) source ) )
109+
return null;
110+
final int[] data = ProjectorUtils.getARGBArrayImgData( source );
111+
if ( data == null )
112+
return null;
113+
sourceData.add( data );
114+
}
115+
116+
return new ProjectorData( targetData, sourceData );
117+
}
118+
119+
private static class SrcOverProjectorARGBArrayData implements VolatileProjector
120+
{
121+
122+
private List< VolatileProjector > sourceProjectors;
123+
124+
private final List< int[] > sources;
125+
126+
private final ArrayData target;
127+
128+
private final float[] sourceAlphas;
129+
130+
private long lastFrameRenderNanoTime;
131+
132+
private volatile boolean canceled = false;
133+
134+
private volatile boolean valid = false;
135+
136+
public SrcOverProjectorARGBArrayData(
137+
final List< VolatileProjector > sourceProjectors,
138+
final ProjectorData projectorData,
139+
final float[] sourceAlphas )
140+
{
141+
142+
this.sourceProjectors = sourceProjectors;
143+
this.target = projectorData.targetData();
144+
this.sources = projectorData.sourceData();
145+
this.sourceAlphas = sourceAlphas;
146+
}
147+
148+
@Override
149+
public boolean map( final boolean clearUntouchedTargetPixels )
150+
{
151+
if ( canceled )
152+
return false;
153+
154+
if ( isValid() )
155+
return true;
156+
157+
final StopWatch stopWatch = StopWatch.createAndStart();
158+
159+
// Render each source
160+
sourceProjectors.forEach( p -> p.map( clearUntouchedTargetPixels ) );
161+
162+
if ( canceled )
163+
return false;
164+
165+
// Composite with SRC_OVER
166+
mapAccumulate();
167+
168+
sourceProjectors = sourceProjectors.stream()
169+
.filter( p -> !p.isValid() )
170+
.collect( Collectors.toList() );
171+
172+
lastFrameRenderNanoTime = stopWatch.nanoTime();
173+
valid = sourceProjectors.isEmpty();
174+
return !canceled;
175+
}
176+
177+
/**
178+
* Accumulate pixels of all sources to target using SRC_OVER blending.
179+
*/
180+
private void mapAccumulate()
181+
{
182+
if ( canceled )
183+
return;
184+
185+
final int numSources = sources.size();
186+
final int width = target.width();
187+
final int height = target.height();
188+
final int[] targetData = target.data();
189+
final int targetStride = target.stride();
190+
final int targetOx = target.ox();
191+
final int targetOy = target.oy();
192+
193+
// Process each row
194+
for ( int y = 0; y < height; ++y )
195+
{
196+
final int oTarget = ( y + targetOy ) * targetStride + targetOx;
197+
final int oSource = y * width;
198+
199+
// Process each pixel in the row
200+
for ( int x = 0; x < width; ++x )
201+
{
202+
int resultARGB = 0;
203+
204+
// Composite each source front-to-back
205+
for ( int s = 0; s < numSources; ++s )
206+
{
207+
final int[] source = sources.get( s );
208+
final int sourceARGB = source[ oSource + x ];
209+
210+
// Get global alpha for this source
211+
final float globalAlpha = ( s < sourceAlphas.length )
212+
? sourceAlphas[ s ]
213+
: 1.0f;
214+
215+
// Extract source components
216+
int srcA = ARGBType.alpha( sourceARGB );
217+
final int srcR = ARGBType.red( sourceARGB );
218+
final int srcG = ARGBType.green( sourceARGB );
219+
final int srcB = ARGBType.blue( sourceARGB );
220+
221+
// Apply global alpha to per-pixel alpha
222+
srcA = ( int ) ( srcA * globalAlpha );
223+
224+
// SRC_OVER blending
225+
if ( s == 0 || resultARGB == 0 )
226+
{
227+
// First layer or nothing underneath
228+
resultARGB = ARGBType.rgba( srcR, srcG, srcB, srcA );
229+
}
230+
else
231+
{
232+
// SRC_OVER: C_out = C_src + C_dst * (1 - alpha_src)
233+
final int dstA = ARGBType.alpha( resultARGB );
234+
final int dstR = ARGBType.red( resultARGB );
235+
final int dstG = ARGBType.green( resultARGB );
236+
final int dstB = ARGBType.blue( resultARGB );
237+
238+
final float srcAlphaF = srcA / 255.0f;
239+
final float oneMinusSrcAlpha = 1.0f - srcAlphaF;
240+
241+
final int outR = ( int ) ( srcR * srcAlphaF + dstR * oneMinusSrcAlpha );
242+
final int outG = ( int ) ( srcG * srcAlphaF + dstG * oneMinusSrcAlpha );
243+
final int outB = ( int ) ( srcB * srcAlphaF + dstB * oneMinusSrcAlpha );
244+
final int outA = ( int ) ( srcA + dstA * oneMinusSrcAlpha );
245+
246+
resultARGB = ARGBType.rgba( outR, outG, outB, Math.min( 255, outA ) );
247+
}
248+
}
249+
250+
targetData[ oTarget + x ] = resultARGB;
251+
}
252+
}
253+
}
254+
255+
@Override
256+
public void cancel()
257+
{
258+
canceled = true;
259+
for ( final VolatileProjector p : sourceProjectors )
260+
p.cancel();
261+
}
262+
263+
@Override
264+
public long getLastFrameRenderNanoTime()
265+
{
266+
return lastFrameRenderNanoTime;
267+
}
268+
269+
@Override
270+
public boolean isValid()
271+
{
272+
return valid;
273+
}
274+
}
275+
}

src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabKitFrame.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
* it under the terms of the GNU General Public License as
99
* published by the Free Software Foundation, either version 3 of the
1010
* License, or (at your option) any later version.
11-
*
11+
*
1212
* This program is distributed in the hope that it will be useful,
1313
* but WITHOUT ANY WARRANTY; without even the implied warranty of
1414
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1515
* GNU General Public License for more details.
16-
*
16+
*
1717
* You should have received a copy of the GNU General Public
1818
* License along with this program. If not, see
1919
* <http://www.gnu.org/licenses/gpl-3.0.html>.
@@ -65,7 +65,10 @@
6565
import bdv.ui.keymap.KeymapManager;
6666
import bdv.util.BdvOptions;
6767
import fiji.plugin.trackmate.gui.Icons;
68+
import fiji.plugin.trackmate.gui.editor.labkit.component.SrcOverAccumulateProjectorARGB.Factory;
6869
import fiji.plugin.trackmate.gui.editor.labkit.model.TMLabKitModel;
70+
import net.imagej.ImgPlus;
71+
import net.imagej.axis.Axes;
6972
import net.imglib2.Dimensions;
7073
import net.imglib2.util.Intervals;
7174
import net.miginfocom.swing.MigLayout;
@@ -97,6 +100,12 @@ public TMLabKitFrame( final TMLabKitModel model )
97100
{
98101
final ImageLabelingModel imageLabelingModel = model.imageLabelingModel();
99102

103+
// How many sources?
104+
final ImgPlus< ? > img = imageLabelingModel.imageForSegmentation().get();
105+
final int cAxis = img.dimensionIndex( Axes.CHANNEL );
106+
final int nChannels = cAxis < 0 ? 1 : ( int ) img.dimension( cAxis );
107+
final int nSources = nChannels + 1; // for the labels
108+
100109
/*
101110
* Here we create a specific config for BDV, so that we can use a custom
102111
* keymap selected not to interfere with the TrackMate-Labkit keymap. I
@@ -109,10 +118,18 @@ public TMLabKitFrame( final TMLabKitModel model )
109118
final KeymapManager bdvKeymapManager = new KeymapManager();
110119
final Keymap bdvKeymap = bdvKeymapManager.getForwardSelectedKeymap();
111120
bdvKeymap.set( TMKeymapManager.loadBDVKeymap() );
121+
122+
// Create factory with initial alphas
123+
// [grayscale alpha, labels alpha]
124+
final float[] alphas = { 1.0f, 0.5f }; // FIXME
125+
final Factory blendingFactory = new SrcOverAccumulateProjectorARGB.Factory( alphas );
126+
112127
final BdvOptions options = BdvOptions.options()
113128
.inputTriggerConfig( bdvKeymap.getConfig() )
114129
.keymapManager( bdvKeymapManager )
115-
.appearanceManager( appearanceManager );
130+
.appearanceManager( appearanceManager )
131+
.accumulateProjectorFactory( blendingFactory )
132+
;
116133
if ( imageLabelingModel.spatialDimensions().numDimensions() < 3 )
117134
options.is2D();
118135

0 commit comments

Comments
 (0)