Skip to content

Commit

Permalink
Merge pull request #175 from saalfeldlab/paintera-show-container-labe…
Browse files Browse the repository at this point in the history
…l-data

Open label data in PainteraShowContainer
  • Loading branch information
hanslovsky authored Dec 18, 2018
2 parents 0e5082c + 980f697 commit 48aa80c
Showing 1 changed file with 296 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,77 @@
import com.google.gson.JsonObject;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import net.imglib2.Cursor;
import net.imglib2.Interval;
import net.imglib2.Volatile;
import net.imglib2.algorithm.util.Grids;
import net.imglib2.cache.img.CachedCellImg;
import net.imglib2.converter.ARGBColorConverter;
import net.imglib2.converter.ARGBCompositeColorConverter;
import net.imglib2.realtransform.AffineTransform3D;
import net.imglib2.type.NativeType;
import net.imglib2.type.label.LabelUtils;
import net.imglib2.type.numeric.IntegerType;
import net.imglib2.type.numeric.RealType;
import net.imglib2.type.volatiles.AbstractVolatileRealType;
import net.imglib2.view.IntervalView;
import net.imglib2.view.Views;
import net.imglib2.view.composite.RealComposite;
import org.controlsfx.control.StatusBar;
import org.janelia.saalfeldlab.fx.ui.NumberField;
import org.janelia.saalfeldlab.fx.ui.ObjectField;
import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread;
import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookup;
import org.janelia.saalfeldlab.n5.DataType;
import org.janelia.saalfeldlab.n5.DatasetAttributes;
import org.janelia.saalfeldlab.n5.N5Reader;
import org.janelia.saalfeldlab.n5.N5Writer;
import org.janelia.saalfeldlab.n5.imglib2.N5Utils;
import org.janelia.saalfeldlab.paintera.composition.ARGBCompositeAlphaAdd;
import org.janelia.saalfeldlab.paintera.composition.ARGBCompositeAlphaYCbCr;
import org.janelia.saalfeldlab.paintera.composition.CompositeCopy;
import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState;
import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsOnlyLocal;
import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
import org.janelia.saalfeldlab.paintera.data.DataSource;
import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder;
import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrderNotSupported;
import org.janelia.saalfeldlab.paintera.data.mask.Masks;
import org.janelia.saalfeldlab.paintera.data.n5.CommitCanvasN5;
import org.janelia.saalfeldlab.paintera.data.n5.DataTypeNotSupported;
import org.janelia.saalfeldlab.paintera.data.n5.N5ChannelDataSource;
import org.janelia.saalfeldlab.paintera.data.n5.N5Meta;
import org.janelia.saalfeldlab.paintera.data.n5.ReflectionException;
import org.janelia.saalfeldlab.paintera.data.n5.VolatileWithSet;
import org.janelia.saalfeldlab.paintera.id.IdService;
import org.janelia.saalfeldlab.paintera.id.N5IdService;
import org.janelia.saalfeldlab.paintera.meshes.InterruptibleFunction;
import org.janelia.saalfeldlab.paintera.meshes.MeshManagerWithAssignmentForSegments;
import org.janelia.saalfeldlab.paintera.state.ChannelSourceState;
import org.janelia.saalfeldlab.paintera.state.LabelSourceState;
import org.janelia.saalfeldlab.paintera.state.RawSourceState;
import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter;
import org.janelia.saalfeldlab.paintera.stream.ModalGoldenAngleSaturatedHighlightingARGBStream;
import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts;
import org.janelia.saalfeldlab.util.MakeUnchecked;
import org.janelia.saalfeldlab.util.NamedThreadFactory;
import org.janelia.saalfeldlab.util.n5.N5Data;
import org.janelia.saalfeldlab.util.n5.N5Helpers;
import org.janelia.saalfeldlab.util.n5.N5Types;
Expand All @@ -39,7 +85,16 @@
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.IntFunction;
import java.util.function.LongConsumer;
import java.util.function.Supplier;
import java.util.stream.IntStream;

public class PainteraShowContainer extends Application {
Expand Down Expand Up @@ -102,6 +157,7 @@ public void start(Stage primaryStage) throws Exception {
if (nDim == 4) {
channelDatasets.add(N5Meta.fromReader(n5WithChannel, dataset));
} else if (isLabelData(n5, dataset)) {
LOG.debug("Detected label dataset {} in container {}", dataset, container);
labelDatasets.add(N5Meta.fromReader(n5, dataset));
} else {
rawDatasets.add(N5Meta.fromReader(n5, dataset));
Expand All @@ -111,13 +167,17 @@ public void start(Stage primaryStage) throws Exception {
}

for (N5Meta rawMeta : rawDatasets) {
addRawSource(viewer.baseView, rawMeta, clArgs.axisOrder, clArgs.revertArrayAttributes);
addRawSource(viewer.baseView, rawMeta, clArgs.revertArrayAttributes);
}

for (N5Meta channelMeta : channelDatasets) {
addChannelSource(viewer.baseView, channelMeta, clArgs.revertArrayAttributes, clArgs.channelAxis, clArgs.maxNumChannels);
}

for (final N5Meta labelMeta : labelDatasets) {
addLabelSource(viewer.baseView, labelMeta, clArgs.revertArrayAttributes);
}


final Scene scene = new Scene(viewer.paneWithStatus.getPane(), clArgs.width, clArgs.height);
viewer.keyTracker.installInto(scene);
Expand Down Expand Up @@ -148,11 +208,13 @@ private static final class CommandLineArgs {

@CommandLine.Option(names = {"--height"})
Integer height = 900;

@CommandLine.Option(names = {"--dataset-axis-order"}, description = "Axis order of data. This is not the axis order of array attributes!")
AxisOrder axisOrder = AxisOrder.XYZ;
}

/* *************************************************** */
/* private static helper methods for opening data sets */
/* TODO move to helper class -- which one? N5Helpers? */
/* *************************************************** */

private static boolean isLabelData(N5Reader reader, String group) throws IOException {
if (N5Helpers.isPainteraDataset(reader, group)) {
JsonObject painteraInfo = reader.getAttribute(group, N5Helpers.PAINTERA_DATA_KEY, JsonObject.class);
Expand All @@ -177,9 +239,8 @@ private static int getNumDimensions(N5Reader n5, String dataset) throws IOExcept
private static <T extends RealType<T> & NativeType<T>, V extends AbstractVolatileRealType<T, V> & NativeType<V>> void addRawSource(
final PainteraBaseView viewer,
final N5Meta rawMeta,
AxisOrder axisOrder,
final boolean revertArrayAttributes
) throws IOException, ReflectionException, AxisOrderNotSupported {
) throws IOException, ReflectionException {
LOG.info("Adding raw source {}", rawMeta);
DataSource<T, V> source = N5Data.openRawAsSource(
rawMeta.writer(),
Expand Down Expand Up @@ -211,6 +272,111 @@ private static <T extends RealType<T> & NativeType<T>, V extends AbstractVolatil
viewer.addState(state);
}

private static <T extends IntegerType<T> & NativeType<T>, V extends Volatile<T> & NativeType<V> & IntegerType<V>> void addLabelSource(
final PainteraBaseView viewer,
final N5Meta labelMeta,
final boolean revertArrayAttributes
) throws IOException, ReflectionException {
LOG.info("Adding label source {}", labelMeta);
final N5Writer writer = labelMeta.writer();
final String dataset = labelMeta.dataset();
final double[] resolution = N5Helpers.getResolution(writer, dataset, revertArrayAttributes);
final double[] offset = N5Helpers.getOffset(writer, dataset, revertArrayAttributes);
final AffineTransform3D transform = N5Helpers.fromResolutionAndOffset(resolution, offset);
final DataSource<T, V> source;
if (N5Types.isLabelMultisetType(writer, dataset))
{
source = (DataSource) N5Data.openLabelMultisetAsSource(
writer,
dataset,
transform,
viewer.getGlobalCache(),
0,
dataset
);
}
else
{
LOG.debug("Getting scalar data source");
source = N5Data.openScalarAsSource(
writer,
dataset,
transform,
viewer.getGlobalCache(),
0,
dataset
);
}

final Supplier<String> canvasCacheDirUpdate = Masks.canvasTmpDirDirectorySupplier(null);

final DataSource<T, V> masked = Masks.mask(
source,
canvasCacheDirUpdate.get(),
canvasCacheDirUpdate,
new CommitCanvasN5(writer, dataset),
viewer.getPropagationQueue()
);
final IdService idService = idService(writer, dataset);
final FragmentSegmentAssignmentState assignment = N5Helpers.assignments(writer, dataset);
final SelectedIds selectedIds = new SelectedIds();
final LockedSegmentsOnlyLocal lockedSegments = new LockedSegmentsOnlyLocal(locked -> {
});
final ModalGoldenAngleSaturatedHighlightingARGBStream stream = new
ModalGoldenAngleSaturatedHighlightingARGBStream(
selectedIds,
assignment,
lockedSegments
);
final HighlightingStreamConverter<V> converter = HighlightingStreamConverter.forType(stream, masked.getType());

final LabelBlockLookup lookup = getLabelBlockLookup(writer, dataset, source);

IntFunction<InterruptibleFunction<Long, Interval[]>> loaderForLevelFactory = level -> InterruptibleFunction.fromFunction(
MakeUnchecked.function(
id -> lookup.read(level, id),
id -> {LOG.debug("Falling back to empty array"); return new Interval[0];}
));

InterruptibleFunction<Long, Interval[]>[] blockLoaders = IntStream
.range(0, masked.getNumMipmapLevels())
.mapToObj(loaderForLevelFactory)
.toArray(InterruptibleFunction[]::new );

MeshManagerWithAssignmentForSegments meshManager = MeshManagerWithAssignmentForSegments.fromBlockLookup(
masked,
selectedIds,
assignment,
stream,
viewer.viewer3D().meshesGroup(),
blockLoaders,
viewer.getGlobalCache()::createNewCache,
viewer.getMeshManagerExecutorService(),
viewer.getMeshWorkerExecutorService());

final LabelSourceState<T, V> state = new LabelSourceState<>(
masked,
converter,
new ARGBCompositeAlphaYCbCr(),
masked.getName(),
assignment,
lockedSegments,
idService,
selectedIds,
meshManager,
lookup);

viewer.addState(state);
}

private static LabelBlockLookup getLabelBlockLookup(final N5Reader reader, final String dataset, final DataSource<?, ?> fallBack) throws IOException {
try {
return N5Helpers.getLabelBlockLookup(reader, dataset);
} catch (N5Helpers.NotAPainteraDataset e) {
return PainteraAlerts.getLabelBlockLookupFromDataSource(fallBack);
}
}

private static <T extends RealType<T> & NativeType<T>, V extends AbstractVolatileRealType<T, V> & NativeType<V>> void addChannelSource(
final PainteraBaseView viewer,
final N5Meta meta,
Expand Down Expand Up @@ -274,4 +440,126 @@ private static <T extends RealType<T> & NativeType<T>, V extends AbstractVolatil
}
}

private static IdService idService(
final N5Writer n5,
final String dataset
) throws IOException
{
try {
LOG.warn("Getting id service for {} -- {}", n5, dataset);
return N5Helpers.idService(n5, dataset);
} catch (final N5Helpers.MaxIDNotSpecified e) {
final Alert alert = PainteraAlerts.alert(Alert.AlertType.CONFIRMATION);
alert.setHeaderText("maxId not specified in dataset.");
final TextArea ta = new TextArea("Could not read maxId attribute from data set. " +
"You can specify the max id manually, or read it from the data set (this can take a long time if your data is big).\n" +
"Alternatively, press cancel to load the data set without an id service. " +
"Fragment-segment-assignments and selecting new (wrt to the data) labels require an id service " +
"and will not be available if you press cancel.");
ta.setEditable(false);
ta.setWrapText(true);
final NumberField<LongProperty> nextIdField = NumberField.longField(0, v -> true, ObjectField.SubmitOn.ENTER_PRESSED, ObjectField.SubmitOn.FOCUS_LOST);
final Button scanButton = new Button("Scan Data");
scanButton.setOnAction(event -> {
event.consume();
try {
findMaxId(n5, dataset, nextIdField.valueProperty()::set);
} catch (IOException e1) {
throw new RuntimeException(e1);
}
});
final HBox maxIdBox = new HBox(new Label("Max Id:"), nextIdField.textField(), scanButton);
HBox.setHgrow(nextIdField.textField(), Priority.ALWAYS);
alert.getDialogPane().setContent(new VBox(ta, maxIdBox));
final Optional<ButtonType> bt = alert.showAndWait();
if (bt.isPresent() && ButtonType.OK.equals(bt.get())) {
long maxId = nextIdField.valueProperty().get() + 1;
n5.setAttribute(dataset, "maxId", maxId);
return new N5IdService(n5, dataset, maxId);
}
else
return new IdService.IdServiceNotProvided();
}
}

private static <I extends IntegerType<I> & NativeType<I>> void findMaxId(
final N5Reader reader,
String dataset,
final LongConsumer maxIdTarget
) throws IOException {
final int numProcessors = Runtime.getRuntime().availableProcessors();
final ExecutorService es = Executors.newFixedThreadPool(numProcessors, new NamedThreadFactory("max-id-discovery-%d", true));
dataset = N5Helpers.isPainteraDataset(reader, dataset) ? dataset + "/data" : dataset;
dataset = N5Helpers.isMultiScale(reader, dataset) ? N5Helpers.getFinestLevel(reader, dataset) : dataset;
final boolean isLabelMultiset = N5Helpers.getBooleanAttribute(reader, dataset, N5Helpers.IS_LABEL_MULTISET_KEY, false);
final CachedCellImg<I, ?> img = isLabelMultiset ? (CachedCellImg<I, ?>) (CachedCellImg) LabelUtils.openVolatile(reader, dataset) : (CachedCellImg<I, ?>) N5Utils.open(reader, dataset);
final int[] blockSize = new int[img.numDimensions()];
img.getCellGrid().cellDimensions(blockSize);
final List<Interval> blocks = Grids.collectAllContainedIntervals(img.getCellGrid().getImgDimensions(), blockSize);
final IntegerProperty completedTasks = new SimpleIntegerProperty(0);
final LongProperty maxId = new SimpleLongProperty(0);
final BooleanProperty wasCanceled = new SimpleBooleanProperty(false);
LOG.debug("Scanning for max id over {} blocks of size {} (total size {}).", blocks.size(), blockSize, img.getCellGrid().getImgDimensions());
final Thread t = new Thread(() -> {
List<Callable<Long>> tasks = new ArrayList<>();
for (final Interval block : blocks) {
tasks.add(() -> {
long localMaxId = 0;
try {
final IntervalView<I> interval = Views.interval(img, block);
final Cursor<I> cursor = interval.cursor();
while (cursor.hasNext() && !wasCanceled.get()) {
final long id = cursor.next().getIntegerLong();
if (id > localMaxId)
localMaxId = id;
}
return localMaxId;
}
finally {
synchronized (completedTasks) {
if (!wasCanceled.get()) {
LOG.trace("Incrementing completed tasks ({}/{}) and maxId ({}) with {}", completedTasks, blocks.size(), maxId, localMaxId);
maxId.setValue(Math.max(maxId.getValue(), localMaxId));
completedTasks.set(completedTasks.get() + 1);
}
}
}
});
}
try {
final List<Future<Long>> futures = es.invokeAll(tasks);
for (final Future<Long> future : futures) {
final Long id = future.get();
}
} catch (final InterruptedException | ExecutionException e) {
synchronized (completedTasks) {
completedTasks.set(-1);
wasCanceled.set(true);
}
LOG.error("Was interrupted while finding max id.", e);
}
});
t.setName("max-id-discovery-main-thread");
t.setDaemon(true);
t.start();
final Alert alert = PainteraAlerts.alert(Alert.AlertType.CONFIRMATION, true);
alert.setHeaderText("Finding max id...");
final BooleanBinding stillRuning = Bindings.createBooleanBinding(() -> completedTasks.get() < blocks.size(), completedTasks);
alert.getDialogPane().lookupButton(ButtonType.OK).disableProperty().bind(stillRuning);
final StatusBar statusBar = new StatusBar();
completedTasks.addListener((obs, oldv, newv) -> InvokeOnJavaFXApplicationThread.invoke(() -> statusBar.setProgress(newv.doubleValue() / blocks.size())));
completedTasks.addListener((obs, oldv, newv) -> InvokeOnJavaFXApplicationThread.invoke(() -> statusBar.setText(String.format("%s/%d", newv, blocks.size()))));
alert.getDialogPane().setContent(statusBar);
wasCanceled.addListener((obs, oldv, newv) -> InvokeOnJavaFXApplicationThread.invoke(() -> alert.setHeaderText("Cancelled")));
completedTasks.addListener((obs, oldv, newv) -> InvokeOnJavaFXApplicationThread.invoke(() -> alert.setHeaderText(newv.intValue() < blocks.size() ? "Finding max id: " + maxId.getValue() : "Found max id: " + maxId.getValue())));
final Optional<ButtonType> bt = alert.showAndWait();
if (bt.isPresent() && ButtonType.OK.equals(bt.get())) {
LOG.warn("Setting max id to {}", maxId.get());
maxIdTarget.accept(maxId.get());
} else {
wasCanceled.set(true);
}

}

}

0 comments on commit 48aa80c

Please sign in to comment.