/*- * #%L * Fiji distribution of ImageJ for the life sciences. * %% * Copyright (C) 2007 - 2017 Fiji developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-2.0.html>. * #L% */ package spim.fiji.datasetmanager; import fiji.util.gui.GenericDialogPlus; import ij.gui.GenericDialog; import java.awt.Font; import java.io.File; import java.util.ArrayList; import mpicbg.spim.data.SpimData; import mpicbg.spim.data.registration.ViewRegistration; import mpicbg.spim.data.registration.ViewRegistrations; import mpicbg.spim.data.registration.ViewTransform; import mpicbg.spim.data.registration.ViewTransformAffine; import mpicbg.spim.data.sequence.Angle; import mpicbg.spim.data.sequence.Channel; import mpicbg.spim.data.sequence.FinalVoxelDimensions; import mpicbg.spim.data.sequence.Illumination; import mpicbg.spim.data.sequence.ImgLoader; import mpicbg.spim.data.sequence.MissingViews; import mpicbg.spim.data.sequence.SequenceDescription; import mpicbg.spim.data.sequence.TimePoint; import mpicbg.spim.data.sequence.TimePoints; import mpicbg.spim.data.sequence.ViewDescription; import mpicbg.spim.data.sequence.ViewSetup; import mpicbg.spim.data.sequence.VoxelDimensions; import mpicbg.spim.io.IOFunctions; import net.imglib2.Dimensions; import net.imglib2.FinalDimensions; import net.imglib2.img.ImgFactory; import net.imglib2.img.array.ArrayImgFactory; import net.imglib2.img.cell.CellImgFactory; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.NativeType; import net.imglib2.type.numeric.real.FloatType; import spim.fiji.plugin.util.GUIHelper; import spim.fiji.spimdata.SpimData2; import spim.fiji.spimdata.ViewSetupUtils; import spim.fiji.spimdata.boundingbox.BoundingBoxes; import spim.fiji.spimdata.imgloaders.SlideBook6ImgLoader; import spim.fiji.spimdata.interestpoints.ViewInterestPoints; public class SlideBook6 implements MultiViewDatasetDefinition { public static String[] rotAxes = new String[]{"X-Axis", "Y-Axis", "Z-Axis"}; public static String defaultFilePath = ""; public static boolean defaultModifyCal = false; public static int defaultCapture = -1; public static double defaultAngles[] = {0.0f, 90.0f}; public static float defaultCalibrations[] = {1.0f, 1.0f, 1.0f}; @Override public String getTitle() { return "Slidebook6 Dataset"; } @Override public String getExtendedDescription() { return "This datset definition supports files saved by SlideBook6"; } @Override public SpimData2 createDataset() { final File sldFile = querySLDFile(); if (sldFile == null) return null; final SlideBook6MetaData meta = new SlideBook6MetaData(); if (!meta.loadMetaData(sldFile)) { IOFunctions.println("Failed to analyze file."); return null; } if (!showDialogs(meta)) { return null; } final String directory = sldFile.getParent(); final ImgFactory<? extends NativeType<?>> imgFactory = selectImgFactory(meta); // assemble timepoints, viewsetups, missingviews and the imgloader final TimePoints timepoints = this.createTimePoints(meta); final ArrayList<ViewSetup> setups = this.createViewSetups(meta); final MissingViews missingViews = null; // instantiate the sequencedescription final SequenceDescription sequenceDescription = new SequenceDescription(timepoints, setups, null, missingViews); final ImgLoader imgLoader = new SlideBook6ImgLoader(sldFile, imgFactory, sequenceDescription); sequenceDescription.setImgLoader(imgLoader); // get the minimal resolution of all calibrations, TODO: different views can have different calibrations? final float zSpacing = defaultCalibrations[2]; final double minResolution = Math.min(defaultCalibrations[0], zSpacing); IOFunctions.println("Minimal resolution in all dimensions is: " + minResolution); IOFunctions.println("(The smallest resolution in any dimension; the distance between two pixels in the output image will be that wide)"); // create the initial view registrations (they are all the identity transform) final ViewRegistrations viewRegistrations = StackList.createViewRegistrations(sequenceDescription.getViewDescriptions(), minResolution); // create the initial view interest point object final ViewInterestPoints viewInterestPoints = new ViewInterestPoints(); viewInterestPoints.createViewInterestPoints(sequenceDescription.getViewDescriptions()); // finally create the SpimData itself based on the sequence description and the view registration final SpimData2 spimData = new SpimData2(new File(directory), sequenceDescription, viewRegistrations, viewInterestPoints, new BoundingBoxes()); // Apply_Transformation.applyAxis( spimData ); applyAxis(spimData, minResolution); return spimData; } public static void applyAxis(final SpimData data, final double minResolution) { ViewRegistrations viewRegistrations = data.getViewRegistrations(); for (final ViewDescription vd : data.getSequenceDescription().getViewDescriptions().values()) { if (vd.isPresent()) { final Angle a = vd.getViewSetup().getAngle(); if (a.hasRotation()) { final ViewRegistration vr = viewRegistrations.getViewRegistration(vd); final Dimensions dim = vd.getViewSetup().getSize(); VoxelDimensions voxelSize = ViewSetupUtils.getVoxelSizeOrLoad(vd.getViewSetup(), vd.getTimePoint(), data.getSequenceDescription().getImgLoader()); final double calX = voxelSize.dimension(0) / minResolution; final double calY = voxelSize.dimension(1) / minResolution; final double calZ = voxelSize.dimension(2) / minResolution; AffineTransform3D calModel = new AffineTransform3D(); calModel.set( 1, 0, 0, -dim.dimension(0) / 2 * calX, 0, 1, 0, -dim.dimension(1) / 2 * calY, 0, 0, 1, -dim.dimension(2) / 2 * calZ); ViewTransform vt = new ViewTransformAffine("Center view", calModel); vr.preconcatenateTransform(vt); final double[] tmp = new double[16]; final double[] axis = a.getRotationAxis(); final double degrees = a.getRotationAngleDegrees(); final AffineTransform3D rotModel = new AffineTransform3D(); final String d; if (axis[0] == 1 && axis[1] == 0 && axis[2] == 0) { rotModel.rotate(0, Math.toRadians(degrees)); d = "Rotation around x-axis by " + degrees + " degrees"; } else if (axis[0] == 0 && axis[1] == 1 && axis[2] == 0) { rotModel.rotate(1, Math.toRadians(degrees)); d = "Rotation around y-axis by " + degrees + " degrees"; } else if (axis[0] == 0 && axis[0] == 0 && axis[2] == 1) { rotModel.rotate(2, Math.toRadians(degrees)); d = "Rotation around z-axis by " + degrees + " degrees"; } else { IOFunctions.println("Arbitrary rotation axis not supported yet."); continue; } vt = new ViewTransformAffine(d, rotModel); vr.preconcatenateTransform(vt); vr.updateModel(); } } } } /** * Creates the List of {@link ViewSetup} for the {@link SpimData} object. * The {@link ViewSetup} are defined independent of the {@link TimePoint}, * each {@link TimePoint} should have the same {@link ViewSetup}s. The {@link MissingViews} * class defines if some of them are missing for some of the {@link TimePoint}s * * @return */ protected ArrayList<ViewSetup> createViewSetups(final SlideBook6MetaData meta) { // TODO: query rotation angle of each SlideBook channel final double[] yaxis = new double[]{0, 1, 0}; final ArrayList<Angle> angles = new ArrayList<Angle>(); final Angle angleA = new Angle(0, "Path_A"); angleA.setRotation(yaxis, defaultAngles[0]); angles.add(angleA); final Angle angleB = new Angle(1, "Path_B"); angleB.setRotation(yaxis, defaultAngles[1]); angles.add(angleB); // define multiple illuminations, one for every capture in the slide file final ArrayList<ViewSetup> viewSetups = new ArrayList<ViewSetup>(); int firstCapture = defaultCapture; int numCaptures = 1; if (defaultCapture == -1) { firstCapture = 0; numCaptures = meta.numCaptures(); } // create one illumination for each capture if defaultCapture == -1, otherwise just one capture for (int capture = firstCapture; capture < firstCapture + numCaptures; capture++) { final int channels = meta.numChannels(capture); // multi-angle captures must have pairs of channels if (channels > 1 && channels % 2 == 0) { String imageName = meta.imageName(capture); imageName = imageName.replaceAll("[^a-zA-Z0-9_-]", "_"); // convert illegal characters to _ imageName = imageName.toLowerCase(); // convert to lowercase // up to 8 illuminations (SlideBook channels) per SlideBook capture final Illumination i = new Illumination(capture * 8, imageName); for (int ch = 0; ch < channels / 2; ch++) { // use name of first channel, SlideBook channels are diSPIM angles and each SlideBook image should have two angles per channel String channelName = meta.channels(capture)[ch * 2]; channelName = channelName.replaceAll("[^a-zA-Z0-9_-]", "_"); // convert illegal characters to _ channelName = channelName.toLowerCase(); // convert to lowercase final Channel channel = new Channel(ch, channelName); for (final Angle a : angles) { float voxelSizeUm = defaultCalibrations[0]; float zSpacing = defaultCalibrations[2]; final VoxelDimensions voxelSize = new FinalVoxelDimensions("um", voxelSizeUm, voxelSizeUm, zSpacing); final Dimensions dim = new FinalDimensions(new long[]{meta.imageSize(capture)[0], meta.imageSize(capture)[1], meta.imageSize(capture)[2]}); viewSetups.add(new ViewSetup(viewSetups.size(), a.getName(), dim, voxelSize, channel, a, i)); } } } } return viewSetups; } /** * Creates the {@link TimePoints} for the {@link SpimData} object */ protected TimePoints createTimePoints(final SlideBook6MetaData meta) { final ArrayList<TimePoint> timepoints = new ArrayList<TimePoint>(); int firstCapture = defaultCapture; int numCaptures = 1; if (defaultCapture == -1) { firstCapture = 0; numCaptures = meta.numCaptures(); } int t = 0; for (; t < meta.numTimepoints(firstCapture); ++t) timepoints.add(new TimePoint(t)); return new TimePoints(timepoints); } protected ImgFactory<? extends NativeType<?>> selectImgFactory(final SlideBook6MetaData meta) { int[] dims = meta.imageSize(0); long maxNumPixels = dims[0]; maxNumPixels *= dims[1]; maxNumPixels *= dims[2]; String s = "Maximum number of pixels in any view: n=" + Long.toString(maxNumPixels) + " px "; if (maxNumPixels < Integer.MAX_VALUE) { IOFunctions.println(s + "< " + Integer.MAX_VALUE + ", using ArrayImg."); return new ArrayImgFactory<FloatType>(); } else { IOFunctions.println(s + ">= " + Integer.MAX_VALUE + ", using CellImg."); return new CellImgFactory<FloatType>(256); } } protected boolean showDialogs(final SlideBook6MetaData meta) { { GenericDialog gd = new GenericDialog("Select SlideBook6 diSPIM Capture"); String[] imageNames = new String[meta.numCaptures() + 1]; imageNames[0] = new String("< All Captures >"); for (int c = 0; c < meta.numCaptures(); c++) { imageNames[c + 1] = meta.imageName(c); } // pick which image capture to process gd.addChoice("Capture Name", imageNames, imageNames[0]); GUIHelper.addScrollBars(gd); gd.showDialog(); if (gd.wasCanceled()) return false; defaultCapture = gd.getNextChoiceIndex() - 1; } int firstCapture = defaultCapture; if (defaultCapture == -1) { firstCapture = 0; } // check for invalid image ( single channel == single angle ) int numChannels = meta.numChannels(firstCapture); if (numChannels < 2 || numChannels % 2 != 0) { IOFunctions.println("ERROR: " + numChannels + " angles detected. No alignment possible."); return false; } { GenericDialog gd = new GenericDialog("SlideBook6 diSPIM Properties"); gd.addMessage("Angles (" + numChannels + " present)", new Font(Font.SANS_SERIF, Font.BOLD, 13)); gd.addMessage(""); for (int ch = 0; ch < numChannels; ch++) { gd.addNumericField("Angle of '" + meta.channels(firstCapture)[ch] + "':", (ch % 2) * 90, 1, 5, "degrees"); } gd.addMessage("Channels (" + meta.numChannels(firstCapture) / 2 + " present)", new Font(Font.SANS_SERIF, Font.BOLD, 13)); gd.addMessage(""); for (int ch = 0; ch < meta.numChannels(firstCapture) / 2; ++ch) { gd.addMessage("Channel_" + (ch + 1) + ": " + meta.channels(firstCapture)[ch* 2]); } gd.addMessage("Timepoints (" + meta.numTimepoints(firstCapture) + " present)", new Font(Font.SANS_SERIF, Font.BOLD, 13)); defaultCalibrations[0] = (float) meta.calX(firstCapture); defaultCalibrations[1] = (float) meta.calY(firstCapture); defaultCalibrations[2] = (float) meta.calZ(firstCapture); gd.addMessage("Calibration", new Font(Font.SANS_SERIF, Font.BOLD, 13)); gd.addCheckbox("Modify_calibration", defaultModifyCal); gd.addMessage( "Pixel Distance X: " + defaultCalibrations[0] + " " + "um" + "\n" + "Pixel Distance Y: " + defaultCalibrations[1] + " " + "um" + "\n" + "Pixel Distance Z: " + defaultCalibrations[2] + " " + "um" + "\n"); gd.addMessage( "Rotation axis: " + "Y" + " axis\n" + "Pixel type: " + "UInt16", new Font(Font.SANS_SERIF, Font.ITALIC, 11)); gd.showDialog(); if (gd.wasCanceled()) return false; // use the user specified angles for (int a = 0; a < meta.numChannels(firstCapture); ++a) { defaultAngles[a % 2] = gd.getNextNumber(); } defaultModifyCal = gd.getNextBoolean(); } if (defaultModifyCal) { GenericDialog gd = new GenericDialog("Modify Meta Data"); gd.addNumericField("Pixel_distance_x", defaultCalibrations[0], 5, 5, "um"); gd.addNumericField("Pixel_distance_y", defaultCalibrations[1], 5, 5, "um"); gd.addNumericField("Pixel_distance_z", defaultCalibrations[2], 5, 5, "um"); gd.showDialog(); if (gd.wasCanceled()) return false; defaultCalibrations[0] = (float) gd.getNextNumber(); defaultCalibrations[1] = (float) gd.getNextNumber(); defaultCalibrations[2] = (float) gd.getNextNumber(); } return true; } protected File querySLDFile() { GenericDialogPlus gd = new GenericDialogPlus("Define SlideBook6 diSPIM Dataset"); gd.addFileField("SlideBook6 SLD file", defaultFilePath, 50); gd.showDialog(); if (gd.wasCanceled()) return null; final File firstFile = new File(defaultFilePath = gd.getNextString()); if (!firstFile.exists()) { IOFunctions.println("File '" + firstFile.getAbsolutePath() + "' does not exist. Stopping"); return null; } else { IOFunctions.println("Investigating file '" + firstFile.getAbsolutePath() + "'."); return firstFile; } } @Override public SlideBook6 newInstance() { return new SlideBook6(); } public static void main(String[] args) { //defaultFirstFile = "/Volumes/My Passport/Zeiss Olaf Lightsheet Z.1/worm7/Track1.czi"; new SlideBook6().createDataset(); } }