diff --git a/build.gradle.kts b/build.gradle.kts index 533340dc..3c9310d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,4 @@ +import groovy.xml.XmlSlurper import org.gradle.kotlin.dsl.implementation import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.net.URL @@ -60,6 +61,9 @@ dependencies { implementation("net.java.dev.jna:jna-platform:5.14.0") implementation("org.janelia.saalfeldlab:n5") implementation("org.janelia.saalfeldlab:n5-imglib2") + implementation("org.janelia.saalfeldlab:n5-blosc") + implementation("org.janelia.saalfeldlab:n5-universe:1.3.2") + implementation("org.janelia.saalfeldlab:n5-viewer_fiji:6.0.1") implementation("org.apache.logging.log4j:log4j-api:2.20.0") implementation("org.apache.logging.log4j:log4j-1.2-api:2.20.0") @@ -67,9 +71,7 @@ dependencies { // SciJava dependencies - implementation("org.yaml:snakeyaml") { - version { strictly("1.33") } - } + implementation("org.yaml:snakeyaml") implementation("org.scijava:scijava-common") implementation("org.scijava:ui-behaviour") implementation("org.scijava:script-editor") @@ -120,6 +122,11 @@ dependencies { implementation("sc.fiji:spim_data") implementation("org.slf4j:slf4j-simple") + implementation("software.amazon.awssdk:s3:2.20.1") + implementation("org.apache.commons:commons-compress:1.21") + // implementation("com.scalableminds:blosc-java:0.1-1.21.4") + implementation("org.lasersonlab:jblosc:1.0.1") + implementation(platform(kotlin("bom"))) implementation(kotlin("stdlib-jdk8")) testImplementation(kotlin("test-junit")) @@ -127,7 +134,8 @@ dependencies { implementation("sc.fiji:bigdataviewer-core") implementation("sc.fiji:bigdataviewer-vistools") - implementation("sc.fiji:bigvolumeviewer:0.3.3") { + //implementation("sc.fiji:bigvolumeviewer:0.3.3") { + implementation("com.github.kephale:bigvolumeviewer-core:1bc0f83") { exclude("org.jogamp.jogl","jogl-all") exclude("org.jogamp.gluegen", "gluegen-rt") } diff --git a/src/main/java/sc/iview/commands/file/OpenN5.kt b/src/main/java/sc/iview/commands/file/OpenN5.kt index 42d3a220..33cdbc5e 100644 --- a/src/main/java/sc/iview/commands/file/OpenN5.kt +++ b/src/main/java/sc/iview/commands/file/OpenN5.kt @@ -28,7 +28,14 @@ */ package sc.iview.commands.file +import bdv.cache.SharedQueue +import bdv.tools.brightness.ConverterSetup +import bdv.util.BdvOptions import bdv.util.volatiles.VolatileViews +import bdv.viewer.SourceAndConverter +import net.imagej.Dataset +import net.imglib2.realtransform.AffineTransform3D +import net.imglib2.type.numeric.RealType import net.imglib2.type.numeric.integer.* import net.imglib2.type.numeric.real.DoubleType import net.imglib2.type.numeric.real.FloatType @@ -36,7 +43,12 @@ import org.janelia.saalfeldlab.n5.DataType import org.janelia.saalfeldlab.n5.N5FSReader import org.janelia.saalfeldlab.n5.N5Reader import org.janelia.saalfeldlab.n5.N5URI +import org.janelia.saalfeldlab.n5.bdv.N5Viewer import org.janelia.saalfeldlab.n5.imglib2.N5Utils +import org.janelia.saalfeldlab.n5.universe.N5Factory +import org.janelia.saalfeldlab.n5.universe.N5MetadataUtils +import org.janelia.saalfeldlab.n5.universe.metadata.N5Metadata +import org.scijava.ItemVisibility import org.scijava.command.DynamicCommand import org.scijava.log.LogService import org.scijava.plugin.Menu @@ -44,12 +56,16 @@ import org.scijava.plugin.Parameter import org.scijava.plugin.Plugin import org.scijava.widget.ChoiceWidget import org.scijava.widget.NumberWidget -import org.scijava.widget.TextWidget import sc.iview.SciView import sc.iview.commands.MenuWeights.FILE import sc.iview.commands.MenuWeights.FILE_OPEN +import ucar.units.StandardUnitFormatConstants.T import java.io.File import java.io.IOException +import java.util.* +import kotlin.collections.ArrayList +import kotlin.math.max + /** * Command to open a file in SciView @@ -70,35 +86,47 @@ class OpenN5 : DynamicCommand() { private lateinit var sciView: SciView // TODO: Find a more extensible way than hard-coding the extensions. - @Parameter(style = "directory", callback = "refreshDatasets", required = true, persist = false) + @Parameter(style = "directory", callback = "refreshDatasets", required = true, persist = true) private lateinit var file: File - @Parameter(required = true, style = ChoiceWidget.LIST_BOX_STYLE, callback = "refreshVoxelSize", persist = false) + @Parameter(required = true, style = ChoiceWidget.LIST_BOX_STYLE, choices = ["(none)"], callback = "refreshVoxelSize", persist = false) private lateinit var dataset: String private lateinit var reader: N5Reader - @Parameter(style = NumberWidget.SPINNER_STYLE + ",format:0.000", stepSize = "0.1") + @Parameter(style = NumberWidget.SPINNER_STYLE + ",format:0.000", stepSize = "0.1", persist = false) private var voxelSizeX = 1.0f - @Parameter(style = NumberWidget.SPINNER_STYLE + ",format:0.000", stepSize = "0.1") + @Parameter(style = NumberWidget.SPINNER_STYLE + ",format:0.000", stepSize = "0.1", persist = false) private var voxelSizeY = 1.0f - @Parameter(style = NumberWidget.SPINNER_STYLE + ",format:0.000", stepSize = "0.1") + @Parameter(style = NumberWidget.SPINNER_STYLE + ",format:0.000", stepSize = "0.1", persist = false) private var voxelSizeZ = 1.0f - @Parameter(style = NumberWidget.SPINNER_STYLE + ",format:0.000", stepSize = "0.1") + @Parameter(style = NumberWidget.SPINNER_STYLE + ",format:0.000", stepSize = "0.1", persist = false) private var unitScaling = 1.0f - @Parameter(style = TextWidget.AREA_STYLE) + @Parameter(visibility = ItemVisibility.MESSAGE, persist = false) private var unitMessage = "" + private var multiscaleDatasets = mutableListOf() + @Suppress("unused") private fun refreshDatasets() { reader = N5FSReader(file.absolutePath) val includedDatasets = reader.deepListDatasets("/") - info.getMutableInput("dataset", String::class.java).choices = includedDatasets.toMutableList() - dataset = includedDatasets.first() + + val isMultiscale = includedDatasets.map { it.split("/").last() }.all { it.startsWith("s") } + if(isMultiscale) { + log.info("Discovered dataset: ${includedDatasets.first()} (multiscale)") + multiscaleDatasets = includedDatasets.toMutableList() + info.getMutableInput("dataset", String::class.java).choices = listOf(includedDatasets.first().split("/").first()) + dataset = includedDatasets.first().split("/").first() + } else { + log.info("Discovered dataset: ${includedDatasets.joinToString(", ")}") + info.getMutableInput("dataset", String::class.java).choices = includedDatasets.toMutableList() + dataset = includedDatasets.first() + } refreshVoxelSize() } @@ -106,43 +134,66 @@ class OpenN5 : DynamicCommand() { private fun refreshVoxelSize() { log.info("dataset is $dataset") reader = N5FSReader(file.absolutePath) - val resolution = reader.getAttribute("volume", "resolution", FloatArray::class.java) ?: return - voxelSizeX = resolution[0] - voxelSizeY = resolution[1] - voxelSizeZ = resolution[2] - - val units = reader.getAttribute("volume", "units", Array::class.java) ?: return - - unitScaling = when(units.first()) { - "nm" -> 0.001f - "µm" -> 1.0f - "mm" -> 1000.0f - else -> 1.0f + val resolution = reader.getAttribute(dataset, "resolution", FloatArray::class.java) + if(resolution != null) { + voxelSizeX = resolution[0] + voxelSizeY = resolution[1] + voxelSizeZ = resolution[2] + } + + val units = reader.getAttribute(dataset, "units", Array::class.java) + + if(units != null) { + unitScaling = when(units.first()) { + "nm" -> 0.001f + "µm" -> 1.0f + "mm" -> 1000.0f + else -> 1.0f + } } - unitMessage = "Individual voxels will appear in the scene as ${voxelSizeX*unitScaling} m (world units) in size." + unitMessage = if(units == null || resolution == null) { + "Dataset is missing resolution or unit information.\nOne voxel will occupy ${voxelSizeX*unitScaling}m in world space." + } else { + "Individual voxels will appear in the scene as ${voxelSizeX * unitScaling} m (world units) in size." + } } + + override fun run() { try { - val attributes = reader.getDatasetAttributes(dataset) - val img = when(attributes.dataType) { - DataType.UINT8 -> N5Utils.openVolatile(reader, dataset) - DataType.UINT16 -> N5Utils.openVolatile(reader, dataset) - DataType.UINT32 -> N5Utils.openVolatile(reader, dataset) - DataType.UINT64 -> N5Utils.openVolatile(reader, dataset) - DataType.INT8 -> N5Utils.openVolatile(reader, dataset) - DataType.INT16 -> N5Utils.openVolatile(reader, dataset) - DataType.INT32 -> N5Utils.openVolatile(reader, dataset) - DataType.INT64 -> N5Utils.openVolatile(reader, dataset) - DataType.FLOAT32 -> N5Utils.openVolatile(reader, dataset) - DataType.FLOAT64 -> N5Utils.openVolatile(reader, dataset) - DataType.OBJECT -> TODO() - null -> TODO() + if(multiscaleDatasets.size == 0) { + val attributes = reader.getDatasetAttributes(dataset) + val img = when(attributes.dataType) { + DataType.UINT8 -> N5Utils.openVolatile(reader, dataset) + DataType.UINT16 -> N5Utils.openVolatile(reader, dataset) + DataType.UINT32 -> N5Utils.openVolatile(reader, dataset) + DataType.UINT64 -> N5Utils.openVolatile(reader, dataset) + DataType.INT8 -> N5Utils.openVolatile(reader, dataset) + DataType.INT16 -> N5Utils.openVolatile(reader, dataset) + DataType.INT32 -> N5Utils.openVolatile(reader, dataset) + DataType.INT64 -> N5Utils.openVolatile(reader, dataset) + DataType.FLOAT32 -> N5Utils.openVolatile(reader, dataset) + DataType.FLOAT64 -> N5Utils.openVolatile(reader, dataset) + DataType.OBJECT -> TODO() + null -> TODO() + DataType.STRING -> TODO() + } + + val wrapped = VolatileViews.wrapAsVolatile(img) + sciView.addVolume( + wrapped, + dataset, + voxelDimensions = floatArrayOf( + voxelSizeX * unitScaling * 1000.0f, + voxelSizeY * unitScaling * 1000.0f, + voxelSizeZ * unitScaling * 1000.0f + ) + ) + } else { + //N5Opener.openN5(sciView, file.absolutePath) } - - val wrapped = VolatileViews.wrapAsVolatile(img) - sciView.addVolume(wrapped, dataset, voxelDimensions = floatArrayOf(voxelSizeX*unitScaling*1000.0f, voxelSizeY*unitScaling*1000.0f, voxelSizeZ*unitScaling*1000.0f)) } catch(exc: IOException) { log.error(exc) } catch(exc: IllegalArgumentException) { diff --git a/src/main/java/sc/iview/mandelbulb/MandelbulbCacheArrayLoader.java b/src/main/java/sc/iview/mandelbulb/MandelbulbCacheArrayLoader.java new file mode 100644 index 00000000..e3172e2b --- /dev/null +++ b/src/main/java/sc/iview/mandelbulb/MandelbulbCacheArrayLoader.java @@ -0,0 +1,140 @@ +package sc.iview.mandelbulb; + +import bdv.img.cache.CacheArrayLoader; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImg; +import net.imglib2.img.array.ArrayImgFactory; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.img.cell.CellImg; +import net.imglib2.img.cell.CellImgFactory; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.view.Views; + +public class MandelbulbCacheArrayLoader implements CacheArrayLoader +{ + private final int maxIter; + private final int order; + + // Static variables for grid sizes, base grid size, and desired finest grid size + public static int[] gridSizes; + public static int baseGridSize; + public static int desiredFinestGridSize; + + public MandelbulbCacheArrayLoader(int maxIter, int order) + { + this.maxIter = maxIter; + this.order = order; + } + + @Override + public VolatileShortArray loadArray(final int timepoint, final int setup, final int level, final int[] cellDims, final long[] cellMin) throws InterruptedException + { + // Generate Mandelbulb for the specific cell region + final RandomAccessibleInterval img = generateMandelbulbForCell(cellDims, cellMin, level, maxIter, order); + + // Create a VolatileShortArray to hold the generated data + final VolatileShortArray shortArray = new VolatileShortArray(cellDims[0] * cellDims[1] * cellDims[2], true); + + // Extract the data into the short array + final short[] data = shortArray.getCurrentStorageArray(); + Views.flatIterable(img).forEach(pixel -> data[(int) pixel.index().get()] = (short) pixel.get()); + + return shortArray; + } + + @Override + public int getBytesPerElement() + { + return 2; // Each element is 2 bytes (16 bits) + } + + public static RandomAccessibleInterval generateMandelbulbForCell(int[] cellDims, long[] cellMin, int level, int maxIter, int order) + { + final RandomAccessibleInterval img = ArrayImgs.unsignedShorts(new long[]{cellDims[0], cellDims[1], cellDims[2]}); + + // Calculate the scaling factor based on the desired finest grid size + double scale = (double) desiredFinestGridSize / gridSizes[level]; + + // Calculate center offset for normalization + double centerOffset = desiredFinestGridSize / 2.0; + + System.out.println("Generating cell level=" + level + " at " + cellMin[0] + ", " + cellMin[1] + ", " + cellMin[2] + " scale " + scale + " centerOffset " + centerOffset); + + for (long z = 0; z < cellDims[2]; z++) + { + for (long y = 0; y < cellDims[1]; y++) + { + for (long x = 0; x < cellDims[0]; x++) + { + // Normalize and center coordinates to range from -1 to 1 + double[] coordinates = new double[]{ + ((x + cellMin[0]) * scale - centerOffset) / centerOffset, + ((y + cellMin[1]) * scale - centerOffset) / centerOffset, + ((z + cellMin[2]) * scale - centerOffset) / centerOffset + }; + // int iterations = (int) (( x + y + z ) % 2); + // int iterations = mandelbulbIter(coordinates, maxIter, order); + int iterations = (int) (255 * level / gridSizes.length); + img.getAt(x, y, z).set((int) (iterations * 65535.0 / maxIter)); // Scale to 16-bit range + } + } + } + return img; + } + + private static int mandelbulbIter(double[] coord, int maxIter, int order) + { + double x = coord[0]; + double y = coord[1]; + double z = coord[2]; + double xn = 0, yn = 0, zn = 0; + int iter = 0; + while (iter < maxIter && xn * xn + yn * yn + zn * zn < 4) + { + double r = Math.sqrt(xn * xn + yn * yn + zn * zn); + double theta = Math.atan2(Math.sqrt(xn * xn + yn * yn), zn); + double phi = Math.atan2(yn, xn); + + double newR = Math.pow(r, order); + double newTheta = theta * order; + double newPhi = phi * order; + + xn = newR * Math.sin(newTheta) * Math.cos(newPhi) + x; + yn = newR * Math.sin(newTheta) * Math.sin(newPhi) + y; + zn = newR * Math.cos(newTheta) + z; + + iter++; + } + return iter; + } + + public static ArrayImg generateFullMandelbulb(int level, int maxIter, int order) + { + long[] dimensions = { (long) gridSizes[level], (long) gridSizes[level], (long) gridSizes[level] }; + + ArrayImgFactory factory = new ArrayImgFactory<>(new UnsignedShortType()); + ArrayImg img = factory.create(dimensions); + + RandomAccess imgRA = img.randomAccess(); + for (long z = 0; z < dimensions[2]; z++) { + for (long y = 0; y < dimensions[1]; y++) { + for (long x = 0; x < dimensions[0]; x++) { + double[] coordinates = new double[]{ + ((double)x / dimensions[0] * 2 - 1), + ((double)y / dimensions[1] * 2 - 1), + ((double)z / dimensions[2] * 2 - 1) + }; + int iterations = mandelbulbIter(coordinates, maxIter, order); + imgRA.setPosition(new long[]{x, y, z}); + imgRA.get().set((int) (iterations * 65535.0 / maxIter)); + } + } + } + + return img; + } +} diff --git a/src/main/java/sc/iview/mandelbulb/MandelbulbImgLoader.java b/src/main/java/sc/iview/mandelbulb/MandelbulbImgLoader.java new file mode 100644 index 00000000..44d5be6d --- /dev/null +++ b/src/main/java/sc/iview/mandelbulb/MandelbulbImgLoader.java @@ -0,0 +1,137 @@ +package sc.iview.mandelbulb; + +import bdv.AbstractViewerSetupImgLoader; +import bdv.ViewerImgLoader; +import bdv.cache.CacheControl; +import bdv.img.cache.CacheArrayLoader; +import bdv.img.cache.VolatileCachedCellImg; +import bdv.img.cache.VolatileGlobalCellCache; +import bdv.img.hdf5.MipmapInfo; +import bdv.img.hdf5.ViewLevelId; +import mpicbg.spim.data.generic.sequence.ImgLoaderHint; +import net.imglib2.Dimensions; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.cache.img.ReadOnlyCachedCellImgFactory; +import net.imglib2.cache.volatiles.CacheHints; +import net.imglib2.cache.volatiles.LoadingStrategy; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.type.volatiles.VolatileUnsignedShortType; +import net.imglib2.util.Intervals; + +import java.util.HashMap; + +import static sc.iview.mandelbulb.MandelbulbCacheArrayLoader.baseGridSize; + +public class MandelbulbImgLoader implements ViewerImgLoader { + private final int[] gridSizes; + private final int maxIter; + private final int order; + + private MipmapInfo mipmapInfo; + private long[][] mipmapDimensions; + private VolatileGlobalCellCache cache; + private CacheArrayLoader loader; + private final HashMap setupImgLoaders; + + public MandelbulbImgLoader(int[] gridSizes, int maxIter, int order) { + this.gridSizes = gridSizes; + this.maxIter = maxIter; + this.order = order; + this.setupImgLoaders = new HashMap<>(); + initialize(); + } + + private void initialize() + { + // Set up mipmap dimensions and info + mipmapDimensions = new long[gridSizes.length][]; + final double[][] resolutions = new double[gridSizes.length][]; + final int[][] subdivisions = new int[gridSizes.length][]; + final AffineTransform3D[] transforms = new AffineTransform3D[gridSizes.length]; + + for (int level = 0; level < gridSizes.length; level++) + { + int gridSize = gridSizes[level]; + mipmapDimensions[level] = new long[]{gridSize, gridSize, gridSize}; + resolutions[level] = new double[]{1.0 / (1 << level), 1.0 / (1 << level), 1.0 / (1 << level)}; + subdivisions[level] = new int[]{128, 128, 128}; // arbitrary cell size + transforms[level] = new AffineTransform3D(); + transforms[level].scale(resolutions[level][0], resolutions[level][1], resolutions[level][2]); + } + + mipmapInfo = new MipmapInfo(resolutions, transforms, subdivisions); + loader = new MandelbulbCacheArrayLoader(maxIter, order); + cache = new VolatileGlobalCellCache(gridSizes.length, 1); + + for (int setupId = 0; setupId < 1; setupId++) + { + setupImgLoaders.put(setupId, new SetupImgLoader(setupId)); + } + } + + protected > VolatileCachedCellImg prepareCachedImage(final ViewLevelId id, final LoadingStrategy loadingStrategy, final T type) { + final int level = id.getLevel(); + final long[] dimensions = mipmapDimensions[level]; + final int[] cellDimensions = mipmapInfo.getSubdivisions()[level]; + final CellGrid grid = new CellGrid(dimensions, cellDimensions); + + final int priority = mipmapInfo.getMaxLevel() - level; + final CacheHints cacheHints = new CacheHints(loadingStrategy, priority, false); + return cache.createImg(grid, id.getTimePointId(), id.getViewSetupId(), level, cacheHints, loader, type); + } + + @Override + public CacheControl getCacheControl() { + return cache; + } + + @Override + public SetupImgLoader getSetupImgLoader(final int setupId) { + return setupImgLoaders.get(setupId); + } + + public class SetupImgLoader extends AbstractViewerSetupImgLoader { + private final int setupId; + + protected SetupImgLoader(final int setupId) { + super(new UnsignedShortType(), new VolatileUnsignedShortType()); + this.setupId = setupId; + } + + @Override + public RandomAccessibleInterval getImage(final int timepointId, final int level, final ImgLoaderHint... hints) { + final ViewLevelId id = new ViewLevelId(timepointId, setupId, level); + return prepareCachedImage(id, LoadingStrategy.BLOCKING, new UnsignedShortType()); + } + + @Override + public RandomAccessibleInterval getVolatileImage(final int timepointId, final int level, final ImgLoaderHint... hints) { + final ViewLevelId id = new ViewLevelId(timepointId, setupId, level); + return prepareCachedImage(id, LoadingStrategy.BUDGETED, new VolatileUnsignedShortType()); + } + + @Override + public double[][] getMipmapResolutions() { + return mipmapInfo.getResolutions(); + } + + @Override + public AffineTransform3D[] getMipmapTransforms() { + return mipmapInfo.getTransforms(); + } + + @Override + public int numMipmapLevels() { + return mipmapInfo.getNumLevels(); + } + } + + public Dimensions dimensions() { + // Return dimensions of the highest resolution + return Intervals.createMinSize(mipmapDimensions[0][0], mipmapDimensions[0][1], mipmapDimensions[0][2]); + } +} diff --git a/src/main/java/sc/iview/mandelbulb/MultiResolutionMandelbulb.java b/src/main/java/sc/iview/mandelbulb/MultiResolutionMandelbulb.java new file mode 100644 index 00000000..640c7bba --- /dev/null +++ b/src/main/java/sc/iview/mandelbulb/MultiResolutionMandelbulb.java @@ -0,0 +1,177 @@ +package sc.iview.mandelbulb; + +import bdv.BigDataViewer; +import bdv.spimdata.SequenceDescriptionMinimal; +import bdv.spimdata.SpimDataMinimal; +import bdv.spimdata.WrapBasicImgLoader; +import bdv.tools.brightness.ConverterSetup; +import bdv.viewer.SourceAndConverter; +import graphics.scenery.volumes.Volume; +import kotlin.Pair; +import mpicbg.spim.data.generic.sequence.BasicViewSetup; +import mpicbg.spim.data.registration.ViewRegistration; +import mpicbg.spim.data.registration.ViewRegistrations; +import mpicbg.spim.data.sequence.DefaultVoxelDimensions; +import mpicbg.spim.data.sequence.TimePoint; +import mpicbg.spim.data.sequence.TimePoints; +import mpicbg.spim.data.sequence.ViewId; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.RealType; +import org.joml.Vector3f; +import org.scijava.listeners.Listeners; +import sc.iview.SciView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class MultiResolutionMandelbulb { + + public static void main(String[] args) throws Exception { + // Define max scale level + int maxScale = 6; // Adjust this value to test rendering at different scales + + // Desired grid size at the finest resolution level + final int desiredFinestGridSize = 8; // Define as per your requirement + + // Compute the base grid size + final int baseGridSize = desiredFinestGridSize * (int) Math.pow(2, maxScale - 1); + + // Generate resolutions and corresponding grid sizes + final double[][] resolutions = new double[maxScale][3]; + final int[] gridSizes = new int[maxScale]; + + for (int i = 0; i < maxScale; i++) { + double scaleFactor = Math.pow(2, i); + + // Ensure resolution stays above a minimum value (0.5) to avoid zero scales + resolutions[i][0] = scaleFactor; + resolutions[i][1] = scaleFactor; + resolutions[i][2] = scaleFactor; + + gridSizes[i] = baseGridSize / (int) scaleFactor; + + System.out.println("Grid size for level " + i + ": " + gridSizes[i]); + System.out.println("Resolution for level " + i + ": " + resolutions[i][0] + " / " + resolutions[i][1] + " / " + resolutions[i][2]); + } + + MandelbulbCacheArrayLoader.gridSizes = gridSizes; + MandelbulbCacheArrayLoader.baseGridSize = baseGridSize; + MandelbulbCacheArrayLoader.desiredFinestGridSize = desiredFinestGridSize; + + // Mandelbulb parameters + final int maxIter = 255; + final int order = 8; + + // Create Mandelbulb ImgLoader + MandelbulbImgLoader imgLoader = new MandelbulbImgLoader(gridSizes, maxIter, order); + + // Create a list of TimePoints (assuming single timepoint) + final List timepoints = Collections.singletonList(new TimePoint(0)); + + // Create BasicViewSetup + final BasicViewSetup viewSetup = new BasicViewSetup(0, "setup0", imgLoader.dimensions(), new DefaultVoxelDimensions(3)); + + // Create SequenceDescriptionMinimal + final SequenceDescriptionMinimal seq = new SequenceDescriptionMinimal(new TimePoints(timepoints), Collections.singletonMap(viewSetup.getId(), viewSetup), imgLoader, null); + + // Define voxel size + final double[] voxelSize = {1.0, 1.0, 1.0}; + + // Create ViewRegistrations + final HashMap registrations = new HashMap<>(); + for (final BasicViewSetup setup : seq.getViewSetupsOrdered()) { + final int setupId = setup.getId(); + for (final TimePoint timepoint : seq.getTimePoints().getTimePointsOrdered()) { + final int timepointId = timepoint.getId(); + for (int level = 0; level < resolutions.length; level++) { + AffineTransform3D transform = new AffineTransform3D(); + transform.set( + voxelSize[0] * resolutions[level][0], 0, 0, 0, + 0, voxelSize[1] * resolutions[level][1], 0, 0, + 0, 0, voxelSize[2] * resolutions[level][2], 0); + registrations.put(new ViewId(timepointId, setupId), new ViewRegistration(timepointId, setupId, transform)); + } + } + } + + // Create SpimDataMinimal + final SpimDataMinimal spimData = new SpimDataMinimal(null, seq, new ViewRegistrations(registrations)); + + // Obtain sources and setups + List> sources = getSourceAndConverters(spimData); + ArrayList converterSetups = getConverterSetups(sources); + + // Define voxel dimensions as a float array + float[] voxelDimensions = {1000.0f, 1000.0f, 1000.0f}; + + @SuppressWarnings("unchecked") + List>> typedSources = (List>>) (List) sources; + + // Create and add volume to SciView + SciView sciview = SciView.create(); + sciview.getCamera().spatial().setPosition(new Vector3f(30, 30, 30)); + Volume volume = sciview.addSpimVolume(spimData, "test", voxelDimensions); + volume.setMultiResolutionLevelLimits(new Pair<>(1,4)); + volume.spatial().setScale(new Vector3f(10, 10, 10)); + } + + private static List> getSourceAndConverters(SpimDataMinimal spimData) { + WrapBasicImgLoader.wrapImgLoaderIfNecessary(spimData); + final ArrayList> sources = new ArrayList<>(); + BigDataViewer.initSetups(spimData, new ArrayList<>(), sources); + WrapBasicImgLoader.removeWrapperIfPresent(spimData); + return sources; + } + + private static ArrayList getConverterSetups(List> sources) { + ArrayList converterSetups = new ArrayList<>(); + for (SourceAndConverter source : sources) { + // Placeholder logic for converter setup; customize as needed + converterSetups.add(new ConverterSetup() { + @Override + public void setDisplayRange(double min, double max) { + // Implement this method based on actual display range logic + } + + @Override + public void setColor(ARGBType color) { + } + + @Override + public boolean supportsColor() { + return false; + } + + @Override + public double getDisplayRangeMin() { + return 0; + } + + @Override + public double getDisplayRangeMax() { + return 0; + } + + @Override + public ARGBType getColor() { + return null; + } + + @Override + public Listeners setupChangeListeners() { + return null; + } + + @Override + public int getSetupId() { + return 0; // Implement unique ID retrieval logic + } + }); + } + return converterSetups; + } + +} diff --git a/src/main/java/sc/iview/zebrahub/RemoteZarrLoader.java b/src/main/java/sc/iview/zebrahub/RemoteZarrLoader.java new file mode 100644 index 00000000..da375f69 --- /dev/null +++ b/src/main/java/sc/iview/zebrahub/RemoteZarrLoader.java @@ -0,0 +1,175 @@ +package sc.iview.zebrahub; + +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.blosc.JBlosc; +import org.blosc.PrimitiveSizes; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; + +public class RemoteZarrLoader { + + private final String zarrUrl; + private final Map scaleUrls; + private final int[][] dimensions; // Spatial dimensions for each scale level + private final int[][] chunkSizes; // Chunk sizes for each scale level + + public RemoteZarrLoader(String zarrUrl) throws IOException { + this.zarrUrl = zarrUrl; + this.scaleUrls = fetchScaleUrls(); + this.dimensions = fetchDimensions(); + this.chunkSizes = fetchChunkSizes(); + } + + private Map fetchScaleUrls() throws IOException { + Map scaleUrls = new HashMap<>(); + for (int i = 0; i < 3; i++) { // Assuming 3 scales + scaleUrls.put(i, zarrUrl + "/" + i); + } + return scaleUrls; + } + + private int[][] fetchDimensions() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + int[][] dims = new int[scaleUrls.size()][]; + + for (Map.Entry entry : scaleUrls.entrySet()) { + URL url = new URL(entry.getValue() + "/.zarray"); + JsonNode root = mapper.readTree(openStream(url)); + JsonNode shape = root.get("shape"); + // Extract only the spatial dimensions: z, y, x + dims[entry.getKey()] = new int[]{shape.get(2).asInt(), shape.get(3).asInt(), shape.get(4).asInt()}; + } + return dims; + } + + private int[][] fetchChunkSizes() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + int[][] sizes = new int[scaleUrls.size()][]; + + for (Map.Entry entry : scaleUrls.entrySet()) { + URL url = new URL(entry.getValue() + "/.zarray"); + JsonNode root = mapper.readTree(openStream(url)); + JsonNode chunks = root.get("chunks"); + // Extract only the spatial chunk sizes: z, y, x + sizes[entry.getKey()] = new int[]{chunks.get(2).asInt(), chunks.get(3).asInt(), chunks.get(4).asInt()}; + } + return sizes; + } + + public int[][] getDimensions() { + return dimensions; + } + + public int[][] getChunkSizes() { + return chunkSizes; + } + + public VolatileShortArray loadData(int scale, int[] cellDims, long[] cellMin) throws IOException { + String scaleUrl = scaleUrls.get(scale); + if (scaleUrl == null) throw new IllegalArgumentException("Invalid scale level"); + + // Convert cell dimensions and cell minimums to 5D, with hardcoded timepoint and channel values + long[] chunkCoords = calculateChunkCoordinates(scale, cellMin); + String chunkUrl = constructChunkUrl(scaleUrl, chunkCoords); + byte[] compressedData = fetchChunkData(chunkUrl); + + // Decompress data using JBLosc + byte[] dataBytes = decompressData(compressedData, cellDims); + + int size = cellDims[0] * cellDims[1] * cellDims[2]; + int expectedSize = size * 2; // 2 bytes per short + + if (dataBytes.length != expectedSize) { + throw new IOException("Fetched data size does not match expected size. Expected: " + expectedSize + " but got: " + dataBytes.length); + } + + VolatileShortArray shortArray = new VolatileShortArray(size, true); + short[] data = shortArray.getCurrentStorageArray(); + + ByteBuffer byteBuffer = ByteBuffer.wrap(dataBytes).order(ByteOrder.LITTLE_ENDIAN); // Assuming little-endian + byteBuffer.asShortBuffer().get(data); + + return shortArray; + } + + private long[] calculateChunkCoordinates(int scale, long[] cellMin) { + int[] chunkSizes = this.chunkSizes[scale]; + long[] chunkCoords = new long[5]; + chunkCoords[0] = 0; // Assuming single timepoint + chunkCoords[1] = 0; // Assuming single channel + chunkCoords[2] = cellMin[0] / chunkSizes[0]; + chunkCoords[3] = cellMin[1] / chunkSizes[1]; + chunkCoords[4] = cellMin[2] / chunkSizes[2]; + return chunkCoords; + } + + private String constructChunkUrl(String scaleUrl, long[] chunkCoords) { + // Construct URL based on the chunk coordinates + // This logic depends on the Zarr chunking scheme and dimension separator "/" + StringBuilder url = new StringBuilder(scaleUrl); + for (long coord : chunkCoords) { + url.append("/").append(coord); + } + return url.toString(); + } + + private byte[] fetchChunkData(String chunkUrl) throws IOException { + URL url = new URL(chunkUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestProperty("Accept", "application/octet-stream"); + connection.setDoInput(true); + connection.connect(); + + try (InputStream inputStream = connection.getInputStream(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { + byte[] data = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, bytesRead); + } + return buffer.toByteArray(); + } finally { + connection.disconnect(); + } + } + + private byte[] decompressData(byte[] compressedData, int[] cellDims) throws IOException { + JBlosc jb = new JBlosc(); + try { + int decompressedSize = cellDims[0] * cellDims[1] * cellDims[2] * PrimitiveSizes.SHORT_FIELD_SIZE; // 2 bytes per short + ByteBuffer compressedBuffer = ByteBuffer.allocateDirect(compressedData.length); + compressedBuffer.put(compressedData); + compressedBuffer.flip(); + + ByteBuffer decompressedBuffer = ByteBuffer.allocateDirect(decompressedSize); + + jb.decompress(compressedBuffer, decompressedBuffer, decompressedSize); + jb.destroy(); + + byte[] decompressedData = new byte[decompressedSize]; + decompressedBuffer.get(decompressedData); + return decompressedData; + } catch (Exception e) { + jb.destroy(); + throw new IOException("Failed to decompress data", e); + } + } + + private InputStream openStream(URL url) throws IOException { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestProperty("Accept", "application/json"); + connection.setDoInput(true); + connection.connect(); + return connection.getInputStream(); + } +} diff --git a/src/main/java/sc/iview/zebrahub/ZebrahubImgLoader.java b/src/main/java/sc/iview/zebrahub/ZebrahubImgLoader.java new file mode 100644 index 00000000..563ec21f --- /dev/null +++ b/src/main/java/sc/iview/zebrahub/ZebrahubImgLoader.java @@ -0,0 +1,154 @@ +package sc.iview.zebrahub; + +import bdv.AbstractViewerSetupImgLoader; +import bdv.ViewerImgLoader; +import bdv.cache.CacheControl; +import bdv.img.cache.CacheArrayLoader; +import bdv.img.cache.VolatileCachedCellImg; +import bdv.img.cache.VolatileGlobalCellCache; +import bdv.img.hdf5.MipmapInfo; +import bdv.img.hdf5.ViewLevelId; +import mpicbg.spim.data.generic.sequence.ImgLoaderHint; +import net.imglib2.Dimensions; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.cache.volatiles.CacheHints; +import net.imglib2.cache.volatiles.LoadingStrategy; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.type.volatiles.VolatileUnsignedShortType; +import net.imglib2.util.Intervals; + +import java.io.IOException; +import java.util.HashMap; + +public class ZebrahubImgLoader implements ViewerImgLoader { + private final RemoteZarrLoader zarrLoader; + public int[] gridSizes; + + public MipmapInfo mipmapInfo; + private long[][] mipmapDimensions; + private VolatileGlobalCellCache cache; + private HashMap setupImgLoaders; + + public ZebrahubImgLoader(String zarrUrl) throws IOException { + this.zarrLoader = new RemoteZarrLoader(zarrUrl); + this.setupImgLoaders = new HashMap<>(); + initialize(); + } + + private void initialize() throws IOException { + // Fetch dimensions and chunk sizes from Zarr metadata + int[][] zarrDimensions = zarrLoader.getDimensions(); + int[][] chunkSizes = zarrLoader.getChunkSizes(); + this.gridSizes = new int[zarrDimensions.length]; + + mipmapDimensions = new long[gridSizes.length][]; + final double[][] resolutions = new double[gridSizes.length][]; + final int[][] subdivisions = new int[gridSizes.length][]; + final AffineTransform3D[] transforms = new AffineTransform3D[gridSizes.length]; + + for (int level = 0; level < zarrDimensions.length; level++) { + int[] dim = zarrDimensions[level]; + int[] chunkSize = chunkSizes[level]; + mipmapDimensions[level] = new long[]{dim[0], dim[1], dim[2]}; + gridSizes[level] = Math.max(dim[0], Math.max(dim[1], dim[2])); // Use the maximum dimension for grid size + + resolutions[level] = new double[]{ + Math.max(1.0 / (1 << level), 0.5), + Math.max(1.0 / (1 << level), 0.5), + Math.max(1.0 / (1 << level), 0.5) + }; + + subdivisions[level] = new int[]{chunkSize[0], chunkSize[1], chunkSize[2]}; // Use chunk size for cell dimensions + + transforms[level] = new AffineTransform3D(); + transforms[level].scale(resolutions[level][0], resolutions[level][1], resolutions[level][2]); + } + + mipmapInfo = new MipmapInfo(resolutions, transforms, subdivisions); + cache = new VolatileGlobalCellCache(gridSizes.length, 1); + + for (int setupId = 0; setupId < 1; setupId++) { + setupImgLoaders.put(setupId, new SetupImgLoader(setupId)); + } + } + + protected > VolatileCachedCellImg prepareCachedImage(final ViewLevelId id, final LoadingStrategy loadingStrategy, final T type) { + final int level = id.getLevel(); + final long[] dimensions = mipmapDimensions[level]; + final int[] cellDimensions = mipmapInfo.getSubdivisions()[level]; + final CellGrid grid = new CellGrid(dimensions, cellDimensions); + + final int priority = mipmapInfo.getMaxLevel() - level; + final CacheHints cacheHints = new CacheHints(loadingStrategy, priority, false); + return cache.createImg(grid, id.getTimePointId(), id.getViewSetupId(), level, cacheHints, new CacheArrayLoader() { + @Override + public VolatileShortArray loadArray(int timepoint, int setup, int level, int[] cellDims, long[] cellMin) throws InterruptedException { + try { + return zarrLoader.loadData(level, cellDims, cellMin); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public int getBytesPerElement() { + return 2; // Each element is 2 bytes (16 bits) + } + }, type); + } + + @Override + public CacheControl getCacheControl() { + return cache; + } + + @Override + public SetupImgLoader getSetupImgLoader(final int setupId) { + return setupImgLoaders.get(setupId); + } + + public class SetupImgLoader extends AbstractViewerSetupImgLoader { + private final int setupId; + + protected SetupImgLoader(final int setupId) { + super(new UnsignedShortType(), new VolatileUnsignedShortType()); + this.setupId = setupId; + } + + @Override + public RandomAccessibleInterval getImage(final int timepointId, final int level, final ImgLoaderHint... hints) { + final ViewLevelId id = new ViewLevelId(timepointId, setupId, level); + return prepareCachedImage(id, LoadingStrategy.BLOCKING, new UnsignedShortType()); + } + + @Override + public RandomAccessibleInterval getVolatileImage(final int timepointId, final int level, final ImgLoaderHint... hints) { + final ViewLevelId id = new ViewLevelId(timepointId, setupId, level); + return prepareCachedImage(id, LoadingStrategy.BUDGETED, new VolatileUnsignedShortType()); + } + + @Override + public double[][] getMipmapResolutions() { + return mipmapInfo.getResolutions(); + } + + @Override + public AffineTransform3D[] getMipmapTransforms() { + return mipmapInfo.getTransforms(); + } + + @Override + public int numMipmapLevels() { + return mipmapInfo.getNumLevels(); + } + } + + public Dimensions dimensions() { + // Return dimensions of the highest resolution + return Intervals.createMinSize(mipmapDimensions[0][0], mipmapDimensions[0][1], mipmapDimensions[0][2]); + } +} diff --git a/src/main/kotlin/sc/iview/AnimatedCenteringBeforeArcBallControl.kt b/src/main/kotlin/sc/iview/AnimatedCenteringBeforeArcBallControl.kt index 423e3795..13860d29 100644 --- a/src/main/kotlin/sc/iview/AnimatedCenteringBeforeArcBallControl.kt +++ b/src/main/kotlin/sc/iview/AnimatedCenteringBeforeArcBallControl.kt @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -36,6 +36,9 @@ import graphics.scenery.utils.extensions.times import org.joml.Quaternionf import org.joml.Vector3f import java.util.function.Supplier +import org.joml.Matrix4f +import kotlin.math.max +import kotlin.math.min /* * A wrapping class for the {@ArcballCameraControl} that calls {@link CenterOnPosition()} @@ -46,9 +49,26 @@ import java.util.function.Supplier * @author Vladimir Ulman * @author Ulrik Guenther */ -class AnimatedCenteringBeforeArcBallControl(val initAction: (Int, Int) -> Any, val scrollAction: (Double, Boolean, Int, Int) -> Any, name: String, n: () -> Camera?, w: Int, h: Int, target: () -> Vector3f) : ArcballCameraControl(name, n, w, h, target) { - protected var lastX = -1 - protected var lastY = -1 + + +class AnimatedCenteringBeforeArcBallControl( + val initAction: (Int, Int) -> Any, + val scrollAction: (Double, Boolean, Int, Int) -> Any, + name: String, + n: () -> Camera?, + w: Int, + h: Int, + target: () -> Vector3f +) : ArcballCameraControl(name, n, w, h, target) { + private var lastX = -1 + private var lastY = -1 + private val rotationSpeed = 1.0f + private val dampingFactor = 0.8f + private var spherical = Spherical() + private var sphericalDelta = Spherical() + private var zoomFactor = 0.0f + private val zoomSpeed = 0.2f + private var isInitialClick = true override fun init(x: Int, y: Int) { initAction.invoke(x, y) @@ -58,6 +78,17 @@ class AnimatedCenteringBeforeArcBallControl(val initAction: (Int, Int) -> Any, v cam?.targeted = true cam?.target = target.invoke() + + if (isInitialClick) { + updateSpherical() + isInitialClick = false + } else { + // For subsequent clicks, we'll set a small delta to trigger a smooth update + sphericalDelta.theta = 0.001f + sphericalDelta.phi = 0.001f + } + + update() } override fun drag(x: Int, y: Int) { @@ -66,31 +97,16 @@ class AnimatedCenteringBeforeArcBallControl(val initAction: (Int, Int) -> Any, v return } - val xoffset: Float = (x - lastX).toFloat() * mouseSpeedMultiplier - val yoffset: Float = (lastY - y).toFloat() * mouseSpeedMultiplier + val deltaX = (x - lastX) * rotationSpeed * mouseSpeedMultiplier + val deltaY = (y - lastY) * rotationSpeed * mouseSpeedMultiplier + + sphericalDelta.theta -= (2 * Math.PI * deltaX / cam!!.width).toFloat() + sphericalDelta.phi -= (2 * Math.PI * deltaY / cam!!.height).toFloat() lastX = x lastY = y - val frameYaw = (xoffset) / 180.0f * Math.PI.toFloat() - val framePitch = yoffset / 180.0f * Math.PI.toFloat() * -1f - - // first calculate the total rotation quaternion to be applied to the camera - val yawQ = Quaternionf().rotateXYZ(0.0f, frameYaw, 0.0f).normalize() - val pitchQ = Quaternionf().rotateXYZ(framePitch, 0.0f, 0.0f).normalize() - - node.ifSpatial { - distance = (target.invoke() - position).length() - node.target = target.invoke() - val currentRotation = rotation - - // Rotate pitch first, then yaw to ensure proper axis alignment - rotation = pitchQ.mul(currentRotation).normalize() - rotation = yawQ.mul(rotation).normalize() - - // Update position based on new rotation - position = target.invoke() + node.forward * distance * (-1.0f) - } + update() node.lock.unlock() } @@ -103,15 +119,75 @@ class AnimatedCenteringBeforeArcBallControl(val initAction: (Int, Int) -> Any, v return } - val sign = if (wheelRotation.toFloat() > 0) 1 else -1 + zoomFactor -= wheelRotation.toFloat() * zoomSpeed + update() + } + + private fun updateSpherical() { + cam?.let { camera -> + val offset = camera.spatial().position - target.invoke() + spherical.setFromVector3f(offset) + } + } - distance = (target.invoke() - cam!!.spatial().position).length() - // This is the difference from scenery's scroll: we use a quadratic speed - distance += sign * wheelRotation.toFloat() * wheelRotation.toFloat() * scrollSpeedMultiplier + private fun update() { + spherical.theta += sphericalDelta.theta * dampingFactor + spherical.phi += sphericalDelta.phi * dampingFactor - if (distance >= maximumDistance) distance = maximumDistance - if (distance <= minimumDistance) distance = minimumDistance + spherical.phi = max(0.000001f, min(Math.PI.toFloat() - 0.000001f, spherical.phi)) + spherical.makeSafe() - cam?.let { node -> node.spatialOrNull()?.position = target.invoke() + node.forward * distance * (-1.0f) } + // Apply zoom + spherical.radius *= Math.pow(0.95, zoomFactor.toDouble()).toFloat() + spherical.radius = max(minimumDistance, min(maximumDistance, spherical.radius)) + + val offset = spherical.toVector3f() + val position = target.invoke() + offset + + cam?.let { camera -> + // Smooth transition for position + camera.spatial().position = camera.spatial().position.lerp(position, dampingFactor) + + // Smooth transition for rotation + val targetRotation = calculateRotation(offset) + camera.spatial().rotation = camera.spatial().rotation.slerp(targetRotation, dampingFactor) + } + + sphericalDelta.theta *= (1f - dampingFactor) + sphericalDelta.phi *= (1f - dampingFactor) + zoomFactor *= (1f - dampingFactor) + + cam?.spatialOrNull()?.needsUpdateWorld = true + } + + private fun calculateRotation(offset: Vector3f): Quaternionf { + val targetToCamera = offset.normalize() + val up = Vector3f(0f, 1f, 0f) + val right = up.cross(targetToCamera, Vector3f()) + up.set(targetToCamera).cross(right) + + val rotationMatrix = Matrix4f().setLookAt(targetToCamera, Vector3f(0f), up) + return Quaternionf().setFromNormalized(rotationMatrix) + } + + private class Spherical(var radius: Float = 1f, var phi: Float = 0f, var theta: Float = 0f) { + fun setFromVector3f(v: Vector3f) { + radius = v.length() + phi = Math.acos(v.y.toDouble() / radius).toFloat() + theta = Math.atan2(v.z.toDouble(), v.x.toDouble()).toFloat() + } + + fun makeSafe() { + theta = max(-Math.PI.toFloat(), min(Math.PI.toFloat(), theta)) + } + + fun toVector3f(): Vector3f { + val sinPhiRadius = Math.sin(phi.toDouble()) * radius + return Vector3f( + (sinPhiRadius * Math.sin(theta.toDouble())).toFloat(), + (Math.cos(phi.toDouble()) * radius).toFloat(), + (sinPhiRadius * Math.cos(theta.toDouble())).toFloat() + ) + } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/sc/iview/SciView.kt b/src/main/kotlin/sc/iview/SciView.kt index 5e7d9257..bf9495ee 100644 --- a/src/main/kotlin/sc/iview/SciView.kt +++ b/src/main/kotlin/sc/iview/SciView.kt @@ -30,6 +30,7 @@ package sc.iview import bdv.BigDataViewer import bdv.cache.CacheControl +import bdv.spimdata.SpimDataMinimal import bdv.tools.brightness.ConverterSetup import bdv.util.AxisOrder import bdv.util.RandomAccessibleIntervalSource @@ -86,7 +87,6 @@ import net.imglib2.view.Views import org.joml.Quaternionf import org.joml.Vector3f import org.scijava.Context -import org.scijava.`object`.ObjectService import org.scijava.display.Display import org.scijava.event.EventHandler import org.scijava.event.EventService @@ -94,6 +94,7 @@ import org.scijava.io.IOService import org.scijava.log.LogLevel import org.scijava.log.LogService import org.scijava.menu.MenuService +import org.scijava.`object`.ObjectService import org.scijava.plugin.Parameter import org.scijava.service.SciJavaService import org.scijava.thread.ThreadService @@ -122,7 +123,6 @@ import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.* import java.util.concurrent.Future -import java.util.concurrent.TimeUnit import java.util.function.Consumer import java.util.function.Function import java.util.function.Predicate @@ -135,6 +135,7 @@ import javax.swing.JOptionPane import kotlin.math.cos import kotlin.math.sin + /** * Main SciView class. * @@ -1503,6 +1504,65 @@ class SciView : SceneryBase, CalibratedRealInterval { return v } + fun > addSpimVolume( + sources: List>>, + converterSetups: ArrayList, + numTimepoints: Int, + name: String, + voxelDimensions: FloatArray + ): Volume? { + // Cast sources to match the expected type for addVolume + @Suppress("UNCHECKED_CAST") + val typedSources = sources as List> + + // Call addVolume with the casted list + return addVolume( + typedSources, + converterSetups, + numTimepoints, + name, + voxelDimensions + ) + } + + fun addSpimVolume( + spimData: SpimDataMinimal, + name: String, + voxelDimensions: FloatArray + ): Volume? { + + val block: Volume.() -> Unit = {} + + val colormapName = "Fire.lut" + + // Create the volume using the companion object's fromSpimData method + val volume = Volume.fromSpimData(spimData, hub, VolumeViewerOptions()) + + // Set properties + volume.name = name + volume.metadata["VoxelDimensions"] = voxelDimensions + volume.spatial().scale = Vector3f(voxelDimensions[0], voxelDimensions[1], voxelDimensions[2]) * volume.pixelToWorldRatio + + // Configure the transfer function + val tf = volume.transferFunction + val rampMin = 0f + val rampMax = 0.1f + tf.clear() + tf.addControlPoint(0.0f, 0.0f) + tf.addControlPoint(rampMin, 0.0f) + tf.addControlPoint(1.0f, rampMax) + + // Set default colormap + volume.metadata["sciview.colormapName"] = colormapName + volume.colormap = Colormap.fromColorTable(getLUT(colormapName)) + + // Add the volume node + return addNode(volume, block = block) + } + + + + /** * Adds a SourceAndConverter to the scene. * @@ -1995,7 +2055,9 @@ class SciView : SceneryBase, CalibratedRealInterval { .redirectError(ProcessBuilder.Redirect.PIPE) .start() - proc.waitFor(60, TimeUnit.MINUTES) + //proc.waitFor(60, TimeUnit()) + Thread.sleep(60) + return proc.inputStream.bufferedReader().readText() } catch(e: IOException) { e.printStackTrace() diff --git a/src/main/kotlin/sc/iview/commands/demo/advanced/LoadMandelbulbOutOfCore.kt b/src/main/kotlin/sc/iview/commands/demo/advanced/LoadMandelbulbOutOfCore.kt new file mode 100644 index 00000000..5bbd15c0 --- /dev/null +++ b/src/main/kotlin/sc/iview/commands/demo/advanced/LoadMandelbulbOutOfCore.kt @@ -0,0 +1,142 @@ +/*- + * #%L + * Scenery-backed 3D visualization package for ImageJ. + * %% + * Copyright (C) 2016 - 2024 sciview developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package sc.iview.commands.demo.advanced + +import bdv.util.volatiles.VolatileViews +import graphics.scenery.PointLight +import graphics.scenery.volumes.TransferFunction +import graphics.scenery.volumes.Volume +import net.imglib2.RandomAccessibleInterval +import net.imglib2.type.volatiles.VolatileUnsignedShortType +import net.imglib2.view.Views +import org.joml.Vector3f +import org.scijava.command.Command +import org.scijava.command.CommandService +import org.scijava.log.LogService +import org.scijava.plugin.Menu +import org.scijava.plugin.Parameter +import org.scijava.plugin.Plugin +import sc.iview.SciView +import sc.iview.commands.MenuWeights +import sc.iview.mandelbulb.MandelbulbCacheArrayLoader +import sc.iview.mandelbulb.MandelbulbImgLoader + +@Plugin(type = Command::class, + label = "Load Mandelbulb Out-of-Core demo", + menuRoot = "SciView", + menu = [Menu(label = "Demo", weight = MenuWeights.DEMO), + Menu(label = "Advanced", weight = MenuWeights.DEMO_ADVANCED), + Menu(label = "Load Mandelbulb Out-of-Core demo", weight = MenuWeights.DEMO_ADVANCED)]) +class LoadMandelbulbOutOfCore : Command { + @Parameter + private lateinit var sciview: SciView + + @Parameter + private lateinit var log: LogService + + override fun run() { + val maxIter = 32 // Example maximum iterations + val order = 8 // Example Mandelbulb order + + // Define max scale level + val maxScale = 8 // Adjust this value to test rendering at different scales + + // Desired grid size at the finest resolution level + val desiredFinestGridSize = 8 + + // Compute the base grid size + val baseGridSize = desiredFinestGridSize * Math.pow(2.0, (maxScale - 1).toDouble()).toInt() + + // Generate resolutions and corresponding grid sizes + val resolutions = Array(maxScale) { DoubleArray(3) } + val gridSizes = IntArray(maxScale) + + for (i in 0 until maxScale) { + val scaleFactor = Math.pow(2.0, i.toDouble()) + + // Ensure resolution stays above a minimum value (0.5) to avoid zero scales + resolutions[i][0] = 1.0 / scaleFactor + resolutions[i][1] = 1.0 / scaleFactor + resolutions[i][2] = 1.0 / scaleFactor + gridSizes[i] = baseGridSize / scaleFactor.toInt() + println("Grid size for level " + i + ": " + gridSizes[i]) + println("Resolution for level " + i + ": " + resolutions[i][0] + " / " + resolutions[i][1] + " / " + resolutions[i][2]) + } + + MandelbulbCacheArrayLoader.gridSizes = gridSizes + MandelbulbCacheArrayLoader.baseGridSize = baseGridSize + MandelbulbCacheArrayLoader.desiredFinestGridSize = desiredFinestGridSize + + log.info("Generating mandelbulb") + + val imgLoader = MandelbulbImgLoader(gridSizes, maxIter, order) + + // val img = imgLoader.getSetupImgLoader(0).getImage(0, maxScale - 1) + val img = imgLoader.getSetupImgLoader(0).getVolatileImage(0, 0) + + log.info("Generated") + + // val wrapped = VolatileViews.wrapAsVolatile(img) + // val wrapped = VolatileViews.wrapAsVolatile(Views.extendZero(img)) + val wrapped = img + + @Suppress("UNCHECKED_CAST") + val volume = Volume.fromRAI( + wrapped as RandomAccessibleInterval, + VolatileUnsignedShortType(), + name ="Mandelbulb OOC", + hub = sciview.hub + ) + volume.converterSetups.first().setDisplayRange(25.0, 512.0) + volume.transferFunction = TransferFunction.ramp(0.01f, 0.03f) + volume.spatial().scale = Vector3f(20.0f) + sciview.addNode(volume) + + val lights = (0 until 3).map { + PointLight(radius = 15.0f) + } + + lights.mapIndexed { i, light -> + light.spatial().position = Vector3f(2.0f * i - 4.0f, i - 1.0f, 0.0f) + light.emissionColor = Vector3f(1.0f, 1.0f, 1.0f) + light.intensity = 50.0f + sciview.addNode(light) + } + } + + companion object { + @JvmStatic + fun main(args: Array) { + val sv = SciView.create() + val command = sv.scijavaContext!!.getService(CommandService::class.java) + val argmap = HashMap() + command.run(LoadMandelbulbOutOfCore::class.java, true, argmap) + } + } +} diff --git a/src/main/kotlin/sc/iview/commands/demo/advanced/LoadN5OutOfCore.kt b/src/main/kotlin/sc/iview/commands/demo/advanced/LoadN5OutOfCore.kt new file mode 100644 index 00000000..42ff567b --- /dev/null +++ b/src/main/kotlin/sc/iview/commands/demo/advanced/LoadN5OutOfCore.kt @@ -0,0 +1,89 @@ +package sc.iview.commands.demo.advanced + +import bdv.util.volatiles.VolatileViews +import graphics.scenery.BoundingGrid +import graphics.scenery.PointLight +import graphics.scenery.volumes.TransferFunction +import graphics.scenery.volumes.Volume +import net.imglib2.RandomAccessibleInterval +import net.imglib2.type.volatiles.VolatileUnsignedShortType +import net.imglib2.type.numeric.integer.UnsignedShortType +import org.janelia.saalfeldlab.n5.N5FSReader +import org.janelia.saalfeldlab.n5.imglib2.N5Utils +import org.joml.Vector3f +import org.scijava.command.Command +import org.scijava.command.CommandService +import org.scijava.log.LogService +import org.scijava.plugin.Menu +import org.scijava.plugin.Parameter +import org.scijava.plugin.Plugin +import sc.iview.SciView +import sc.iview.commands.MenuWeights + +@Plugin(type = Command::class, + label = "Load N5 Out-of-Core demo", + menuRoot = "SciView", + menu = [Menu(label = "Demo", weight = MenuWeights.DEMO), + Menu(label = "Advanced", weight = MenuWeights.DEMO_ADVANCED), + Menu(label = "Load Mandelbulb Out-of-Core demo", weight = MenuWeights.DEMO_ADVANCED)]) +class LoadN5OutOfCore : Command { + @Parameter + private lateinit var sciview: SciView + + @Parameter + private lateinit var log: LogService + + override fun run() { + // Load data from N5 + val n5path = "/Users/kharrington/Library/CloudStorage/Dropbox/sciview_data/data/test_n5_error/input-multiscale.n5" + val datasetName = "setup0/timepoint0/s0" + + log.info("Loading N5 data from $n5path with dataset $datasetName") + + val ooc: RandomAccessibleInterval = N5Utils.openVolatile(N5FSReader(n5path), datasetName) + val wrapped = VolatileViews.wrapAsVolatile(ooc) + + // When loading datasets with multiple resolution levels, it's important to use Volatile types + // here, such as VolatileUnsignedShortType, otherwise loading volume blocks will not work correctly. + @Suppress("UNCHECKED_CAST") + val volume = Volume.fromRAI( + wrapped as RandomAccessibleInterval, + VolatileUnsignedShortType(), + name = "N5 OOC", + hub = sciview.hub + ) + volume.converterSetups.first().setDisplayRange(25.0, 512.0) + volume.transferFunction = TransferFunction.ramp(0.01f, 0.03f) + volume.spatial().scale = Vector3f(20.0f) + sciview.addNode(volume) + + log.info("Volume added to the scene") + + val lights = (0 until 3).map { + PointLight(radius = 15.0f) + } + + lights.mapIndexed { i, light -> + light.spatial().position = Vector3f(2.0f * i - 4.0f, i - 1.0f, 0.0f) + light.emissionColor = Vector3f(1.0f, 1.0f, 1.0f) + light.intensity = 50.0f + sciview.addNode(light) + } + + val newBg = BoundingGrid() + newBg.node = volume + sciview.publishNode(newBg) + + log.info("Lights added to the scene") + } + + companion object { + @JvmStatic + fun main(args: Array) { + val sv = SciView.create() + val command = sv.scijavaContext!!.getService(CommandService::class.java) + val argmap = HashMap() + command.run(LoadN5OutOfCore::class.java, true, argmap) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/sc/iview/commands/demo/advanced/LoadZebrahubSingleScale.kt b/src/main/kotlin/sc/iview/commands/demo/advanced/LoadZebrahubSingleScale.kt new file mode 100644 index 00000000..6be43505 --- /dev/null +++ b/src/main/kotlin/sc/iview/commands/demo/advanced/LoadZebrahubSingleScale.kt @@ -0,0 +1,101 @@ +package sc.iview.commands.demo.advanced + +import graphics.scenery.* +import graphics.scenery.volumes.TransferFunction +import graphics.scenery.volumes.Volume +import net.imglib2.RandomAccessibleInterval +import net.imglib2.converter.Converter +import net.imglib2.realtransform.AffineTransform3D +import net.imglib2.type.numeric.integer.UnsignedShortType +import net.imglib2.util.Intervals +import net.imglib2.view.Views +import org.joml.Vector3f +import org.scijava.command.Command +import org.scijava.command.CommandService +import org.scijava.log.LogService +import org.scijava.plugin.Menu +import org.scijava.plugin.Parameter +import org.scijava.plugin.Plugin +import sc.iview.SciView +import sc.iview.commands.MenuWeights +import sc.iview.zebrahub.ZebrahubImgLoader + +@Plugin(type = Command::class, + label = "Load Zebrahub Single Scale", + menuRoot = "SciView", + menu = [Menu(label = "Demo", weight = MenuWeights.DEMO), + Menu(label = "Advanced", weight = MenuWeights.DEMO_ADVANCED), + Menu(label = "Load Zebrahub Single Scale", weight = MenuWeights.DEMO_ADVANCED_FLYBRAIN)]) +class LoadZebrahubSingleScale : Command { + @Parameter + private lateinit var sciview: SciView + + @Parameter + private lateinit var log: LogService + + override fun run() { + log.info("Loading Zarr dataset...") + + // URL of the remote Zarr dataset + val zarrUrl = "https://public.czbiohub.org/royerlab/zebrahub/imaging/single-objective/ZSNS001.ome.zarr/" + + // Create Zarr ImgLoader + val imgLoader = ZebrahubImgLoader(zarrUrl) + + // Fetch a specific chunk by specifying a region + val chunkMin = longArrayOf(100, 250, 250) // Minimum coordinates of the chunk + // val chunkMax = longArrayOf(99, 99, 99) // Maximum coordinates of the chunk (example values) + val chunkMax = longArrayOf(299, 1750, 1750) // Maximum coordinates of the chunk (example values) + + // s: 448, 2174, 2423 + + // Load the dataset from the first scale and crop it to the specific chunk + val fullDataset: RandomAccessibleInterval = imgLoader.getSetupImgLoader(0) + .getImage(250, 0) // Single timepoint (0) and level (0) + + // Crop the dataset to the desired chunk + val croppedDataset = Views.interval(fullDataset, chunkMin, chunkMax) + // val croppedDataset = fullDataset + + + println("Pixel check: " + croppedDataset.getAt(50, 50, 50).get()) + + // Display cropped dataset + val dimensions = Intervals.dimensionsAsLongArray(croppedDataset) + log.info("Cropped dataset dimensions: ${dimensions.joinToString(", ")}") + + // Apply any desired transformations (e.g., scaling) + val transform = AffineTransform3D() + val scaleFactor = 40.0 + transform.scale(scaleFactor, scaleFactor, scaleFactor) + + // Create the volume node + val volume = Volume.fromRAI( + croppedDataset, + UnsignedShortType(), // Use UnsignedShortType directly + name = "Zarr Dataset (Cropped)", + hub = sciview.hub + ) + + // Apply the transfer function to the volume + volume.transferFunction = TransferFunction.ramp(0.001f, 0.4f) + volume.converterSetups.first().setDisplayRange(0.0, 65535.0) // Adjust range to 16-bit + volume.spatial().position = Vector3f(0f, 0f, 0.0f) + volume.spatial().scale = Vector3f(scaleFactor.toFloat(), scaleFactor.toFloat(), scaleFactor.toFloat()) + + // Add the volume to the scene + sciview.addNode(volume) + + log.info("Zarr dataset loaded and cropped successfully.") + } + + companion object { + @JvmStatic + fun main(args: Array) { + val sv = SciView.create() + val command = sv.scijavaContext!!.getService(CommandService::class.java) + val argmap = HashMap() + command.run(LoadZebrahubSingleScale::class.java, true, argmap) + } + } +}