diff --git a/pom.xml b/pom.xml index 9befd9e..9086898 100644 --- a/pom.xml +++ b/pom.xml @@ -85,31 +85,42 @@ net.imagej ij + compile net.imagej imagej-common + compile org.jogamp.jocl jocl-main - 2.3.2 + 2.5.0 + compile org.jogamp.gluegen gluegen-rt-main - 2.3.2 + 2.5.0 + compile org.apache.commons commons-math3 3.6.1 + compile org.junit.jupiter junit-jupiter-api - 5.10.0 - test + 5.10.0 + compile + + + org.junit.jupiter + junit-jupiter-engine + 5.10.0 + compile diff --git a/src/main/java/BlockRedundancy2DT_.java b/src/main/java/BlockRedundancy2DT_.java deleted file mode 100644 index 525f276..0000000 --- a/src/main/java/BlockRedundancy2DT_.java +++ /dev/null @@ -1,844 +0,0 @@ -/** - * - * This class calculates block repetition maps for 2D+T data. - * - * @author Afonso Mendes - * - **/ - -import com.jogamp.opencl.*; -import ij.*; -import ij.gui.NonBlockingGenericDialog; -import ij.measure.Calibration; -import ij.plugin.PlugIn; -import ij.process.FloatProcessor; -import ij.process.ImageProcessor; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.FloatBuffer; - -import static com.jogamp.opencl.CLMemory.Mem.READ_ONLY; -import static com.jogamp.opencl.CLMemory.Mem.READ_WRITE; -import static ij.WindowManager.getIDList; -import static ij.WindowManager.getImageCount; -import static java.lang.Math.*; - - -public class BlockRedundancy2DT_ implements PlugIn { - - // ------------------------ // - // ---- OpenCL formats ---- // - // ------------------------ // - - static private CLContext context; - - static private CLProgram programGetPatchMeans, programGetPatchPearson, programGetRelevanceMap; - - static private CLKernel kernelGetPatchMeans, kernelGetPatchPearson, kernelGetRelevanceMap; - - static private CLPlatform clPlatformMaxFlop; - - static private CLCommandQueue queue; - - private CLBuffer clPatchPixels, clRefPixels, clLocalMeans, clLocalStds, clPearsonMap, clRelevanceMap; - - @Override - public void run(String s) { - - float EPSILON = 0.0000001f; - - // -------------------- // - // ---- Dialog box ---- // - // -------------------- // - - // Get all open image titles - int nImages = getImageCount(); - if (nImages < 2) { - IJ.error("Not enough images found. You need at least two."); - return; - } - - int[] ids = getIDList(); - String[] titles = new String[nImages]; - for(int i=0; i imgMax) { - imgMax = pixelValue; - } - } - // Remap pixels - for (int i=0; i nFlops) { - nFlops = clDevice.getMaxComputeUnits() * clDevice.getMaxClockFrequency(); - clPlatformMaxFlop = allPlatform; - } - } - } - IJ.log("--------"); - - // Create context - context = CLContext.create(clPlatformMaxFlop); - - // Choose the best device (i.e., Filter out CPUs if GPUs are available (FLOPS calculation was giving higher ratings to CPU vs. GPU)) - //TODO: Rate devices on Mandelbrot and chooose the best, GPU will perform better - CLDevice[] allDevices = context.getDevices(); - - boolean hasGPU = false; - for (int i = 0; i < allDevices.length; i++) { - if (allDevices[i].getType() == CLDevice.Type.GPU) { - hasGPU = true; - } - } - CLDevice chosenDevice; - if (hasGPU) { - chosenDevice = context.getMaxFlopsDevice(CLDevice.Type.GPU); - } else { - chosenDevice = context.getMaxFlopsDevice(); - } - - // Get chosen device from preferences - if(useDevice){ - String deviceName = Prefs.get("SReD.OpenCL.device", null); - for (CLDevice device : allDevices) { - if (device.getName().equals(deviceName)) { - chosenDevice = device; - break; - } - } - } - - IJ.log("Chosen device: " + chosenDevice.getName()); - IJ.log("--------"); - - // Create command queue - queue = chosenDevice.createCommandQueue(); - int elementCount = wh; - int localWorkSize = min(chosenDevice.getMaxWorkGroupSize(), 256); - int globalWorkSize = roundUp(localWorkSize, elementCount); - - - // -------------------------------------------------------- // - // ---- Calculate repetition for each frame separately ---- // - // -------------------------------------------------------- // - - // Create stack to store final results - ImageStack imsFinal = new ImageStack(w, h, nFrames); - - // Process frames - for(int f=0; f0.0f) { - IJ.showStatus("Calculating relevance map... Frame " + (f+1) + "/" + nFrames); - IJ.showProgress(f, nFrames); - - // Create OpenCL program - String programStringGetRelevanceMap = getResourceAsString(BlockRedundancy2DT_.class, "kernelGetRelevanceMask2D.cl"); - programStringGetRelevanceMap = replaceFirst(programStringGetRelevanceMap, "$WIDTH$", "" + w); - programStringGetRelevanceMap = replaceFirst(programStringGetRelevanceMap, "$HEIGHT$", "" + h); - programStringGetRelevanceMap = replaceFirst(programStringGetRelevanceMap, "$PATCH_SIZE$", "" + patchSize); - programStringGetRelevanceMap = replaceFirst(programStringGetRelevanceMap, "$BRW$", "" + bRW); - programStringGetRelevanceMap = replaceFirst(programStringGetRelevanceMap, "$BRH$", "" + bRH); - programStringGetRelevanceMap = replaceFirst(programStringGetRelevanceMap, "$EPSILON$", "" + EPSILON); - programGetRelevanceMap = context.createProgram(programStringGetRelevanceMap).build(); - - // Create and fill buffers - float[] relevanceMap = new float[wh]; - clRelevanceMap = context.createFloatBuffer(wh, READ_WRITE); - fillBufferWithFloatArray(clRelevanceMap, relevanceMap); - queue.putWriteBuffer(clRelevanceMap, true); - queue.finish(); - - // Create OpenCL kernel and set args - kernelGetRelevanceMap = programGetRelevanceMap.createCLKernel("kernelGetRelevanceMask2D"); - - argn = 0; - kernelGetRelevanceMap.setArg(argn++, clRefPixels); - kernelGetRelevanceMap.setArg(argn++, clRelevanceMap); - - // Calculate - queue.put2DRangeKernel(kernelGetRelevanceMap, 0, 0, w, h, 0, 0); - queue.finish(); - - // Read the relevance mask back from the device - queue.putReadBuffer(clRelevanceMap, true); - for (int y=0; y noiseMeanVar * filterConstant) { - float pixelValue = pearsonMap[j * w + i]; - if (pixelValue > pearsonMax) { - pearsonMax = pixelValue; - } - if (pixelValue < pearsonMin) { - pearsonMin = pixelValue; - } - } - } - } - - // Remap pixels - for (int j = bRH; j < h - bRH; j++) { - for (int i = bRW; i < w - bRW; i++) { - if (relevanceMap[j * w + i] > noiseMeanVar * filterConstant) { - pearsonMap[j * w + i] = (pearsonMap[j * w + i] - pearsonMin) / (pearsonMax - pearsonMin); - } - } - } - } - - }else if(normalizeOutput){ - - // -------------------------- // - // ---- Normalize output ---- // - // -------------------------- // - - // Find min and max - float pearsonMin = Float.MAX_VALUE; - float pearsonMax = -Float.MAX_VALUE; - - for (int j=bRH; j pearsonMax) { - pearsonMax = pixelValue; - } - if (pixelValue < pearsonMin) { - pearsonMin = pixelValue; - } - } - } - - // Remap pixels - for (int j=bRH; j clBuffer, float[] pixels) { - FloatBuffer buffer = clBuffer.getBuffer(); - for(int n=0; n minMax[1]){ - minMax[1] = inputArray[j*w+i]; - } - } - } - return minMax; - } - - public static float[] normalize(float[] rawPixels, int w, int h, int offsetX, int offsetY, float[] minMax, float tMin, float tMax){ - float rMin = minMax[0]; - float rMax = minMax[1]; - float denominator = rMax - rMin + 0.000001f; - float factor; - - if(tMax == 0 && tMin == 0){ - factor = 1; // So that the users can say they don't want an artificial range by choosing tMax and tMin = 0 - }else { - factor = tMax - tMin; - } - float[] normalizedPixels = new float[w*h]; - - for(int j=offsetY; j clRefPixels, clLocalMeans, clLocalStds, clPatchPixels, clCosineSimMap, clDiffStdMap, clPearsonMap, - clHuMap, clSsimMap, clRelevanceMap, clRmseMap; - - @Override - public void run(String s) { - - float EPSILON = 0.0000001f; - - // Install SReD LUT - installLUT.run(); - - // -------------------- // - // ---- Dialog box ---- // - // -------------------- // - - // Get all open image titles - int nImages = getImageCount(); - if (nImages < 2) { - IJ.error("Not enough images found. You need at least two."); - return; - } - - int[] ids = getIDList(); - String[] titles = new String[nImages]; - for (int i = 0; i < nImages; i++) { - titles[i] = WindowManager.getImage(ids[i]).getTitle(); - } - - // Define metric possibilities - String[] metrics = new String[4]; - metrics[0] = "Pearson's R"; - metrics[1] = "Cosine similarity"; - metrics[2] = "SSIM"; - metrics[3] = "NRMSE (inverted)"; - - // Initialize dialog box - NonBlockingGenericDialog gd = new NonBlockingGenericDialog("SReD: Block Repetition (2D)"); - gd.addChoice("Block:", titles, titles[1]); - gd.addChoice("Image:", titles, titles[0]); - gd.addSlider("Relevance constant: ", 0.0f, 5.0f, 0.0f, 0.1f); - gd.addChoice("Metric:", metrics, metrics[0]); - gd.addCheckbox("Normalize output?", true); - gd.addCheckbox("Use device from preferences?", false); - - gd.showDialog(); - if (gd.wasCanceled()) return; - - // Get parameters from dialog box - String patchTitle = gd.getNextChoice(); - int patchID = 0; - for (int i = 0; i < nImages; i++) { - if (titles[i].equals(patchTitle)) { // .equals() instead of "==" required to run from macro - patchID = ids[i]; - } - } - - String imgTitle = gd.getNextChoice(); - int imgID = 0; - for (int i = 0; i < nImages; i++) { - if (titles[i].equals(imgTitle)) { // .equals() instead of "==" required to run from macro - imgID = ids[i]; - } - } - - float filterConstant = (float) gd.getNextNumber(); - - String metric = gd.getNextChoice(); - - boolean normalizeOutput = gd.getNextBoolean(); - - boolean useDevice = gd.getNextBoolean(); - - - // --------------------- // - // ---- Start timer ---- // - // --------------------- // - - IJ.log("SReD has started, please wait."); - long start = System.currentTimeMillis(); - - - // ------------------------------------------------- // - // ---- Get reference patch and some parameters ---- // - // ------------------------------------------------- // - ImagePlus imp = WindowManager.getImage(patchID); - if (imp == null) { - IJ.error("Block image not found. Try again."); - return; - } - ImageProcessor ip = imp.getProcessor(); - FloatProcessor fp = ip.convertToFloatProcessor(); - float[] patchPixels = (float[]) fp.getPixels(); - int bW = fp.getWidth(); // Patch width - int bH = fp.getHeight(); // Patch height - - // Check if patch dimensions are odd, otherwise kill program - if (bW % 2 == 0 || bH % 2 == 0) { - IJ.error("Block dimensions must be odd (e.g., 3x3 or 5x5). Please try again."); - return; - } - - // Calculate block radius - int bRW = bW / 2; // Patch radius (x-axis) - int bRH = bH / 2; // Patch radius (y-axis) - - - // ------------------------------------------------- // - // ---- Get reference image and some parameters ---- // - // ------------------------------------------------- // - - ImagePlus imp0 = WindowManager.getImage(imgID); - if (imp0 == null) { - IJ.error("Image not found. Try again."); - return; - } - ImageProcessor ip0 = imp0.getProcessor(); - FloatProcessor fp0 = ip0.convertToFloatProcessor(); - float[] refPixels = (float[]) fp0.getPixels(); - int w = fp0.getWidth(); - int h = fp0.getHeight(); - int wh = w * h; - //int sizeWithoutBorders = (w-bRW*2)*(h-bRH*2); // The area of the search field (= image without borders) - - - // ---------------------------------- // - // ---- Stabilize noise variance ---- // - // ---------------------------------- // - - // Patch - //IJ.log("Stabilising noise variance of the patch..."); - //GATMinimizer2D minimizer = new GATMinimizer2D(patchPixels, bW, bH, 0, 100, 0); - //minimizer.run(); - //patchPixels = VarianceStabilisingTransform2D_.getGAT(patchPixels, minimizer.gain, minimizer.sigma, minimizer.offset); - //IJ.log("Done."); - - // Image - IJ.log("Stabilising noise variance of the image..."); - GATMinimizer2D minimizer = new GATMinimizer2D(refPixels, w, h, 0, 100, 0); - minimizer.run(); - refPixels = VarianceStabilisingTransform2D_.getGAT(refPixels, minimizer.gain, minimizer.sigma, minimizer.offset); - IJ.log("Done."); - - - // --------------------------------- // - // ---- Process reference block ---- // - // --------------------------------- // - - // Get final block size (after removing pixels outside inbound circle/ellipse) - int patchSize = 0; - for (int j = 0; j < bH; j++) { - for (int i = 0; i < bW; i++) { - float dx = (float) (i - bRW); - float dy = (float) (j - bRH); - if (((dx * dx) / (float) (bRW * bRW)) + ((dy * dy) / (float) (bRH * bRH)) <= 1.0f) { - patchSize++; - } - } - } - - // Convert patch to "double" type (keeping only the pixels within the inbound circle/ellipse) - double[] patchPixelsDouble = new double[patchSize]; - int index = 0; - for (int j = 0; j < bH; j++) { - for (int i = 0; i < bW; i++) { - float dx = (float) (i - bRW); - float dy = (float) (j - bRH); - if (((dx * dx) / (float) (bRW * bRW)) + ((dy * dy) / (float) (bRH * bRH)) <= 1.0f) { - patchPixelsDouble[index] = (double) patchPixels[j * bW + i]; - index++; - } - } - } - - // Find min and max - double patchMin = Double.MAX_VALUE; // Initialize as a very large number - double patchMax = -Double.MAX_VALUE; // Initialize as a very small number - - for (int i = 0; i < patchSize; i++) { - patchMin = min(patchMin, patchPixelsDouble[i]); - patchMax = max(patchMax, patchPixelsDouble[i]); - } - - // Normalize and calculate mean - double patchMean = 0.0; - for (int i = 0; i < patchSize; i++) { - patchPixelsDouble[i] = (patchPixelsDouble[i] - patchMin) / (patchMax - patchMin + EPSILON); - patchMean += patchPixelsDouble[i]; - - } - patchMean /= (double) patchSize; - - // Subtract mean - for (int i = 0; i < patchSize; i++) { - patchPixelsDouble[i] = patchPixelsDouble[i] - patchMean; - } - - // Typecast back to float - float[] patchPixelsFloat = new float[patchSize]; - for (int i = 0; i < patchSize; i++) { - patchPixelsFloat[i] = (float) patchPixelsDouble[i]; - } - - // Calculate standard deviation - float patchMeanFloat = (float) patchMean; - double patchStdDev = 0.0; - for (int i = 0; i < patchSize; i++) { - patchStdDev += (patchPixelsDouble[i] - patchMean) * (patchPixelsDouble[i] - patchMean); - } - patchStdDev = (float) sqrt(patchStdDev / (patchSize - 1)); - - - // ----------------------- // - // ---- Process image ---- // - // ----------------------- // - - // Cast to "double" type - double[] refPixelsDouble = new double[wh]; - for (int i = 0; i < wh; i++) { - refPixelsDouble[i] = (double) refPixels[i]; - } - - // Get min and max - double imgMin = Double.MAX_VALUE; - double imgMax = -Double.MAX_VALUE; - for (int i = 0; i < w * h; i++) { - double pixelValue = refPixelsDouble[i]; - imgMin = min(imgMin, pixelValue); - imgMax = max(imgMax, pixelValue); - } - - // Normalize - for (int i = 0; i < wh; i++) { - refPixelsDouble[i] = (refPixelsDouble[i] - imgMin) / (imgMax - imgMin + EPSILON); - } - - // Cast back to float - for (int i = 0; i < wh; i++) { - refPixels[i] = (float) refPixelsDouble[i]; - } - - - // --------------------------- // - // ---- Initialize OpenCL ---- // - // --------------------------- // - - // Check OpenCL devices - CLPlatform[] allPlatforms = CLPlatform.listCLPlatforms(); - - try { - allPlatforms = CLPlatform.listCLPlatforms(); - } catch (CLException ex) { - IJ.log("Something went wrong while initialising OpenCL."); - throw new RuntimeException("Something went wrong while initialising OpenCL."); - } - - double nFlops = 0; - - for (CLPlatform allPlatform : allPlatforms) { - CLDevice[] allCLdeviceOnThisPlatform = allPlatform.listCLDevices(); - - for (CLDevice clDevice : allCLdeviceOnThisPlatform) { - //IJ.log("--------"); - //IJ.log("Device name: " + clDevice.getName()); - //IJ.log("Device type: " + clDevice.getType()); - //IJ.log("Max clock: " + clDevice.getMaxClockFrequency() + " MHz"); - //IJ.log("Number of compute units: " + clDevice.getMaxComputeUnits()); - //IJ.log("Max work group size: " + clDevice.getMaxWorkGroupSize()); - if (clDevice.getMaxComputeUnits() * clDevice.getMaxClockFrequency() > nFlops) { - nFlops = clDevice.getMaxComputeUnits() * clDevice.getMaxClockFrequency(); - clPlatformMaxFlop = allPlatform; - } - } - } - IJ.log("--------"); - - // Create OpenCL context - context = CLContext.create(clPlatformMaxFlop); - - // Choose the best device (i.e., Filter out CPUs if GPUs are available (FLOPS calculation was giving - // higher ratings to CPU vs. GPU)) - //TODO: Rate devices on Mandelbrot and chooose the best, GPU will perform better - CLDevice[] allDevices = context.getDevices(); - - boolean hasGPU = false; - for (int i = 0; i < allDevices.length; i++) { - if (allDevices[i].getType() == CLDevice.Type.GPU) { - hasGPU = true; - } - } - CLDevice chosenDevice; - if (hasGPU) { - chosenDevice = context.getMaxFlopsDevice(CLDevice.Type.GPU); - } else { - chosenDevice = context.getMaxFlopsDevice(); - } - - // Get chosen device from preferences - if (useDevice) { - String deviceName = Prefs.get("SReD.OpenCL.device", null); - for (CLDevice device : allDevices) { - if (device.getName().equals(deviceName)) { - chosenDevice = device; - break; - } - } - } - - IJ.log("Chosen device: " + chosenDevice.getName()); - IJ.log("--------"); - - // Create command queue - queue = chosenDevice.createCommandQueue(); - int elementCount = w * h; - int localWorkSize = min(chosenDevice.getMaxWorkGroupSize(), 256); - int globalWorkSize = roundUp(localWorkSize, elementCount); - - IJ.log("Calculating block repetition..."); - - - // ------------------------------- // - // ---- Calculate local means ---- // - // ------------------------------- // - - // Create buffers - clRefPixels = context.createFloatBuffer(wh, READ_ONLY); - clLocalMeans = context.createFloatBuffer(wh, READ_WRITE); - clLocalStds = context.createFloatBuffer(wh, READ_WRITE); - - // Create OpenCL program - String programStringGetPatchMeans = getResourceAsString(BlockRedundancy2D_.class, "kernelGetPatchMeans2D.cl"); - programStringGetPatchMeans = replaceFirst(programStringGetPatchMeans, "$WIDTH$", "" + w); - programStringGetPatchMeans = replaceFirst(programStringGetPatchMeans, "$HEIGHT$", "" + h); - programStringGetPatchMeans = replaceFirst(programStringGetPatchMeans, "$PATCH_SIZE$", "" + patchSize); - programStringGetPatchMeans = replaceFirst(programStringGetPatchMeans, "$BRW$", "" + bRW); - programStringGetPatchMeans = replaceFirst(programStringGetPatchMeans, "$BRH$", "" + bRH); - programStringGetPatchMeans = replaceFirst(programStringGetPatchMeans, "$EPSILON$", "" + EPSILON); - programGetPatchMeans = context.createProgram(programStringGetPatchMeans).build(); - - // Fill OpenCL buffers - fillBufferWithFloatArray(clRefPixels, refPixels); - - float[] localMeans = new float[wh]; - fillBufferWithFloatArray(clLocalMeans, localMeans); - - float[] localStds = new float[wh]; - fillBufferWithFloatArray(clLocalStds, localStds); - - // Create OpenCL kernel and set args - kernelGetPatchMeans = programGetPatchMeans.createCLKernel("kernelGetPatchMeans2D"); - - int argn = 0; - kernelGetPatchMeans.setArg(argn++, clRefPixels); - kernelGetPatchMeans.setArg(argn++, clLocalMeans); - kernelGetPatchMeans.setArg(argn++, clLocalStds); - - // Calculate - queue.putWriteBuffer(clRefPixels, true); - queue.putWriteBuffer(clLocalMeans, true); - queue.putWriteBuffer(clLocalStds, true); - - showStatus("Calculating local means..."); - - queue.put2DRangeKernel(kernelGetPatchMeans, 0, 0, w, h, 0, 0); - queue.finish(); - - // Read the local means map back from the device - queue.putReadBuffer(clLocalMeans, true); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - localMeans[y * w + x] = clLocalMeans.getBuffer().get(y * w + x); - queue.finish(); - - } - } - - // Read the local stds map back from the device - queue.putReadBuffer(clLocalStds, true); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - localStds[y * w + x] = clLocalStds.getBuffer().get(y * w + x); - queue.finish(); - } - } - - // Release memory - kernelGetPatchMeans.release(); - programGetPatchMeans.release(); - - - // --------------------------------------------------------------- // - // ---- Calculate block repetition map with the chosen metric ---- // - // --------------------------------------------------------------- // - - float[] repetitionMap = new float[wh]; // Array to store output repetition map - - if (metric == metrics[0]) { // Pearson correlation - showStatus("Calculating Pearson correlations..."); - - // Build OpenCL program - String programStringGetPatchPearson = getResourceAsString(BlockRedundancy2D_.class, "kernelGetPatchPearson2D.cl"); - programStringGetPatchPearson = replaceFirst(programStringGetPatchPearson, "$WIDTH$", "" + w); - programStringGetPatchPearson = replaceFirst(programStringGetPatchPearson, "$HEIGHT$", "" + h); - programStringGetPatchPearson = replaceFirst(programStringGetPatchPearson, "$PATCH_SIZE$", "" + patchSize); - programStringGetPatchPearson = replaceFirst(programStringGetPatchPearson, "$BW$", "" + bW); - programStringGetPatchPearson = replaceFirst(programStringGetPatchPearson, "$BH$", "" + bH); - programStringGetPatchPearson = replaceFirst(programStringGetPatchPearson, "$BRW$", "" + bRW); - programStringGetPatchPearson = replaceFirst(programStringGetPatchPearson, "$BRH$", "" + bRH); - programStringGetPatchPearson = replaceFirst(programStringGetPatchPearson, "$PATCH_MEAN$", "" + patchMeanFloat); - programStringGetPatchPearson = replaceFirst(programStringGetPatchPearson, "$PATCH_STD$", "" + patchStdDev); - programStringGetPatchPearson = replaceFirst(programStringGetPatchPearson, "$EPSILON$", "" + EPSILON); - - programGetPatchPearson = context.createProgram(programStringGetPatchPearson).build(); - //System.out.println(programGetPatchPearson.getBuildLog()); // Print program build log to check for errors - - // Fill OpenCL buffers - clPatchPixels = context.createFloatBuffer(patchSize, READ_ONLY); - fillBufferWithFloatArray(clPatchPixels, patchPixelsFloat); - - clPearsonMap = context.createFloatBuffer(wh, READ_WRITE); - fillBufferWithFloatArray(clPearsonMap, repetitionMap); - - // Create kernel and set args - kernelGetPatchPearson = programGetPatchPearson.createCLKernel("kernelGetPatchPearson2D"); - - argn = 0; - kernelGetPatchPearson.setArg(argn++, clPatchPixels); - kernelGetPatchPearson.setArg(argn++, clRefPixels); - kernelGetPatchPearson.setArg(argn++, clLocalMeans); - kernelGetPatchPearson.setArg(argn++, clLocalStds); - kernelGetPatchPearson.setArg(argn++, clPearsonMap); - - // Calculate Pearson's correlation coefficient (reference patch vs. all) - queue.putWriteBuffer(clPatchPixels, true); - queue.putWriteBuffer(clPearsonMap, true); - queue.put2DRangeKernel(kernelGetPatchPearson, 0, 0, w, h, 0, 0); - queue.finish(); - - // Read Pearson's coefficients back from the GPU - queue.putReadBuffer(clPearsonMap, true); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - repetitionMap[y * w + x] = clPearsonMap.getBuffer().get(y * w + x); - queue.finish(); - } - } - queue.finish(); - - // Release GPU resources - kernelGetPatchPearson.release(); - clPatchPixels.release(); - clPearsonMap.release(); - programGetPatchPearson.release(); - } - - if (metric == metrics[1]) { // Cosine similarity - showStatus("Calculating Cosine similarity..."); - - // Build OpenCL program - String programStringGetPatchCosineSim = getResourceAsString(BlockRedundancy2D_.class, "kernelGetPatchCosineSim2D.cl"); - programStringGetPatchCosineSim = replaceFirst(programStringGetPatchCosineSim, "$WIDTH$", "" + w); - programStringGetPatchCosineSim = replaceFirst(programStringGetPatchCosineSim, "$HEIGHT$", "" + h); - programStringGetPatchCosineSim = replaceFirst(programStringGetPatchCosineSim, "$BRW$", "" + bRW); - programStringGetPatchCosineSim = replaceFirst(programStringGetPatchCosineSim, "$BRH$", "" + bRH); - programStringGetPatchCosineSim = replaceFirst(programStringGetPatchCosineSim, "$PATCH_STD$", "" + patchStdDev); - programStringGetPatchCosineSim = replaceFirst(programStringGetPatchCosineSim, "$EPSILON$", "" + EPSILON); - programGetPatchCosineSim = context.createProgram(programStringGetPatchCosineSim).build(); - - // Fill OpenCL buffers - clCosineSimMap = context.createFloatBuffer(wh, READ_WRITE); - fillBufferWithFloatArray(clCosineSimMap, repetitionMap); - - // Create kernel and set args - kernelGetPatchCosineSim = programGetPatchCosineSim.createCLKernel("kernelGetPatchCosineSim2D"); - - argn = 0; - kernelGetPatchCosineSim.setArg(argn++, clLocalStds); - kernelGetPatchCosineSim.setArg(argn++, clCosineSimMap); - - // Calculate absolute difference of StdDevs - queue.putWriteBuffer(clCosineSimMap, true); - queue.put2DRangeKernel(kernelGetPatchCosineSim, 0, 0, w, h, 0, 0); - queue.finish(); - - // Read results back from the OpenCL device - queue.putReadBuffer(clCosineSimMap, true); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - repetitionMap[y * w + x] = clCosineSimMap.getBuffer().get(y * w + x); - queue.finish(); - } - } - queue.finish(); - - // Release GPU resources - kernelGetPatchCosineSim.release(); - clCosineSimMap.release(); - programGetPatchCosineSim.release(); - } - - if (metric == metrics[2]) { // SSIM - showStatus("Calculating SSIM..."); - - // Build OpenCL program - String programStringGetPatchSsim = getResourceAsString(BlockRedundancy2D_.class, "kernelGetPatchSsim2D.cl"); - programStringGetPatchSsim = replaceFirst(programStringGetPatchSsim, "$WIDTH$", "" + w); - programStringGetPatchSsim = replaceFirst(programStringGetPatchSsim, "$HEIGHT$", "" + h); - programStringGetPatchSsim = replaceFirst(programStringGetPatchSsim, "$PATCH_SIZE$", "" + patchSize); - programStringGetPatchSsim = replaceFirst(programStringGetPatchSsim, "$BW$", "" + bW); - programStringGetPatchSsim = replaceFirst(programStringGetPatchSsim, "$BH$", "" + bH); - programStringGetPatchSsim = replaceFirst(programStringGetPatchSsim, "$BRW$", "" + bRW); - programStringGetPatchSsim = replaceFirst(programStringGetPatchSsim, "$BRH$", "" + bRH); - programStringGetPatchSsim = replaceFirst(programStringGetPatchSsim, "$PATCH_MEAN$", "" + patchMeanFloat); - programStringGetPatchSsim = replaceFirst(programStringGetPatchSsim, "$PATCH_STD$", "" + patchStdDev); - programStringGetPatchSsim = replaceFirst(programStringGetPatchSsim, "$EPSILON$", "" + EPSILON); - programGetPatchSsim = context.createProgram(programStringGetPatchSsim).build(); - //System.out.println(programGetPatchSsim.getBuildLog()); // Print program build log to check for errors - - // Fill OpenCL buffers - clPatchPixels = context.createFloatBuffer(patchSize, READ_ONLY); - fillBufferWithFloatArray(clPatchPixels, patchPixelsFloat); - - clSsimMap = context.createFloatBuffer(wh, READ_WRITE); - fillBufferWithFloatArray(clSsimMap, repetitionMap); - - // Create kernel and set args - kernelGetPatchSsim = programGetPatchSsim.createCLKernel("kernelGetPatchSsim2D"); - - argn = 0; - kernelGetPatchSsim.setArg(argn++, clPatchPixels); - kernelGetPatchSsim.setArg(argn++, clRefPixels); - kernelGetPatchSsim.setArg(argn++, clLocalMeans); - kernelGetPatchSsim.setArg(argn++, clLocalStds); - kernelGetPatchSsim.setArg(argn++, clSsimMap); - - // Calculate SSIM - queue.putWriteBuffer(clPatchPixels, true); - queue.putWriteBuffer(clSsimMap, true); - queue.put2DRangeKernel(kernelGetPatchSsim, 0, 0, w, h, 0, 0); - queue.finish(); - - // Read SSIM back from the GPU - queue.putReadBuffer(clSsimMap, true); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - repetitionMap[y * w + x] = clSsimMap.getBuffer().get(y * w + x); - queue.finish(); - } - } - queue.finish(); - - // Release GPU resources - kernelGetPatchSsim.release(); - clPatchPixels.release(); - clSsimMap.release(); - programGetPatchSsim.release(); - } - - if (metric == metrics[3]) { // NRMSE (inverted) - showStatus("Calculating NRMSE..."); - - // Build OpenCL program - String programStringGetPatchRmse = getResourceAsString(BlockRedundancy2D_.class, "kernelGetPatchRmse2D.cl"); - programStringGetPatchRmse = replaceFirst(programStringGetPatchRmse, "$WIDTH$", "" + w); - programStringGetPatchRmse = replaceFirst(programStringGetPatchRmse, "$HEIGHT$", "" + h); - programStringGetPatchRmse = replaceFirst(programStringGetPatchRmse, "$PATCH_SIZE$", "" + patchSize); - programStringGetPatchRmse = replaceFirst(programStringGetPatchRmse, "$BW$", "" + bW); - programStringGetPatchRmse = replaceFirst(programStringGetPatchRmse, "$BH$", "" + bH); - programStringGetPatchRmse = replaceFirst(programStringGetPatchRmse, "$BRW$", "" + bRW); - programStringGetPatchRmse = replaceFirst(programStringGetPatchRmse, "$BRH$", "" + bRH); - programStringGetPatchRmse = replaceFirst(programStringGetPatchRmse, "$PATCH_MEAN$", "" + patchMeanFloat); - programStringGetPatchRmse = replaceFirst(programStringGetPatchRmse, "$EPSILON$", "" + EPSILON); - programGetPatchRmse = context.createProgram(programStringGetPatchRmse).build(); - //System.out.println(programGetPatchSsim.getBuildLog()); // Print program build log to check for errors - - // Fill OpenCL buffers - clPatchPixels = context.createFloatBuffer(patchSize, READ_ONLY); - fillBufferWithFloatArray(clPatchPixels, patchPixelsFloat); - - clRmseMap = context.createFloatBuffer(wh, READ_WRITE); - fillBufferWithFloatArray(clRmseMap, repetitionMap); - - // Create kernel and set args - kernelGetPatchRmse = programGetPatchRmse.createCLKernel("kernelGetPatchRmse2D"); - - argn = 0; - kernelGetPatchRmse.setArg(argn++, clPatchPixels); - kernelGetPatchRmse.setArg(argn++, clRefPixels); - kernelGetPatchRmse.setArg(argn++, clLocalMeans); - kernelGetPatchRmse.setArg(argn++, clRmseMap); - - // Calculate RMSE - queue.putWriteBuffer(clPatchPixels, true); - queue.putWriteBuffer(clRmseMap, true); - queue.put2DRangeKernel(kernelGetPatchRmse, 0, 0, w, h, 0, 0); - queue.finish(); - - // Read RMSE back from the GPU - queue.putReadBuffer(clRmseMap, true); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - repetitionMap[y * w + x] = clRmseMap.getBuffer().get(y * w + x); - queue.finish(); - } - } - queue.finish(); - - // Release GPU resources - kernelGetPatchRmse.release(); - clPatchPixels.release(); - clRmseMap.release(); - programGetPatchRmse.release(); - - // Invert RMSE - for (int y = bRH; y < h-bRH; y++) { - for (int x = bRW; x < w-bRW; x++) { - float rmse = repetitionMap[y*w+x]; - - if(rmse == 0.0f){ // Special case where RMSE is 0, 1/rmse would be undefined but we want perfect similarity - repetitionMap[y*w+x] = 1.0f; - }else{ - repetitionMap[y*w+x] = 1.0f / rmse; - } - } - } - } - - - // --------------------------------------- // - // ---- Filter out irrelevant regions ---- // - // --------------------------------------- // - - if (filterConstant > 0.0f) { - showStatus("Calculating relevance mask..."); - - // Create OpenCL program - String programStringGetRelevanceMap = getResourceAsString(BlockRedundancy2D_.class, "kernelGetRelevanceMask2D.cl"); - programStringGetRelevanceMap = replaceFirst(programStringGetRelevanceMap, "$WIDTH$", "" + w); - programStringGetRelevanceMap = replaceFirst(programStringGetRelevanceMap, "$HEIGHT$", "" + h); - programStringGetRelevanceMap = replaceFirst(programStringGetRelevanceMap, "$PATCH_SIZE$", "" + patchSize); - programStringGetRelevanceMap = replaceFirst(programStringGetRelevanceMap, "$BRW$", "" + bRW); - programStringGetRelevanceMap = replaceFirst(programStringGetRelevanceMap, "$BRH$", "" + bRH); - programStringGetRelevanceMap = replaceFirst(programStringGetRelevanceMap, "$EPSILON$", "" + EPSILON); - programGetRelevanceMap = context.createProgram(programStringGetRelevanceMap).build(); - - // Create and fill buffers - float[] relevanceMap = new float[wh]; - clRelevanceMap = context.createFloatBuffer(wh, READ_WRITE); - fillBufferWithFloatArray(clRelevanceMap, relevanceMap); - queue.putWriteBuffer(clRelevanceMap, true); - queue.finish(); - - // Create OpenCL kernel and set args - kernelGetRelevanceMap = programGetRelevanceMap.createCLKernel("kernelGetRelevanceMask2D"); - - argn = 0; - kernelGetRelevanceMap.setArg(argn++, clRefPixels); - kernelGetRelevanceMap.setArg(argn++, clRelevanceMap); - - // Calculate - queue.put2DRangeKernel(kernelGetRelevanceMap, 0, 0, w, h, 0, 0); - queue.finish(); - - // Read the relevance map back from the device - queue.putReadBuffer(clRelevanceMap, true); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - relevanceMap[y * w + x] = clRelevanceMap.getBuffer().get(y * w + x); - } - } - queue.finish(); - - // Release resources - kernelGetRelevanceMap.release(); - clRelevanceMap.release(); - programGetRelevanceMap.release(); - - /* OLD - - // Calculate mean noise variance - float noiseMeanVar = 0.0f; - float n = 0.0f; - for (int j = bRH; j < h - bRH; j++) { - for (int i = bRW; i < w - bRW; i++) { - noiseMeanVar += relevanceMap[j * w + i]; - n += 1.0f; - } - } - noiseMeanVar /= n; - - */ - - // Calculate mean noise variance - float noiseMeanVar = getMeanNoiseVar(relevanceMap, w, h, wh); - - // Filter out irrelevant regions - for (int j = bRH; j < h - bRH; j++) { - for (int i = bRW; i < w - bRW; i++) { - if (relevanceMap[j * w + i] <= noiseMeanVar * filterConstant) { - repetitionMap[j * w + i] = 0.0f; - } - } - } - - if (normalizeOutput) { - // ----------------------------------------------------------------------- // - // ---- Normalize output (avoiding pixels outside the relevance mask) ---- // - // ----------------------------------------------------------------------- // - - // Find min and max within the relevance mask - float repetitionMin = Float.MAX_VALUE; - float repetitionMax = -Float.MAX_VALUE; - - for (int j = bRH; j < h - bRH; j++) { - for (int i = bRW; i < w - bRW; i++) { - if (relevanceMap[j * w + i] > noiseMeanVar * filterConstant) { - float pixelValue = repetitionMap[j * w + i]; - if (pixelValue > repetitionMax) { - repetitionMax = pixelValue; - } - if (pixelValue < repetitionMin) { - repetitionMin = pixelValue; - } - } - } - } - - // Remap pixels - for (int j = bRH; j < h - bRH; j++) { - for (int i = bRW; i < w - bRW; i++) { - if (relevanceMap[j * w + i] > noiseMeanVar * filterConstant) { - repetitionMap[j * w + i] = (repetitionMap[j * w + i] - repetitionMin) / (repetitionMax - repetitionMin + EPSILON); - } - } - } - } - } - - // Release resources - context.release(); - - - // ------------------------- // - // ---- Display results ---- // - // ------------------------- // - - FloatProcessor fp1 = new FloatProcessor(w, h, repetitionMap); - ImagePlus imp1 = new ImagePlus("Block Repetition Map", fp1); - - // Apply SReD LUT - InputStream lutStream = getClass().getResourceAsStream("/luts/sred-jet.lut"); - if (lutStream == null) { - IJ.error("Could not load SReD LUT. Using default LUT."); - }else{ - try { - // Load LUT file - IndexColorModel icm = LutLoader.open(lutStream); - byte[] r = new byte[256]; - byte[] g = new byte[256]; - byte[] b = new byte[256]; - icm.getReds(r); - icm.getGreens(g); - icm.getBlues(b); - LUT lut = new LUT(8, 256, r, g, b); - - // Apply LUT to image - imp1.getProcessor().setLut(lut); - //imp1.updateAndDraw(); - } catch (IOException e) { - IJ.error("Could not load SReD LUT"); - } - } - - // Display - imp1.show(); - - - // -------------------- // - // ---- Stop timer ---- // - // -------------------- // - - IJ.log("Finished!"); - long elapsedTime = System.currentTimeMillis() - start; - IJ.log("Elapsed time: " + elapsedTime/1000 + " sec"); - IJ.log("--------"); - } - - // ------------------------ // - // ---- USER FUNCTIONS ---- // - // ------------------------ // - - public static float getMeanNoiseVar(float[] refPixels, int w, int h, int wh) { - - int blockWidth, blockHeight; - int CIF = 352 * 288; // Resolution of a CIF file - - if (wh <= CIF) { - blockWidth = 8; - blockHeight = 8; - } else { - blockWidth = 16; - blockHeight = 16; - } - - int nBlocksX = w / blockWidth; // number of blocks in each row - int nBlocksY = h / blockHeight; // number of blocks in each column - int nBlocks = nBlocksX * nBlocksY; // total number of blocks - float[] localVars = new float[nBlocks]; - Arrays.fill(localVars, 0.0f); - - // Calculate local variances - int index = 0; - - for (int y = 0; y < nBlocksY; y++) { - for (int x = 0; x < nBlocksX; x++) { - float[] meanVar = getMeanAndVarBlock(refPixels, w, x * blockWidth, y * blockHeight, (x + 1) * blockWidth, (y + 1) * blockHeight); - localVars[index] = (float) meanVar[1]; - index++; - } - } - - // Sort the local variances - float[] sortedVars = new float[nBlocks]; - Arrays.fill(sortedVars, 0.0f); - - index = 0; - for (int i = 0; i < nBlocks; i++) { - sortedVars[index] = localVars[index]; - index++; - } - Arrays.sort(sortedVars); - - // Get the 3% lowest variances and calculate their average - int nVars = (int) (0.03f * (float) nBlocks + 1.0f); // Number of blocks corresponding to 3% of the total amount of blocks - float noiseVar = 0.0f; - - for (int i = 0; i < nVars; i++) { - noiseVar += sortedVars[i]; - } - noiseVar = abs(noiseVar / (float) nVars); - noiseVar = (1.0f + 0.001f * (noiseVar - 40.0f)) * noiseVar; - - return noiseVar; - } - - public static float[] getMeanAndVarBlock(float[] pixels, int width, int xStart, int yStart, int xEnd, int yEnd) { - float mean = 0; - float var; - - float sq_sum = 0; - - int bWidth = xEnd-xStart; - int bHeight = yEnd - yStart; - int bWH = bWidth*bHeight; - - for (int j=yStart; j clBuffer, float pixel) { - FloatBuffer buffer = clBuffer.getBuffer(); - buffer.put(pixel); - } - - public static void fillBufferWithFloatArray(CLBuffer clBuffer, float[] pixels) { - FloatBuffer buffer = clBuffer.getBuffer(); - for(int n=0; n clBuffer, double[] pixels) { - DoubleBuffer buffer = clBuffer.getBuffer(); - for(int n=0; n< pixels.length; n++) { - buffer.put(n, pixels[n]); - } - } - - public static void fillBufferWithShortArray(CLBuffer clBuffer, short[] pixels) { - ShortBuffer buffer = clBuffer.getBuffer(); - for(int n=0; n< pixels.length; n++) { - buffer.put(n, pixels[n]); - } - } - - // Read a kernel from the resources - public static String getResourceAsString(Class c, String resourceName) { - InputStream programStream = c.getResourceAsStream("/" + resourceName); - String programString = ""; - - try { - programString = inputStreamToString(programStream); - } catch (IOException var5) { - var5.printStackTrace(); - } - - return programString; - } - - public static String inputStreamToString(InputStream inputStream) throws IOException { - ByteArrayOutputStream result = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - while((length = inputStream.read(buffer)) != -1) { - result.write(buffer, 0, length); - } - return result.toString("UTF-8"); - } - - private static int roundUp(int groupSize, int globalSize) { - int r = globalSize % groupSize; - if (r == 0) { - return globalSize; - } else { - return globalSize + groupSize - r; - } - } - - public static float[] findMinMax(float[] inputArray, int w, int h, int offsetX, int offsetY){ - float[] minMax = {Float.MAX_VALUE, -Float.MAX_VALUE}; - - for(int j=offsetY; j minMax[1]){ - minMax[1] = inputArray[j*w+i]; - } - } - } - return minMax; - } - - public static String replaceFirst(String source, String target, String replacement) { - int index = source.indexOf(target); - if (index == -1) { - return source; - } - - return source.substring(0, index) - .concat(replacement) - .concat(source.substring(index+target.length())); - } - - public static float[] normalize(float[] rawPixels, int w, int h, int offsetX, int offsetY, float[] minMax, float tMin, float tMax){ - float rMin = minMax[0]; - float rMax = minMax[1]; - float denominator = rMax - rMin + 0.000001f; - float factor; - - if(tMax == 0 && tMin == 0){ - factor = 1; // So that the users can say they don't want an artificial range by choosing tMax and tMin = 0 - }else { - factor = tMax - tMin; - } - float[] normalizedPixels = new float[w*h]; - - for(int j=offsetY; j clRefPixels, clLocalMeans, clLocalStds, clPatchPixels, clDiffStdMap, clPearsonMap, - clHuMap, clSsimMap, clRelevanceMap, clGaussianWindow; - - @Override - public void run(String s) { - - float EPSILON = 0.0000001f; - - - // -------------------- // - // ---- Dialog box ---- // - // -------------------- // - - // Get all open image titles - int nImages = getImageCount(); - if (nImages < 2) { - IJ.error("Not enough images found. You need at least two."); - return; - } - - int[] ids = getIDList(); - String[] titles = new String[nImages]; - for(int i=0; i z) { - IJ.error("Patch must have at least 3 slices. Please try again."); - return; - } - - //int sizeWithoutBorders = (w-bRW*2)*(h-bRH*2); // The area of the search field (= image without borders) - - - // ---------------------------------- // - // ---- Stabilize noise variance ---- // - // ---------------------------------- // - - // Patch - IJ.log("Stabilizing noise variance of the patch..."); - - float[][] patchPixels = new float[bZ][bW*bH]; - for(int n=0; nimgMax){ - imgMax = pixelValue; - } - } - - // Normalize - for(int i=0; i nFlops) { - nFlops = clDevice.getMaxComputeUnits() * clDevice.getMaxClockFrequency(); - clPlatformMaxFlop = allPlatform; - } - } - } - IJ.log("--------"); - - // Create OpenCL context - context = CLContext.create(clPlatformMaxFlop); - - // Choose the best device (i.e., Filter out CPUs if GPUs are available (FLOPS calculation was giving - // higher ratings to CPU vs. GPU)) - //TODO: Rate devices on Mandelbrot and chooose the best, GPU will perform better - CLDevice[] allDevices = context.getDevices(); - - boolean hasGPU = false; - for (int i=0; i0.0f) { - showStatus("Calculating relevance map..."); - - // Create OpenCL program - String programStringGetRelevanceMap3D = getResourceAsString(BlockRedundancy3D_.class, "kernelGetRelevanceMask3D.cl"); - programStringGetRelevanceMap3D = replaceFirst(programStringGetRelevanceMap3D, "$WIDTH$", "" + w); - programStringGetRelevanceMap3D = replaceFirst(programStringGetRelevanceMap3D, "$HEIGHT$", "" + h); - programStringGetRelevanceMap3D = replaceFirst(programStringGetRelevanceMap3D, "$DEPTH$", "" + z); - programStringGetRelevanceMap3D = replaceFirst(programStringGetRelevanceMap3D, "$PATCH_SIZE$", "" + patchSize); - programStringGetRelevanceMap3D = replaceFirst(programStringGetRelevanceMap3D, "$BRW$", "" + bRW); - programStringGetRelevanceMap3D = replaceFirst(programStringGetRelevanceMap3D, "$BRH$", "" + bRH); - programStringGetRelevanceMap3D = replaceFirst(programStringGetRelevanceMap3D, "$BRZ$", "" + bRZ); - programStringGetRelevanceMap3D = replaceFirst(programStringGetRelevanceMap3D, "$EPSILON$", "" + EPSILON); - programGetRelevanceMap3D = context.createProgram(programStringGetRelevanceMap3D).build(); - - // Create and fill buffers - float[] relevanceMap = new float[whz]; - clRelevanceMap = context.createFloatBuffer(wh, READ_WRITE); - fillBufferWithFloatArray(clRelevanceMap, relevanceMap); - queue.putWriteBuffer(clRelevanceMap, true); - queue.finish(); - - // Create OpenCL kernel and set args - kernelGetRelevanceMap3D = programGetRelevanceMap3D.createCLKernel("kernelGetRelevanceMask3D"); - - argn = 0; - kernelGetRelevanceMap3D.setArg(argn++, clRefPixels); - kernelGetRelevanceMap3D.setArg(argn++, clRelevanceMap); - - // Calculate - queue.put3DRangeKernel(kernelGetRelevanceMap3D, 0, 0, 0, w, h, z, 0, 0, 0); - queue.finish(); - - // Read the relevance mask back from the device - queue.putReadBuffer(clRelevanceMap, true); - for(int n=0; n noiseMeanVar * filterConstant) { - float pixelValue = pearsonMap[w*h*n+j*w+i]; - if (pixelValue > pearsonMax) { - pearsonMax = pixelValue; - } - if (pixelValue < pearsonMin) { - pearsonMin = pixelValue; - } - } - } - } - } - - // Remap pixels - for(int n=bRZ; n noiseMeanVar * filterConstant) { - pearsonMap[w*h*n+j*w+i] = (pearsonMap[w*h*n+j*w+i] - pearsonMin) / (pearsonMax - pearsonMin); - } - } - } - } - } - - }else if(normalizeOutput){ - - // -------------------------- // - // ---- Normalize output ---- // - // -------------------------- // - - // Find min and max - float pearsonMin = Float.MAX_VALUE; - float pearsonMax = -Float.MAX_VALUE; - - for(int n=bRZ; n pearsonMax) { - pearsonMax = pixelValue; - } - if (pixelValue < pearsonMin) { - pearsonMin = pixelValue; - } - } - } - } - - // Remap pixels - for(int n=bRZ; n clBuffer, float[] pixels) { - FloatBuffer buffer = clBuffer.getBuffer(); - for(int n=0; n minMax[1]){ - minMax[1] = inputArray[j*w+i]; - } - } - } - return minMax; - } - - public static float[] normalize(float[] rawPixels, int w, int h, int offsetX, int offsetY, float[] minMax, float tMin, float tMax){ - float rMin = minMax[0]; - float rMax = minMax[1]; - float denominator = rMax - rMin + 0.000001f; - float factor; - - if(tMax == 0 && tMin == 0){ - factor = 1; // So that the users can say they don't want an artificial range by choosing tMax and tMin = 0 - }else { - factor = tMax - tMin; - } - float[] normalizedPixels = new float[w*h]; - - for(int j=offsetY; j inputImage.getWidth() || referenceBlock.getHeight() > inputImage.getHeight() || referenceBlock.getDepth() > inputImage.getDepth()) { + IJ.error("Block dimensions can't be larger than image dimensions."); + throw new IllegalArgumentException("Block dimensions must be smaller than image dimensions."); + } + + // Calculate repetition map + float[] repetitionMap = CLUtils.calculateBlockRepetitionMap3D(metric, inputImage, referenceBlock, relevanceConstant, normalizeOutput, useDevice); + + // Display results + Utils.displayResults3D(inputImage, repetitionMap); + + // Stop timer + long elapsedTime = System.currentTimeMillis() - start; + + IJ.log("Finished!"); + IJ.log("Elapsed time: " + elapsedTime/1000 + " sec"); + IJ.log("--------"); + + } +} \ No newline at end of file diff --git a/src/main/java/CLUtils.java b/src/main/java/CLUtils.java new file mode 100644 index 0000000..aa7d0cb --- /dev/null +++ b/src/main/java/CLUtils.java @@ -0,0 +1,3169 @@ +import com.jogamp.opencl.*; +import ij.IJ; +import ij.Prefs; + +import java.io.*; +import java.nio.FloatBuffer; +import java.util.Arrays; +import java.util.logging.Logger; + +import static com.jogamp.opencl.CLMemory.Mem.*; +import static ij.IJ.showStatus; +import static java.lang.Math.max; +import static java.lang.Math.min; + +public class CLUtils { + + // Private constructor to prevent instantiation + private CLUtils() { + throw new UnsupportedOperationException("Utility class"); + } + + + // ---------------------------- // + // ---- OBJECTS FOR OPENCL ---- // + // ---------------------------- // + + /** + * This class encapsulates the OpenCL resources required for executing + * OpenCL kernels, including the context, device, and command queue. + */ + public static class OpenCLResources + { + + private final CLContext context; + private final CLDevice device; + private final CLCommandQueue queue; + + /** + * Constructs an instance of OpenCLResources with the specified OpenCL context, device, and command queue. + * + * @param context the OpenCL context + * @param device the OpenCL device + * @param queue the command queue associated with the OpenCL device + */ + public OpenCLResources(CLContext context, CLDevice device, CLCommandQueue queue) { + this.context = context; + this.device = device; + this.queue = queue; + } + + /** + * Returns the OpenCL context associated with this instance. + * + * @return the OpenCL context + */ + public CLContext getContext() { + return context; + } + + /** + * Returns the OpenCL device associated with this instance. + * + * @return the OpenCL device + */ + public CLDevice getDevice() { + return device; + } + + /** + * Returns the command queue associated with the OpenCL device. + * + * @return the command queue + */ + public CLCommandQueue getQueue() { + return queue; + } + } + + + /** + * This class encapsulates local statistics for a 2D image, including the local means, local standard deviations, + * and associated OpenCL buffers. + */ + public static class CLLocalStatistics + { + + private float[] localMeans; + private float[] localStds; + private CLBuffer clImageArray; + private CLBuffer clLocalMeans; + private CLBuffer clLocalStds; + + /** + * Constructs an instance of CLLocalStatistics with the specified local means, local standard deviations, + * and OpenCL buffers for the image array, local means, and local standard deviations. + * + * @param localMeans an array of local means calculated from the image + * @param localStds an array of local standard deviations calculated from the image + * @param clImageArray OpenCL buffer containing the image data + * @param clLocalMeans OpenCL buffer containing the local means + * @param clLocalStds OpenCL buffer containing the local standard deviations + */ + public CLLocalStatistics(float[] localMeans, float[] localStds, CLBuffer clImageArray, CLBuffer clLocalMeans, CLBuffer clLocalStds) { + this.localMeans = localMeans; + this.localStds = localStds; + this.clImageArray = clImageArray; + this.clLocalMeans = clLocalMeans; + this.clLocalStds = clLocalStds; + } + + /** + * Returns the local means calculated for the image. + * + * @return an array of local means + */ + public float[] getLocalMeans() { + return localMeans; + } + + /** + * Returns the local standard deviations calculated for the image. + * + * @return an array of local standard deviations + */ + public float[] getLocalStds() { + return localStds; + } + + /** + * Returns the OpenCL buffer containing the image data. + * + * @return the OpenCL buffer for the image array + */ + public CLBuffer getCLImageArray() { + return clImageArray; + } + /** + * Returns the OpenCL buffer for storing the local means. + * + * @return the OpenCL buffer for local means + */ + public CLBuffer getCLLocalMeans() { + return clLocalMeans; + } + + /** + * Returns the OpenCL buffer for storing the local standard deviations. + * + * @return the OpenCL buffer for local standard deviations + */ + public CLBuffer getCLLocalStds() { + return clLocalStds; + } + } + + + // ------------------------------------------------------------------ // + // ---- METHODS FOR OPENCL INITIALISATION AND RESOURCE MANAGEMENT---- // + // ------------------------------------------------------------------ // + /** + * Initializes OpenCL and retrieves objects related to OpenCL resources. + * + * @param useDevice A boolean indicating if the OpenCL device should be chosen + * according to the user-defined preference. + * @return A {@link OpenCLResources} object containing the initialized OpenCL + * {@code CLContext}, {@code CLDevice}, and {@code CLCommandQueue}. + * @throws RuntimeException if there is an error during OpenCL initialization. + */ + public static OpenCLResources getOpenCLResources(boolean useDevice) + { + IJ.log("Initialising OpenCL..."); + + // Check OpenCL devices + CLPlatform[] allPlatforms = CLPlatform.listCLPlatforms(); + try { + allPlatforms = CLPlatform.listCLPlatforms(); + } catch (CLException ex) { + IJ.log("Something went wrong while initialising OpenCL."); + throw new RuntimeException("Something went wrong while initialising OpenCL."); + } + + double nFlops = 0; + + CLPlatform clPlatformMaxFlop = null; + for (CLPlatform allPlatform : allPlatforms) { + CLDevice[] allCLdeviceOnThisPlatform = allPlatform.listCLDevices(); + + for (CLDevice clDevice : allCLdeviceOnThisPlatform) { + //IJ.log("--------"); + //IJ.log("Device name: " + clDevice.getName()); + //IJ.log("Device type: " + clDevice.getType()); + //IJ.log("Max clock: " + clDevice.getMaxClockFrequency() + " MHz"); + //IJ.log("Number of compute units: " + clDevice.getMaxComputeUnits()); + //IJ.log("Max work group size: " + clDevice.getMaxWorkGroupSize()); + if (clDevice.getMaxComputeUnits() * clDevice.getMaxClockFrequency() > nFlops) { + nFlops = clDevice.getMaxComputeUnits() * clDevice.getMaxClockFrequency(); + clPlatformMaxFlop = allPlatform; + } + } + } + + // Create OpenCL context + CLContext context = CLContext.create(clPlatformMaxFlop); + + // Choose the best device (filter out CPUs if GPUs are available) + CLDevice[] allDevices = context.getDevices(); + boolean hasGPU = false; + + for (int i = 0; i < allDevices.length; i++) { + if (allDevices[i].getType() == CLDevice.Type.GPU) { + hasGPU = true; + } + } + CLDevice chosenDevice; + if (hasGPU) { + chosenDevice = context.getMaxFlopsDevice(CLDevice.Type.GPU); + } else { + chosenDevice = context.getMaxFlopsDevice(); + } + + // Get chosen device from preferences (optional) + if (useDevice) { + String deviceName = Prefs.get("SReD.OpenCL.device", null); + for (CLDevice device : allDevices) { + if (device.getName().equals(deviceName)) { + chosenDevice = device; + break; + } + } + } + + IJ.log("Device chosen: " + chosenDevice.getName()); + + // Create command queue + CLCommandQueue queue = chosenDevice.createCommandQueue(); + + return new OpenCLResources(context, chosenDevice, queue); + } + + + /** + * Creates an OpenCL buffer and fills it with the provided float array. + * + * @param context the OpenCL context in which to create the buffer + * @param size the size of the buffer to be created + * @param flag the memory flag to specify the allocation behavior of the buffer + * @param array the float array containing the data to fill the buffer with + * @return a {@link CLBuffer} object filled with the data from the provided float array + */ + public static CLBuffer createAndFillCLBuffer(CLContext context, int size, CLMemory.Mem flag, float[] array) + { + CLBuffer clBuffer = context.createFloatBuffer(size, flag); + fillBufferWithFloatArray(clBuffer, array); + + return clBuffer; + } + + + /** + * Fills an OpenCL buffer with data from the provided float array. + * + * @param clBuffer the OpenCL buffer to be filled with data + * @param array the float array containing the data to be copied into the buffer + * @throws IndexOutOfBoundsException if the specified index is out of range + */ + public static void fillBufferWithFloatArray(CLBuffer clBuffer, float[] array) + { + FloatBuffer buffer = clBuffer.getBuffer(); + for(int n=0; n clImageArray = CLUtils.createAndFillCLBuffer(context, imageSize, READ_ONLY, inputImage2D.getImageArray()); + queue.putWriteBuffer(clImageArray, true); + + float[] localMeans = new float[inputImage2D.getSize()]; + CLBuffer clLocalMeans = CLUtils.createAndFillCLBuffer(context, imageSize, READ_WRITE, localMeans); + queue.putWriteBuffer(clLocalMeans, true); + + float[] localStds = new float[inputImage2D.getSize()]; + CLBuffer clLocalStds = CLUtils.createAndFillCLBuffer(context, imageSize, READ_WRITE, localStds); + queue.putWriteBuffer(clLocalStds, true); + + // Create OpenCL program + String programStringGetLocalStatistics2D = getResourceAsString(CLUtils.class, "kernelGetLocalStatistics2D.cl"); + programStringGetLocalStatistics2D = replaceFirst(programStringGetLocalStatistics2D, "$WIDTH$", "" + imageWidth); + programStringGetLocalStatistics2D = replaceFirst(programStringGetLocalStatistics2D, "$HEIGHT$", "" + imageHeight); + programStringGetLocalStatistics2D = replaceFirst(programStringGetLocalStatistics2D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetLocalStatistics2D = replaceFirst(programStringGetLocalStatistics2D, "$BRW$", "" + bRW); + programStringGetLocalStatistics2D = replaceFirst(programStringGetLocalStatistics2D, "$BRH$", "" + bRH); + programStringGetLocalStatistics2D = replaceFirst(programStringGetLocalStatistics2D, "$EPSILON$", "" + EPSILON); + CLProgram programGetLocalStatistics2D = context.createProgram(programStringGetLocalStatistics2D).build(); + + // Crete OpenCL kernel and set args + CLKernel kernelGetLocalStatistics2D = programGetLocalStatistics2D.createCLKernel("kernelGetLocalStatistics2D"); + + int argn = 0; + kernelGetLocalStatistics2D.setArg(argn++, clImageArray); + kernelGetLocalStatistics2D.setArg(argn++, clLocalMeans); + kernelGetLocalStatistics2D.setArg(argn++, clLocalStds); + + int localWorkSize = min(device.getMaxWorkGroupSize(), 256); + int globalWorkSize = roundUp(localWorkSize, imageSize); + + queue.put2DRangeKernel(kernelGetLocalStatistics2D, + 0, + 0, + imageWidth, + imageHeight, + 0, + 0); + queue.finish(); + + // Read the local means map back from the device + queue.putReadBuffer(clLocalMeans, true); + for (int y=0; y clBlockPixels = CLUtils.createAndFillCLBuffer(context, blockSize, READ_ONLY, + referenceBlock2D.getPixels()); + queue.putWriteBuffer(clBlockPixels, true); + + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(context, imageSize, READ_WRITE, + repetitionMap); + + // Build OpenCL program + String programStringGetBlockPearson2D = getResourceAsString(CLUtils.class, "kernelGetBlockPearson2D.cl"); + programStringGetBlockPearson2D = replaceFirst(programStringGetBlockPearson2D, "$WIDTH$", "" + imageWidth); + programStringGetBlockPearson2D = replaceFirst(programStringGetBlockPearson2D, "$HEIGHT$", "" + imageHeight); + programStringGetBlockPearson2D = replaceFirst(programStringGetBlockPearson2D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetBlockPearson2D = replaceFirst(programStringGetBlockPearson2D, "$BLOCK_WIDTH$", "" + blockWidth); + programStringGetBlockPearson2D = replaceFirst(programStringGetBlockPearson2D, "$BLOCK_HEIGHT$", "" + blockHeight); + programStringGetBlockPearson2D = replaceFirst(programStringGetBlockPearson2D, "$BRW$", "" + bRW); + programStringGetBlockPearson2D = replaceFirst(programStringGetBlockPearson2D, "$BRH$", "" + bRH); + programStringGetBlockPearson2D = replaceFirst(programStringGetBlockPearson2D, "$BLOCK_STD$", "" + referenceBlock2D.getStd()); + programStringGetBlockPearson2D = replaceFirst(programStringGetBlockPearson2D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetBlockPearson2D = context.createProgram(programStringGetBlockPearson2D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetBlockPearson2D = programGetBlockPearson2D.createCLKernel("kernelGetBlockPearson2D"); + + int argn = 0; + kernelGetBlockPearson2D.setArg(argn++, clBlockPixels); + kernelGetBlockPearson2D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetBlockPearson2D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetBlockPearson2D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetBlockPearson2D.setArg(argn++, clRepetitionMap); + + int localWorkSize = min(device.getMaxWorkGroupSize(), 256); + int globalWorkSize = roundUp(localWorkSize, imageSize); + + queue.put2DRangeKernel(kernelGetBlockPearson2D, 0, 0, imageWidth, imageHeight, + 0, 0); + queue.finish(); + + // Read the repetition map back from the device + queue.putReadBuffer(clRepetitionMap, true); + for (int y=0; y0.0f) { + Utils.RelevanceMask relevanceMask = Utils.getRelevanceMask(inputImage2D.getImageArray(), imageWidth, + imageHeight, bRW, bRH, localStatistics.getLocalStds(), relevanceConstant); + relevanceMaskArray = relevanceMask.getRelevanceMask(); + repetitionMap = Utils.applyMask2D(repetitionMap, imageWidth, imageHeight, relevanceMaskArray); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage2D(repetitionMap, imageWidth, imageHeight, bRW, bRH, + relevanceMaskArray); + } + + // Release memory + context.release(); + + return repetitionMap; + } + + + /** + * Computes the block-wise cosine similarity for a 2D image using OpenCL. + * + * This method calculates the cosine similarity between a reference block and all blocks + * of the input image. The result is stored in a repetition map, indicating the degree + * of similarity across the image. Optionally, a relevance mask can be applied to filter + * the results based on local standard deviations and a relevance constant. + * + * @param inputImage2D the input 2D image for similarity calculation + * @param referenceBlock2D the reference block used for comparison in the similarity calculation + * @param relevanceConstant a constant value used to cintrol the strength of the structural relevance filter + * @param normalizeOutput a boolean indicating whether to normalize the output repetition map + * @param useDevice a boolean indicating whether to use the user-defined OpenCL device preference + * @return a float array representing the repetition map, indicating the cosine similarity + * coefficients across the image + */ + public static float[] getBlockCosineSimilarity2D(Utils.InputImage2D inputImage2D, + Utils.ReferenceBlock2D referenceBlock2D, + float relevanceConstant, + boolean normalizeOutput, + boolean useDevice) + { + // Cache variables + int imageWidth = inputImage2D.getWidth(); + int imageHeight = inputImage2D.getHeight(); + int imageSize = inputImage2D.getSize(); + int blockWidth = referenceBlock2D.getWidth(); + int blockHeight = referenceBlock2D.getHeight(); + int blockSize = referenceBlock2D.getSize(); + int bRW = referenceBlock2D.getRadiusWidth(); + int bRH = referenceBlock2D.getRadiusHeight(); + + // Initialize OpenCL + CLUtils.OpenCLResources openCLResources = CLUtils.getOpenCLResources(useDevice); + + // Retrieve OpenCL context, device and queue + CLContext context = openCLResources.getContext(); + CLDevice device = openCLResources.getDevice(); + CLCommandQueue queue = openCLResources.getQueue(); + + // Calculate local statistics + CLLocalStatistics localStatistics = getLocalStatistics2D(context, device, queue, inputImage2D, + blockWidth, blockHeight, Utils.EPSILON); + + // Create and fill OpenCL buffers + IJ.log("Calculating Structural Repetition Scores..."); + + CLBuffer clBlockPixels = createAndFillCLBuffer(context, blockSize, READ_ONLY, + referenceBlock2D.getPixels()); + queue.putWriteBuffer(clBlockPixels, true); + + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = createAndFillCLBuffer(context, imageSize, READ_WRITE, + repetitionMap); + + // Build OpenCL program + String programStringGetBlockCosineSimilarity2D = getResourceAsString(CLUtils.class, "kernelGetBlockCosineSimilarity2D.cl"); + programStringGetBlockCosineSimilarity2D = replaceFirst(programStringGetBlockCosineSimilarity2D, "$WIDTH$", "" + imageWidth); + programStringGetBlockCosineSimilarity2D = replaceFirst(programStringGetBlockCosineSimilarity2D, "$HEIGHT$", "" + imageHeight); + programStringGetBlockCosineSimilarity2D = replaceFirst(programStringGetBlockCosineSimilarity2D, "$BRW$", "" + bRW); + programStringGetBlockCosineSimilarity2D = replaceFirst(programStringGetBlockCosineSimilarity2D, "$BRH$", "" + bRH); + programStringGetBlockCosineSimilarity2D = replaceFirst(programStringGetBlockCosineSimilarity2D, "$BLOCK_STD$", "" + referenceBlock2D.getStd()); + programStringGetBlockCosineSimilarity2D = replaceFirst(programStringGetBlockCosineSimilarity2D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetBlockCosineSimilarity2D = context.createProgram(programStringGetBlockCosineSimilarity2D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetBlockCosineSimilarity2D = programGetBlockCosineSimilarity2D.createCLKernel("kernelGetBlockCosineSimilarity2D"); + + int argn = 0; + kernelGetBlockCosineSimilarity2D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetBlockCosineSimilarity2D.setArg(argn++, clRepetitionMap); + + int localWorkSize = min(device.getMaxWorkGroupSize(), 256); + int globalWorkSize = roundUp(localWorkSize, imageSize); + + queue.put2DRangeKernel(kernelGetBlockCosineSimilarity2D, 0, 0, imageWidth, + imageHeight, 0, 0); + queue.finish(); + + // Read the repetition map back from the device + queue.putReadBuffer(clRepetitionMap, true); + for (int y=0; y0.0f) { + Utils.RelevanceMask relevanceMask = Utils.getRelevanceMask(inputImage2D.getImageArray(), imageWidth, + imageHeight, bRW, bRH, localStatistics.getLocalStds(), relevanceConstant); + relevanceMaskArray = relevanceMask.getRelevanceMask(); + repetitionMap = Utils.applyMask2D(repetitionMap, imageWidth, imageHeight, relevanceMaskArray); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage2D(repetitionMap, imageWidth, imageHeight, bRW, bRH, + relevanceMaskArray); + } + + // Release memory + context.release(); + + return repetitionMap; + } + + + /** + * Computes the block-wise Structural Similarity Index (SSIM) for a 2D image using OpenCL. + * + * This method calculates the SSIM between a reference block and all blocks of the input image, + * generating a repetition map that indicates the similarity strength across the image. + * Optionally, a relevance mask can be applied to filter the results based on local + * standard deviations and a relevance constant. + * + * @param inputImage2D the input 2D image for SSIM calculation + * @param referenceBlock2D the reference block used for comparison in the SSIM calculation + * @param relevanceConstant a constant value used to control the strength of the structural relevance filter + * @param normalizeOutput a boolean indicating whether to normalize the output repetition map + * @param useDevice a boolean indicating whether to use the user-defined OpenCL device preference + * @return a float array representing the repetition map, indicating the SSIM coefficients + * across the image + */ + public static float[] getBlockSsim2D(Utils.InputImage2D inputImage2D, + Utils.ReferenceBlock2D referenceBlock2D, + float relevanceConstant, + boolean normalizeOutput, + boolean useDevice) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage2D.getWidth(); + int imageHeight = inputImage2D.getHeight(); + int imageSize = inputImage2D.getSize(); + int blockWidth = referenceBlock2D.getWidth(); + int blockHeight = referenceBlock2D.getHeight(); + int blockSize = referenceBlock2D.getSize(); + int bRW = referenceBlock2D.getRadiusWidth(); + int bRH = referenceBlock2D.getRadiusHeight(); + + // Initialize OpenCL + CLUtils.OpenCLResources openCLResources = CLUtils.getOpenCLResources(useDevice); + + // Retrieve OpenCL context, device and queue + CLContext context = openCLResources.getContext(); + CLDevice device = openCLResources.getDevice(); + CLCommandQueue queue = openCLResources.getQueue(); + + // Calculate local statistics + CLLocalStatistics localStatistics = CLUtils.getLocalStatistics2D(context, device, queue, inputImage2D, + blockWidth, blockHeight, Utils.EPSILON); + + + // Create and fill OpenCL buffers + CLBuffer clBlockPixels = CLUtils.createAndFillCLBuffer(context, blockSize, READ_ONLY, + referenceBlock2D.getPixels()); + queue.putWriteBuffer(clBlockPixels, true); + + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(context, imageSize, READ_WRITE, + repetitionMap); + + // Build OpenCL program + String programStringGetBlockSsim2D = getResourceAsString(CLUtils.class, "kernelGetBlockSsim2D.cl"); + programStringGetBlockSsim2D = replaceFirst(programStringGetBlockSsim2D, "$WIDTH$", "" + imageWidth); + programStringGetBlockSsim2D = replaceFirst(programStringGetBlockSsim2D, "$HEIGHT$", "" + imageHeight); + programStringGetBlockSsim2D = replaceFirst(programStringGetBlockSsim2D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetBlockSsim2D = replaceFirst(programStringGetBlockSsim2D, "$BW$", "" + blockWidth); + programStringGetBlockSsim2D = replaceFirst(programStringGetBlockSsim2D, "$BH$", "" + blockHeight); + programStringGetBlockSsim2D = replaceFirst(programStringGetBlockSsim2D, "$BRW$", "" + bRW); + programStringGetBlockSsim2D = replaceFirst(programStringGetBlockSsim2D, "$BRH$", "" + bRH); + programStringGetBlockSsim2D = replaceFirst(programStringGetBlockSsim2D, "$BLOCK_MEAN$", "" + referenceBlock2D.getMean()); + programStringGetBlockSsim2D = replaceFirst(programStringGetBlockSsim2D, "$BLOCK_STD$", "" + referenceBlock2D.getStd()); + programStringGetBlockSsim2D = replaceFirst(programStringGetBlockSsim2D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetBlockSsim2D = context.createProgram(programStringGetBlockSsim2D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetBlockSsim2D = programGetBlockSsim2D.createCLKernel("kernelGetBlockSsim2D"); + + int argn = 0; + kernelGetBlockSsim2D.setArg(argn++, clBlockPixels); + kernelGetBlockSsim2D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetBlockSsim2D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetBlockSsim2D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetBlockSsim2D.setArg(argn++, clRepetitionMap); + + int localWorkSize = min(device.getMaxWorkGroupSize(), 256); + int globalWorkSize = roundUp(localWorkSize, imageSize); + + queue.put2DRangeKernel(kernelGetBlockSsim2D, 0, 0, imageWidth, imageHeight, + 0, 0); + queue.finish(); + + // Read the repetition map back from the device + queue.putReadBuffer(clRepetitionMap, true); + for (int y=0; y0.0f) { + Utils.RelevanceMask relevanceMask = Utils.getRelevanceMask(inputImage2D.getImageArray(), imageWidth, + imageHeight, bRW, bRH, localStatistics.getLocalStds(), relevanceConstant); + relevanceMaskArray = relevanceMask.getRelevanceMask(); + repetitionMap = Utils.applyMask2D(repetitionMap, imageWidth, imageHeight, relevanceMaskArray); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage2D(repetitionMap, imageWidth, imageHeight, bRW, bRH, + relevanceMaskArray); + } + + // Release memory + context.release(); + + return repetitionMap; + } + + + /** + * Computes the block-wise Normalized Root Mean Square Error (NRMSE) for a 2D image using OpenCL. + * + * This method calculates the NRMSE between a reference block and blocks of the input image, + * generating a repetition map that indicates the similarity (inverted NRMSE) across the image. + * It allows for optional relevance masking based on local standard deviations and a relevance constant. + * + * @param inputImage2D the input 2D image for NRMSE calculation + * @param referenceBlock2D the reference block used for comparison in the NRMSE calculation + * @param relevanceConstant a constant value used to control the strength of the structural relevance filter + * @param normalizeOutput a boolean indicating whether to normalize the output repetition map + * @param useDevice a boolean indicating whether to use the user-defined OpenCL device preference + * @return a float array representing the repetition map, indicating the inverted NRMSE coefficients + * across the image (higher values indicate greater similarity) + */ + public static float[] getBlockNrmse2D(Utils.InputImage2D inputImage2D, + Utils.ReferenceBlock2D referenceBlock2D, + float relevanceConstant, + boolean normalizeOutput, + boolean useDevice) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage2D.getWidth(); + int imageHeight = inputImage2D.getHeight(); + int imageSize = inputImage2D.getSize(); + int blockWidth = referenceBlock2D.getWidth(); + int blockHeight = referenceBlock2D.getHeight(); + int blockSize = referenceBlock2D.getSize(); + int bRW = referenceBlock2D.getRadiusWidth(); + int bRH = referenceBlock2D.getRadiusHeight(); + + // Initialize OpenCL + CLUtils.OpenCLResources openCLResources = CLUtils.getOpenCLResources(useDevice); + + // Retrieve OpenCL context, device and queue + CLContext context = openCLResources.getContext(); + CLDevice device = openCLResources.getDevice(); + CLCommandQueue queue = openCLResources.getQueue(); + + // Calculate local statistics + CLLocalStatistics localStatistics = CLUtils.getLocalStatistics2D(context, device, queue, inputImage2D, + blockWidth, blockHeight, Utils.EPSILON); + + // Create and fill OpenCL buffers + CLBuffer clBlockPixels = CLUtils.createAndFillCLBuffer(context, blockSize, READ_ONLY, + referenceBlock2D.getPixels()); + queue.putWriteBuffer(clBlockPixels, true); + + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(context, imageSize, READ_WRITE, + repetitionMap); + + // Build OpenCL program + String programStringGetBlockNrmse2D = getResourceAsString(CLUtils.class, "kernelGetBlockNrmse2D.cl"); + programStringGetBlockNrmse2D = replaceFirst(programStringGetBlockNrmse2D, "$WIDTH$", "" + imageWidth); + programStringGetBlockNrmse2D = replaceFirst(programStringGetBlockNrmse2D, "$HEIGHT$", "" + imageHeight); + programStringGetBlockNrmse2D = replaceFirst(programStringGetBlockNrmse2D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetBlockNrmse2D = replaceFirst(programStringGetBlockNrmse2D, "$BW$", "" + blockWidth); + programStringGetBlockNrmse2D = replaceFirst(programStringGetBlockNrmse2D, "$BH$", "" + blockHeight); + programStringGetBlockNrmse2D = replaceFirst(programStringGetBlockNrmse2D, "$BRW$", "" + bRW); + programStringGetBlockNrmse2D = replaceFirst(programStringGetBlockNrmse2D, "$BRH$", "" + bRH); + programStringGetBlockNrmse2D = replaceFirst(programStringGetBlockNrmse2D, "$BLOCK_MEAN$", "" + referenceBlock2D.getMean()); + programStringGetBlockNrmse2D = replaceFirst(programStringGetBlockNrmse2D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetBlockNrmse2D = context.createProgram(programStringGetBlockNrmse2D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetBlockNrmse2D = programGetBlockNrmse2D.createCLKernel("kernelGetBlockNrmse2D"); + + int argn = 0; + kernelGetBlockNrmse2D.setArg(argn++, clBlockPixels); + kernelGetBlockNrmse2D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetBlockNrmse2D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetBlockNrmse2D.setArg(argn++, clRepetitionMap); + + int localWorkSize = min(device.getMaxWorkGroupSize(), 256); + int globalWorkSize = roundUp(localWorkSize, imageSize); + + queue.put2DRangeKernel(kernelGetBlockNrmse2D, 0, 0, + imageWidth, imageHeight, + 0, 0); + queue.finish(); + + // Read the repetition map back from the device + queue.putReadBuffer(clRepetitionMap, true); + for (int y=bRH; y0.0f) { + float rmse = repetitionMap[index]; + if (rmse == 0.0f) { // Special case where RMSE is 0, 1/rmse would be undefined but we want perfect similarity + repetitionMap[index] = 1.0f; + } else { + repetitionMap[index] = 1.0f / rmse; + } + } + } + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage2D(repetitionMap, imageWidth, imageHeight, bRW, bRH, + relevanceMaskArray); + } + + // Release memory + context.release(); + + return repetitionMap; + } + + + /** + * Computes the block-wise absolute difference of standard deviations for a 2D image using OpenCL. + * + * This method calculates the absolute difference between the standard deviation of a reference block + * and the local standard deviations of the input image. The resulting repetition map indicates the + * dissimilarity (inverted similarity) across the image, allowing for optional relevance masking + * based on local standard deviations and a relevance constant. + * + * @param inputImage2D the input 2D image for dissimilarity calculation + * @param referenceBlock2D the reference block used for comparison in the dissimilarity calculation + * @param relevanceConstant a constant value used to control the strength of the structural relevance filter + * @param normalizeOutput a boolean indicating whether to normalize the output repetition map + * @param useDevice a boolean indicating whether to use the user-defined OpenCL device preference + * @return a float array representing the repetition map, indicating the similarity based on + * absolute differences in standard deviations (higher values indicate greater similarity) + */ + public static float[] getBlockAbsDiffStds2D(Utils.InputImage2D inputImage2D, + Utils.ReferenceBlock2D referenceBlock2D, + float relevanceConstant, + boolean normalizeOutput, + boolean useDevice) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage2D.getWidth(); + int imageHeight = inputImage2D.getHeight(); + int imageSize = inputImage2D.getSize(); + int blockWidth = referenceBlock2D.getWidth(); + int blockHeight = referenceBlock2D.getHeight(); + int blockSize = referenceBlock2D.getSize(); + int bRW = referenceBlock2D.getRadiusWidth(); + int bRH = referenceBlock2D.getRadiusHeight(); + + // Initialize OpenCL + CLUtils.OpenCLResources openCLResources = CLUtils.getOpenCLResources(useDevice); + + // Retrieve OpenCL context, device and queue + CLContext context = openCLResources.getContext(); + CLDevice device = openCLResources.getDevice(); + CLCommandQueue queue = openCLResources.getQueue(); + + // Calculate local statistics + CLLocalStatistics localStatistics = CLUtils.getLocalStatistics2D(context, device, queue, inputImage2D, + blockWidth, blockHeight, Utils.EPSILON); + + // Create and fill OpenCL buffers + CLBuffer clBlockPixels = CLUtils.createAndFillCLBuffer(context, blockSize, READ_ONLY, + referenceBlock2D.getPixels()); + queue.putWriteBuffer(clBlockPixels, true); + + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(context, imageSize, READ_WRITE, + repetitionMap); + + // Build OpenCL program + String programStringGetBlockAbsDiffStds2D = getResourceAsString(CLUtils.class, "kernelGetBlockAbsDiffStds2D.cl"); + programStringGetBlockAbsDiffStds2D = replaceFirst(programStringGetBlockAbsDiffStds2D, "$WIDTH$", "" + imageWidth); + programStringGetBlockAbsDiffStds2D = replaceFirst(programStringGetBlockAbsDiffStds2D, "$HEIGHT$", "" + imageHeight); + programStringGetBlockAbsDiffStds2D = replaceFirst(programStringGetBlockAbsDiffStds2D, "$BRW$", "" + bRW); + programStringGetBlockAbsDiffStds2D = replaceFirst(programStringGetBlockAbsDiffStds2D, "$BRH$", "" + bRH); + programStringGetBlockAbsDiffStds2D = replaceFirst(programStringGetBlockAbsDiffStds2D, "$BLOCK_STD$", "" + referenceBlock2D.getStd()); + programStringGetBlockAbsDiffStds2D = replaceFirst(programStringGetBlockAbsDiffStds2D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetBlockAbsDiffStds2D = context.createProgram(programStringGetBlockAbsDiffStds2D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetBlockAbsDiffStds2D = programGetBlockAbsDiffStds2D.createCLKernel("kernelGetBlockAbsDiffStds2D"); + + int argn = 0; + kernelGetBlockAbsDiffStds2D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetBlockAbsDiffStds2D.setArg(argn++, clRepetitionMap); + + int localWorkSize = min(device.getMaxWorkGroupSize(), 256); + int globalWorkSize = roundUp(localWorkSize, imageSize); + + queue.put2DRangeKernel(kernelGetBlockAbsDiffStds2D, 0, 0, imageWidth, + imageHeight, 0, 0); + queue.finish(); + + // Read the repetition map back from the device + queue.putReadBuffer(clRepetitionMap, true); + for (int y=0; y0.0f) { + Utils.RelevanceMask relevanceMask = Utils.getRelevanceMask(inputImage2D.getImageArray(), imageWidth, + imageHeight, bRW, bRH, localStatistics.getLocalStds(), relevanceConstant); + relevanceMaskArray = relevanceMask.getRelevanceMask(); + repetitionMap = Utils.applyMask2D(repetitionMap, imageWidth, imageHeight, relevanceMaskArray); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage2D(repetitionMap, imageWidth, imageHeight, bRW, bRH, + relevanceMaskArray); + } + + // Release memory + context.release(); + + return repetitionMap; + } + + + /** + * Calculate the 2D repetition map based on the selected metric. + * + * @param metric The selected metric for repetition calculation. + * @param inputImage The input image data. + * @param referenceBlock The reference block for comparison. + * @param relevanceConstant The relevance constant for calculation. + * @param normalizeOutput Whether to normalize the output. + * @param useDevice Whether to use the device from preferences. + * @return The calculated repetition map. + */ + public static float[] calculateBlockRepetitionMap2D(String metric, Utils.InputImage2D inputImage, + Utils.ReferenceBlock2D referenceBlock, float relevanceConstant, + boolean normalizeOutput, boolean useDevice) + { + if (metric.equals(BlockRepetition2D_.METRICS[0])) { + return CLUtils.getBlockPearson2D(inputImage, referenceBlock, relevanceConstant, normalizeOutput, useDevice); + } else if (metric.equals(BlockRepetition2D_.METRICS[1])) { + return CLUtils.getBlockCosineSimilarity2D(inputImage, referenceBlock, relevanceConstant, normalizeOutput, useDevice); + } else if (metric.equals(BlockRepetition2D_.METRICS[2])) { + return CLUtils.getBlockSsim2D(inputImage, referenceBlock, relevanceConstant, normalizeOutput, useDevice); + } else if (metric.equals(BlockRepetition2D_.METRICS[3])) { + return CLUtils.getBlockNrmse2D(inputImage, referenceBlock, relevanceConstant, normalizeOutput, useDevice); + } else if (metric.equals(BlockRepetition2D_.METRICS[4])) { + return CLUtils.getBlockAbsDiffStds2D(inputImage, referenceBlock, relevanceConstant, normalizeOutput, useDevice); + } else { + return null; // This is here just because the method requires a return statement outside the IF/ELSE clauses. + } + } + + + // ----------------------------------------- // + // ---- METHODS FOR BLOCK REPETITION 3D ---- // + // ----------------------------------------- // + + /** + * Calculates the local statistics (mean and standard deviation) for a 3D image using OpenCL. + * + * @param openCLResources an {@link OpenCLResources} object + * @param inputImage3D the input 3D image for which local statistics are computed, given as a flattened array + * @param blockSize the block size after removing pixels outside the inbound sphere/spheroid + * @param blockRadiusWidth the block radius along the width + * @param blockRadiusHeight the block radius along the height + * @param blockRadiusDepth the block radius along the depth + * @param EPSILON a small value to prevent division by zero in calculations + * @return a {@link CLLocalStatistics} object containing the calculated local means and standard deviations, + * as well as the OpenCL buffers used during computation + */ + public static CLLocalStatistics getLocalStatistics3D(OpenCLResources openCLResources, + Utils.InputImage3D inputImage3D, + int blockSize, int blockRadiusWidth, int blockRadiusHeight, + int blockRadiusDepth, float EPSILON) + { + + IJ.log("Calculating local statistics..."); + + // Cache variables + int imageWidth = inputImage3D.getWidth(); + int imageHeight = inputImage3D.getHeight(); + int imageDepth = inputImage3D.getDepth(); + int imageSize = inputImage3D.getSize(); + + // Create and fill OpenCL buffers + CLBuffer clImageArray = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, READ_ONLY, inputImage3D.getImageArray()); + openCLResources.getQueue().putWriteBuffer(clImageArray, true); + + float[] localMeans = new float[inputImage3D.getSize()]; + CLBuffer clLocalMeans = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, READ_WRITE, localMeans); + openCLResources.getQueue().putWriteBuffer(clLocalMeans, true); + + float[] localStds = new float[inputImage3D.getSize()]; + CLBuffer clLocalStds = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, READ_WRITE, localStds); + openCLResources.getQueue().putWriteBuffer(clLocalStds, true); + + // Create OpenCL program + String programStringGetLocalStatistics3D = getResourceAsString(CLUtils.class, "kernelGetLocalStatistics3D.cl"); + programStringGetLocalStatistics3D = replaceFirst(programStringGetLocalStatistics3D, "$WIDTH$", "" + imageWidth); + programStringGetLocalStatistics3D = replaceFirst(programStringGetLocalStatistics3D, "$HEIGHT$", "" + imageHeight); + programStringGetLocalStatistics3D = replaceFirst(programStringGetLocalStatistics3D, "$DEPTH$", "" + imageDepth); + programStringGetLocalStatistics3D = replaceFirst(programStringGetLocalStatistics3D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetLocalStatistics3D = replaceFirst(programStringGetLocalStatistics3D, "$BRW$", "" + blockRadiusWidth); + programStringGetLocalStatistics3D = replaceFirst(programStringGetLocalStatistics3D, "$BRH$", "" + blockRadiusHeight); + programStringGetLocalStatistics3D = replaceFirst(programStringGetLocalStatistics3D, "$BRZ$", "" + blockRadiusDepth); + programStringGetLocalStatistics3D = replaceFirst(programStringGetLocalStatistics3D, "$EPSILON$", "" + EPSILON); + CLProgram programGetLocalStatistics3D = openCLResources.getContext().createProgram(programStringGetLocalStatistics3D).build(); + + // Crete OpenCL kernel and set args + CLKernel kernelGetLocalStatistics3D = programGetLocalStatistics3D.createCLKernel("kernelGetLocalStatistics3D"); + + int argn = 0; + kernelGetLocalStatistics3D.setArg(argn++, clImageArray); + kernelGetLocalStatistics3D.setArg(argn++, clLocalMeans); + kernelGetLocalStatistics3D.setArg(argn++, clLocalStds); + + int localWorkSize = min(openCLResources.getDevice().getMaxWorkGroupSize(), 256); + int globalWorkSize = roundUp(localWorkSize, imageSize); + + openCLResources.getQueue().put3DRangeKernel(kernelGetLocalStatistics3D, + 0, 0, 0, + imageWidth, imageHeight, imageDepth, + 0, 0, 0); + openCLResources.getQueue().finish(); + + // Read the local means map back from the device + openCLResources.getQueue().putReadBuffer(clLocalMeans, true); + for(int z=0; z clBlockPixels = CLUtils.createAndFillCLBuffer(context, blockSize, READ_ONLY, + referenceBlock3D.getPixels()); + queue.putWriteBuffer(clBlockPixels, true); + + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(context, imageSize, READ_WRITE, + repetitionMap); + + // Build OpenCL program + String programStringGetBlockPearson3D = getResourceAsString(CLUtils.class, "kernelGetBlockPearson3D.cl"); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$WIDTH$", "" + imageWidth); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$HEIGHT$", "" + imageHeight); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$DEPTH$", "" + imageDepth); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$BLOCK_WIDTH$", "" + blockWidth); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$BLOCK_HEIGHT$", "" + blockHeight); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$BLOCK_DEPTH$", "" + blockDepth); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$BRW$", "" + blockRadiusWidth); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$BRH$", "" + blockRadiusHeight); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$BRZ$", "" + blockRadiusDepth); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$BLOCK_MEAN$", "" + referenceBlock3D.getMean()); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$BLOCK_STD$", "" + referenceBlock3D.getStd()); + programStringGetBlockPearson3D = replaceFirst(programStringGetBlockPearson3D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetBlockPearson3D = context.createProgram(programStringGetBlockPearson3D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetBlockPearson3D = programGetBlockPearson3D.createCLKernel("kernelGetBlockPearson3D"); + + int argn = 0; + kernelGetBlockPearson3D.setArg(argn++, clBlockPixels); + kernelGetBlockPearson3D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetBlockPearson3D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetBlockPearson3D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetBlockPearson3D.setArg(argn++, clRepetitionMap); + + int localWorkSize = min(device.getMaxWorkGroupSize(), 256); + int globalWorkSize = roundUp(localWorkSize, imageSize); + + queue.put3DRangeKernel(kernelGetBlockPearson3D, + 0, 0, 0, + imageWidth, imageHeight, imageDepth, + 0, 0, 0); + queue.finish(); + + // Read the repetition map back from the device + queue.putReadBuffer(clRepetitionMap, true); + for (int z=blockRadiusDepth; z0.0f) { + relevanceMaskArray = Utils.getRelevanceMask3D(imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, localStatistics.getLocalStds(), relevanceConstant); + + repetitionMap = Utils.applyMask3D(repetitionMap, imageWidth, imageHeight, imageDepth, relevanceMaskArray); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage3D(repetitionMap, imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, relevanceMaskArray); + } + + // Release memory + context.release(); + + return repetitionMap; + } + + + /** + * Computes the block-wise cosine similarity for a 3D image using OpenCL. + * + * This method calculates the cosine similarity between a reference block and all blocks + * of the input image. The result is stored in a repetition map, indicating the degree + * of similarity across the image. Optionally, a relevance mask can be applied to filter + * the results based on local standard deviations and a relevance constant. + * + * @param inputImage3D the input 3D image for similarity calculation + * @param referenceBlock3D the reference block used for comparison in the similarity calculation + * @param relevanceConstant a constant value used to control the strength of the structural relevance filter + * @param normalizeOutput a boolean indicating whether to normalize the output repetition map + * @param useDevice a boolean indicating whether to use the user-defined OpenCL device preference + * @return a float array representing the repetition map, indicating the cosine similarity + * coefficients across the image + */ + public static float[] getBlockCosineSimilarity3D(Utils.InputImage3D inputImage3D, + Utils.ReferenceBlock3D referenceBlock3D, float relevanceConstant, + boolean normalizeOutput, boolean useDevice) + { + // Cache variables + int imageWidth = inputImage3D.getWidth(); + int imageHeight = inputImage3D.getHeight(); + int imageSize = inputImage3D.getSize(); + int imageDepth = inputImage3D.getDepth(); + int blockSize = referenceBlock3D.getSize(); + int blockRadiusWidth = referenceBlock3D.getRadiusWidth(); + int blockRadiusHeight = referenceBlock3D.getRadiusHeight(); + int blockRadiusDepth = referenceBlock3D.getRadiusDepth(); + + // Initialize OpenCL + CLUtils.OpenCLResources openCLResources = CLUtils.getOpenCLResources(useDevice); + + // Retrieve OpenCL context, device and queue + CLContext context = openCLResources.getContext(); + CLDevice device = openCLResources.getDevice(); + CLCommandQueue queue = openCLResources.getQueue(); + + // Calculate local statistics + CLLocalStatistics localStatistics = getLocalStatistics3D(openCLResources, inputImage3D, blockSize, + blockRadiusWidth, blockRadiusHeight, blockRadiusDepth, Utils.EPSILON); + + // Create and fill OpenCL buffers + IJ.log("Calculating Structural Repetition Scores..."); + + CLBuffer clBlockPixels = createAndFillCLBuffer(context, blockSize, READ_ONLY, + referenceBlock3D.getPixels()); + queue.putWriteBuffer(clBlockPixels, true); + + float[] repetitionMap = new float[imageSize]; // Create array to hold the repetition map + Arrays.fill(repetitionMap, 0.0f); // Fill with zeroes just be sure + + CLBuffer clRepetitionMap = createAndFillCLBuffer(context, imageSize, READ_WRITE, + repetitionMap); + + // Build OpenCL program + String programStringGetBlockCosineSimilarity3D = getResourceAsString(CLUtils.class, "kernelGetBlockCosineSimilarity3D.cl"); + programStringGetBlockCosineSimilarity3D = replaceFirst(programStringGetBlockCosineSimilarity3D, "$WIDTH$", "" + imageWidth); + programStringGetBlockCosineSimilarity3D = replaceFirst(programStringGetBlockCosineSimilarity3D, "$HEIGHT$", "" + imageHeight); + programStringGetBlockCosineSimilarity3D = replaceFirst(programStringGetBlockCosineSimilarity3D, "$DEPTH$", "" + imageDepth); + programStringGetBlockCosineSimilarity3D = replaceFirst(programStringGetBlockCosineSimilarity3D, "$BRW$", "" + blockRadiusWidth); + programStringGetBlockCosineSimilarity3D = replaceFirst(programStringGetBlockCosineSimilarity3D, "$BRH$", "" + blockRadiusHeight); + programStringGetBlockCosineSimilarity3D = replaceFirst(programStringGetBlockCosineSimilarity3D, "$BRZ$", "" + blockRadiusDepth); + programStringGetBlockCosineSimilarity3D = replaceFirst(programStringGetBlockCosineSimilarity3D, "$BLOCK_STD$", "" + referenceBlock3D.getStd()); + programStringGetBlockCosineSimilarity3D = replaceFirst(programStringGetBlockCosineSimilarity3D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetBlockCosineSimilarity3D = context.createProgram(programStringGetBlockCosineSimilarity3D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetBlockCosineSimilarity3D = programGetBlockCosineSimilarity3D.createCLKernel("kernelGetBlockCosineSimilarity3D"); + + int argn = 0; + kernelGetBlockCosineSimilarity3D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetBlockCosineSimilarity3D.setArg(argn++, clRepetitionMap); + + int localWorkSize = min(device.getMaxWorkGroupSize(), 256); + int globalWorkSize = roundUp(localWorkSize, imageSize); + + queue.put3DRangeKernel(kernelGetBlockCosineSimilarity3D, + 0, 0, 0, + imageWidth, imageHeight, imageDepth, + 0, 0, 0); + + queue.finish(); + + // Read the repetition map back from the device + queue.putReadBuffer(clRepetitionMap, true); + for(int z=blockRadiusDepth; z0.0f) { + relevanceMaskArray = Utils.getRelevanceMask3D(imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, localStatistics.getLocalStds(), relevanceConstant); + + repetitionMap = Utils.applyMask3D(repetitionMap, imageWidth, imageHeight, imageDepth, relevanceMaskArray); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage3D(repetitionMap, imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, relevanceMaskArray); + } + + // Release memory + context.release(); + + return repetitionMap; + } + + + /** + * Computes the block-wise Structural Similarity Index (SSIM) for a 3D image using OpenCL. + * + * This method calculates the SSIM between a reference block and all blocks of the input image, + * generating a repetition map that indicates the similarity strength across the image. + * Optionally, a relevance mask can be applied to filter the results based on local + * standard deviations and a relevance constant. + * + * @param inputImage3D the input 3D image for SSIM calculation + * @param referenceBlock3D the reference block used for comparison in the SSIM calculation + * @param relevanceConstant a constant value used to control the strength of the structural relevance filter + * @param normalizeOutput a boolean indicating whether to normalize the output repetition map + * @param useDevice a boolean indicating whether to use the user-defined OpenCL device preference + * @return a float array representing the repetition map, indicating the SSIM coefficients + * across the image + */ + public static float[] getBlockSsim3D(Utils.InputImage3D inputImage3D, Utils.ReferenceBlock3D referenceBlock3D, + float relevanceConstant, boolean normalizeOutput, boolean useDevice) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage3D.getWidth(); + int imageHeight = inputImage3D.getHeight(); + int imageDepth = inputImage3D.getDepth(); + int imageSize = inputImage3D.getSize(); + int blockWidth = referenceBlock3D.getWidth(); + int blockHeight = referenceBlock3D.getHeight(); + int blockDepth = referenceBlock3D.getDepth(); + int blockSize = referenceBlock3D.getSize(); + int blockRadiusWidth = referenceBlock3D.getRadiusWidth(); + int blockRadiusHeight = referenceBlock3D.getRadiusHeight(); + int blockRadiusDepth = referenceBlock3D.getRadiusDepth(); + + // Initialize OpenCL + CLUtils.OpenCLResources openCLResources = CLUtils.getOpenCLResources(useDevice); + + // Retrieve OpenCL context, device and queue + CLContext context = openCLResources.getContext(); + CLDevice device = openCLResources.getDevice(); + CLCommandQueue queue = openCLResources.getQueue(); + + // Calculate local statistics + CLLocalStatistics localStatistics = CLUtils.getLocalStatistics3D(openCLResources, inputImage3D, blockSize, + blockRadiusWidth, blockRadiusHeight, blockRadiusDepth, Utils.EPSILON); + + // Create and fill OpenCL buffers + CLBuffer clBlockPixels = CLUtils.createAndFillCLBuffer(context, blockSize, READ_ONLY, + referenceBlock3D.getPixels()); + queue.putWriteBuffer(clBlockPixels, true); + + float[] repetitionMap = new float[imageSize]; + Arrays.fill(repetitionMap, 0.0f); + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(context, imageSize, READ_WRITE, + repetitionMap); + + // Build OpenCL program + String programStringGetBlockSsim3D = getResourceAsString(CLUtils.class, "kernelGetBlockSsim3D.cl"); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$WIDTH$", "" + imageWidth); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$HEIGHT$", "" + imageHeight); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$DEPTH$", "" + imageDepth); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$BW$", "" + blockWidth); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$BH$", "" + blockHeight); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$BZ$", "" + blockDepth); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$BRW$", "" + blockWidth); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$BRH$", "" + blockRadiusHeight); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$BRZ$", "" + blockRadiusDepth); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$BLOCK_MEAN$", "" + referenceBlock3D.getMean()); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$BLOCK_STD$", "" + referenceBlock3D.getStd()); + programStringGetBlockSsim3D = replaceFirst(programStringGetBlockSsim3D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetBlockSsim3D = context.createProgram(programStringGetBlockSsim3D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetBlockSsim3D = programGetBlockSsim3D.createCLKernel("kernelGetBlockSsim3D"); + + int argn = 0; + kernelGetBlockSsim3D.setArg(argn++, clBlockPixels); + kernelGetBlockSsim3D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetBlockSsim3D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetBlockSsim3D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetBlockSsim3D.setArg(argn++, clRepetitionMap); + + int localWorkSize = min(device.getMaxWorkGroupSize(), 256); + + queue.put3DRangeKernel(kernelGetBlockSsim3D, 0, 0, 0, + imageWidth, imageHeight, imageDepth, + 0, 0, 0); + queue.finish(); + + // Read the repetition map back from the device + queue.putReadBuffer(clRepetitionMap, true); + for(int z=blockRadiusDepth; z0.0f) { + relevanceMaskArray = Utils.getRelevanceMask3D(imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, localStatistics.getLocalStds(), relevanceConstant); + + repetitionMap = Utils.applyMask3D(repetitionMap, imageWidth, imageHeight, imageDepth, relevanceMaskArray); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage3D(repetitionMap, imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, relevanceMaskArray); + } + + // Release memory + context.release(); + + return repetitionMap; + } + + + /** + * Computes the block-wise Normalized Root Mean Square Error (NRMSE) for a 3D image using OpenCL. + * + * This method calculates the NRMSE between a reference block and blocks of the input image, + * generating a repetition map that indicates the similarity (inverted NRMSE) across the image. + * It allows for optional relevance masking based on local standard deviations and a relevance constant. + * + * @param inputImage3D the input 3D image for NRMSE calculation + * @param referenceBlock3D the reference block used for comparison in the NRMSE calculation + * @param relevanceConstant a constant value used to control the strength of the structural relevance filter + * @param normalizeOutput a boolean indicating whether to normalize the output repetition map + * @param useDevice a boolean indicating whether to use the user-defined OpenCL device preference + * @return a float array representing the repetition map, indicating the inverted NRMSE coefficients + * across the image (higher values indicate greater similarity) + */ + public static float[] getBlockNrmse3D(Utils.InputImage3D inputImage3D, Utils.ReferenceBlock3D referenceBlock3D, + float relevanceConstant, boolean normalizeOutput, boolean useDevice) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage3D.getWidth(); + int imageHeight = inputImage3D.getHeight(); + int imageDepth = inputImage3D.getDepth(); + int imageSize = inputImage3D.getSize(); + int blockSize = referenceBlock3D.getSize(); + int blockRadiusWidth = referenceBlock3D.getRadiusWidth(); + int blockRadiusHeight = referenceBlock3D.getRadiusHeight(); + int blockRadiusDepth = referenceBlock3D.getRadiusDepth(); + + // Initialize OpenCL + CLUtils.OpenCLResources openCLResources = CLUtils.getOpenCLResources(useDevice); + + // Retrieve OpenCL context, device and queue + CLContext context = openCLResources.getContext(); + CLDevice device = openCLResources.getDevice(); + CLCommandQueue queue = openCLResources.getQueue(); + + // Calculate local statistics + CLLocalStatistics localStatistics = CLUtils.getLocalStatistics3D(openCLResources, inputImage3D, blockSize, + blockRadiusWidth, blockRadiusHeight, blockRadiusDepth, Utils.EPSILON); + + // Create and fill OpenCL buffers + CLBuffer clBlockPixels = CLUtils.createAndFillCLBuffer(context, blockSize, READ_ONLY, + referenceBlock3D.getPixels()); + queue.putWriteBuffer(clBlockPixels, true); + + float[] repetitionMap = new float[imageSize]; + Arrays.fill(repetitionMap, 0.0f); + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(context, imageSize, READ_WRITE, + repetitionMap); + + // Build OpenCL program + String programStringGetBlockNrmse3D = getResourceAsString(CLUtils.class, "kernelGetBlockNrmse3D.cl"); + programStringGetBlockNrmse3D = replaceFirst(programStringGetBlockNrmse3D, "$WIDTH$", "" + imageWidth); + programStringGetBlockNrmse3D = replaceFirst(programStringGetBlockNrmse3D, "$HEIGHT$", "" + imageHeight); + programStringGetBlockNrmse3D = replaceFirst(programStringGetBlockNrmse3D, "$DEPTH$", "" + imageDepth); + programStringGetBlockNrmse3D = replaceFirst(programStringGetBlockNrmse3D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetBlockNrmse3D = replaceFirst(programStringGetBlockNrmse3D, "$BRW$", "" + blockRadiusWidth); + programStringGetBlockNrmse3D = replaceFirst(programStringGetBlockNrmse3D, "$BRH$", "" + blockRadiusHeight); + programStringGetBlockNrmse3D = replaceFirst(programStringGetBlockNrmse3D, "$BRZ$", "" + blockRadiusDepth); + programStringGetBlockNrmse3D = replaceFirst(programStringGetBlockNrmse3D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetBlockNrmse3D = context.createProgram(programStringGetBlockNrmse3D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetBlockNrmse3D = programGetBlockNrmse3D.createCLKernel("kernelGetBlockNrmse3D"); + + int argn = 0; + kernelGetBlockNrmse3D.setArg(argn++, clBlockPixels); + kernelGetBlockNrmse3D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetBlockNrmse3D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetBlockNrmse3D.setArg(argn++, clRepetitionMap); + + queue.put3DRangeKernel(kernelGetBlockNrmse3D, 0, 0, 0, + imageWidth, imageHeight, imageDepth, + 0, 0, 0); + queue.finish(); + + // Read the repetition map back from the device + queue.putReadBuffer(clRepetitionMap, true); + for(int z=blockRadiusDepth; z0.0f) { + relevanceMask = Utils.getRelevanceMask3D(imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, localStatistics.getLocalStds(), relevanceConstant); + + repetitionMap = Utils.applyMask3D(repetitionMap, imageWidth, imageHeight, imageDepth, relevanceMask); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + if(relevanceConstant>0.0f) { + repetitionMap = Utils.normalizeImage3D(repetitionMap, + imageWidth, imageHeight, imageDepth, blockRadiusWidth, blockRadiusHeight, blockRadiusDepth, + relevanceMask); + }else{ + repetitionMap = Utils.normalizeImage3D(repetitionMap, + imageWidth, imageHeight, imageDepth, blockRadiusWidth, blockRadiusHeight, blockRadiusDepth, + null); + } + } + + // Release memory + context.release(); + + return repetitionMap; + } + + + /** + * Calculate the 3D repetition map based on the selected metric. + * + * @param metric The selected metric for repetition calculation. + * @param inputImage The input image data. + * @param referenceBlock The reference block for comparison. + * @param relevanceConstant The relevance constant for calculation. + * @param normalizeOutput Whether to normalize the output. + * @param useDevice Whether to use the device from preferences. + * @return The calculated repetition map. + */ + public static float[] calculateBlockRepetitionMap3D(String metric, Utils.InputImage3D inputImage, + Utils.ReferenceBlock3D referenceBlock, float relevanceConstant, + boolean normalizeOutput, boolean useDevice) + { + if (metric.equals(BlockRepetition3D_.METRICS[0])) { + return CLUtils.getBlockPearson3D(inputImage, referenceBlock, relevanceConstant, normalizeOutput, useDevice); + } else if (metric.equals(BlockRepetition3D_.METRICS[1])) { + return CLUtils.getBlockCosineSimilarity3D(inputImage, referenceBlock, relevanceConstant, normalizeOutput, useDevice); + } else if (metric.equals(BlockRepetition3D_.METRICS[2])) { + return CLUtils.getBlockSsim3D(inputImage, referenceBlock, relevanceConstant, normalizeOutput, useDevice); + } else if (metric.equals(BlockRepetition3D_.METRICS[3])) { + return CLUtils.getBlockNrmse3D(inputImage, referenceBlock, relevanceConstant, normalizeOutput, useDevice); + //} else if (metric.equals(BlockRepetition3D_.METRICS[4])) { + //return CLUtils.getBlockAbsDiffStds3D(inputImage, referenceBlock, relevanceConstant, normalizeOutput, useDevice); + } else { + return null; // This is here just because the method requires a return statement outside the IF clauses. + } + } + + + // ------------------------------------------ // + // ---- METHODS FOR GLOBAL REPETITION 2D ---- // + // ------------------------------------------ // + + /** + * Computes the Global Repetition Map based on Pearson correlations for a 2D image using OpenCL. + * + * This method calculates the global repetition map based on Pearson correlations. The result is stored in a global + * repetition map, indicating the relative repetition of each structural element across the image. + * Optionally, a relevance mask can be applied to filter the results based on local standard deviations and a + * relevance constant. + * + * @param inputImage a {@link Utils.InputImage2D} object + * @param localStatistics a {@link CLLocalStatistics} object + * @param blockWidth the width of the block used for the analysis + * @param blockHeight the height of the block used for the analysis + * @param blockSize the size of the block used for the analysis, after removing pixels outside the inbound circle/ellipse + * @param relevanceMask a {@link Utils.RelevanceMask} object + * @param nPixels the number of structurally relevant pixels (i.e., non-masked pixels) + * @param normalizeOutput a boolean to either normalize the output or not + * @param openCLResources an {@link OpenCLResources} object + * @return a float array representing the global repetition map, indicating the degree of repetition of each + * structural element across the entire image. + */ + public static float[] getGlobalPearson2D(Utils.InputImage2D inputImage, CLLocalStatistics localStatistics, + int blockWidth, int blockHeight, int blockSize, + Utils.RelevanceMask relevanceMask, float nPixels, boolean normalizeOutput, + OpenCLResources openCLResources + ){ + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage.getWidth(); + int imageHeight = inputImage.getHeight(); + int imageSize = inputImage.getSize(); + int blockRadiusWidth = blockWidth/2; + int blockRadiusHeight = blockHeight/2; + + // Create and fill OpenCL buffers + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, repetitionMap); + + float[] weightsSumMap = new float[imageSize]; + CLBuffer clWeightsSumMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, weightsSumMap); + + // Build OpenCL program + String programStringGetGlobalPearson2D = getResourceAsString(CLUtils.class, "kernelGetGlobalPearson2D.cl"); + programStringGetGlobalPearson2D = replaceFirst(programStringGetGlobalPearson2D, "$WIDTH$", "" + imageWidth); + programStringGetGlobalPearson2D = replaceFirst(programStringGetGlobalPearson2D, "$HEIGHT$", "" + imageHeight); + programStringGetGlobalPearson2D = replaceFirst(programStringGetGlobalPearson2D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetGlobalPearson2D = replaceFirst(programStringGetGlobalPearson2D, "$BRW$", "" + blockRadiusWidth); + programStringGetGlobalPearson2D = replaceFirst(programStringGetGlobalPearson2D, "$BRH$", "" + blockRadiusHeight); + programStringGetGlobalPearson2D = replaceFirst(programStringGetGlobalPearson2D, "$FILTER_PARAM$", "" + relevanceMask.getNoiseMeanVariance()); + programStringGetGlobalPearson2D = replaceFirst(programStringGetGlobalPearson2D, "$THRESHOLD$", "" + relevanceMask.getRelevanceThreshold()); + programStringGetGlobalPearson2D = replaceFirst(programStringGetGlobalPearson2D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetGlobalPearson2D = openCLResources.getContext().createProgram(programStringGetGlobalPearson2D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetGlobalPearson2D = programGetGlobalPearson2D.createCLKernel("kernelGetGlobalPearson2D"); + + int argn = 0; + kernelGetGlobalPearson2D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetGlobalPearson2D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetGlobalPearson2D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetGlobalPearson2D.setArg(argn++, clWeightsSumMap); + kernelGetGlobalPearson2D.setArg(argn++, clRepetitionMap); + + + // Calculate weighted mean Pearson's map + int nXBlocks = imageWidth/64 + ((imageWidth%64==0)?0:1); + int nYBlocks = imageHeight/64 + ((imageHeight%64==0)?0:1); + int totalBlocks = nXBlocks * nYBlocks; // Total number of blocks + + for (int nYB=0; nYB0.0f) { + repetitionMap = Utils.applyMask2D(repetitionMap, imageWidth, imageHeight, relevanceMask.getRelevanceMask()); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage2D(repetitionMap, imageWidth, imageHeight, blockRadiusWidth, + blockRadiusHeight, relevanceMask.getRelevanceMask()); + } + + // Release memory + openCLResources.getContext().release(); + + return repetitionMap; + } + + + /** + * Computes the Global Repetition Map based on the Cosine Similarity metric for a 2D image using OpenCL. + * + * This method calculates the global repetition map based on the Cosine Similarity metric. The result is stored in a global + * repetition map, indicating the relative repetition of each structural element across the image. + * Optionally, a relevance mask can be applied to filter the results based on local standard deviations and a + * relevance constant. + * + * @param inputImage a {@link Utils.InputImage2D} object + * @param localStatistics a {@link CLLocalStatistics} object + * @param blockWidth the width of the block used for the analysis + * @param blockHeight the height of the block used for the analysis + * @param relevanceMask a {@link Utils.RelevanceMask} object + * @param nPixels the number of structurally relevant pixels (i.e., non-masked pixels) + * @param normalizeOutput a boolean to either normalize the output or not + * @param openCLResources an {@link OpenCLResources} object + * @return a float array representing the global repetition map, indicating the degree of repetition of each + * structural element across the entire image. + */ + public static float[] getGlobalCosineSimilarity2D(Utils.InputImage2D inputImage, CLLocalStatistics localStatistics, + int blockWidth, int blockHeight, Utils.RelevanceMask relevanceMask, + float nPixels, boolean normalizeOutput, OpenCLResources openCLResources) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage.getWidth(); + int imageHeight = inputImage.getHeight(); + int imageSize = inputImage.getSize(); + int blockRadiusWidth = blockWidth/2; + int blockRadiusHeight = blockHeight/2; + + // Create and fill OpenCL buffers + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, repetitionMap); + + float[] weightsSumMap = new float[imageSize]; + CLBuffer clWeightsSumMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, weightsSumMap); + + // Build OpenCL program + String programStringGetGlobalCosineSimilarity2D = getResourceAsString(CLUtils.class, "kernelGetGlobalCosineSimilarity2D.cl"); + programStringGetGlobalCosineSimilarity2D = replaceFirst(programStringGetGlobalCosineSimilarity2D, "$WIDTH$", "" + imageWidth); + programStringGetGlobalCosineSimilarity2D = replaceFirst(programStringGetGlobalCosineSimilarity2D, "$HEIGHT$", "" + imageHeight); + programStringGetGlobalCosineSimilarity2D = replaceFirst(programStringGetGlobalCosineSimilarity2D, "$BRW$", "" + blockRadiusWidth); + programStringGetGlobalCosineSimilarity2D = replaceFirst(programStringGetGlobalCosineSimilarity2D, "$BRH$", "" + blockRadiusHeight); + programStringGetGlobalCosineSimilarity2D = replaceFirst(programStringGetGlobalCosineSimilarity2D, "$FILTER_PARAM$", "" + relevanceMask.getNoiseMeanVariance()); + programStringGetGlobalCosineSimilarity2D = replaceFirst(programStringGetGlobalCosineSimilarity2D, "$THRESHOLD$", "" + relevanceMask.getRelevanceThreshold()); + programStringGetGlobalCosineSimilarity2D = replaceFirst(programStringGetGlobalCosineSimilarity2D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetGlobalCosineSimilarity2D = openCLResources.getContext().createProgram(programStringGetGlobalCosineSimilarity2D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetGlobalCosineSimilarity2D = programGetGlobalCosineSimilarity2D.createCLKernel("kernelGetGlobalCosineSimilarity2D"); + + int argn = 0; + kernelGetGlobalCosineSimilarity2D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetGlobalCosineSimilarity2D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetGlobalCosineSimilarity2D.setArg(argn++, clWeightsSumMap); + kernelGetGlobalCosineSimilarity2D.setArg(argn++, clRepetitionMap); + + // Calculate weighted mean Pearson's map + int nXBlocks = imageWidth/64 + ((imageWidth%64==0)?0:1); + int nYBlocks = imageHeight/64 + ((imageHeight%64==0)?0:1); + int totalBlocks = nXBlocks * nYBlocks; // Total number of blocks + + for (int nYB=0; nYB0.0f) { + repetitionMap = Utils.applyMask2D(repetitionMap, imageWidth, imageHeight, relevanceMask.getRelevanceMask()); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage2D(repetitionMap, imageWidth, imageHeight, blockRadiusWidth, + blockRadiusHeight, relevanceMask.getRelevanceMask()); + } + + // Release memory + openCLResources.getContext().release(); + + return repetitionMap; + } + + + /** + * Computes the Global Repetition Map based on the Structural Similarity Index Metric (SSIM) for a 2D image using OpenCL. + * + * This method calculates the global repetition map based on the Structural Similarity Index Metric (SSIM). The result is stored in a global + * repetition map, indicating the relative repetition of each structural element across the image. + * Optionally, a relevance mask can be applied to filter the results based on local standard deviations and a + * relevance constant. + * + * @param inputImage a {@link Utils.InputImage2D} object + * @param localStatistics a {@link CLLocalStatistics} object + * @param blockWidth the width of the block used for the analysis + * @param blockHeight the height of the block used for the analysis + * @param blockSize the size of the block (in pixels) after removing pixels outside the inbound circle/ellipse + * @param relevanceMask a {@link Utils.RelevanceMask} object + * @param nPixels the number of structurally relevant pixels (i.e., non-masked pixels) + * @param normalizeOutput a boolean to either normalize the output or not + * @param openCLResources an OpenCLResources object + * @return a float array representing the global repetition map, indicating the degree of repetition of each + * structural element across the entire image. + */ + public static float[] getGlobalSsim2D(Utils.InputImage2D inputImage, CLLocalStatistics localStatistics, + int blockWidth, int blockHeight, int blockSize, + Utils.RelevanceMask relevanceMask, float nPixels, boolean normalizeOutput, + OpenCLResources openCLResources) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage.getWidth(); + int imageHeight = inputImage.getHeight(); + int imageSize = inputImage.getSize(); + int blockRadiusWidth = blockWidth/2; + int blockRadiusHeight = blockHeight/2; + + // Create and fill OpenCL buffers + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, repetitionMap); + + float[] weightsSumMap = new float[imageSize]; + CLBuffer clWeightsSumMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, weightsSumMap); + + // Build OpenCL program + String programStringGetGlobalSsim2D = getResourceAsString(CLUtils.class, "kernelGetGlobalSsim2D.cl"); + programStringGetGlobalSsim2D = replaceFirst(programStringGetGlobalSsim2D, "$WIDTH$", "" + imageWidth); + programStringGetGlobalSsim2D = replaceFirst(programStringGetGlobalSsim2D, "$HEIGHT$", "" + imageHeight); + programStringGetGlobalSsim2D = replaceFirst(programStringGetGlobalSsim2D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetGlobalSsim2D = replaceFirst(programStringGetGlobalSsim2D, "$BRW$", "" + blockRadiusWidth); + programStringGetGlobalSsim2D = replaceFirst(programStringGetGlobalSsim2D, "$BRH$", "" + blockRadiusHeight); + programStringGetGlobalSsim2D = replaceFirst(programStringGetGlobalSsim2D, "$FILTER_PARAM$", "" + relevanceMask.getNoiseMeanVariance()); + programStringGetGlobalSsim2D = replaceFirst(programStringGetGlobalSsim2D, "$THRESHOLD$", "" + relevanceMask.getRelevanceThreshold()); + programStringGetGlobalSsim2D = replaceFirst(programStringGetGlobalSsim2D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetGlobalSsim2D = openCLResources.getContext().createProgram(programStringGetGlobalSsim2D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetGlobalSsim2D = programGetGlobalSsim2D.createCLKernel("kernelGetGlobalSsim2D"); + + int argn = 0; + kernelGetGlobalSsim2D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetGlobalSsim2D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetGlobalSsim2D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetGlobalSsim2D.setArg(argn++, clWeightsSumMap); + kernelGetGlobalSsim2D.setArg(argn++, clRepetitionMap); + + // Calculate weighted mean Pearson's map + int nXBlocks = imageWidth/64 + ((imageWidth%64==0)?0:1); + int nYBlocks = imageHeight/64 + ((imageHeight%64==0)?0:1); + int totalBlocks = nXBlocks * nYBlocks; // Total number of blocks + + for (int nYB=0; nYB0.0f) { + repetitionMap = Utils.applyMask2D(repetitionMap, imageWidth, imageHeight, relevanceMask.getRelevanceMask()); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage2D(repetitionMap, imageWidth, imageHeight, blockRadiusWidth, + blockRadiusHeight, relevanceMask.getRelevanceMask()); + } + + // Release memory + openCLResources.getContext().release(); + + return repetitionMap; + } + + + /** + * Computes the Global Repetition Map based on the Normalized Root Mean Squared Error (NRMSE) for a 2D image using OpenCL. + * + * This method calculates the global repetition map based on the (inverted) Normalized Root Mean Squared Error (NRMSE). The result is stored in a global + * repetition map, indicating the relative repetition of each structural element across the image. + * Optionally, a relevance mask can be applied to filter the results based on local standard deviations and a + * relevance constant. + * + * @param inputImage a {@link Utils.InputImage2D} object + * @param localStatistics a {@link CLLocalStatistics} object + * @param blockWidth the width of the block used for the analysis + * @param blockHeight the height of the block used for the analysis + * @param blockSize the size of the block used for the anaysis, in pixels, after removing the pixels outside the inbound circle/ellipse + * @param relevanceMask a {@link Utils.RelevanceMask} object + * @param nPixels the number of structurally relevant pixels (i.e., non-masked pixels) + * @param normalizeOutput a boolean to either normalize the output or not + * @param openCLResources an {@link OpenCLResources} object + * @return a float array representing the global repetition map, indicating the degree of repetition of each + * structural element across the entire image. + */ + public static float[] getGlobalNrmse2D(Utils.InputImage2D inputImage, CLLocalStatistics localStatistics, + int blockWidth, int blockHeight, int blockSize, + Utils.RelevanceMask relevanceMask, float nPixels, boolean normalizeOutput, + OpenCLResources openCLResources) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage.getWidth(); + int imageHeight = inputImage.getHeight(); + int imageSize = inputImage.getSize(); + int blockRadiusWidth = blockWidth/2; + int blockRadiusHeight = blockHeight/2; + + // Create and fill OpenCL buffers + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, repetitionMap); + + float[] weightsSumMap = new float[imageSize]; + CLBuffer clWeightsSumMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, weightsSumMap); + + // Build OpenCL program + String programStringGetGlobalNrmse2D = getResourceAsString(CLUtils.class, "kernelGetGlobalNrmse2D.cl"); + programStringGetGlobalNrmse2D = replaceFirst(programStringGetGlobalNrmse2D, "$WIDTH$", "" + imageWidth); + programStringGetGlobalNrmse2D = replaceFirst(programStringGetGlobalNrmse2D, "$HEIGHT$", "" + imageHeight); + programStringGetGlobalNrmse2D = replaceFirst(programStringGetGlobalNrmse2D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetGlobalNrmse2D = replaceFirst(programStringGetGlobalNrmse2D, "$BRW$", "" + blockRadiusWidth); + programStringGetGlobalNrmse2D = replaceFirst(programStringGetGlobalNrmse2D, "$BRH$", "" + blockRadiusHeight); + programStringGetGlobalNrmse2D = replaceFirst(programStringGetGlobalNrmse2D, "$FILTER_PARAM$", "" + relevanceMask.getNoiseMeanVariance()); + programStringGetGlobalNrmse2D = replaceFirst(programStringGetGlobalNrmse2D, "$THRESHOLD$", "" + relevanceMask.getRelevanceThreshold()); + programStringGetGlobalNrmse2D = replaceFirst(programStringGetGlobalNrmse2D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetGlobalNrmse2D = openCLResources.getContext().createProgram(programStringGetGlobalNrmse2D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetGlobalNrmse2D = programGetGlobalNrmse2D.createCLKernel("kernelGetGlobalNrmse2D"); + + int argn = 0; + kernelGetGlobalNrmse2D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetGlobalNrmse2D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetGlobalNrmse2D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetGlobalNrmse2D.setArg(argn++, clWeightsSumMap); + kernelGetGlobalNrmse2D.setArg(argn++, clRepetitionMap); + + // Calculate weighted mean Pearson's map + int nXBlocks = imageWidth/64 + ((imageWidth%64==0)?0:1); + int nYBlocks = imageHeight/64 + ((imageHeight%64==0)?0:1); + int totalBlocks = nXBlocks * nYBlocks; // Total number of blocks + + for (int nYB=0; nYB0.0f) { + float rmse = repetitionMap[index]; + if (rmse == 0.0f) { // Special case where RMSE is 0, 1/rmse would be undefined but we want perfect similarity + repetitionMap[index] = 1.0f; + } else { + repetitionMap[index] = 1.0f / rmse; + } + } + } + } + + // Apply relevance mask + if(relevanceMask.getRelevanceConstant()>0.0f) { + repetitionMap = Utils.applyMask2D(repetitionMap, imageWidth, imageHeight, relevanceMask.getRelevanceMask()); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage2D(repetitionMap, imageWidth, imageHeight, blockRadiusWidth, + blockRadiusHeight, relevanceMask.getRelevanceMask()); + } + + // Release memory + openCLResources.getContext().release(); + + return repetitionMap; + } + + + /** + * Computes the Global Repetition Map based on the absolute difference of standard deviations for a 2D image using OpenCL. + * + * This method calculates the global repetition map based on the absolute difference of standard deviations. The result is stored in a global + * repetition map, indicating the relative repetition of each structural element across the image. + * Optionally, a relevance mask can be applied to filter the results based on local standard deviations and a + * relevance constant. + * + * @param inputImage a {@link Utils.InputImage2D} object + * @param localStatistics a {@link CLLocalStatistics} object + * @param blockWidth the width of the block used for the analysis + * @param blockHeight the height of the block used for the analysis + * @param relevanceMask a {@link Utils.RelevanceMask} object + * @param nPixels the number of structurally relevant pixels (i.e., non-masked pixels) + * @param normalizeOutput a boolean to either normalize the output or not + * @param openCLResources an OpenCLResources object + * @return a float array representing the global repetition map, indicating the degree of repetition of each + * structural element across the entire image. + */ + public static float[] getGlobalAbsDiffStds2D(Utils.InputImage2D inputImage, CLLocalStatistics localStatistics, + int blockWidth, int blockHeight, Utils.RelevanceMask relevanceMask, + float nPixels, boolean normalizeOutput, OpenCLResources openCLResources) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage.getWidth(); + int imageHeight = inputImage.getHeight(); + int imageSize = inputImage.getSize(); + int blockRadiusWidth = blockWidth/2; + int blockRadiusHeight = blockHeight/2; + + // Create and fill OpenCL buffers + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, repetitionMap); + + float[] weightsSumMap = new float[imageSize]; + CLBuffer clWeightsSumMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, weightsSumMap); + + // Build OpenCL program + String programStringGetGlobalAbsDiffStds2D = getResourceAsString(CLUtils.class, "kernelGetGlobalAbsDiffStds2D.cl"); + programStringGetGlobalAbsDiffStds2D = replaceFirst(programStringGetGlobalAbsDiffStds2D, "$WIDTH$", "" + imageWidth); + programStringGetGlobalAbsDiffStds2D = replaceFirst(programStringGetGlobalAbsDiffStds2D, "$HEIGHT$", "" + imageHeight); + programStringGetGlobalAbsDiffStds2D = replaceFirst(programStringGetGlobalAbsDiffStds2D, "$BRW$", "" + blockRadiusWidth); + programStringGetGlobalAbsDiffStds2D = replaceFirst(programStringGetGlobalAbsDiffStds2D, "$BRH$", "" + blockRadiusHeight); + programStringGetGlobalAbsDiffStds2D = replaceFirst(programStringGetGlobalAbsDiffStds2D, "$FILTER_PARAM$", "" + relevanceMask.getNoiseMeanVariance()); + programStringGetGlobalAbsDiffStds2D = replaceFirst(programStringGetGlobalAbsDiffStds2D, "$THRESHOLD$", "" + relevanceMask.getRelevanceThreshold()); + programStringGetGlobalAbsDiffStds2D = replaceFirst(programStringGetGlobalAbsDiffStds2D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetGlobalAbsDiffStds2D = openCLResources.getContext().createProgram(programStringGetGlobalAbsDiffStds2D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetGlobalAbsDiffStds2D = programGetGlobalAbsDiffStds2D.createCLKernel("kernelGetGlobalAbsDiffStds2D"); + + int argn = 0; + kernelGetGlobalAbsDiffStds2D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetGlobalAbsDiffStds2D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetGlobalAbsDiffStds2D.setArg(argn++, clWeightsSumMap); + kernelGetGlobalAbsDiffStds2D.setArg(argn++, clRepetitionMap); + + // Calculate weighted mean Pearson's map + int nXBlocks = imageWidth/64 + ((imageWidth%64==0)?0:1); + int nYBlocks = imageHeight/64 + ((imageHeight%64==0)?0:1); + int totalBlocks = nXBlocks * nYBlocks; // Total number of blocks + + for (int nYB=0; nYB0.0f) { + float srs = repetitionMap[index]; + if (srs == 0.0f) { // Special case where SRS is 0, 1/SRS would be undefined but we want perfect similarity + repetitionMap[index] = 1.0f; + } else { + repetitionMap[index] = 1.0f / srs; + } + } + } + } + + // Apply relevance mask + if(relevanceMask.getRelevanceConstant()>0.0f) { + repetitionMap = Utils.applyMask2D(repetitionMap, imageWidth, imageHeight, relevanceMask.getRelevanceMask()); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage2D(repetitionMap, imageWidth, imageHeight, blockRadiusWidth, + blockRadiusHeight, relevanceMask.getRelevanceMask()); + } + + // Release memory + openCLResources.getContext().release(); + + return repetitionMap; + } + + + /** + * Calculate a global repetition map for 2D data, based on the selected metric. + * + * @param metric The selected metric for repetition calculation. + * @param inputImage A {@link Utils.InputImage2D}. + * @param blockWidth The width of the block used for the analysis. + * @param blockHeight The height of the block used for the analysis. + * @param blockSize The size of the block used for the analysis, after removing pixels outside the inbound circle/ellipse. + * @param localStatistics A {@link CLLocalStatistics} object. + * @param relevanceMask A {@link Utils.RelevanceMask} object + * @param nPixels The number of structurally relevant pixels (i.e., non-masked pixels) + * @param normalizeOutput A boolean to either normalize the output or not. + * + * @return The calculated repetition map. + */ + public static float[] calculateGlobalRepetitionMap2D(String metric, Utils.InputImage2D inputImage, int blockWidth, + int blockHeight, int blockSize, + CLLocalStatistics localStatistics, + Utils.RelevanceMask relevanceMask, float nPixels, + boolean normalizeOutput, OpenCLResources openCLResources) + { + + if(metric.equals("Pearson's R")) { + return CLUtils.getGlobalPearson2D(inputImage, localStatistics, blockWidth, blockHeight, blockSize, + relevanceMask, nPixels, normalizeOutput, openCLResources); + }else if(metric.equals("Cosine similarity")) { + return CLUtils.getGlobalCosineSimilarity2D(inputImage, localStatistics, blockWidth, blockHeight, + relevanceMask, nPixels, normalizeOutput, openCLResources); + }else if(metric.equals("SSIM")){ + return CLUtils.getGlobalSsim2D(inputImage, localStatistics, blockWidth, blockHeight, blockSize, + relevanceMask, nPixels, normalizeOutput, openCLResources); + }else if(metric.equals("NRMSE")){ + return CLUtils.getGlobalNrmse2D(inputImage, localStatistics, blockWidth, blockHeight, blockSize, + relevanceMask, nPixels, normalizeOutput, openCLResources); + }else if(metric.equals("Abs. diff. of StdDevs")){ + return CLUtils.getGlobalAbsDiffStds2D(inputImage, localStatistics, blockWidth, blockHeight, + relevanceMask, nPixels, normalizeOutput, openCLResources); + }else{ + return null; + } + } + + + // ------------------------------------------ // + // ---- METHODS FOR GLOBAL REPETITION 3D ---- // + // ------------------------------------------ // + + + /** + * Computes the Global Repetition Map based on Pearson correlations for a 3D image using OpenCL. + * + * This method calculates the global repetition map based on Pearson correlations. The result is stored in a global + * repetition map, indicating the relative repetition of each structural element across the image. + * Optionally, a relevance mask can be applied to filter the results based on local standard deviations and a + * relevance constant. + * + * @param inputImage a {@link Utils.InputImage3D} object + * @param localStatistics a {@link CLLocalStatistics} object + * @param blockWidth the width of the block used for the analysis + * @param blockHeight the height of the block used for the analysis + * @param blockDepth the depth of the block used for the analysis + * @param blockSize the size of the block used for the analysis, after removing pixels outside the inbound circle/ellipse + * @param relevanceMask a {@link Utils.RelevanceMask} object + * @param nPixels the number of structurally relevant pixels (i.e., non-masked pixels) + * @param normalizeOutput a boolean to either normalize the output or not + * @param openCLResources an {@link OpenCLResources} object + * @return a float array representing the global repetition map, indicating the degree of repetition of each + * structural element across the entire image. + */ + public static float[] getGlobalPearson3D(Utils.InputImage3D inputImage, CLLocalStatistics localStatistics, + int blockWidth, int blockHeight, int blockDepth, int blockSize, + Utils.RelevanceMask relevanceMask, float nPixels, boolean normalizeOutput, + OpenCLResources openCLResources) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage.getWidth(); + int imageHeight = inputImage.getHeight(); + int imageDepth = inputImage.getDepth(); + int imageSize = inputImage.getSize(); + int blockRadiusWidth = blockWidth / 2; + int blockRadiusHeight = blockHeight / 2; + int blockRadiusDepth = blockDepth / 2; + + // Create and fill OpenCL buffers + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, repetitionMap); + + float[] weightsSumMap = new float[imageSize]; + CLBuffer clWeightsSumMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, weightsSumMap); + + // Build OpenCL program + String programStringGetGlobalPearson3D = getResourceAsString(CLUtils.class, "kernelGetGlobalPearson3D.cl"); + programStringGetGlobalPearson3D = replaceFirst(programStringGetGlobalPearson3D, "$WIDTH$", "" + imageWidth); + programStringGetGlobalPearson3D = replaceFirst(programStringGetGlobalPearson3D, "$HEIGHT$", "" + imageHeight); + programStringGetGlobalPearson3D = replaceFirst(programStringGetGlobalPearson3D, "$DEPTH$", "" + imageDepth); + programStringGetGlobalPearson3D = replaceFirst(programStringGetGlobalPearson3D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetGlobalPearson3D = replaceFirst(programStringGetGlobalPearson3D, "$BRW$", "" + blockRadiusWidth); + programStringGetGlobalPearson3D = replaceFirst(programStringGetGlobalPearson3D, "$BRH$", "" + blockRadiusHeight); + programStringGetGlobalPearson3D = replaceFirst(programStringGetGlobalPearson3D, "$BRZ$", "" + blockRadiusDepth); + programStringGetGlobalPearson3D = replaceFirst(programStringGetGlobalPearson3D, "$FILTER_PARAM$", "" + relevanceMask.getNoiseMeanVariance()); + programStringGetGlobalPearson3D = replaceFirst(programStringGetGlobalPearson3D, "$THRESHOLD$", "" + relevanceMask.getRelevanceThreshold()); + programStringGetGlobalPearson3D = replaceFirst(programStringGetGlobalPearson3D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetGlobalPearson3D = openCLResources.getContext().createProgram(programStringGetGlobalPearson3D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetGlobalPearson3D = programGetGlobalPearson3D.createCLKernel("kernelGetGlobalPearson3D"); + + int argn = 0; + kernelGetGlobalPearson3D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetGlobalPearson3D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetGlobalPearson3D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetGlobalPearson3D.setArg(argn++, clWeightsSumMap); + kernelGetGlobalPearson3D.setArg(argn++, clRepetitionMap); + + + // Calculate weighted mean Pearson's map + int nXBlocks = imageWidth / 64 + ((imageWidth % 64 == 0) ? 0 : 1); + int nYBlocks = imageHeight / 64 + ((imageHeight % 64 == 0) ? 0 : 1); + int nZBlocks = imageDepth / blockDepth + ((imageDepth % blockDepth == 0) ? 0 : 1); + int totalBlocks = nXBlocks * nYBlocks * nZBlocks; + + for (int nZB = 0; nZB < nZBlocks; nZB++) { + int zWorkSize = min(blockDepth, imageDepth - nZB * blockDepth); + for (int nYB = 0; nYB < nYBlocks; nYB++) { + int yWorkSize = min(64, imageHeight - nYB * 64); + for (int nXB = 0; nXB < nXBlocks; nXB++) { + int xWorkSize = min(64, imageWidth - nXB * 64); + float progressPercentage = ((nZB * nYBlocks * nXBlocks) + (nYB * nXBlocks) + nXB) / (float) totalBlocks * 100; + showStatus(String.format("Calculating global repetition... %d%%", Math.round(progressPercentage))); + openCLResources.getQueue().put3DRangeKernel(kernelGetGlobalPearson3D, + nXB * 64, nYB * 64, nZB * blockDepth, + xWorkSize, yWorkSize, zWorkSize, 0, 0, 0); + openCLResources.getQueue().finish(); + } + } + } + showStatus("Calculating global repetition... 100%"); + + // Read the repetition map back from the device and calculate the weighted average + openCLResources.getQueue().putReadBuffer(clRepetitionMap, true); + openCLResources.getQueue().putReadBuffer(clWeightsSumMap, true); + + for(int z=blockRadiusDepth; z0.0f) { + repetitionMap = Utils.applyMask3D(repetitionMap, imageWidth, imageHeight, imageDepth, + relevanceMask.getRelevanceMask()); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage3D(repetitionMap, imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, relevanceMask.getRelevanceMask()); + } + + // Release memory + openCLResources.getContext().release(); + + return repetitionMap; + } + + + /** + * Computes the Global Repetition Map based on the Cosine Similarity metric for a 3D image using OpenCL. + * + * This method calculates the global repetition map based on the Cosine Similarity metric. The result is stored in a global + * repetition map, indicating the relative repetition of each structural element across the image. + * Optionally, a relevance mask can be applied to filter the results based on local standard deviations and a + * relevance constant. + * + * @param inputImage a {@link Utils.InputImage3D} object + * @param localStatistics a {@link CLLocalStatistics} object + * @param blockWidth the width of the block used for the analysis + * @param blockHeight the height of the block used for the analysis + * @param blockDepth the depth of the block used for the analysis + * @param relevanceMask a {@link Utils.RelevanceMask} object + * @param nPixels the number of structurally relevant pixels (i.e., non-masked pixels) + * @param normalizeOutput a boolean to either normalize the output or not + * @param openCLResources an {@link OpenCLResources} object + * @return a float array representing the global repetition map, indicating the degree of repetition of each + * structural element across the entire image. + */ + public static float[] getGlobalCosineSimilarity3D(Utils.InputImage3D inputImage, CLLocalStatistics localStatistics, + int blockWidth, int blockHeight, int blockDepth, + Utils.RelevanceMask relevanceMask, float nPixels, + boolean normalizeOutput, OpenCLResources openCLResources) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage.getWidth(); + int imageHeight = inputImage.getHeight(); + int imageDepth = inputImage.getDepth(); + int imageSize = inputImage.getSize(); + int blockRadiusWidth = blockWidth / 2; + int blockRadiusHeight = blockHeight / 2; + int blockRadiusDepth = blockDepth / 2; + + // Create and fill OpenCL buffers + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, repetitionMap); + + float[] weightsSumMap = new float[imageSize]; + CLBuffer clWeightsSumMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, weightsSumMap); + + // Build OpenCL program + String programStringGetGlobalCosineSimilarity3D = getResourceAsString(CLUtils.class, "kernelGetGlobalCosineSimilarity3D.cl"); + programStringGetGlobalCosineSimilarity3D = replaceFirst(programStringGetGlobalCosineSimilarity3D, "$WIDTH$", "" + imageWidth); + programStringGetGlobalCosineSimilarity3D = replaceFirst(programStringGetGlobalCosineSimilarity3D, "$HEIGHT$", "" + imageHeight); + programStringGetGlobalCosineSimilarity3D = replaceFirst(programStringGetGlobalCosineSimilarity3D, "$DEPTH$", "" + imageDepth); + programStringGetGlobalCosineSimilarity3D = replaceFirst(programStringGetGlobalCosineSimilarity3D, "$BRW$", "" + blockRadiusWidth); + programStringGetGlobalCosineSimilarity3D = replaceFirst(programStringGetGlobalCosineSimilarity3D, "$BRH$", "" + blockRadiusHeight); + programStringGetGlobalCosineSimilarity3D = replaceFirst(programStringGetGlobalCosineSimilarity3D, "$BRZ$", "" + blockRadiusDepth); + programStringGetGlobalCosineSimilarity3D = replaceFirst(programStringGetGlobalCosineSimilarity3D, "$FILTER_PARAM$", "" + relevanceMask.getNoiseMeanVariance()); + programStringGetGlobalCosineSimilarity3D = replaceFirst(programStringGetGlobalCosineSimilarity3D, "$THRESHOLD$", "" + relevanceMask.getRelevanceThreshold()); + programStringGetGlobalCosineSimilarity3D = replaceFirst(programStringGetGlobalCosineSimilarity3D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetGlobalCosineSimilarity3D = openCLResources.getContext().createProgram(programStringGetGlobalCosineSimilarity3D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetGlobalCosineSimilarity3D = programGetGlobalCosineSimilarity3D.createCLKernel("kernelGetGlobalCosineSimilarity3D"); + + int argn = 0; + kernelGetGlobalCosineSimilarity3D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetGlobalCosineSimilarity3D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetGlobalCosineSimilarity3D.setArg(argn++, clWeightsSumMap); + kernelGetGlobalCosineSimilarity3D.setArg(argn++, clRepetitionMap); + + // Calculate weighted mean Pearson's map + int nXBlocks = imageWidth / 64 + ((imageWidth % 64 == 0) ? 0 : 1); + int nYBlocks = imageHeight / 64 + ((imageHeight % 64 == 0) ? 0 : 1); + int nZBlocks = imageDepth / blockDepth + ((imageDepth % blockDepth == 0) ? 0 : 1); + int totalBlocks = nXBlocks * nYBlocks * nZBlocks; + + for (int nZB = 0; nZB < nZBlocks; nZB++) { + int zWorkSize = min(blockDepth, imageDepth - nZB * blockDepth); + for (int nYB = 0; nYB < nYBlocks; nYB++) { + int yWorkSize = min(64, imageHeight - nYB * 64); + for (int nXB = 0; nXB < nXBlocks; nXB++) { + int xWorkSize = min(64, imageWidth - nXB * 64); + float progressPercentage = ((nZB * nYBlocks * nXBlocks) + (nYB * nXBlocks) + nXB) / (float) totalBlocks * 100; + showStatus(String.format("Calculating global repetition... %d%%", Math.round(progressPercentage))); + openCLResources.getQueue().put3DRangeKernel(kernelGetGlobalCosineSimilarity3D, + nXB * 64, nYB * 64, nZB * blockDepth, + xWorkSize, yWorkSize, zWorkSize, 0, 0, 0); + openCLResources.getQueue().finish(); + } + } + } + showStatus("Calculating global repetition... 100%"); + + // Read the repetition map back from the device and calculate the weighted average + openCLResources.getQueue().putReadBuffer(clRepetitionMap, true); + openCLResources.getQueue().putReadBuffer(clWeightsSumMap, true); + + for(int z=blockRadiusDepth; z0.0f) { + repetitionMap = Utils.applyMask3D(repetitionMap, imageWidth, imageHeight, imageDepth, + relevanceMask.getRelevanceMask()); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage3D(repetitionMap, imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, relevanceMask.getRelevanceMask()); + } + + // Release memory + openCLResources.getContext().release(); + + return repetitionMap; + } + + + /** + * Computes the Global Repetition Map based on the Structural Similarity Index metric (SSIM) for a 3D image using OpenCL. + * + * This method calculates the global repetition map based on the Structural Similarity Index metric (SSIM). The result is stored in a global + * repetition map, indicating the relative repetition of each structural element across the image. + * Optionally, a relevance mask can be applied to filter the results based on local standard deviations and a + * relevance constant. + * + * @param inputImage a {@link Utils.InputImage3D} object + * @param localStatistics a {@link CLLocalStatistics} object + * @param blockWidth the width of the block used for the analysis + * @param blockHeight the height of the block used for the analysis + * @param blockDepth the depth of the block used for the analysis + * @param blockSize the size of the block used for the analysis, after removing pixels outside the inbound circle/ellipse + * @param relevanceMask a {@link Utils.RelevanceMask} object + * @param nPixels the number of structurally relevant pixels (i.e., non-masked pixels) + * @param normalizeOutput a boolean to either normalize the output or not + * @param openCLResources an {@link OpenCLResources} object + * @return a float array representing the global repetition map, indicating the degree of repetition of each + * structural element across the entire image. + */ + public static float[] getGlobalSsim3D(Utils.InputImage3D inputImage, CLLocalStatistics localStatistics, + int blockWidth, int blockHeight, int blockDepth, int blockSize, + Utils.RelevanceMask relevanceMask, float nPixels, boolean normalizeOutput, + OpenCLResources openCLResources) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage.getWidth(); + int imageHeight = inputImage.getHeight(); + int imageDepth = inputImage.getDepth(); + int imageSize = inputImage.getSize(); + int blockRadiusWidth = blockWidth / 2; + int blockRadiusHeight = blockHeight / 2; + int blockRadiusDepth = blockDepth / 2; + + // Create and fill OpenCL buffers + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, repetitionMap); + + float[] weightsSumMap = new float[imageSize]; + CLBuffer clWeightsSumMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, weightsSumMap); + + // Build OpenCL program + String programStringGetGlobalSsim3D = getResourceAsString(CLUtils.class, "kernelGetGlobalSsim3D.cl"); + programStringGetGlobalSsim3D = replaceFirst(programStringGetGlobalSsim3D, "$WIDTH$", "" + imageWidth); + programStringGetGlobalSsim3D = replaceFirst(programStringGetGlobalSsim3D, "$HEIGHT$", "" + imageHeight); + programStringGetGlobalSsim3D = replaceFirst(programStringGetGlobalSsim3D, "$DEPTH$", "" + imageDepth); + programStringGetGlobalSsim3D = replaceFirst(programStringGetGlobalSsim3D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetGlobalSsim3D = replaceFirst(programStringGetGlobalSsim3D, "$BRW$", "" + blockRadiusWidth); + programStringGetGlobalSsim3D = replaceFirst(programStringGetGlobalSsim3D, "$BRH$", "" + blockRadiusHeight); + programStringGetGlobalSsim3D = replaceFirst(programStringGetGlobalSsim3D, "$BRZ$", "" + blockRadiusDepth); + programStringGetGlobalSsim3D = replaceFirst(programStringGetGlobalSsim3D, "$FILTER_PARAM$", "" + relevanceMask.getNoiseMeanVariance()); + programStringGetGlobalSsim3D = replaceFirst(programStringGetGlobalSsim3D, "$THRESHOLD$", "" + relevanceMask.getRelevanceThreshold()); + programStringGetGlobalSsim3D = replaceFirst(programStringGetGlobalSsim3D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetGlobalSsim3D = openCLResources.getContext().createProgram(programStringGetGlobalSsim3D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetGlobalSsim3D = programGetGlobalSsim3D.createCLKernel("kernelGetGlobalSsim3D"); + + int argn = 0; + kernelGetGlobalSsim3D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetGlobalSsim3D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetGlobalSsim3D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetGlobalSsim3D.setArg(argn++, clWeightsSumMap); + kernelGetGlobalSsim3D.setArg(argn++, clRepetitionMap); + + + // Calculate weighted mean Pearson's map + int nXBlocks = imageWidth / 64 + ((imageWidth % 64 == 0) ? 0 : 1); + int nYBlocks = imageHeight / 64 + ((imageHeight % 64 == 0) ? 0 : 1); + int nZBlocks = imageDepth / blockDepth + ((imageDepth % blockDepth == 0) ? 0 : 1); + int totalBlocks = nXBlocks * nYBlocks * nZBlocks; + + for (int nZB = 0; nZB < nZBlocks; nZB++) { + int zWorkSize = min(blockDepth, imageDepth - nZB * blockDepth); + for (int nYB = 0; nYB < nYBlocks; nYB++) { + int yWorkSize = min(64, imageHeight - nYB * 64); + for (int nXB = 0; nXB < nXBlocks; nXB++) { + int xWorkSize = min(64, imageWidth - nXB * 64); + float progressPercentage = ((nZB * nYBlocks * nXBlocks) + (nYB * nXBlocks) + nXB) / (float) totalBlocks * 100; + showStatus(String.format("Calculating global repetition... %d%%", Math.round(progressPercentage))); + openCLResources.getQueue().put3DRangeKernel(kernelGetGlobalSsim3D, + nXB * 64, nYB * 64, nZB * blockDepth, + xWorkSize, yWorkSize, zWorkSize, 0, 0, 0); + openCLResources.getQueue().finish(); + } + } + } + showStatus("Calculating global repetition... 100%"); + + // Read the repetition map back from the device and calculate the weighted average + openCLResources.getQueue().putReadBuffer(clRepetitionMap, true); + openCLResources.getQueue().putReadBuffer(clWeightsSumMap, true); + + for(int z=blockRadiusDepth; z0.0f) { + repetitionMap = Utils.applyMask3D(repetitionMap, imageWidth, imageHeight, imageDepth, + relevanceMask.getRelevanceMask()); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage3D(repetitionMap, imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, relevanceMask.getRelevanceMask()); + } + + // Release memory + openCLResources.getContext().release(); + + return repetitionMap; + } + + + /** + * Computes the Global Repetition Map based on the Normalized Root Mean Squared Error (NRMSE) for a 3D image using OpenCL. + * + * This method calculates the global repetition map based on the Normalized Root Mean Squared Error (NRMSE). The result is stored in a global + * repetition map, indicating the relative repetition of each structural element across the image. + * Optionally, a relevance mask can be applied to filter the results based on local standard deviations and a + * relevance constant. + * + * @param inputImage a {@link Utils.InputImage3D} object + * @param localStatistics a {@link CLLocalStatistics} object + * @param blockWidth the width of the block used for the analysis + * @param blockHeight the height of the block used for the analysis + * @param blockDepth the depth of the block used for the analysis + * @param blockSize the size of the block used for the analysis, after removing pixels outside the inbound circle/ellipse + * @param relevanceMask a {@link Utils.RelevanceMask} object + * @param nPixels the number of structurally relevant pixels (i.e., non-masked pixels) + * @param normalizeOutput a boolean to either normalize the output or not + * @param openCLResources an {@link OpenCLResources} object + * @return a float array representing the global repetition map, indicating the degree of repetition of each + * structural element across the entire image. + */ + public static float[] getGlobalNrmse3D(Utils.InputImage3D inputImage, CLLocalStatistics localStatistics, + int blockWidth, int blockHeight, int blockDepth, int blockSize, + Utils.RelevanceMask relevanceMask, float nPixels, boolean normalizeOutput, + OpenCLResources openCLResources) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage.getWidth(); + int imageHeight = inputImage.getHeight(); + int imageDepth = inputImage.getDepth(); + int imageSize = inputImage.getSize(); + int blockRadiusWidth = blockWidth / 2; + int blockRadiusHeight = blockHeight / 2; + int blockRadiusDepth = blockDepth / 2; + + // Create and fill OpenCL buffers + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, repetitionMap); + + float[] weightsSumMap = new float[imageSize]; + CLBuffer clWeightsSumMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, weightsSumMap); + + // Build OpenCL program + String programStringGetGlobalNrmse3D = getResourceAsString(CLUtils.class, "kernelGetGlobalNrmse3D.cl"); + programStringGetGlobalNrmse3D = replaceFirst(programStringGetGlobalNrmse3D, "$WIDTH$", "" + imageWidth); + programStringGetGlobalNrmse3D = replaceFirst(programStringGetGlobalNrmse3D, "$HEIGHT$", "" + imageHeight); + programStringGetGlobalNrmse3D = replaceFirst(programStringGetGlobalNrmse3D, "$DEPTH$", "" + imageDepth); + programStringGetGlobalNrmse3D = replaceFirst(programStringGetGlobalNrmse3D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetGlobalNrmse3D = replaceFirst(programStringGetGlobalNrmse3D, "$BRW$", "" + blockRadiusWidth); + programStringGetGlobalNrmse3D = replaceFirst(programStringGetGlobalNrmse3D, "$BRH$", "" + blockRadiusHeight); + programStringGetGlobalNrmse3D = replaceFirst(programStringGetGlobalNrmse3D, "$BRZ$", "" + blockRadiusDepth); + programStringGetGlobalNrmse3D = replaceFirst(programStringGetGlobalNrmse3D, "$FILTER_PARAM$", "" + relevanceMask.getNoiseMeanVariance()); + programStringGetGlobalNrmse3D = replaceFirst(programStringGetGlobalNrmse3D, "$THRESHOLD$", "" + relevanceMask.getRelevanceThreshold()); + programStringGetGlobalNrmse3D = replaceFirst(programStringGetGlobalNrmse3D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetGlobalNrmse3D = openCLResources.getContext().createProgram(programStringGetGlobalNrmse3D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetGlobalNrmse3D = programGetGlobalNrmse3D.createCLKernel("kernelGetGlobalNrmse3D"); + + int argn = 0; + kernelGetGlobalNrmse3D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetGlobalNrmse3D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetGlobalNrmse3D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetGlobalNrmse3D.setArg(argn++, clWeightsSumMap); + kernelGetGlobalNrmse3D.setArg(argn++, clRepetitionMap); + + + // Calculate weighted mean Pearson's map + int nXBlocks = imageWidth / 64 + ((imageWidth % 64 == 0) ? 0 : 1); + int nYBlocks = imageHeight / 64 + ((imageHeight % 64 == 0) ? 0 : 1); + int nZBlocks = imageDepth / blockDepth + ((imageDepth % blockDepth == 0) ? 0 : 1); + int totalBlocks = nXBlocks * nYBlocks * nZBlocks; + + for (int nZB = 0; nZB < nZBlocks; nZB++) { + int zWorkSize = min(blockDepth, imageDepth - nZB * blockDepth); + for (int nYB = 0; nYB < nYBlocks; nYB++) { + int yWorkSize = min(64, imageHeight - nYB * 64); + for (int nXB = 0; nXB < nXBlocks; nXB++) { + int xWorkSize = min(64, imageWidth - nXB * 64); + float progressPercentage = ((nZB * nYBlocks * nXBlocks) + (nYB * nXBlocks) + nXB) / (float) totalBlocks * 100; + showStatus(String.format("Calculating global repetition... %d%%", Math.round(progressPercentage))); + openCLResources.getQueue().put3DRangeKernel(kernelGetGlobalNrmse3D, + nXB * 64, nYB * 64, nZB * blockDepth, + xWorkSize, yWorkSize, zWorkSize, 0, 0, 0); + openCLResources.getQueue().finish(); + } + } + } + showStatus("Calculating global repetition... 100%"); + + // Read the repetition map back from the device and calculate the weighted average + openCLResources.getQueue().putReadBuffer(clRepetitionMap, true); + openCLResources.getQueue().putReadBuffer(clWeightsSumMap, true); + + for(int z=blockRadiusDepth; z 0.0f) { + float rmse = repetitionMap[index]; + if (rmse == 0.0f) { // Special case where RMSE is 0, 1/rmse would be undefined but we want perfect similarity + repetitionMap[index] = 1.0f; + } else { + repetitionMap[index] = 1.0f / rmse; + } + } + } + } + } + + // Apply relevance mask + if(relevanceMask.getRelevanceConstant()>0.0f) { + repetitionMap = Utils.applyMask3D(repetitionMap, imageWidth, imageHeight, imageDepth, + relevanceMask.getRelevanceMask()); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage3D(repetitionMap, imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, relevanceMask.getRelevanceMask()); + } + + // Release memory + openCLResources.getContext().release(); + + return repetitionMap; + } + + + /** + * Computes the Global Repetition Map based on the absolute difference of standard deviations for a 3D image using OpenCL. + * + * This method calculates the global repetition map based on the absolute difference of standard deviations. The result is stored in a global + * repetition map, indicating the relative repetition of each structural element across the image. + * Optionally, a relevance mask can be applied to filter the results based on local standard deviations and a + * relevance constant. + * + * @param inputImage a {@link Utils.InputImage3D} object + * @param localStatistics a {@link CLLocalStatistics} object + * @param blockWidth the width of the block used for the analysis + * @param blockHeight the height of the block used for the analysis + * @param blockDepth the depth of the block used for the analysis + * @param blockSize the size of the block used for the analysis, after removing pixels outside the inbound circle/ellipse + * @param relevanceMask a {@link Utils.RelevanceMask} object + * @param nPixels the number of structurally relevant pixels (i.e., non-masked pixels) + * @param normalizeOutput a boolean to either normalize the output or not + * @param openCLResources an {@link OpenCLResources} object + * @return a float array representing the global repetition map, indicating the degree of repetition of each + * structural element across the entire image. + */ + public static float[] getGlobalAbsDiffStds3D(Utils.InputImage3D inputImage, CLLocalStatistics localStatistics, + int blockWidth, int blockHeight, int blockDepth, int blockSize, + Utils.RelevanceMask relevanceMask, float nPixels, boolean normalizeOutput, + OpenCLResources openCLResources) + { + IJ.log("Calculating Structural Repetition Scores..."); + + // Cache variables + int imageWidth = inputImage.getWidth(); + int imageHeight = inputImage.getHeight(); + int imageDepth = inputImage.getDepth(); + int imageSize = inputImage.getSize(); + int blockRadiusWidth = blockWidth / 2; + int blockRadiusHeight = blockHeight / 2; + int blockRadiusDepth = blockDepth / 2; + + // Create and fill OpenCL buffers + float[] repetitionMap = new float[imageSize]; + CLBuffer clRepetitionMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, repetitionMap); + + float[] weightsSumMap = new float[imageSize]; + CLBuffer clWeightsSumMap = CLUtils.createAndFillCLBuffer(openCLResources.getContext(), imageSize, + READ_WRITE, weightsSumMap); + + // Build OpenCL program + String programStringGetGlobalAbsDiffStds3D = getResourceAsString(CLUtils.class, "kernelGetGlobalAbsDiffStds3D.cl"); + programStringGetGlobalAbsDiffStds3D = replaceFirst(programStringGetGlobalAbsDiffStds3D, "$WIDTH$", "" + imageWidth); + programStringGetGlobalAbsDiffStds3D = replaceFirst(programStringGetGlobalAbsDiffStds3D, "$HEIGHT$", "" + imageHeight); + programStringGetGlobalAbsDiffStds3D = replaceFirst(programStringGetGlobalAbsDiffStds3D, "$DEPTH$", "" + imageDepth); + programStringGetGlobalAbsDiffStds3D = replaceFirst(programStringGetGlobalAbsDiffStds3D, "$BLOCK_SIZE$", "" + blockSize); + programStringGetGlobalAbsDiffStds3D = replaceFirst(programStringGetGlobalAbsDiffStds3D, "$BRW$", "" + blockRadiusWidth); + programStringGetGlobalAbsDiffStds3D = replaceFirst(programStringGetGlobalAbsDiffStds3D, "$BRH$", "" + blockRadiusHeight); + programStringGetGlobalAbsDiffStds3D = replaceFirst(programStringGetGlobalAbsDiffStds3D, "$BRZ$", "" + blockRadiusDepth); + programStringGetGlobalAbsDiffStds3D = replaceFirst(programStringGetGlobalAbsDiffStds3D, "$FILTER_PARAM$", "" + relevanceMask.getNoiseMeanVariance()); + programStringGetGlobalAbsDiffStds3D = replaceFirst(programStringGetGlobalAbsDiffStds3D, "$THRESHOLD$", "" + relevanceMask.getRelevanceThreshold()); + programStringGetGlobalAbsDiffStds3D = replaceFirst(programStringGetGlobalAbsDiffStds3D, "$EPSILON$", "" + Utils.EPSILON); + CLProgram programGetGlobalAbsDiffStds3D = openCLResources.getContext().createProgram(programStringGetGlobalAbsDiffStds3D).build(); + + // Create OpenCL kernel and set args + CLKernel kernelGetGlobalAbsDiffStds3D = programGetGlobalAbsDiffStds3D.createCLKernel("kernelGetGlobalAbsDiffStds3D"); + + int argn = 0; + kernelGetGlobalAbsDiffStds3D.setArg(argn++, localStatistics.getCLImageArray()); + kernelGetGlobalAbsDiffStds3D.setArg(argn++, localStatistics.getCLLocalMeans()); + kernelGetGlobalAbsDiffStds3D.setArg(argn++, localStatistics.getCLLocalStds()); + kernelGetGlobalAbsDiffStds3D.setArg(argn++, clWeightsSumMap); + kernelGetGlobalAbsDiffStds3D.setArg(argn++, clRepetitionMap); + + + // Calculate weighted mean Pearson's map + int nXBlocks = imageWidth / 64 + ((imageWidth % 64 == 0) ? 0 : 1); + int nYBlocks = imageHeight / 64 + ((imageHeight % 64 == 0) ? 0 : 1); + int nZBlocks = imageDepth / blockDepth + ((imageDepth % blockDepth == 0) ? 0 : 1); + int totalBlocks = nXBlocks * nYBlocks * nZBlocks; + + for (int nZB = 0; nZB < nZBlocks; nZB++) { + int zWorkSize = min(blockDepth, imageDepth - nZB * blockDepth); + for (int nYB = 0; nYB < nYBlocks; nYB++) { + int yWorkSize = min(64, imageHeight - nYB * 64); + for (int nXB = 0; nXB < nXBlocks; nXB++) { + int xWorkSize = min(64, imageWidth - nXB * 64); + float progressPercentage = ((nZB * nYBlocks * nXBlocks) + (nYB * nXBlocks) + nXB) / (float) totalBlocks * 100; + showStatus(String.format("Calculating global repetition... %d%%", Math.round(progressPercentage))); + openCLResources.getQueue().put3DRangeKernel(kernelGetGlobalAbsDiffStds3D, + nXB * 64, nYB * 64, nZB * blockDepth, + xWorkSize, yWorkSize, zWorkSize, 0, 0, 0); + openCLResources.getQueue().finish(); + } + } + } + showStatus("Calculating global repetition... 100%"); + + // Read the repetition map back from the device and calculate the weighted average + openCLResources.getQueue().putReadBuffer(clRepetitionMap, true); + openCLResources.getQueue().putReadBuffer(clWeightsSumMap, true); + + for(int z=blockRadiusDepth; z 0.0f) { + float diffStd = repetitionMap[index]; + if (diffStd == 0.0f) { // Special case where diffStd is 0, 1/diffStd would be undefined but we want perfect similarity + repetitionMap[index] = 1.0f; + } else { + repetitionMap[index] = 1.0f / diffStd; + } + } + } + } + } + + // Apply relevance mask + if(relevanceMask.getRelevanceConstant()>0.0f) { + repetitionMap = Utils.applyMask3D(repetitionMap, imageWidth, imageHeight, imageDepth, + relevanceMask.getRelevanceMask()); + } + + // Normalize repetition map (avoiding masked pixels) + if(normalizeOutput) { + repetitionMap = Utils.normalizeImage3D(repetitionMap, imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, relevanceMask.getRelevanceMask()); + } + + // Release memory + openCLResources.getContext().release(); + + return repetitionMap; + } + + + /** + * Calculate a global repetition map for 3D data, based on the selected metric. + * + * @param metric The selected metric for repetition calculation. + * @param inputImage A {@link Utils.InputImage2D}. + * @param blockWidth The width of the block used for the analysis. + * @param blockHeight The height of the block used for the analysis. + * @param blockDepth The depth of the block used for the analysis. + * @param blockSize The size of the block used for the analysis, after removing pixels outside the inbound circle/ellipse. + * @param localStatistics A {@link CLLocalStatistics} object. + * @param relevanceMask A {@link Utils.RelevanceMask} object + * @param nPixels The number of structurally relevant pixels (i.e., non-masked pixels) + * @param normalizeOutput A boolean to either normalize the output or not. + * + * @return The calculated repetition map. + */ + public static float[] calculateGlobalRepetitionMap3D(String metric, Utils.InputImage3D inputImage, int blockWidth, + int blockHeight, int blockDepth, int blockSize, + CLLocalStatistics localStatistics, + Utils.RelevanceMask relevanceMask, float nPixels, + boolean normalizeOutput, OpenCLResources openCLResources) + { + + if(metric.equals("Pearson's R")) { + return CLUtils.getGlobalPearson3D(inputImage, localStatistics, blockWidth, blockHeight, blockDepth, + blockSize, relevanceMask, nPixels, normalizeOutput, openCLResources); + }else if(metric.equals("Cosine similarity")) { + return CLUtils.getGlobalCosineSimilarity3D(inputImage, localStatistics, blockWidth, blockHeight, blockDepth, + relevanceMask, nPixels, normalizeOutput, openCLResources); + }else if(metric.equals("SSIM")){ + return CLUtils.getGlobalSsim3D(inputImage, localStatistics, blockWidth, blockHeight, blockDepth, blockSize, + relevanceMask, nPixels, normalizeOutput, openCLResources); + }else if(metric.equals("NRMSE")){ + return CLUtils.getGlobalNrmse3D(inputImage, localStatistics, blockWidth, blockHeight, blockDepth, blockSize, + relevanceMask, nPixels, normalizeOutput, openCLResources); + }else if(metric.equals("Abs. diff. of StdDevs")){ + return CLUtils.getGlobalAbsDiffStds3D(inputImage, localStatistics, blockWidth, blockHeight, blockDepth, + blockSize, relevanceMask, nPixels, normalizeOutput, openCLResources); + }else{ + return null; + } + } + + +} diff --git a/src/main/java/CalculateLocalMeans_.java b/src/main/java/CalculateLocalMeans_.java index c7b39a6..a0186c9 100644 --- a/src/main/java/CalculateLocalMeans_.java +++ b/src/main/java/CalculateLocalMeans_.java @@ -261,7 +261,7 @@ public void run(String s) { clLocalStds = context.createFloatBuffer(wh, READ_WRITE); // Create OpenCL program - String programStringGetPatchMeans = getResourceAsString(BlockRedundancy2D_.class, "kernelGetPatchMeans2D.cl"); + String programStringGetPatchMeans = getResourceAsString(BlockRepetition2D_.class, "kernelGetLocalStatistics2D.cl"); programStringGetPatchMeans = replaceFirst(programStringGetPatchMeans, "$WIDTH$", "" + w); programStringGetPatchMeans = replaceFirst(programStringGetPatchMeans, "$HEIGHT$", "" + h); programStringGetPatchMeans = replaceFirst(programStringGetPatchMeans, "$PATCH_SIZE$", "" + patchSize); diff --git a/src/main/java/GATMinimizer2D.java b/src/main/java/GATMinimizer2D.java index be6e12e..dc0dc02 100644 --- a/src/main/java/GATMinimizer2D.java +++ b/src/main/java/GATMinimizer2D.java @@ -13,7 +13,7 @@ public class GATMinimizer2D implements UserFunction { - +//TODO: THIS BUGS OUT WHEN IMAGE IS TOO SMAL DUE TO HOW WE DEFINE BLOCK SIZE public double gain, sigma, offset; private final int width, height; //public boolean isCalculated = false; diff --git a/src/main/java/GATMinimizer3D.java b/src/main/java/GATMinimizer3D.java index 730a7cb..0f7b253 100644 --- a/src/main/java/GATMinimizer3D.java +++ b/src/main/java/GATMinimizer3D.java @@ -10,6 +10,8 @@ import ij.measure.Minimizer; import ij.measure.UserFunction; + +import static java.lang.Math.max; import static java.lang.Math.sqrt; @@ -95,8 +97,9 @@ public double userFunction(double[] params, double v) { } // Define the dimensions of the window used to estimate the noise variance (adjusted to image size to avoid out of bounds) - int blockWidth = width/6; // bounding box width - int blockHeight = height/6; // bounding box height + // max() is used to prevent block dimensions == 0 + int blockWidth = max(width/6, 1); // bounding box width + int blockHeight = max(height/6, 1); // bounding box height // Get number of blocks int nBlocksX = width / blockWidth; diff --git a/src/main/java/GlobalRedundancy.java b/src/main/java/GlobalRedundancy.java deleted file mode 100644 index 6313ff6..0000000 --- a/src/main/java/GlobalRedundancy.java +++ /dev/null @@ -1,621 +0,0 @@ -/** - * Calculates the Global Repetition Map. Each local neighbourhood is used as a reference for a round of Block Repetition. - * Each pairwise comparison is weighted based on the similarity between reference and test blocks, and the resulting Block Repetition Map is averaged. - * The average value is plotted at the center position of the reference neighbourhood. - * - * @author Afonso Mendes - * - **/ - - -import com.jogamp.opencl.*; -import ij.IJ; -import ij.ImagePlus; -import ij.measure.UserFunction; -import ij.plugin.LutLoader; -import ij.process.FloatProcessor; -import ij.process.ImageProcessor; -import ij.process.LUT; - -import java.awt.image.IndexColorModel; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.FloatBuffer; -import java.util.*; -import static com.jogamp.opencl.CLMemory.Mem.READ_ONLY; -import static com.jogamp.opencl.CLMemory.Mem.READ_WRITE; -import static ij.IJ.showStatus; -import static java.lang.Math.*; - -public class GlobalRedundancy implements Runnable, UserFunction{ - - // ------------------------ // - // ---- OpenCL formats ---- // - // ------------------------ // - - static private CLContext context; - static private CLCommandQueue queue; - static private CLProgram programGetLocalMeans, programGetPearsonMap, programGetDiffStdMap; - static private CLKernel kernelGetLocalMeans, kernelGetPearsonMap, kernelGetDiffStdMap; - - private CLBuffer clRefPixels, clLocalMeans, clLocalStds, clPearsonMap, clDiffStdMap, clWeightsSumMap; - - - // -------------------------- // - // ---- Image parameters ---- // - // -------------------------- // - - public float[] refPixels; - public int w, h, wh, bW, bH, patchSize, bRW, bRH, sizeWithoutBorders, scaleFactor, level, w0, h0; - public float filterConstant, EPSILON; - public String metric; - - public GlobalRedundancy(float[] refPixels, int w, int h, int bW, int bH, int bRW, int bRH, int patchSize, float EPSILON, CLContext context, - CLCommandQueue queue, int scaleFactor, float filterConstant, int level, int w0, int h0, String metric){ - - this.refPixels = refPixels; - this.w = w; - this.h = h; - wh = w * h; - this.bW = bW; - this.bH = bH; - this.bRW = bRW; - this.bRH = bRH; - sizeWithoutBorders = (w-bRW*2)*(h-bRH*2); - this.patchSize = patchSize; // Number of pixels in an elliptical patch - this.EPSILON = EPSILON; - this.context = context; - this.queue = queue; - this.scaleFactor = scaleFactor; - this.filterConstant = filterConstant; - this.level = level; - this.w0 = w0; - this.h0 = h0; - this.metric = metric; - } - - @Override - public void run(){ - - IJ.log("Calculating at scale level "+level+"..."); - - - // ------------------------------------------------------------------------------------------------------ // - // ---- Write input image (variance-stabilized, mean-subtracted and normalized) to the OpenCL device ---- // - // ------------------------------------------------------------------------------------------------------ // - - clRefPixels = context.createFloatBuffer(wh, READ_ONLY); - fillBufferWithFloatArray(clRefPixels, refPixels); - queue.putWriteBuffer(clRefPixels, true); - - - // ------------------------------------------------------------------ // - // ---- Calculate local means and local standard deviations maps ---- // - // ------------------------------------------------------------------ // - - float[] localMeans = new float[wh]; - Arrays.fill(localMeans, 0.0f); - - float[] localStds = new float[wh]; - Arrays.fill(localStds, 0.0f); - - // Create OpenCL program - String programStringGetLocalMeans = getResourceAsString(RedundancyMap_.class, "kernelGetLocalMeans.cl"); - programStringGetLocalMeans = replaceFirst(programStringGetLocalMeans, "$WIDTH$", "" + w); - programStringGetLocalMeans = replaceFirst(programStringGetLocalMeans, "$HEIGHT$", "" + h); - programStringGetLocalMeans = replaceFirst(programStringGetLocalMeans, "$PATCH_SIZE$", "" + patchSize); - programStringGetLocalMeans = replaceFirst(programStringGetLocalMeans, "$BRW$", "" + bRW); - programStringGetLocalMeans = replaceFirst(programStringGetLocalMeans, "$BRH$", "" + bRH); - programStringGetLocalMeans = replaceFirst(programStringGetLocalMeans, "$EPSILON$", "" + EPSILON); - programGetLocalMeans = context.createProgram(programStringGetLocalMeans).build(); - System.out.println(programGetLocalMeans.getBuildLog()); - // Create, fill and write OpenCL buffers - clLocalMeans = context.createFloatBuffer(wh, READ_WRITE); - fillBufferWithFloatArray(clLocalMeans, localMeans); - queue.putWriteBuffer(clLocalMeans, true); - - clLocalStds = context.createFloatBuffer(wh, READ_WRITE); - fillBufferWithFloatArray(clLocalStds, localStds); - queue.putWriteBuffer(clLocalStds, true); - - // Create OpenCL kernel and set kernel arguments - kernelGetLocalMeans = programGetLocalMeans.createCLKernel("kernelGetLocalMeans"); - - int argn = 0; - kernelGetLocalMeans.setArg(argn++, clRefPixels); - kernelGetLocalMeans.setArg(argn++, clLocalMeans); - kernelGetLocalMeans.setArg(argn++, clLocalStds); - - // Calculate local means and standard deviations map in the GPU - showStatus("Calculating local means..."); - queue.put2DRangeKernel(kernelGetLocalMeans, 0, 0, w, h, 0, 0); - queue.finish(); - - // Read the local standard deviations map from the OpenCL device - queue.putReadBuffer(clLocalStds, true); - for (int y=bRH; y clBuffer, float[] pixels) { - FloatBuffer buffer = clBuffer.getBuffer(); - for(int n=0; n 0.0f) { + nPixels += 1.0f; + } + } + } + + // Calculate global repetition map + float[] repetitionMap; + repetitionMap = CLUtils.calculateGlobalRepetitionMap2D(metric, inputImage, blockWidth, blockHeight, blockSize, + localStatistics, relevanceMask, nPixels, normalizeOutput, openCLResources); + + // Display results + Utils.displayResults2D(inputImage, repetitionMap); + + // Stop timer + long elapsedTime = System.currentTimeMillis() - start; + IJ.log("Finished!"); + IJ.log("Elapsed time: " + elapsedTime/1000 + " sec"); + IJ.log("--------"); + + } +} diff --git a/src/main/java/GlobalRepetition3D_.java b/src/main/java/GlobalRepetition3D_.java index fd17e37..fd9a0ad 100644 --- a/src/main/java/GlobalRepetition3D_.java +++ b/src/main/java/GlobalRepetition3D_.java @@ -1,764 +1,158 @@ +/** + * This class calculates block repetition maps for 3D data. + * + * @author Afonso Mendes + */ import com.jogamp.opencl.*; import ij.*; import ij.gui.NonBlockingGenericDialog; -import ij.measure.Calibration; -import ij.plugin.LutLoader; import ij.plugin.PlugIn; -import ij.process.FloatProcessor; -import ij.process.LUT; - -import java.awt.image.IndexColorModel; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.FloatBuffer; -import java.util.Arrays; - -import static com.jogamp.opencl.CLMemory.Mem.READ_ONLY; -import static com.jogamp.opencl.CLMemory.Mem.READ_WRITE; -import static ij.IJ.showStatus; -import static java.lang.Math.*; +import static ij.WindowManager.getIDList; +import static ij.WindowManager.getImageCount; public class GlobalRepetition3D_ implements PlugIn { - static private CLContext context; - static private CLPlatform clPlatformMaxFlop; - static private CLCommandQueue queue; - static private CLProgram programGetLocalMeans3D, programGetCosineSimMap3D, programGetRelevanceMap3D; - - static private CLKernel kernelGetLocalMeans3D, kernelGetCosineSimMap3D, kernelGetRelevanceMap3D; - private CLBuffer clRefPixels, clLocalMeans, clLocalStds, clCosineSimMap, clRelevanceMap, clWeightsSumMap; + // Define constants for metric choices + public static final String[] METRICS = { + "Pearson's R", + "Cosine similarity", + "SSIM", + "NRMSE", + "Abs. diff. of StdDevs" + }; @Override public void run(String s) { - float EPSILON = 0.0000001f; - // -------------------- // // ---- Dialog box ---- // // -------------------- // - // Define metric possibilities - String[] metrics = new String[2]; - metrics[0] = "Pearson's R"; - metrics[1] = "Cosine similarity"; + // Get all open image titles + int nImages = getImageCount(); + if (nImages < 1) { + IJ.error("No images found. Open an image and try again."); + return; + } + + // Get image IDs from titles + int[] ids = getIDList(); + String[] titles = new String[nImages]; + for (int i = 0; i < nImages; i++) { + titles[i] = WindowManager.getImage(ids[i]).getTitle(); + } // Initialize dialog box NonBlockingGenericDialog gd = new NonBlockingGenericDialog("SReD: Global Repetition (3D)"); - gd.addNumericField("Block width (px): ", 3, 2); - gd.addNumericField("Block height (px): ", 3, 2); - gd.addNumericField("Block depth (px): ", 3, 2); - gd.addSlider("Filter constant: ", 0.0f, 10.0f, 0.0, 0.1f); - gd.addChoice("Metric:", metrics, metrics[1]); + gd.addChoice("Image:", titles, titles[0]); + gd.addNumericField("Block width (px):", 5); + gd.addNumericField("Block height (px):", 5); + gd.addNumericField("Block depth (px):", 5); + gd.addNumericField("Relevance constant:", 0.0f); + gd.addChoice("Metric:", METRICS, METRICS[0]); gd.addCheckbox("Normalize output?", true); gd.addCheckbox("Use device from preferences?", false); - + gd.addHelp("https://github.com/HenriquesLab/SReD/wiki"); gd.showDialog(); if(gd.wasCanceled()) return; // Get dialog parameters - int bW = (int) gd.getNextNumber(); // Patch width - int bH = (int) gd.getNextNumber(); // Patch height - int bZ = (int) gd.getNextNumber(); // Patch depth - - float filterConstant = (float) gd.getNextNumber(); - + String imageTitle = gd.getNextChoice(); + int imageID = Utils.getImageIDByTitle(titles, ids, imageTitle); + int blockWidth = (int) gd.getNextNumber(); + int blockHeight = (int) gd.getNextNumber(); + int blockDepth = (int) gd.getNextNumber(); + float relevanceConstant = (float) gd.getNextNumber(); String metric = gd.getNextChoice(); - boolean normalizeOutput = gd.getNextBoolean(); - boolean useDevice = gd.getNextBoolean(); - // Check if patch dimensions are odd, otherwise kill program - if(bW%2==0 || bH%2==0) { - IJ.error("Block dimensions must be odd (e.g., 3x3 or 5x5). Please try again."); - return; - } - - // Check if patch has at least 3 slices, otherwise kill program - if(bZ<3) { - IJ.error("Block must have at least 3 slices. Please try again."); + // Check if block dimensions are odd, otherwise kill program + if(blockWidth%2==0 || blockHeight%2==0 || blockDepth%2==0) { + IJ.error("Block dimensions must be odd. Please try again."); return; } - // Calculate block radius - int bRW = bW/2; // Patch radius (x-axis) - int bRH = bH/2; // Patch radius (y-axis) - int bRZ = bZ/2; // Patch radius (z-axis) - - // Get ImagePlus and ImageStack - ImagePlus imp0 = WindowManager.getCurrentImage(); - if(imp0==null) { - IJ.error("Image not found. Try again."); + // Check if block has at least 3 slices, otherwise kill program + if(blockDepth<3) { + IJ.error("Block depth must be at least 3. Please try again."); return; } - ImageStack ims0 = imp0.getStack(); - - // Get calibration parameters - Calibration calibration = imp0.getCalibration(); - - // Get image dimensions - int w = ims0.getWidth(); - int h = ims0.getHeight(); - int z = ims0.getSize(); - int wh = w*h; - int whz = w*h*z; - - - // --------------------- // - // ---- Start timer ---- // - // --------------------- // + IJ.log("--------"); + IJ.log("SReD is running, please wait..."); - IJ.log("SReD has started, please wait."); + // Start timer long start = System.currentTimeMillis(); - - // ---------------------------------- // - // ---- Stabilize noise variance ---- // - // ---------------------------------- // - - IJ.log("Stabilizing noise variance of the image..."); - - float[][] refPixels = new float[z][wh]; - for(int n=0; nimgMax) { - imgMax = pixelValue; - } - } - - // Normalize - for(int i=0; i nFlops) { - nFlops = clDevice.getMaxComputeUnits() * clDevice.getMaxClockFrequency(); - clPlatformMaxFlop = allPlatform; - } - } - } - IJ.log("--------"); - - // Create OpenCL context - context = CLContext.create(clPlatformMaxFlop); - - // Choose the best device (i.e., Filter out CPUs if GPUs are available (FLOPS calculation was giving - // higher ratings to CPU vs. GPU)) - //TODO: Rate devices on Mandelbrot and chooose the best, GPU will perform better - CLDevice[] allDevices = context.getDevices(); - - boolean hasGPU = false; - for(int i=0; i 0.0f) { + nPixels += 1.0f; } } } } + // Calculate global repetition map + float[] repetitionMap; + repetitionMap = CLUtils.calculateGlobalRepetitionMap3D(metric, inputImage, blockWidth, blockHeight, blockDepth, + blockSize, localStatistics, relevanceMask, nPixels, normalizeOutput, openCLResources); + // Display results + Utils.displayResults3D(inputImage, repetitionMap); - - // --------------------------------------------------------------- // - // ---- Calculate block repetition map with the chosen metric ---- // - // --------------------------------------------------------------- // - - float[] repetitionMap = new float[whz]; - Arrays.fill(repetitionMap, 0.0f); - - float[] weightSumMap = new float[whz]; - Arrays.fill(weightSumMap, 0.0f); - - if(metric==metrics[1]) { // Cosine similarity - showStatus("Calculating global repetition... 0%"); - - // Build OpenCL program - String programStringGetCosineSim3D = getResourceAsString(GlobalRepetition3D_.class, "kernelGetCosineSimMap3D.cl"); - programStringGetCosineSim3D = replaceFirst(programStringGetCosineSim3D, "$WIDTH$", "" + w); - programStringGetCosineSim3D = replaceFirst(programStringGetCosineSim3D, "$HEIGHT$", "" + h); - programStringGetCosineSim3D = replaceFirst(programStringGetCosineSim3D, "$DEPTH$", "" + z); - programStringGetCosineSim3D = replaceFirst(programStringGetCosineSim3D, "$PATCH_SIZE$", "" + patchSize); - programStringGetCosineSim3D = replaceFirst(programStringGetCosineSim3D, "$BRW$", "" + bRW); - programStringGetCosineSim3D = replaceFirst(programStringGetCosineSim3D, "$BRH$", "" + bRH); - programStringGetCosineSim3D = replaceFirst(programStringGetCosineSim3D, "$BRZ$", "" + bRZ); - programStringGetCosineSim3D = replaceFirst(programStringGetCosineSim3D, "$FILTERPARAM$", "" + noiseVar); - programStringGetCosineSim3D = replaceFirst(programStringGetCosineSim3D, "$THRESHOLD$", "" + threshold); - programStringGetCosineSim3D = replaceFirst(programStringGetCosineSim3D, "$EPSILON$", "" + EPSILON); - programGetCosineSimMap3D = context.createProgram(programStringGetCosineSim3D).build(); - - // Fill OpenCL buffers - clCosineSimMap = context.createFloatBuffer(whz, READ_WRITE); - fillBufferWithFloatArray(clCosineSimMap, repetitionMap); - queue.putWriteBuffer(clCosineSimMap, true); - - clWeightsSumMap = context.createFloatBuffer(whz, READ_WRITE); - fillBufferWithFloatArray(clWeightsSumMap, weightSumMap); - queue.putWriteBuffer(clWeightsSumMap, true); - - // Create kernel and set args - kernelGetCosineSimMap3D = programGetCosineSimMap3D.createCLKernel("kernelGetCosineSimMap3D"); - - argn = 0; - kernelGetCosineSimMap3D.setArg(argn++, clRefPixels); - kernelGetCosineSimMap3D.setArg(argn++, clLocalStds); - kernelGetCosineSimMap3D.setArg(argn++, clWeightsSumMap); - kernelGetCosineSimMap3D.setArg(argn++, clCosineSimMap); - - // Calculate Pearson's correlation coefficient - currentBlock = 1.0f; // Restart the counter - for(int nZB=0; nZB clBuffer, float[] pixels) { - FloatBuffer buffer = clBuffer.getBuffer(); - for (int n = 0; n < pixels.length; n++) { - buffer.put(n, pixels[n]); - } - } - - // Read a kernel from the resources - public static String getResourceAsString(Class c, String resourceName) { - InputStream programStream = c.getResourceAsStream("/" + resourceName); - String programString = ""; - - try { - programString = inputStreamToString(programStream); - } catch (IOException var5) { - var5.printStackTrace(); - } - - return programString; - } - - public static String inputStreamToString(InputStream inputStream) throws IOException { - ByteArrayOutputStream result = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - while ((length = inputStream.read(buffer)) != -1) { - result.write(buffer, 0, length); - } - return result.toString("UTF-8"); - } - - public static String replaceFirst(String source, String target, String replacement) { - int index = source.indexOf(target); - if (index == -1) { - return source; - } - - return source.substring(0, index) - .concat(replacement) - .concat(source.substring(index + target.length())); } - - // Get mean and variance of a patch - public double[] getMeanAndVarBlock3D(float[] pixels, int width, int height, int depth, int xStart, int yStart, int zStart, int xEnd, int yEnd, int zEnd) { - double mean = 0; - double var; - - double sq_sum = 0; - - int bWidth = xEnd-xStart; - int bHeight = yEnd - yStart; - int bDepth = zEnd - zStart; - int bWHZ = bWidth*bHeight*bDepth; - - for(int z=zStart; z nFlops) { - nFlops = clDevice.getMaxComputeUnits() * clDevice.getMaxClockFrequency(); - clPlatformMaxFlop = allPlatform; - } - } - } - IJ.log("--------"); - - // Create context - context = CLContext.create(clPlatformMaxFlop); - - // Choose the best device (i.e., Filter out CPUs if GPUs are available (FLOPS calculation was giving - // higher ratings to CPU vs. GPU)) - //TODO: Rate devices on Mandelbrot and chooose the best, GPU will perform better - CLDevice[] allDevices = context.getDevices(); - - boolean hasGPU = false; - for (int i = 0; i < allDevices.length; i++) { - if (allDevices[i].getType() == CLDevice.Type.GPU) { - hasGPU = true; - } - } - CLDevice chosenDevice; - if (hasGPU) { - chosenDevice = context.getMaxFlopsDevice(CLDevice.Type.GPU); - } else { - chosenDevice = context.getMaxFlopsDevice(); - } - - // Get chosen device from preferences - if(useDevice){ - String deviceName = Prefs.get("SReD.OpenCL.device", null); - for (CLDevice device : allDevices) { - if (device.getName().equals(deviceName)) { - chosenDevice = device; - break; - } - } - } - - IJ.log("Chosen device: " + chosenDevice.getName()); - IJ.log("--------"); - - // Create command queue - queue = chosenDevice.createCommandQueue(); - - - // ------------------------------------------------- // - // ---- Get reference image and some parameters ---- // - // ------------------------------------------------- // - - ImagePlus imp0 = WindowManager.getCurrentImage(); - if (imp0 == null) { - IJ.error("No image found. Please open an image and try again."); - return; - } - - FloatProcessor fp0 = imp0.getProcessor().convertToFloatProcessor(); - float[] refPixels0 = (float[]) fp0.getPixels(); - int w0 = fp0.getWidth(); - int h0 = fp0.getHeight(); - - - //int elementCount = w*h; - //int localWorkSize = min(chosenDevice.getMaxWorkGroupSize(), 256); - //int globalWorkSize = roundUp(localWorkSize, elementCount); - - - // ---------------------------------- // - // ---- Calculate Redundancy Map ---- // - // ---------------------------------- // - - IJ.log("Calculating redundancy...please wait..."); - - int rounds = 5; // How many scale levels should be analyzed - if(multiScale == 0){ - - int scaleFactor = 1; - - if(useTime == 0){ - - // --------------------------------------------------------------------------- // - // ---- Stabilize noise variance using the Generalized Anscombe transform ---- // - // --------------------------------------------------------------------------- // - GATMinimizer2D minimizer = new GATMinimizer2D(refPixels0, w0, h0, 0, 100, 0); - minimizer.run(); - - refPixels0 = VarianceStabilisingTransform2D_.getGAT(refPixels0, minimizer.gain, minimizer.sigma, minimizer.offset); - - - // ------------------- // - // ---- Normalize ---- // - // ------------------- // - - // Cast to "double" type - double[] refPixelsDouble = new double[w0*h0]; - for(int i=0; iimgMax){ - imgMax = pixelValue; - } - } - - // Remap pixels - for(int i=0; iimgMax){ - imgMax = pixelValue; - } - } - - // Normalize - for(int i=0; i1) { - // Sequential blur and downscale until reaching the desired dimensions (skip first iteration) - for(int j=0; j featureVectorList = new ArrayList<>(); - for(double[] featureVector : featureVectors){ - featureVectorList.add(new DoublePoint(featureVector)); - } - - int optimalK = 1; - double previousWCSS = Double.MAX_VALUE; - for(int k=1; k<=maxK; k++){ - KMeansPlusPlusClusterer kMeans = new KMeansPlusPlusClusterer<>(k, 1000, new EuclideanDistance()); - List> clusters = kMeans.cluster(featureVectorList); - double wcss = 0.0; - for (CentroidCluster cluster : clusters) { - for (DoublePoint featureVector : cluster.getPoints()) { - double distance = new EuclideanDistance().compute(cluster.getCenter().getPoint(), featureVector.getPoint()); - wcss += distance * distance; - } - } - if (wcss > previousWCSS) { - break; - } - previousWCSS = wcss; - optimalK = k; - } - return optimalK; - } - - public static float[] applyGaussianBlur(float[] input, int width, int height, float sigma) { - // Create a Gaussian kernel - int size = (int) Math.ceil(sigma * 3) * 2 + 1; - float[] kernel = new float[size]; - float sum = 0; - for (int i = 0; i < size; i++) { - float x = (float) (i - (size - 1) / 2.0); - kernel[i] = (float) Math.exp(-x * x / (2 * sigma * sigma)); - sum += kernel[i]; - } - for (int i = 0; i < size; i++) { - kernel[i] /= sum; - } - - // Create a temporary array for the blurred image - float[] temp = new float[input.length]; - - // Blur each row - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - float sum1 = 0; - float sum2 = 0; - for (int i = 0; i < size; i++) { - int x1 = x + i - (size - 1) / 2; - if(x1 < 0 || x1 >= width) { - continue; - } - int index = y*width+x1; - float value = input[index]; - float weight = kernel[i]; - sum1 += value * weight; - sum2 += weight; - } - int index = y * width + x; - temp[index] = sum1 / sum2; - } - } - - // Blur each column - for (int x = 0; x < width; x++) { - for (int y = 0; y < height; y++) { - float sum1 = 0; - float sum2 = 0; - for (int i = 0; i < size; i++) { - int y1 = y + i - (size - 1) / 2; - if (y1 < 0 || y1 >= height) { - continue; - } - int index = y1 * width + x; - float value = temp[index]; - float weight = kernel[i]; - sum1 += value * weight; - sum2 += weight; - } - int index = y * width + x; - input[index] = sum1 / sum2; - } - } - - return input; - } -} \ No newline at end of file diff --git a/src/main/java/RelevanceMaskStack2D_.java b/src/main/java/RelevanceMaskStack2D_.java index 6beeabf..2c7be2d 100644 --- a/src/main/java/RelevanceMaskStack2D_.java +++ b/src/main/java/RelevanceMaskStack2D_.java @@ -199,28 +199,28 @@ public void run(String s) { // Write input image to the OpenCL device clRefPixels = context.createFloatBuffer(wh, READ_ONLY); - GlobalRedundancy.fillBufferWithFloatArray(clRefPixels, refPixels); + CLUtils.fillBufferWithFloatArray(clRefPixels, refPixels); queue.putWriteBuffer(clRefPixels, true); // Create OpenCL program - String programStringGetLocalStatistics = GlobalRedundancy.getResourceAsString(RelevanceMask2D_.class, "kernelGetRelevanceMask2D.cl"); - programStringGetLocalStatistics = GlobalRedundancy.replaceFirst(programStringGetLocalStatistics, "$WIDTH$", "" + w); - programStringGetLocalStatistics = GlobalRedundancy.replaceFirst(programStringGetLocalStatistics, "$HEIGHT$", "" + h); - programStringGetLocalStatistics = GlobalRedundancy.replaceFirst(programStringGetLocalStatistics, "$PATCH_SIZE$", "" + patchSize); - programStringGetLocalStatistics = GlobalRedundancy.replaceFirst(programStringGetLocalStatistics, "$BRW$", "" + bRW); - programStringGetLocalStatistics = GlobalRedundancy.replaceFirst(programStringGetLocalStatistics, "$BRH$", "" + bRH); - programStringGetLocalStatistics = GlobalRedundancy.replaceFirst(programStringGetLocalStatistics, "$EPSILON$", "" + EPSILON); + String programStringGetLocalStatistics = CLUtils.getResourceAsString(RelevanceMask2D_.class, "kernelGetRelevanceMask2D.cl"); + programStringGetLocalStatistics = CLUtils.replaceFirst(programStringGetLocalStatistics, "$WIDTH$", "" + w); + programStringGetLocalStatistics = CLUtils.replaceFirst(programStringGetLocalStatistics, "$HEIGHT$", "" + h); + programStringGetLocalStatistics = CLUtils.replaceFirst(programStringGetLocalStatistics, "$PATCH_SIZE$", "" + patchSize); + programStringGetLocalStatistics = CLUtils.replaceFirst(programStringGetLocalStatistics, "$BRW$", "" + bRW); + programStringGetLocalStatistics = CLUtils.replaceFirst(programStringGetLocalStatistics, "$BRH$", "" + bRH); + programStringGetLocalStatistics = CLUtils.replaceFirst(programStringGetLocalStatistics, "$EPSILON$", "" + EPSILON); programGetLocalStatistics = context.createProgram(programStringGetLocalStatistics).build(); // Create, fill and write buffers float[] localMeans = new float[wh]; clLocalMeans = context.createFloatBuffer(wh, READ_WRITE); - GlobalRedundancy.fillBufferWithFloatArray(clLocalMeans, localMeans); + CLUtils.fillBufferWithFloatArray(clLocalMeans, localMeans); queue.putWriteBuffer(clLocalMeans, true); float[] localStds = new float[wh]; clLocalStds = context.createFloatBuffer(wh, READ_WRITE); - GlobalRedundancy.fillBufferWithFloatArray(clLocalStds, localStds); + CLUtils.fillBufferWithFloatArray(clLocalStds, localStds); queue.putWriteBuffer(clLocalStds, true); // Create kernel and set kernel arguments diff --git a/src/main/java/Utils.java b/src/main/java/Utils.java new file mode 100644 index 0000000..764babc --- /dev/null +++ b/src/main/java/Utils.java @@ -0,0 +1,1768 @@ +import ij.IJ; +import ij.ImagePlus; +import ij.ImageStack; +import ij.WindowManager; +import ij.measure.Calibration; +import ij.plugin.LutLoader; +import ij.process.FloatProcessor; +import ij.process.ImageProcessor; +import ij.process.LUT; +import java.awt.image.IndexColorModel; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import static java.lang.Math.*; + +public class Utils { + + public static float EPSILON = 0.0000001f; + + // Private constructor to prevent instantiation + private Utils(){ + throw new UnsupportedOperationException("Utility class"); + } + + + // -------------------------- // + // ---- UNSORTED OBJECTS ---- // + // -------------------------- // + + /** + * Custom object to hold a relevance mask. + */ + public static class RelevanceMask { + private final float[] relevanceMask; + private final float relevanceConstant; + private final float threshold; + private final float noiseMeanVariance; + + + /** + * Constructs a {@link RelevanceMask} object. + * + * @param relevanceMask A flattened array of float values representing the binary relevance mask (values are either 0.0 or 1.0) + * @param threshold The relevance threshold used to calculate the relevance mask. + * @param relevanceConstant The relevance constant used to calculate the relevance mask. + * @param noiseMeanVariance The mean variance of the noise in the input image used to calculate the relevance mask. + */ + public RelevanceMask(float[] relevanceMask, float relevanceConstant, float threshold, float noiseMeanVariance) { + this.relevanceMask = relevanceMask; + this.threshold = threshold; + this.relevanceConstant = relevanceConstant; + this.noiseMeanVariance = noiseMeanVariance; + } + + + /** + * Returns the binary relevance mask as flattened 1D array. + * + * @return A flattened 1D array of float values containing the relevance mask. + */ + public float[] getRelevanceMask() { + return relevanceMask; + } + + + /** + * Returns the relevance constant used to calculate the relevance mask. + * + * @return The relevance constant used to calculate the relevance mask. + */ + public float getRelevanceConstant() { + return relevanceConstant; + } + + + /** + * Returns the relevance threshold used to calculate the relevance mask (relevance constant * noiseMeanVariance). + * + * @return The relevance constant used to calculate the relevance mask. + */ + public float getRelevanceThreshold() { + return threshold; + } + + + /** + * Returns the relevance threshold used to calculate the relevance mask (relevance constant * noiseMeanVariance). + * + * @return The relevance constant used to calculate the relevance mask. + */ + public float getNoiseMeanVariance() { + return noiseMeanVariance; + } + } + + + // ----------------------------------------- // + // ---- OBJECTS FOR BLOCK REPETITION 2D ---- // + // ----------------------------------------- // + + /** + * This class represents a 2D reference block after processing an input square or rectangle. + * Processing involves discarding pixels outside the inbound ellipse and normalizing the range. + * It provides accessor methods to retrieve block dimensions and statistical information. + */ + public static class ReferenceBlock2D { + private final float[] pixels; + private final int width; + private final int height; + private final int radiusWidth; + private final int radiusHeight; + private final int size; + private final float mean; + private final float std; + + /** + * Constructs a ReferenceBlock2D object with the specified parameters. + * + * @param pixels A 1D array of float values containing the normalized block pixels. + * @param width Block width in pixels. + * @param height Block height in pixels. + * @param radiusWidth Block radius along its width in pixels. + * @param radiusHeight Block radius along its height in pixels. + * @param size Total number of pixels in the block. + * @param mean Mean value of the block pixels. + * @param std Standard deviation of the block pixels. + */ + public ReferenceBlock2D(float[] pixels, int width, int height, int radiusWidth, + int radiusHeight, int size, float mean, float std){ + + this.pixels = pixels; + this.width = width; + this.height = height; + this.radiusWidth = radiusWidth; + this.radiusHeight = radiusHeight; + this.size = size; + this.mean = mean; + this.std = std; + } + + + /** + * Returns the normalized block pixels. + * + * @return A 1D array of float values containing the normalized block pixels retained + * after processing. + */ + public float[] getPixels() { + return pixels; + } + + /** + * Returns the block width in pixels. + * + * @return Block width. + */ + public int getWidth() { + return width; + } + + /** + * Returns the block height in pixels. + * + * @return Block height. + */ + public int getHeight() { + return height; + } + + /** + * Returns the block radius along its width in pixels. + * + * @return Block width radius. + */ + public int getRadiusWidth() { + return radiusWidth; + } + + /** + * Returns the block radius along its height in pixels. + * + * @return Block height radius. + */ + public int getRadiusHeight() { + return radiusHeight; + } + + /** + * Returns the total number of pixels in the block. + * + * @return Size of the block. + */ + public int getSize() { + return size; + } + + /** + * Returns the mean value of the block pixels. + * + * @return Mean of the block. + */ + public float getMean() { + return mean; + } + + /** + * Returns the standard deviation of the block pixels. + * + * @return Standard deviation of the block. + */ + public float getStd() { + return std; + } + } + + + /** + * Custom object to hold a 2D input image along with its dimensions and statistics. + */ + public static class InputImage2D + { + private final float[] imageArray; + private final int width; + private final int height; + private final int size; + + /** + * Constructs an InputImage2D object with the specified image data and dimensions. + * + * @param imageArray A 1D array of float values representing the input image pixels. + * @param width The width of the image in pixels. + * @param height The height of the image in pixels. + * @param size The total number of pixels in the image. + */ + public InputImage2D(float[] imageArray, int width, int height, int size) { + this.imageArray = imageArray; + this.width = width; + this.height = height; + this.size = size; + } + + /** + * Returns the input image pixels as a 1D array. + * + * @return A 1D array of float values containing the input image pixels. + */ + public float[] getImageArray() { + return imageArray; + } + + /** + * Returns the width of the image in pixels. + * + * @return The width of the image. + */ + public int getWidth() { + return width; + } + + /** + * Returns the height of the image in pixels. + * + * @return The height of the image. + */ + public int getHeight() { + return height; + } + + /** + * Returns the total number of pixels in the image. + * + * @return The size of the image. + */ + public int getSize() { + return size; + } + } + + + // ----------------------------------------- // + // ---- OBJECTS FOR BLOCK REPETITION 3D ---- // + // ----------------------------------------- // + + /** + * This class represents a 3D reference block after processing an input square or rectangle. + * Processing involves discarding pixels outside the inbound spheroid and normalizing the range. + * It provides accessor methods to retrieve block dimensions and statistical information. + */ + public static class ReferenceBlock3D + { + private final float[] pixels; + private final int width; + private final int height; + private final int depth; + private final int radiusWidth; + private final int radiusHeight; + private final int radiusDepth; + private final int size; + private final float mean; + private final float std; + + /** + * Constructs a ReferenceBlock3D object with the specified parameters. + * + * @param pixels A 1D array of float values containing the normalized block pixels. + * @param width Block width in pixels. + * @param height Block height in pixels. + * @param radiusWidth Block radius along its width in pixels. + * @param radiusHeight Block radius along its height in pixels. + * @param size Total number of pixels in the block. + * @param mean Mean value of the block pixels. + * @param std Standard deviation of the block pixels. + */ + public ReferenceBlock3D(float[] pixels, int width, int height, int depth, int radiusWidth, + int radiusHeight, int radiusDepth, int size, float mean, float std){ + + this.pixels = pixels; + this.width = width; + this.height = height; + this.depth = depth; + this.radiusWidth = radiusWidth; + this.radiusHeight = radiusHeight; + this.radiusDepth = radiusDepth; + this.size = size; + this.mean = mean; + this.std = std; + } + + + /** + * Returns the normalized block pixels. + * + * @return A 1D array of float values containing the normalized block pixels retained + * after processing. + */ + public float[] getPixels() { + return pixels; + } + + /** + * Returns the block width in pixels. + * + * @return Block width. + */ + public int getWidth() { + return width; + } + + /** + * Returns the block height in pixels. + * + * @return Block height. + */ + public int getHeight() { + return height; + } + + /** + * Returns the block depth in pixels. + * + * @return Block height. + */ + public int getDepth() { + return depth; + } + + /** + * Returns the block radius along its width in pixels. + * + * @return Block width radius. + */ + public int getRadiusWidth() { + return radiusWidth; + } + + /** + * Returns the block radius along its height in pixels. + * + * @return Block height radius. + */ + public int getRadiusHeight() { + return radiusHeight; + } + + /** + * Returns the block radius along its height in pixels. + * + * @return Block height radius. + */ + public int getRadiusDepth() { + return radiusDepth; + } + + /** + * Returns the total number of pixels in the block. + * + * @return Size of the block. + */ + public int getSize() { + return size; + } + + /** + * Returns the mean value of the block pixels. + * + * @return Mean of the block. + */ + public float getMean() { + return mean; + } + + /** + * Returns the standard deviation of the block pixels. + * + * @return Standard deviation of the block. + */ + public float getStd() { + return std; + } + } + + /** + * Custom object to hold a 3D input image along with its dimensions and statistics. + */ + public static class InputImage3D + { + private final float[] imageArray; + private final int width; + private final int height; + private final int depth; + private final int size; + private final Calibration calibration; + private final float gain; + private final float sigma; + private final float offset; + + /** + * Constructs an InputImage2D object with the specified image data and dimensions. + * + * @param imageArray A 1D array of float values representing the input image pixels. + * @param width The width of the image in pixels. + * @param height The height of the image in pixels. + * @param size The total number of pixels in the image. + */ + public InputImage3D(float[] imageArray, int width, int height, int depth, int size, Calibration calibration, + float gain, float sigma, float offset) + { + this.imageArray = imageArray; + this.width = width; + this.height = height; + this.depth = depth; + this.size = size; + this.calibration = calibration; + this.gain = gain; + this.sigma = sigma; + this.offset = offset; + } + + /** + * Returns the input image pixels as a 1D array. + * + * @return A 1D array of float values containing the input image pixels. + */ + public float[] getImageArray() { + return imageArray; + } + + /** + * Returns the width of the image in pixels. + * + * @return The width of the image. + */ + public int getWidth() { + return width; + } + + /** + * Returns the height of the image in pixels. + * + * @return The height of the image. + */ + public int getHeight() { + return height; + } + + /** + * Returns the depth of the image in pixels. + * + * @return The height of the image. + */ + public int getDepth() { + return depth; + } + + /** + * Returns the total number of pixels in the image. + * + * @return The size of the image. + */ + public int getSize() { + return size; + } + + /** + * Returns the ImageJ calibration object of the image. + * + * @return The calibration object of the image. + */ + public Calibration getCalibration(){ + return calibration; + } + + /** + * Returns the optimised "gain" parameter used in the VST. + * + * @return The calibration object of the image. + */ + public float getGain(){ + return gain; + } + + /** + * Returns the optimised "sigma" parameter used in the VST. + * + * @return The calibration object of the image. + */ + public float getSigma(){ + return sigma; + } + + /** + * Returns the optimised "offset" parameter used in the VST. + * + * @return The calibration object of the image. + */ + public float getOffset(){ + return offset; + } + } + + + // ----------------------------------------- // + // ---- METHODS FOR BLOCK REPETITION 2D ---- // + // ----------------------------------------- // + + /** + * Retrieves a ReferenceBlock2D object from a 2D image containing a reference block. + * + *

If the blockID or the block's dimensions are invalid (e.g., not odd), the function will return {@code null}.

+ *

The block image must be 8-bit, 16-bit, or 32-bit; RGB is not supported.

+ * + * @param blockID The ImageJ/Fiji window ID of the image containing the reference block. + * @return A ReferenceBlock2D object, or {@code null} if the input image is invalid (i.e., window not found or even block dimensions). + */ + public static ReferenceBlock2D getReferenceBlock2D(int blockID) + { + ImagePlus imp = WindowManager.getImage(blockID); + + if (imp == null) { + IJ.error("Block image not found. Try again."); + return null; + } + + ImageProcessor ip = imp.getProcessor(); + FloatProcessor fp = ip.convertToFloatProcessor(); + float[] blockProcessor = (float[]) fp.getPixels(); + int bW = fp.getWidth(); // Block width + int bH = fp.getHeight(); // Block height + IJ.log("Block dimensions: " + bW + "x" + bH); + + // Check if block dimensions are odd; return null if not + if (bW % 2 == 0 || bH % 2 == 0) { + IJ.error("Block dimensions must be odd (e.g., 3x3 or 5x5). Please try again."); + return null; + } + + // Calculate block radius + int bRW = bW / 2; // Patch radius (x-axis) + int bRH = bH / 2; // Patch radius (y-axis) + + // Get final block size (after removing pixels outside inbound circle/ellipse) + int blockSize = 0; + for (int j = 0; j < bH; j++) { + for (int i = 0; i < bW; i++) { + float dx = (float) (i - bRW); + float dy = (float) (j - bRH); + if (((dx * dx) / (float) (bRW * bRW)) + ((dy * dy) / (float) (bRH * bRH)) <= 1.0f) { + blockSize++; + } + } + } + + // Get flattened block array (keeping only the pixels within the inbound circle/ellipse) + float[] blockArray = new float[blockSize]; + int index = 0; + for (int j = 0; j < bH; j++) { + for (int i = 0; i < bW; i++) { + float dx = (float) (i - bRW); + float dy = (float) (j - bRH); + if (((dx * dx) / (float) (bRW * bRW)) + ((dy * dy) / (float) (bRH * bRH)) <= 1.0f) { + blockArray[index] = blockProcessor[j * bW + i]; + index++; + } + } + } + + // Calculate block min and max + float blockMin = Float.MAX_VALUE; // Initialize as a very large number + float blockMax = -Float.MAX_VALUE; // Initialize as a very small number + + for (int i = 0; i < blockSize; i++) { + blockMin = Math.min(blockMin, blockArray[i]); + blockMax = Math.max(blockMax, blockArray[i]); + } + + // Normalize and calculate mean + float blockMean = 0.0f; + for (int i = 0; i < blockSize; i++) { + blockArray[i] = (blockArray[i] - blockMin) / (blockMax - blockMin + EPSILON); + blockMean += blockArray[i]; + } + blockMean /= (float) blockSize; + + // Subtract mean + for (int i = 0; i < blockSize; i++) { + blockArray[i] = blockArray[i] - blockMean; + } + + // Calculate block standard deviation + float blockStd = 0.0f; + for (int i = 0; i < blockSize; i++) { + blockStd += (blockArray[i] - blockMean) * (blockArray[i] - blockMean); + } + blockStd = (float) Math.sqrt(blockStd / ((float) (blockSize - 1))); + + return new ReferenceBlock2D(blockArray, bW, bH, bRW, bRH, blockSize, blockMean, blockStd); + } + + + /** + * Retrieves an InputImage2D object from a 2D image, optionally stabilizing noise variance + * and normalizing the output. + * + *

If the imageID is invalid, the function will return {@code null}.

+ * + * @param imageID The ImageJ/Fiji window ID of the image to be retrieved. + * @param stabiliseNoiseVariance If {@code true}, applies variance stabilization to the image. + * @param normalizeOutput If {@code true}, normalizes the pixel values of the image. + * @return An InputImage2D object, or {@code null} if the input image is invalid (i.e., window not found). + */ + public static InputImage2D getInputImage2D(int imageID, boolean stabiliseNoiseVariance, boolean normalizeOutput) + { + // Get ImagePlus object + ImagePlus imp = WindowManager.getImage(imageID); + + // Check if image dimensions are odd; return null if not found + if (imp == null) { + IJ.error("Image not found. Try again."); + return null; + } + + // Get processors and dimensions + ImageProcessor ip = imp.getProcessor(); + FloatProcessor fp = ip.convertToFloatProcessor(); + float[] imageArray = (float[]) fp.getPixels(); + int width = fp.getWidth(); + int height = fp.getHeight(); + int size = width * height; + IJ.log("Image dimensions: "+width+"x"+height); + + float gain = 0.0f; // Placeholder + float sigma = 0.0f; // Placeholder + float offset = 0.0f; // Placeholder + + if (stabiliseNoiseVariance) { + IJ.log("Stabilizing noise variance of the image..."); + GATMinimizer2D minimizer = new GATMinimizer2D(imageArray, width, height, 1, 10, 100); + minimizer.run(); + gain = (float) minimizer.gain; + sigma = (float) minimizer.sigma; + offset = (float) minimizer.offset; + + imageArray = VarianceStabilisingTransform2D_.getGAT(imageArray, gain, sigma, offset); + } + + if (normalizeOutput) { + // Get min and max + float imageMin = Float.MAX_VALUE; + float imageMax = -Float.MAX_VALUE; + for (int i = 0; i < size; i++) { + float pixelValue = imageArray[i]; + imageMin = Math.min(imageMin, pixelValue); + imageMax = Math.max(imageMax, pixelValue); + } + + // Normalize + for (int i = 0; i < size; i++) { + imageArray[i] = (imageArray[i] - imageMin) / (imageMax - imageMin + EPSILON); + } + } + + return new InputImage2D(imageArray, width, height, size); + } + + /** + * Retrieves an InputImage2D object from a 2D image, optionally stabilizing noise variance + * and normalizing the output. + * + * NOTE: This is an OVERLOAD method that takes an image array instead of an ImageJ image ID uses specific GAT parameter values to calculate the VST. + * + *

If the imageID is invalid, the function will return {@code null}.

+ * + * @param imageArray An image array (float). + * @param imageWidth The width of the image. + * @param imageHeight The height of the image. + * @param stabiliseNoiseVariance If {@code true}, applies variance stabilization to the image. + * @param gain The gain to be used in the VST + * @param sigma The sigma to be used in the VST + * @param offset The offset to be used in the VST + * @param normalizeOutput If {@code true}, normalizes the pixel values of the image. + * @return An InputImage2D object, or {@code null} if the input image is invalid (i.e., window not found). + */ + public static InputImage2D getInputImage2D(float[] imageArray, int imageWidth, int imageHeight, + boolean stabiliseNoiseVariance, float gain, float sigma, float offset, + boolean normalizeOutput) + { + + int imageSize = imageWidth * imageHeight; + + if (stabiliseNoiseVariance) { + IJ.log("Stabilizing noise variance of the image..."); + imageArray = VarianceStabilisingTransform2D_.getGAT(imageArray, gain, sigma, offset); + } + + if (normalizeOutput) { + // Get min and max + float imageMin = Float.MAX_VALUE; + float imageMax = -Float.MAX_VALUE; + for (int i = 0; i < imageSize; i++) { + float pixelValue = imageArray[i]; + imageMin = Math.min(imageMin, pixelValue); + imageMax = Math.max(imageMax, pixelValue); + } + + // Normalize + for (int i = 0; i < imageSize; i++) { + imageArray[i] = (imageArray[i] - imageMin) / (imageMax - imageMin + EPSILON); + } + } + + return new InputImage2D(imageArray, imageWidth, imageHeight, imageSize); + } + + + /** + * Calculates the mean noise variance for a 2D reference pixel array. + * + *

This method divides the reference pixel array into blocks and computes local variances. + * It then returns the average of the lowest 3% of these variances, scaled based on a specified formula.

+ * + * @param refPixels A 1D array (float32) containing the reference pixel values. + * @param w The width of the image (in pixels). + * @param h The height of the image (in pixels). + * @param wh The total number of pixels in the image (width * height). + * @return The calculated mean noise variance. + */ + public static float getMeanNoiseVar2D(float[] refPixels, int w, int h, int wh) + { + int blockWidth, blockHeight; + int CIF = 352 * 288; // Resolution of a CIF file + + // Determine block dimensions based on image size + if (wh <= CIF) { + blockWidth = 8; + blockHeight = 8; + } else { + blockWidth = 16; + blockHeight = 16; + } + + // Calculate the number of blocks in each dimension + int nBlocksX = w / blockWidth; // number of blocks in each row + int nBlocksY = h / blockHeight; // number of blocks in each column + int nBlocks = nBlocksX * nBlocksY; // total number of blocks + float[] localVars = new float[nBlocks]; + Arrays.fill(localVars, 0.0f); + + // Calculate local variances + int index = 0; + for (int y = 0; y < nBlocksY; y++) { + for (int x = 0; x < nBlocksX; x++) { + float[] meanVar = getMeanAndVarBlock2D(refPixels, w, x*blockWidth, y*blockHeight, (x+1)*blockWidth, (y+1)*blockHeight); + localVars[index] = meanVar[1]; // Store variance + index++; + } + } + + // Sort the local variances + Arrays.sort(localVars); + + // Get the 3% lowest variances and calculate their average + int nVars = (int) (0.03f * (float) nBlocks + 1.0f); // Number of blocks corresponding to 3% of the total + float noiseVar = 0.0f; + + for (int i = 0; i < nVars; i++) { + noiseVar += localVars[i]; + } + + // Calculate average of the lowest variances + noiseVar = Math.abs(noiseVar / (float) nVars); + noiseVar = (1.0f + 0.001f * (noiseVar - 40.0f)) * noiseVar; + + return noiseVar; + } + + + /** + * Calculates the mean and variance of a block of pixels from a 1D pixel array in a single pass. + * + *

This method computes the mean and variance for a specified rectangular block of pixels + * defined by its start and end coordinates.

+ * + * @param imageArray A 1D array (float32) containing the pixel values. + * @param imageWidth The width of the image (in pixels), used for indexing into the pixel array. + * @param xStart The starting x-coordinate (inclusive) of the block. + * @param yStart The starting y-coordinate (inclusive) of the block. + * @param xEnd The ending x-coordinate (exclusive) of the block. + * @param yEnd The ending y-coordinate (exclusive) of the block. + * @return A float array where the first element is the mean and the second element is the variance of the block. + */ + public static float[] getMeanAndVarBlock2D(float[] imageArray, int imageWidth, int xStart, int yStart, int xEnd, + int yEnd) + { + float mean = 0.0f; + float var; + float sq_sum = 0.0f; + + int blockWidth = xEnd - xStart; // Block width + int blockHeight = yEnd - yStart; // Block height + int blockSize = blockWidth * blockHeight; // Total number of pixels in the block + + // Calculate the sum of pixel values and the sum of squared pixel values + for (int y=yStart; yThis method modifies the input image array by applying a provided mask, + * which scales each pixel in the image according to the corresponding value in the mask.

+ * + * @param imageArray An input image array (float32). + * @param imageWidth The width of the input image array. + * @param imageHeight The height of the input image array. + * @param mask A user-provided mask (float32). + * @return A masked image array (the modified input image array). + */ + public static float[] applyMask2D(float[] imageArray, int imageWidth, int imageHeight, float[] mask) + { + // Apply mask + for (int y = 0; y < imageHeight; y++) { + for (int x = 0; x < imageWidth; x++) { + imageArray[y * imageWidth + x] *= mask[y * imageWidth + x]; + } + } + + return imageArray; + } + + + /** + * Calculate relevance mask based on structural relevance. + * + *

This method generates a binary mask where the center pixels of each local neighborhood are assigned a value of zero + * if the local variance is below or equal to the relevance threshold, which is determined by the mean noise variance + * multiplied by a user-defined relevance constant.

+ * + * @param imageArray An input image array (float) used to calculate the mean noise variance and the relevance mask. + * @param imageWidth The width of the input image array. + * @param imageHeight The height of the input image array. + * @param blockRadiusWidth The width radius of the block used in the repetition analysis. + * @param blockRadiusHeight The height radius of the block used in the repetition analysis. + * @param localStds The local standard deviations array calculated with {@link CLUtils.getLocalStatistics2D()}. + * @param relevanceConstant A user-provided relevance constant to control the strength of the relevance filter. + * @return A {@link RelevanceMask} object. + */ + public static RelevanceMask getRelevanceMask(float[] imageArray, int imageWidth, int imageHeight, + int blockRadiusWidth, int blockRadiusHeight, float[] localStds, + float relevanceConstant) + { + // Calculate noise mean variance + int imageSize = imageWidth * imageHeight; + + float noiseMeanVariance = getMeanNoiseVar2D(imageArray, imageWidth, imageHeight, imageSize); + float relevanceThreshold = noiseMeanVariance*relevanceConstant; + + // Calculate relevance mask + float[] relevanceMask = new float[imageSize]; + Arrays.fill(relevanceMask, 0.0f); // Fill with zeros just to be sure + for (int y = blockRadiusHeight; y < imageHeight - blockRadiusHeight; y++) { + for (int x = blockRadiusWidth; x < imageWidth - blockRadiusWidth; x++) { + if ((localStds[y * imageWidth + x] * localStds[y * imageWidth + x]) <= relevanceThreshold) { + relevanceMask[y * imageWidth + x] = 0.0f; // Assign zero if local variance is below threshold + } else { + relevanceMask[y * imageWidth + x] = 1.0f; // Assign one if local variance is above threshold + } + } + } + + return new RelevanceMask(relevanceMask, relevanceConstant, relevanceThreshold, noiseMeanVariance); // Return the relevance mask + } + + + /** + * Normalize an image (in-place) to a specified range, with the option to avoid masked pixels. + * + *

This method calculates the minimum and maximum pixel values of the input image while excluding + * any masked pixels, and then normalizes the image based on these values.

+ * + * @param imageArray The input image array (float) to normalize. + * @param imageWidth The width of the input image array. + * @param imageHeight The height of the input image array. + * @param borderWidth The width of the border to exclude from the normalization calculation. + * @param borderHeight The height of the border to exclude from the normalization calculation. + * @param mask A binary mask (float) where masked pixels will be excluded from the calculation. + * @return A normalized image array (float) where pixel values are remapped to the range [0, 1]. + */ + public static float[] normalizeImage2D(float[] imageArray, int imageWidth, int imageHeight, int borderWidth, + int borderHeight, float[] mask) + { + // Find min and max + float imageMin = Float.MAX_VALUE; + float imageMax = -Float.MAX_VALUE; + + if(mask==null) { + for (int y=borderHeight; y 0.0f) { + imageMin = min(imageArray[y*imageWidth+x], imageMin); + imageMax = max(imageArray[y*imageWidth+x], imageMax); + } + } + } + } + + // Remap pixels + float[] normalizedArray = imageArray.clone(); + if(mask==null) { + for (int y=borderHeight; y0.0f) { + normalizedArray[y*imageWidth+x] = (normalizedArray[y*imageWidth+x]-imageMin)/(imageMax-imageMin+EPSILON); + } + } + } + } + return normalizedArray; + } + + + // ----------------------------------------- // + // ---- METHODS FOR BLOCK REPETITION 3D ---- // + // ----------------------------------------- // + + /** + * Retrieves a ReferenceBlock3D object from a 3D image containing a reference block. + * + *

If the blockID or the block's dimensions are invalid (e.g., not odd), the function will return {@code null}.

+ *

The block image must be 8-bit, 16-bit, or 32-bit; RGB is not supported.

+ * + * @param blockID The ImageJ/Fiji window ID of the image containing the reference block. + * @return A ReferenceBlock2D object, or {@code null} if the input image is invalid (i.e., window not found or even block dimensions). + */ + public static ReferenceBlock3D getReferenceBlock3D(int blockID) + { + ImagePlus imp = WindowManager.getImage(blockID); + + // Check if image is found + if (imp == null) { + IJ.error("Block image not found. Try again."); + throw new IllegalArgumentException("Block image not found."); + } + + // Get block dimensions + ImageStack ims = imp.getStack(); + int bW = ims.getWidth(); // Block width + int bH = ims.getHeight(); // Block height + int bZ = ims.getSize(); // block depth + int bRW = bW / 2; // Patch radius (x-axis) + int bRH = bH / 2; // Patch radius (y-axis) + int bRZ = bZ / 2; // Patch radius (z-axis) + IJ.log("Block dimensions: " + bW + "x" + bH + "x" + bZ); + + // Check if block dimensions are odd + if (bW % 2 == 0 || bH % 2 == 0 || bZ % 2 == 0) { + IJ.error("Block dimensions must be odd (e.g., 3x3x3 or 5x5x5). Please try again."); + throw new IllegalArgumentException("Block dimensions must be odd."); + } + + // Check if patch has at least 3 slices + if (bZ < 3) { + IJ.error("Block must have at least 3 slices. Please try again."); + throw new IllegalArgumentException("Reference block must have at least 3 slices."); + } + + // Get final block size (after removing pixels outside the sphere/ellipsoid) + int blockSize = 0; + for (int z = 0; z < bZ; z++) { + for (int y = 0; y < bH; y++) { + for (int x = 0; x < bW; x++) { + float dx = (float) (x - bRW); + float dy = (float) (y - bRH); + float dz = (float) (z - bRZ); + if (((dx * dx) / (float) (bRW * bRW)) + ((dy * dy) / (float) (bRH * bRH)) + ((dz * dz) / (float) (bRZ * bRZ)) <= 1.0f) { + blockSize++; + } + } + } + } + + // Get block array + float[][] blockArray = new float[bZ][bW * bH]; + for (int z = 0; z < bZ; z++) { + for (int y = 0; y < bH; y++) { + for (int x = 0; x < bW; x++) { + blockArray[z][y*bW+x] = ims.getProcessor(z+1).convertToFloatProcessor().getf(x,y); + } + } + } + + // Get flattened block array (keeping only the pixels within the inbound spheroid) + float[] blockArray1D = new float[blockSize]; + int index = 0; + for (int z=0; zIf the imageID is invalid, the function will return {@code null}.

+ * + * @param imageID The ImageJ/Fiji window ID of the image to be retrieved. + * @param stabiliseNoiseVariance If {@code true}, applies variance stabilization to the image. + * @param normalizeOutput If {@code true}, normalizes the pixel values of the image. + * @return An InputImage3D object, or {@code null} if the input image is invalid (i.e., window not found). + */ + public static InputImage3D getInputImage3D(int imageID, boolean stabiliseNoiseVariance, boolean normalizeOutput) + { + // Get ImagePlus object + ImagePlus imp = WindowManager.getImage(imageID); + + // Check if image dimensions are odd; return null if not found + if (imp == null) { + IJ.error("Image not found. Try again."); + return null; + } + + // Get calibration parameters + Calibration calibration = imp.getCalibration(); + + // Get ImageStack object + ImageStack ims = imp.getStack(); + + // Get processors and dimensions + //float[] imageArray = (float[]) fp.getPixels(); + int width = ims.getWidth(); + int height = ims.getHeight(); + int depth = ims.getSize(); + int size = width * height * depth; + IJ.log("Image dimensions: "+width+"x"+height+"x"+depth); + + // Check if image has at least 3 slices, otherwise kill program + if (depth < 3) { + IJ.error("Image must have at least 3 slices. Please try again."); + return null; + } + + // Get image stack array (float[z][wh]) + float[][] stackArray = new float[depth][width*height]; + for(int z=0; zIf the imageID is invalid, the function will return {@code null}.

+ * + * @param imageArray An image array of the image (float). + * @param stabiliseNoiseVariance If {@code true}, applies variance stabilization to the image. + * @param normalizeOutput If {@code true}, normalizes the pixel values of the image. + * @return An InputImage3D object, or {@code null} if the input image is invalid (i.e., window not found). + */ + public static InputImage3D getInputImage3D(float[] imageArray, int width, int height, int depth, boolean stabiliseNoiseVariance, boolean normalizeOutput) + { + + int size = width*height*depth; + + // Stabilise noise variance + float gain = 0.0f; // Placeholder + float sigma = 0.0f; // Placeholder + float offset = 0.0f; // Placeholder + + if (stabiliseNoiseVariance) { + IJ.log("Stabilizing noise variance of the image..."); + + // Get image stack array (float[z][wh]) + float[][] stackArray = new float[depth][width*height]; + for(int z=0; zThis method computes the mean and variance for a specified rectangular block of pixels + * defined by its start and end coordinates.

+ * + * @param pixels A 1D array (float32) containing the pixel values. + * @param imageWidth The width of the image (in pixels), used for indexing into the pixel array. + * @param imageHeight The height of the image (in pixels), used for indexing into the pixel array. + * @param xStart The starting x-coordinate (inclusive) of the block. + * @param yStart The starting y-coordinate (inclusive) of the block. + * @param zStart The starting z-coordinate (incusive) of the block. + * @param xEnd The ending x-coordinate (exclusive) of the block. + * @param yEnd The ending y-coordinate (exclusive) of the block. + * @param zEnd The ending z-coordinate (exclusive) of the block. + * @return A float array where the first element is the mean and the second element is the variance of the block. + */ + public static float[] getMeanAndVarBlock3D(float[] pixels, int imageWidth, int imageHeight, int xStart, int yStart, + int zStart, int xEnd, int yEnd, int zEnd) + { + float mean = 0.0f; + float var; + float sq_sum = 0.0f; + + int blockWidth = xEnd - xStart; // Block width + int blockHeight = yEnd - yStart; // Block height + int blockDepth = zEnd - zStart; // Block depth + int blockSize = blockWidth * blockHeight * blockDepth; // Total number of pixels in the block + + // Calculate the sum of pixel values and the sum of squared pixel values + for(int z=zStart; zThis method divides the reference pixel array into blocks and computes local variances. + * It then returns the average of the lowest 3% of these variances, scaled based on a specified formula.

+ * + * @param refPixels A flattened 1-D array (float32) containing the reference pixel values. + * @param imageWidth The width of the image (in pixels). + * @param imageHeight The height of the image (in pixels). + * @param imageDepth the depth of the image (in pixels). + * @param imageSize The total number of pixels in the image (width * height * depth). + * @return The calculated mean noise variance. + * + public static float getMeanNoiseVar3D(float[] imageArray, int imageWidth, int imageHeight, int imageDepth, int imageSize) { + int blockWidth, blockHeight, blockDepth; + int CIF = 352 * 288; // Resolution of a CIF file + + // Determine block dimensions based on image size + if (imageWidth*imageHeight <= CIF) { + blockWidth = 8; + blockHeight = 8; + blockDepth = 5; // TODO: this was arbitrary, didn't want to use more slices + } else { + blockWidth = 16; + blockHeight = 16; + blockDepth = 5; // TODO: this was arbitrary, didn't want to use more slices + } + + // Calculate the number of blocks in each dimension + int nBlocksX = imageWidth / blockWidth; // number of blocks in each row + int nBlocksY = imageHeight / blockHeight; // number of blocks in each column + int nBlocksZ = imageDepth / blockDepth; // number of bocks in depth + int nBlocks = nBlocksX * nBlocksY * nBlocksZ; // total number of blocks + float[] localVars = new float[nBlocks]; + Arrays.fill(localVars, 0.0f); + + // Calculate local variances + int index = 0; + for(int z=0; zThis method generates a 3D binary mask where the center pixels of each local neighborhood are assigned a value of zero + * if the local variance is below or equal to the relevance threshold, which is determined by the mean noise variance + * multiplied by a user-defined relevance constant.

+ * + * @param imageWidth The width of the input image. + * @param imageHeight The height of the input image. + * @param imageDepth The depth of the input image. + * @param blockRadiusWidth The width radius of the block used in the repetition analysis. + * @param blockRadiusHeight The height radius of the block used in the repetition analysis. + * @param blockRadiusDepth The depth radius of the block used in the repetition analysis. + * @param localStds The local standard deviations array calculated with {@link CLUtils.getLocalStatistics3D()}. + * @param relevanceConstant A user-provided relevance constant to control the strength of the relevance filter. + * @return A binary mask (float) where center pixels of each local neighbourhood are assigned a value of zero if the local variance is below or equal + * to the relevance threshold; otherwise, they are assigned a value of one. + */ + public static float[] getRelevanceMask3D(int imageWidth, int imageHeight, int imageDepth, int blockRadiusWidth, + int blockRadiusHeight, int blockRadiusDepth, float[] localStds, + float relevanceConstant) + { + // Calculate noise mean variance + float noiseMeanVariance = getMeanNoiseVar3D(imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, localStds); + + // Calculate relevance mask + float[] relevanceMask = new float[imageWidth*imageHeight*imageDepth]; + Arrays.fill(relevanceMask, 0.0f); // Fill with zeros just to be sure + + for(int z=blockRadiusDepth; zThis method modifies the input image array by applying a provided mask, + * which scales each pixel in the image according to the corresponding value in the mask.

+ * + * @param imageArray An input image array (float32). + * @param imageWidth The width of the input image. + * @param imageHeight The height of the input image. + * @param imageDepth The depth of the input image. + * @param mask A user-provided binary mask (float32). + * @return A masked image array (the modified input image array). + */ + public static float[] applyMask3D(float[] imageArray, int imageWidth, int imageHeight, int imageDepth, float[] mask) + { + // Apply mask + for(int z=0; zThis method calculates the minimum and maximum pixel values of the input image while excluding + * any masked pixels, and then normalizes the image based on these values.

+ * + * @param imageArray The input image array (float) to normalize. + * @param imageWidth The width of the input image array. + * @param imageHeight The height of the input image array. + * @param borderWidth The width of the border to exclude from the normalization calculation. + * @param borderHeight The height of the border to exclude from the normalization calculation. + * @param mask A binary mask (float) where masked pixels will be excluded from the calculation. + * @return A normalized image array (float) where pixel values are remapped to the range [0, 1]. + */ + public static float[] normalizeImage3D(float[] imageArray, int imageWidth, int imageHeight, int imageDepth, int borderWidth, + int borderHeight, int borderDepth, float[] mask) + { + + // Find min and max + float imageMin = Float.MAX_VALUE; + float imageMax = -Float.MAX_VALUE; + + if(mask==null) { + for (int z = borderDepth; z < imageDepth - borderDepth; z++) { + for (int y = borderHeight; y < imageHeight - borderHeight; y++) { + for (int x = borderWidth; x < imageWidth - borderWidth; x++) { + int index = imageWidth * imageHeight * z + y * imageWidth + x; + imageMin = min(imageArray[index], imageMin); + imageMax = max(imageArray[index], imageMax); + } + } + } + }else{ + for (int z = borderDepth; z < imageDepth - borderDepth; z++) { + for (int y = borderHeight; y < imageHeight - borderHeight; y++) { + for (int x = borderWidth; x < imageWidth - borderWidth; x++) { + int index = imageWidth * imageHeight * z + y * imageWidth + x; + if (mask[index] > 0.0f) { + imageMin = min(imageArray[index], imageMin); + imageMax = max(imageArray[index], imageMax); + } + } + } + } + } + + // Remap pixels + float[] normalizedImage = imageArray.clone(); + if(mask==null) { + for (int z = borderDepth; z < imageDepth - borderDepth; z++) { + for (int y = borderHeight; y < imageHeight - borderHeight; y++) { + for (int x = borderWidth; x < imageWidth - borderWidth; x++) { + int index = imageWidth * imageHeight * z + y * imageWidth + x; + normalizedImage[index] = (imageArray[index] - imageMin) / (imageMax - imageMin + EPSILON); + } + } + } + }else{ + for (int z = borderDepth; z < imageDepth - borderDepth; z++) { + for (int y = borderHeight; y < imageHeight - borderHeight; y++) { + for (int x = borderWidth; x < imageWidth - borderWidth; x++) { + int index = imageWidth * imageHeight * z + y * imageWidth + x; + if (mask[index] > 0.0f) { + normalizedImage[index] = (imageArray[index] - imageMin) / (imageMax - imageMin + EPSILON); + } + } + } + } + } + + return normalizedImage; + } + + + // -------------------------- // + // ---- UNSORTED METHODS ---- // + // -------------------------- // + + /** + * Normalizes an array to its range. + * + * @param array + * @return A copy of the original array, normalized to the range + */ + public static float[] normalizeArray(float[] array) + { + + // Cache variables + int arrayLength = array.length; + + // Get min max + float arrayMin = Float.MAX_VALUE; + float arrayMax = -Float.MAX_VALUE; + for(int i=0; iThis method computes the mean, variance, and standard deviation of the input array.

+ * + * @param a The input array of float values. + * @return An array of float containing the mean, variance, and standard deviation in that order. + * Returns an array of zeros if the input array is empty. + */ + private float[] meanVarStd(float[] a) { + int n = a.length; + if (n == 0) return new float[]{0, 0, 0}; // Return zeros for empty array + + double sum = 0.0; + double sq_sum = 0.0; + + // Calculate sum and squared sum + for (int i = 0; i < n; i++) { + sum += a[i]; + sq_sum += a[i] * a[i]; + } + + double mean = sum / n; // Calculate mean + double variance = Math.abs(sq_sum / n - mean * mean); // Calculate variance + // abs() solves a bug where negative zeros appeared + + return new float[]{(float) mean, (float) variance, (float) Math.sqrt(variance)}; // Return mean, variance, and std + } + + + /** + * Retrieve the image ID corresponding to a given title. + * + * @param titles Array of image titles. + * @param ids Array of image IDs. + * @param title The title to match. + * @return The matching image ID, or 0 if not found. + */ + public static int getImageIDByTitle(String[] titles, int[] ids, String title) { + for (int i = 0; i < titles.length; i++) { + if (titles[i].equals(title)) { + return ids[i]; // Return the corresponding ID if title matches + } + } + throw new IllegalArgumentException("Title not found: " + title); // Throw exception if title not found + } + + + /** + * Display the results as a 2D image. + * + * @param inputImage The input image used for display dimensions. + * @param repetitionMap The calculated repetition map to display. + */ + public static void displayResults2D(Utils.InputImage2D inputImage, float[] repetitionMap) { + FloatProcessor fp1 = new FloatProcessor(inputImage.getWidth(), inputImage.getHeight(), repetitionMap); + ImagePlus imp1 = new ImagePlus("Repetition Map", fp1); + + // Apply SReD LUT + InputStream lutStream = Utils.class.getResourceAsStream("/luts/sred-jet.lut"); + if (lutStream == null) { + IJ.error("Could not load SReD LUT. Using default LUT."); + } else { + try { + // Load LUT file + IndexColorModel icm = LutLoader.open(lutStream); + byte[] r = new byte[256]; + byte[] g = new byte[256]; + byte[] b = new byte[256]; + icm.getReds(r); + icm.getGreens(g); + icm.getBlues(b); + LUT lut = new LUT(8, 256, r, g, b); + + // Apply LUT to image + imp1.getProcessor().setLut(lut); + } catch (IOException e) { + IJ.error("Could not load SReD LUT: " + e.getMessage()); + } + } + + // Display results + imp1.show(); + } + + + /** + * Display the results as a 3D image. + * + * @param inputImage The input image used for display dimensions. + * @param repetitionMap The calculated repetition map to display. + */ + public static void displayResults3D(Utils.InputImage3D inputImage, float[] repetitionMap) { + + int imageWidth = inputImage.getWidth(); + int imageHeight = inputImage.getHeight(); + int imageDepth = inputImage.getDepth(); + ImageStack ims = new ImageStack(imageWidth, imageHeight, imageDepth); + + for(int z=0; z=w-bRW || gy=h-bRH){ + diff_std_map[gy*w+gx] = 0.0f; + return; + } + + + // -------------------------------------------------------------- // + // ---- Calculate absolute difference of standard deviations ---- // + // -------------------------------------------------------------- // + + float test_std = local_stds[gy*w+gx]; + + diff_std_map[gy*w+gx] = fabs((float)ref_std - (float)test_std); +} diff --git a/src/main/resources/kernelGetPatchCosineSim2D.cl b/src/main/resources/kernelGetBlockCosineSimilarity2D.cl similarity index 55% rename from src/main/resources/kernelGetPatchCosineSim2D.cl rename to src/main/resources/kernelGetBlockCosineSimilarity2D.cl index d132013..9d2f041 100644 --- a/src/main/resources/kernelGetPatchCosineSim2D.cl +++ b/src/main/resources/kernelGetBlockCosineSimilarity2D.cl @@ -3,18 +3,20 @@ #define h $HEIGHT$ #define bRW $BRW$ #define bRH $BRH$ -#define ref_std $PATCH_STD$ +#define ref_std $BLOCK_STD$ #define EPSILON $EPSILON$ -kernel void kernelGetPatchCosineSim2D( + +kernel void kernelGetBlockCosineSimilarity2D( global float* local_stds, - global float* diff_std_map + global float* cosine_similarity_map ){ int gx = get_global_id(0); int gy = get_global_id(1); - // Bound check (avoids borders dynamically based on patch dimensions) + // Bound check (avoids borders dynamically based on block dimensions) if(gx=w-bRW || gy=h-bRH){ + cosine_similarity_map[gy*w+gx] = 0.0f; return; } @@ -25,6 +27,6 @@ kernel void kernelGetPatchCosineSim2D( float test_std = local_stds[gy*w+gx]; - float similarity = (ref_std*test_std) / (sqrt(ref_std*ref_std)*sqrt(test_std*test_std)+EPSILON); - diff_std_map[gy*w+gx] = (float)fmax(0.0f, similarity); + float similarity = (ref_std*test_std) / (sqrt((float)ref_std*(float)ref_std)*sqrt((float)test_std*(float)test_std)+EPSILON); + cosine_similarity_map[gy*w+gx] = (float)fmax(0.0f, similarity); } diff --git a/src/main/resources/kernelGetBlockCosineSimilarity3D.cl b/src/main/resources/kernelGetBlockCosineSimilarity3D.cl new file mode 100644 index 0000000..ff714ae --- /dev/null +++ b/src/main/resources/kernelGetBlockCosineSimilarity3D.cl @@ -0,0 +1,31 @@ +//#pragma OPENCL EXTENSION cl_khr_fp64 : enable +#define imageWidth $WIDTH$ +#define imageHeight $HEIGHT$ +#define imageDepth $DEPTH$ +#define bRW $BRW$ +#define bRH $BRH$ +#define bRZ $BRZ$ +#define ref_std $BLOCK_STD$ +#define EPSILON $EPSILON$ + +kernel void kernelGetBlockCosineSimilarity3D( + global float* local_stds, + global float* cosine_similarity_map +){ + + int gx = get_global_id(0); + int gy = get_global_id(1); + int gz = get_global_id(2); + + // Bound check (avoids borders dynamically based on block dimensions) + if(gx=imageWidth-bRW || gy=imageHeight-bRH || gz=imageDepth-bRZ){ + cosine_similarity_map[imageWidth*imageHeight*gz+gy*imageWidth+gx] = 0.0f; + return; + } + + + // Calculate cosine similarity + float test_std = local_stds[imageWidth*imageHeight*gz+gy*imageWidth+gx]; + float similarity = (ref_std*test_std) / (sqrt(ref_std*ref_std)*sqrt(test_std*test_std)+EPSILON); + cosine_similarity_map[imageWidth*imageHeight*gz+gy*imageWidth+gx] = (float)fmax(0.0f, similarity); +} diff --git a/src/main/resources/kernelGetPatchRmse2D.cl b/src/main/resources/kernelGetBlockNrmse2D.cl similarity index 61% rename from src/main/resources/kernelGetPatchRmse2D.cl rename to src/main/resources/kernelGetBlockNrmse2D.cl index ca39c4b..7f3d344 100644 --- a/src/main/resources/kernelGetPatchRmse2D.cl +++ b/src/main/resources/kernelGetBlockNrmse2D.cl @@ -1,16 +1,16 @@ //#pragma OPENCL EXTENSION cl_khr_fp64 : enable #define w $WIDTH$ #define h $HEIGHT$ -#define patch_size $PATCH_SIZE$ +#define block_size $BLOCK_SIZE$ #define bW $BW$ #define bH $BH$ #define bRW $BRW$ #define bRH $BRH$ -#define ref_mean $PATCH_MEAN$ +#define ref_mean $BLOCK_MEAN$ #define EPSILON $EPSILON$ -kernel void kernelGetPatchRmse2D( - global float* patch_pixels, +kernel void kernelGetBlockNrmse2D( + global float* block_pixels, global float* ref_pixels, global float* local_means, global float* rmse_map @@ -19,8 +19,9 @@ kernel void kernelGetPatchRmse2D( int gx = get_global_id(0); int gy = get_global_id(1); - // Bound check (avoids borders dynamically based on patch dimensions) + // Bound check (avoids borders dynamically based on block dimensions) if(gx=w-bRW || gy=h-bRH){ + rmse_map[gy*w+gx] = 0.0f; return; } @@ -29,35 +30,35 @@ kernel void kernelGetPatchRmse2D( // ---- Get mean-subtracted reference block ---- // // --------------------------------------------- // - __local float ref_patch[patch_size]; // Make a local copy to avoid slower reads from global memory + __local float ref_block[block_size]; // Make a local copy to avoid slower reads from global memory - for(int i=0; i=image_width-bRW || gy=image_height-bRH || gz=image_depth-bRZ){ + rmse_map[image_width*image_height*gz+gy*image_width+gx] = 0.0f; + return; + } + + + // Get mean-subtracted reference block + __local float ref_block[block_size]; // Make a local copy to avoid slower reads from global memory + + for(int i=0; i=w-bRW || gy=h-bRH){ + pearson_map[gy*w+gx] = 0.0f; return; } @@ -31,35 +31,35 @@ kernel void kernelGetPatchPearson2D( // ---- Get mean-subtracted reference block ---- // // --------------------------------------------- // - __local float ref_patch[patch_size]; // Make a local copy to avoid slower reads from global memory + __local float ref_block[block_size]; // Make a local copy to avoid slower reads from global memory - for(int i=0; i=imageWidth-bRW || gy=imageHeight-bRH || gz=imageDepth-bRZ){ + pearson_map[imageWidth*imageHeight*gz+gy*imageWidth+gx] = 0.0f; + return; + } + + // Get mean-subtracted and normalized reference block from buffer + __local float ref_block[block_size]; // Make a local copy to avoid slower reads from global memory + + for(int i=0; i=image_width-bRW || gy=image_height-bRH){ + ssim_map[gy*image_width+gx] = 0.0f; + return; + } + + // Get mean-subtracted reference block + __local float ref_block[block_size]; // Make a local copy to avoid slower reads from global memory + + for(int i=0; i=image_width-bRW || gy=image_height-bRH || gz=image_depth-bRZ){ + ssim_map[image_width*image_height*gz+gy*image_width+gx] = 0.0f; + return; + } + + + // --------------------------------------------- // + // ---- Get mean-subtracted reference block ---- // + // --------------------------------------------- // + + __local float ref_block[block_size]; // Make a local copy to avoid slower reads from global memory + + for(int i=0; i=w-bRW || gy=h-bRH){ - return; - } - - - // ------------------------------------------------------------ // - // ---- Check to avoid blocks with no structural relevance ---- // - // ------------------------------------------------------------ // - - float ref_std = local_stds[gy*w+gx]; - float ref_var = (float)ref_std*(float)ref_std; - - if(ref_var=image_width-bRW || gy=image_height-bRH){ + diff_std_map[gy*image_width+gx] = 0.0f; + return; + } + + // Check to avoid blocks with no structural relevance + float ref_std = local_stds[gy*image_width+gx]; + float ref_var = (float)ref_std*(float)ref_std; + + if(ref_var=image_width-bRW || gy=image_height-bRH || gz=image_depth-bRZ){ + diff_std_map[image_width*image_height*gz+gy*image_width+gx] = 0.0f; + return; + } + + // Check to avoid blocks with no structural relevance + int ref_index = image_width*image_height*gz+gy*image_width+gx; + float ref_std = local_stds[ref_index]; + float ref_var = ref_std*ref_std; + + if(ref_var=image_width-bRW || gy=image_height-bRH){ + cosine_sim_map[gy*image_width+gx] = 0.0f; + return; + } + + // Check to avoid blocks with no structural relevance + float ref_std = local_stds[gy*image_width+gx]; + float ref_var = (float)ref_std*(float)ref_std; + + if(ref_var=w-bRW || gy=h-bRH || gz=z-bRZ){ + // Bound check to avoid borders + if(gx=image_width-bRW || gy=image_height-bRH || gz=image_depth-bRZ){ + cosine_sim_map[image_width*image_height*gz+gy*image_width+gx] = 0.0f; return; } - - // ------------------------------------------------------------ // - // ---- Check to avoid blocks with no structural relevance ---- // - // ------------------------------------------------------------ // - - int ref_index = w * h * gz + gy * w + gx; - + // Check to avoid blocks with no structural relevance + int ref_index = image_width*image_height*gz+gy*image_width+gx; float ref_std = local_stds[ref_index]; float ref_var = ref_std*ref_std; @@ -48,26 +37,22 @@ kernel void kernelGetCosineSimMap3D( return; } - - // -------------------------------------------------------------------- // - // ---- Calculate similarity between the reference and test blocks ---- // - // -------------------------------------------------------------------- // - + // Calculate similarity between the reference and test blocks float weight_sum = 0.0f; float cosine_similarity_sum = 0.0f; - for(int n=bRZ; n=image_width-bRW || gy=image_height-bRH){ + return; + } + + // Check to avoid blocks with no structural relevance + float ref_std = local_stds[gy*image_width+gx]; + float ref_var = (float)ref_std*(float)ref_std; + + if(ref_var=image_width-bRW || gy=image_height-bRH || gz=image_depth-bRZ){ + rmse_map[image_width*image_height*gz+gy*image_width+gx] = 0.0f; + return; + } + + // Check to avoid blocks with no structural relevance + int ref_index = image_width*image_height*gz+gy*image_width+gx; + float ref_std = local_stds[ref_index]; + float ref_var = ref_std*ref_std; + + if(ref_var=w-bRW || gy=h-bRH){ - return; - } - - - // ------------------------------------------------------------ // - // ---- Check to avoid blocks with no structural relevance ---- // - // ------------------------------------------------------------ // - - float ref_std = local_stds[gy*w+gx]; - float ref_var = (float)ref_std*(float)ref_std; - - if(ref_var=image_width-bRW || gy=image_height-bRH){ + pearson_map[gy*image_width+gx] = 0.0f; + return; + } + + // Check to avoid blocks with no structural relevance + float ref_std = local_stds[gy*image_width+gx]; + float ref_var = (float)ref_std*(float)ref_std; + + if(ref_var=image_width-bRW || gy=image_height-bRH || gz=image_depth-bRZ){ + pearson_map[image_width*image_height*gz+gy*image_width+gx] = 0.0f; + return; + } + + // Check to avoid blocks with no structural relevance + int ref_index = image_width*image_height*gz+gy*image_width+gx; + float ref_std = local_stds[ref_index]; + float ref_var = ref_std*ref_std; + + if(ref_var=image_width-bRW || gy=image_height-bRH){ + ssim_map[gy*image_width+gx] = 0.0f; + return; + } + + // Check to avoid blocks with no structural relevance + float ref_std = local_stds[gy*image_width+gx]; + float ref_var = (float)ref_std*(float)ref_std; + + if(ref_var=image_width-bRW || gy=image_height-bRH || gz=image_depth-bRZ){ + ssim_map[image_width*image_height*gz+gy*image_width+gx] = 0.0f; + return; + } + + // Check to avoid blocks with no structural relevance + int ref_index = image_width*image_height*gz+gy*image_width+gx; + float ref_std = local_stds[ref_index]; + float ref_var = ref_std*ref_std; + + if(ref_var=image_width-bRW || gy=image_height-bRH){ + local_means[gy*image_width+gx] = 0.0f; + local_stds[gy*image_width+gx] = 0.0f; + return; + } + + // Get block pixels + float block[block_size]; + int index = 0; + for(int y=gy-bRH; y<=gy+bRH; y++){ + for(int x=gx-bRW; x<=gx+bRW; x++){ + // Extract only pixels within the inbound circle/ellipse + float dx = (float)(x-gx); + float dy = (float)(y-gy); + if(((dx*dx)/(float)(bRW*bRW))+((dy*dy)/(float)(bRH*bRH)) <= 1.0f){ + block[index] = ref_pixels[y*image_width+x]; + index++; + } + } + } + + // Calculate block mean + float mean = 0.0f; + for(int i=0; i=image_width-bRW || gy=image_height-bRH || gz=image_depth-bRZ){ + local_means[image_width*image_height*gz+gy*image_width+gx] = 0.0f; + local_stds[image_width*image_height*gz+gy*image_width+gx] = 0.0f; + return; + } + + // Get block pixels + float block[block_size]; + int index = 0; + for(int z=gz-bRZ; z<=gz+bRZ; z++){ + for(int y=gy-bRH; y<=gy+bRH; y++){ + for(int x=gx-bRW; x<=gx+bRW; x++){ + // Extract only pixels within the inbound spheroid + float dx = (float)(x-gx); + float dy = (float)(y-gy); + float dz = (float)(z-gz); + if(((dx*dx)/(float)(bRW*bRW))+((dy*dy)/(float)(bRH*bRH))+((dz*dz)/(float)(bRZ*bRZ)) <= 1.0f){ + block[index] = ref_pixels[image_width*image_height*z+y*image_width+x]; + index++; + } + } + } + } + + // Calculate block mean + float mean = 0.0f; + for(int i=0; i=w-bRW || y0=h-bRH){ - return; - } - - // Get reference patch max and min - float min_x = ref_pixels[y0*w+x0]; - float max_x = ref_pixels[y0*w+x0]; - - for(int j0=y0-bRH; j0<=y0+bRH; j0++){ - for(int i0=x0-bRW; i0<=x0+bRW; i0++){ - if(ref_pixels[j0*w+i0] < min_x){ - min_x = ref_pixels[j0*w+i0]; - } - if(ref_pixels[j0*w+i0] > max_x){ - max_x = ref_pixels[j0*w+i0]; - } - } - } - - // Get normalized reference patch - float ref_patch[patch_size] = {0.0f}; - float ref_mean = local_means[y0*w+x0]; - float ref_std = local_stds[y0*w+x0]; - - int ref_counter = 0; - for(int j0=y0-bRH; j0<=y0+bRH; j0++){ - for(int i0=x0-bRW; i0<=x0+bRW; i0++){ - ref_patch[ref_counter] = (ref_pixels[j0*w+i0] - ref_mean); - ref_counter++; - } - } - - // For each comparison pixel... - float weight = 0.0f; - for(int y1=bRH; y1 max_y){ - max_y = ref_pixels[j1*w+i1]; - } - } - } - - // Get normalized comparison patch - float comp_patch[patch_size]; - float comp_mean = local_means[y1*w+x1]; - float comp_std = local_stds[y1*w+x1]; - int comp_counter = 0; - for(int j1=y1-bRH; j1<=y1+bRH; j1++){ - for(int i1=x1-bRW; i1<=x1+bRW; i1++){ - comp_patch[comp_counter] = (ref_pixels[j1*w+i1] - comp_mean); - comp_counter++; - } - } - - // Calculate weight - weight = getExpDecayWeight(local_stds[y0*w+x0], local_stds[y1*w+x1]); - - // Calculate NRMSE and MAE - float nrmse = 0.0f; - float mae = 0.0f; - - for(int i=0; i=w-bRW || gy=h-bRH){ - return; - } - - - // -------------------------- // - // ---- Get patch pixels ---- // - // -------------------------- // - - float patch[patch_size]; - int index = 0; - for(int j=gy-bRH; j<=gy+bRH; j++){ - for(int i=gx-bRW; i<=gx+bRW; i++){ - // Extract only pixels within the inbound circle/ellipse - float dx = (float)(i-gx); - float dy = (float)(j-gy); - if(((dx*dx)/(float)(bRW*bRW))+((dy*dy)/(float)(bRH*bRH)) <= 1.0f){ - patch[index] = ref_pixels[j*w+i]; - index++; - } - } - } - - - // ------------------------------ // - // ---- Calculate patch mean ---- // - // ------------------------------ // - - float mean = 0.0f; - for(int i=0; i=w-bRW || gy=h-bRH || gz=z-bRZ){ - return; - } - - - // -------------------------- // - // ---- Get patch pixels ---- // - // -------------------------- // - - float patch[patch_size]; - int index = 0; - for(int n=gz-bRZ; n<=gz+bRZ; n++){ - for(int j=gy-bRH; j<=gy+bRH; j++){ - for(int i=gx-bRW; i<=gx+bRW; i++){ - // Extract only pixels within the inbound circle/ellipse - float dx = (float)(i-gx); - float dy = (float)(j-gy); - float dz = (float)(n-gz); - if(((dx*dx)/(float)(bRW*bRW))+((dy*dy)/(float)(bRH*bRH))+((dz*dz)/(float)(bRZ*bRZ)) <= 1.0f){ - patch[index] = ref_pixels[w*h*n+j*w+i]; - index++; - } - } - } - } - - - // ------------------------------ // - // ---- Calculate patch mean ---- // - // ------------------------------ // - - float mean = 0.0f; - for(int i=0; i=w-bRW || gy=h-bRH || gz=z-bRZ){ - return; - } - - - // ------------------------------------------------------------------------ // - // ---- Get mean-subtracted and normalized reference patch from buffer ---- // - // ------------------------------------------------------------------------ // - - __local float ref_patch[patch_size]; // Make a local copy to avoid slower reads from global memory - - for(int i=0; i=w-bRW || gy=h-bRH){ - return; - } - - - // --------------------------------------------- // - // ---- Get mean-subtracted reference block ---- // - // --------------------------------------------- // - - __local float ref_patch[patch_size]; // Make a local copy to avoid slower reads from global memory - - for(int i=0; i=w-bRW || y0=h-bRH){ - return; - } - - // Get reference patch - float ref_patch[patch_size] = {0.0f}; - float ref_mean = local_means[y0*w+x0]; - float ref_std = local_stds[y0*w+x0]; - float ref_var = ref_std * ref_std; - - int ref_counter = 0; - for(int j0=y0-bRH; j0<=y0+bRH; j0++){ - for(int i0=x0-bRW; i0<=x0+bRW; i0++){ - ref_patch[ref_counter] = ref_pixels[j0*w+i0] - ref_mean; - ref_counter++; - } - } - - // For each comparison pixel... - float weight = 0.0f; - - for(int y1=bRH; y1SReD>Block repetition, "Find block repetition (2D)", BlockRepetition2D_ +Plugins>SReD>Block repetition, "Find block repetition (3D)", BlockRepetition3D_ + +Plugins>SReD>Global Repetition, "Global repetition (2D)", GlobalRepetition2D_ +Plugins>SReD>Global Repetition, "Global repetition (3D)", GlobalRepetition3D_ + Plugins>SReD>Noise variance stabilisation, "Stabilise noise variance (2D)", VarianceStabilisingTransform2D_ Plugins>SReD>Noise variance stabilisation, "Stabilise noise variance (3D)", VarianceStabilisingTransform3D_ -Plugins>SReD>Block repetition, "Find block repetition (2D)", BlockRedundancy2D_ -Plugins>SReD>Block repetition, "Find block repetition (3D)", BlockRedundancy3D_ -Plugins>SReD>Block repetition, "Find block repetition (2D+T)", BlockRedundancy2DT_ -Plugins>SReD>Global Repetition, "Global repetition (2D)", RedundancyMap_ -Plugins>SReD>Global Repetition, "Global repetition (3D)", GlobalRepetition3D_ -Plugins>SReD, "Cluster repetition scores", ClassifyRedundancy_ -Plugins>SReD, "OpenCL Preferences...", OpenCLPreferences_ + Plugins>SReD>Relevance Mask, "Calculate Relevance Mask (2D)", RelevanceMask2D_ Plugins>SReD>Relevance Mask, "Calculate Relevance Mask stack (2D)", RelevanceMaskStack2D_ Plugins>SReD>Relevance Mask, "Optimise Relevance Constant (2D)", OptimiseRelevanceMask_ + Plugins>SReD>Tools, "Calculate image statistics (2D)", CalculateLocalMeans_ +Plugins>SReD>Tools, "Cluster repetition scores", ClassifyRedundancy_ +Plugins>SReD>Tools, "Go to SReD Wiki page", OpenGithubWiki_ + +Plugins>SReD, "OpenCL Preferences...", OpenCLPreferences_ + diff --git a/src/test/java/CLUtilsTest.java b/src/test/java/CLUtilsTest.java new file mode 100644 index 0000000..7966f10 --- /dev/null +++ b/src/test/java/CLUtilsTest.java @@ -0,0 +1,1069 @@ +import static java.lang.Math.*; +import static org.junit.jupiter.api.Assertions.*; +import com.jogamp.opencl.*; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.FloatBuffer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class CLUtilsTest { + + @Nested + class OpenCLResourcesTest { + + @Test + public void testOpenCLResourcesConstructor() { + // Initialize OpenCL context, device, and queue + CLContext context = null; + CLDevice device = null; + CLCommandQueue queue = null; + + try { + // Create a default OpenCL context + CLPlatform platform = CLPlatform.listCLPlatforms()[0]; // Get the first platform + context = CLContext.create(platform); + device = context.getMaxFlopsDevice(); // Choose the best device + queue = device.createCommandQueue(); // Create a command queue + + // Create an instance of OpenCLResources + CLUtils.OpenCLResources resources = new CLUtils.OpenCLResources(context, device, queue); + + // Assertions to verify the properties of the OpenCLResources instance + assertNotNull(resources, "OpenCLResources instance should not be null."); + assertNotNull(resources.getContext(), "OpenCL context should not be null."); + assertNotNull(resources.getDevice(), "OpenCL device should not be null."); + assertNotNull(resources.getQueue(), "OpenCL command queue should not be null."); + assertEquals(context, resources.getContext(), "The context should match."); + assertEquals(device, resources.getDevice(), "The device should match."); + assertEquals(queue, resources.getQueue(), "The command queue should match."); + + } catch (CLException e) { + fail("OpenCL initialization failed: " + e.getMessage()); + } finally { + // Release resources if they were created + if (queue != null) queue.release(); + if (context != null) context.release(); + } + } + } + + @Nested + class CLLocalStatisticsTest { + + @Test + public void testCLLocalStatisticsConstructor() { + // Sample data for local means and standard deviations + float[] localMeans = {1.0f, 2.0f, 3.0f}; + float[] localStds = {0.1f, 0.2f, 0.3f}; + + // Initialize OpenCL context, device, and queue + CLContext context = null; + CLDevice device = null; + CLCommandQueue queue = null; + + CLPlatform platform = CLPlatform.listCLPlatforms()[0]; // Get the first platform + context = CLContext.create(platform); + device = context.getMaxFlopsDevice(); // Choose the best device + queue = device.createCommandQueue(); // Create a command queue + + // Create mock OpenCL buffers (assuming these are valid) + CLBuffer clImageArray = context.createFloatBuffer(localMeans.length); + CLBuffer clLocalMeans = context.createFloatBuffer(localMeans.length); + CLBuffer clLocalStds = context.createFloatBuffer(localMeans.length); + + // Create an instance of CLLocalStatistics + CLUtils.CLLocalStatistics localStatistics = new CLUtils.CLLocalStatistics(localMeans, localStds, clImageArray, clLocalMeans, clLocalStds); + + // Assertions to verify the properties of the CLLocalStatistics instance + assertNotNull(localStatistics, "CLLocalStatistics instance should not be null."); + assertArrayEquals(localMeans, localStatistics.getLocalMeans(), "Local means should match."); + assertArrayEquals(localStds, localStatistics.getLocalStds(), "Local standard deviations should match."); + assertEquals(clImageArray, localStatistics.getCLImageArray(), "CL image array should match."); + assertEquals(clLocalMeans, localStatistics.getCLLocalMeans(), "CL local means buffer should match."); + assertEquals(clLocalStds, localStatistics.getCLLocalStds(), "CL local standard deviations buffer should match."); + } + } + + + // ------------------------------------------------------------------ // + // ---- METHODS FOR OPENCL INITIALISATION AND RESOURCE MANAGEMENT---- // + // ------------------------------------------------------------------ // + + @Nested + class GetOpenCLResourcesTest { + + @Test + public void testGetOpenCLResources() { + + System.out.println("Library path: " + System.getProperty("java.library.path")); + + + // Call the method to get OpenCL resources + CLUtils.OpenCLResources resources = null; + boolean useDevice = false; // You can change this to true if you want to test user-defined device preferences + + try { + resources = CLUtils.getOpenCLResources(useDevice); + } catch (RuntimeException e) { + fail("OpenCL initialization failed: " + e.getMessage()); + } + + // Assert that the resources are not null + assertNotNull(resources, "OpenCLResources should not be null."); + + // Assert that the context, device, and queue are initialized + assertNotNull(resources.getContext(), "OpenCL context should not be null."); + assertNotNull(resources.getDevice(), "OpenCL device should not be null."); + assertNotNull(resources.getQueue(), "OpenCL command queue should not be null."); + + // Assert properties of the device or context + CLDevice device = resources.getDevice(); + assertTrue(device.getMaxComputeUnits() > 0, "Device should have compute units."); + assertTrue(device.getMaxClockFrequency() > 0, "Device should have a clock frequency."); + } + } + + + @Test + public void testCreateAndFillCLBuffer() { + // Create a CLContext + CLUtils.OpenCLResources resources = null; + try { + resources = CLUtils.getOpenCLResources(false); + } catch (RuntimeException e) { + fail("OpenCL initialization failed: " + e.getMessage()); + } + + // Sample data + int size = 5; + float[] inputArray = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + + // Create and fill CLBuffer + CLBuffer clBuffer = CLUtils.createAndFillCLBuffer(resources.getContext(), size, CLMemory.Mem.READ_WRITE, inputArray); + + // Validate the CLBuffer is not null + assertNotNull(clBuffer, "CLBuffer should not be null."); + + // Validate that the buffer has the expected size + assertEquals(size, clBuffer.getBuffer().capacity(), "CLBuffer size should match the input size."); + + // Validate the contents of the buffer + FloatBuffer bufferContent = clBuffer.getBuffer(); + for (int i = 0; i < size; i++) { + assertEquals(inputArray[i], bufferContent.get(i), "Buffer content at index " + i + " should match the input array."); + } + + // Cleanup + clBuffer.release(); // Release the CLBuffer + resources.getContext().release(); + } + + + @Test + public void testFillBufferWithFloatArray() { + // Create a CLContext + CLUtils.OpenCLResources resources = null; + try { + resources = CLUtils.getOpenCLResources(false); + } catch (RuntimeException e) { + fail("OpenCL initialization failed: " + e.getMessage()); + } + + // Sample data + int size = 5; + float[] inputArray = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + + // Create an empty CLBuffer + CLBuffer clBuffer = resources.getContext().createFloatBuffer(size, CLMemory.Mem.READ_WRITE); + + // Fill the CLBuffer with the input array + CLUtils.fillBufferWithFloatArray(clBuffer, inputArray); + + // Validate the CLBuffer is not null + assertNotNull(clBuffer, "CLBuffer should not be null."); + + // Validate that the buffer has the expected size + assertEquals(size, clBuffer.getBuffer().capacity(), "CLBuffer size should match the input size."); + + // Validate the contents of the buffer + FloatBuffer bufferContent = clBuffer.getBuffer(); + for (int i = 0; i < size; i++) { + assertEquals(inputArray[i], bufferContent.get(i), "Buffer content at index " + i + " should match the input array."); + } + + // Cleanup + clBuffer.release(); + resources.getContext().release(); + } + + + @Test + public void testGetResourceAsString() { + // Get the resource as a string + String resourceContent = CLUtils.getResourceAsString(CLUtils.class, "kernelGetBlockPearson2D.cl"); + + // Validate that the content is not null + assertNotNull(resourceContent, "Resource content should not be null."); + + // Validate the content is as expected (update with your expected content) + String expectedContent = "//#pragma OPENCL EXTENSION cl_khr_fp64 : enable\n" + + "#define w $WIDTH$\n" + + "#define h $HEIGHT$\n" + + "#define block_size $BLOCK_SIZE$\n" + + "#define bW $BW$\n" + + "#define bH $BH$\n" + + "#define bRW $BRW$\n" + + "#define bRH $BRH$\n" + + "#define ref_std $BLOCK_STD$\n" + + "#define EPSILON $EPSILON$\n" + + "\n" + + "kernel void kernelGetBlockPearson2D(\n" + + " global float* block_pixels,\n" + + " global float* ref_pixels,\n" + + " global float* local_means,\n" + + " global float* local_stds,\n" + + " global float* pearson_map\n" + + "){\n" + + "\n" + + " int gx = get_global_id(0);\n" + + " int gy = get_global_id(1);\n" + + "\n" + + " // Bound check (avoids borders dynamically based on block dimensions)\n" + + " if(gx=w-bRW || gy=h-bRH){\n" + + " pearson_map[gy*w+gx] = 0.0f;\n" + + " return;\n" + + " }\n" + + "\n" + + "\n" + + " // --------------------------------------------- //\n" + + " // ---- Get mean-subtracted reference block ---- //\n" + + " // --------------------------------------------- //\n" + + "\n" + + " __local float ref_block[block_size]; // Make a local copy to avoid slower reads from global memory\n" + + "\n" + + " for(int i=0; i= 0.0f && value <= 1.0f); // Assuming result is normalized + } + + // Check if result is as expected + for(int i=0; i= 0.0f && value <= 1.0f); // Assuming result is normalized + } + + // Check if result is as expected + for(int i=0; i= 0.0f && value <= 1.0f); // Assuming result is normalized + } + + // Check if result is as expected + for(int i=0; i= 0.0f && value <= 1.0f); // Assuming result is normalized + } + + // Check if result is as expected + for(int i=0; i= 0.0f && value <= 1.0f); // Assuming result is normalized + } + + // Check if result is as expected + for(int i=0; i 0); + + // Check mean and std dev (expected values would depend on the generated block pixels) + assertEquals(0.5f, block.getMean(), 1e-6); + assertEquals(0.64549720287323f, block.getStd(), 1e-6); + } + + @Test + public void testGetReferenceBlock2DEvenDimensions() { + // Create a new image with even dimensions (invalid case) + float[] evenPixels = { + 1, 2, 3, 4, + 2, 3, 4, 5, + 3, 4, 5, 6, + 4, 5, 6, 7 + }; + FloatProcessor evenFp = new FloatProcessor(4, 4, evenPixels); + ImagePlus evenImage = new ImagePlus("Even Dimension Image", evenFp); + + // Set the new image as the current image + WindowManager.setTempCurrentImage(evenImage); + + // Retrieve the block + Utils.ReferenceBlock2D block = Utils.getReferenceBlock2D(evenImage.getID()); + + assertNull(block, "Block should be null for even dimensions"); + } + + @Test + public void testGetReferenceBlock2DNonExistentBlock() { + // Simulate a case where block image doesn't exist + WindowManager.setTempCurrentImage(null); + + Utils.ReferenceBlock2D block = Utils.getReferenceBlock2D(999); // Non-existent blockID + + assertNull(block, "Block should be null if image not found"); + } + } + + + @Nested + class GetInputImage2DTest { + + @BeforeEach + public void setUp() { + // Clean WindowManager before each test + WindowManager.closeAllWindows(); + } + + @Test + public void testGetInputImage2D_NoStabilization_NoNormalization() { + // Set up image dimensions and data + int width = 3; + int height = 3; + float[] pixels = { + 0.0f, 0.1f, 0.2f, + 0.3f, 0.4f, 0.5f, + 0.6f, 0.7f, 0.8f + }; + + // Create FloatProcessor and ImagePlus + FloatProcessor fp = new FloatProcessor(width, height, pixels); + ImagePlus imp = new ImagePlus("Test Image", fp); + + // Add the ImagePlus to the WindowManager + WindowManager.setTempCurrentImage(imp); + int imageID = imp.getID(); + + // Call the method + Utils.InputImage2D result = Utils.getInputImage2D(imageID, false, false); + + // Verify the result + assertNotNull(result); + assertArrayEquals(pixels, result.getImageArray()); + assertEquals(width, result.getWidth()); + assertEquals(height, result.getHeight()); + assertEquals(width * height, result.getSize()); + } + + @Test + public void testGetInputImage2D_WithNormalization() { + // Set up image dimensions and data + int width = 2; + int height = 2; + float[] pixels = { + 10.0f, 20.0f, + 30.0f, 40.0f + }; + + // Expected normalized output + float[] normalizedPixels = { + 0.0f, 0.3333f, + 0.6667f, 1.0f + }; + + // Create FloatProcessor and ImagePlus + FloatProcessor fp = new FloatProcessor(width, height, pixels); + ImagePlus imp = new ImagePlus("Test Image", fp); + + // Add the ImagePlus to the WindowManager + WindowManager.setTempCurrentImage(imp); + int imageID = imp.getID(); + + // Call the method with normalization + Utils.InputImage2D result = Utils.getInputImage2D(imageID, false, true); + + // Verify the result + assertNotNull(result); + assertEquals(width, result.getWidth()); + assertEquals(height, result.getHeight()); + assertEquals(width * height, result.getSize()); + + // Check normalized values with a small delta for floating-point comparison + for (int i = 0; i < normalizedPixels.length; i++) { + assertEquals(normalizedPixels[i], result.getImageArray()[i], 0.0001); + } + } + + @Test + public void testGetInputImage2D_ImageNotFound() { + // Call the method with an invalid imageID + Utils.InputImage2D result = Utils.getInputImage2D(999, false, false); + + // Verify that the method returns null when image is not found + assertNull(result); + } + } + + + @Nested + class GetInputImage2DTestOverloaded { + + @Test + public void testGetInputImage2D_NoStabilization_NoNormalization() { + // Image setup + int width = 3; + int height = 3; + float[] pixels = { + 0.0f, 0.1f, 0.2f, + 0.3f, 0.4f, 0.5f, + 0.6f, 0.7f, 0.8f + }; + + // Call the method with no stabilization and no normalization + Utils.InputImage2D result = Utils.getInputImage2D(pixels, width, height, false, 0.0f, 0.0f, 0.0f, false); + + // Verify the result + assertNotNull(result); + assertArrayEquals(pixels, result.getImageArray()); + assertEquals(width, result.getWidth()); + assertEquals(height, result.getHeight()); + assertEquals(width * height, result.getSize()); + } + + @Test + public void testGetInputImage2D_WithNormalization() { + // Image setup + int width = 2; + int height = 2; + float[] pixels = { + 10.0f, 20.0f, + 30.0f, 40.0f + }; + + // Expected normalized output + float[] normalizedPixels = { + 0.0f, 0.3333f, + 0.6667f, 1.0f + }; + + // Call the method with normalization enabled + Utils.InputImage2D result = Utils.getInputImage2D(pixels, width, height, false, 0.0f, 0.0f, 0.0f, true); + + // Verify the result + assertNotNull(result); + assertEquals(width, result.getWidth()); + assertEquals(height, result.getHeight()); + assertEquals(width * height, result.getSize()); + + // Check normalized values with a small delta for floating-point comparison + for (int i = 0; i < normalizedPixels.length; i++) { + assertEquals(normalizedPixels[i], result.getImageArray()[i], 0.0001); + } + } + + + } + + + @Nested + class GetMeanNoiseVarTest { + + @Test + public void testGetMeanNoiseVar2D() { + // Setup a 100x100 image + int width = 100; // Image width + int height = 100; // Image height + float[] refPixels = new float[width * height]; // Initialize array for 100x100 image + + // Fill the array with a simple pattern, e.g., a gradient + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + refPixels[y * width + x] = (float) (x + y * width) / (width * height); + } + } + + // Calculate total number of pixels + int wh = width * height; // Total number of pixels + + // Execute + float result = Utils.getMeanNoiseVar2D(refPixels, width, height, wh); + + // Assertions + // Check that the result is greater than or equal to zero + assertTrue(result >= 0.0f, "The noise variance should be non-negative."); + + // You can also add checks for specific expected values based on known input + // For instance, if you know the expected variance for this input + // assertEquals(expectedVariance, result, 0.01f); // Adjust tolerance as necessary + } + } + + + @Nested + class GetMeanAndVarBlock2DTest { + + @Test + public void testGetMeanAndVarBlock2D() { + // Setup a 100x100 image with known pixel values + int width = 100; + int height = 100; + float[] imageArray = new float[width * height]; + + // Fill the array with a simple pattern (e.g., x + y) + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + imageArray[y * width + x] = (float) (x + y); + } + } + + // Define the block to test (10x10 block starting at (10, 10)) + int xStart = 10; + int yStart = 10; + int xEnd = 20; // Exclusive + int yEnd = 20; // Exclusive + + // Execute the method + float[] meanVar = Utils.getMeanAndVarBlock2D(imageArray, width, xStart, yStart, xEnd, yEnd); + + // Calculate expected mean and variance manually for the block (10x10 block from (10, 10) to (19, 19)) + float sum = 0.0f; + int totalPixels = (xEnd - xStart) * (yEnd - yStart); // 10x10 = 100 pixels + for (int y = yStart; y < yEnd; y++) { + for (int x = xStart; x < xEnd; x++) { + sum += (x + y); + } + } + float expectedMean = sum / totalPixels; + + // For variance, you can compute it similarly by iterating over the block and calculating the sum of squared differences + float varianceSum = 0.0f; + for (int y = yStart; y < yEnd; y++) { + for (int x = xStart; x < xEnd; x++) { + float value = (x + y); + varianceSum += Math.pow(value - expectedMean, 2); + } + } + float expectedVar = varianceSum / totalPixels; + + // Assertions + assertEquals(expectedMean, meanVar[0], 0.01f, "Mean value should match the expected value."); + assertEquals(expectedVar, meanVar[1], 0.01f, "Variance value should match the expected value."); + } + } + + + @Nested + class ApplyMaskTest { + + @Test + public void testApplyMask2D() { + // Setup a 3x3 image with known pixel values + int width = 3; + int height = 3; + float[] imageArray = new float[] { + 1.0f, 2.0f, 3.0f, + 4.0f, 5.0f, 6.0f, + 7.0f, 8.0f, 9.0f + }; + + // Create a mask with known values (e.g., a mask that zeroes out some values) + float[] mask = new float[] { + 1.0f, 0.0f, 1.0f, + 0.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 0.0f + }; + + // Expected result after applying the mask + float[] expectedResult = new float[] { + 1.0f, 0.0f, 3.0f, + 0.0f, 5.0f, 0.0f, + 7.0f, 8.0f, 0.0f + }; + + // Execute the method + float[] result = Utils.applyMask2D(imageArray, width, height, mask); + + // Assertions to check if the result matches the expected result + assertArrayEquals(expectedResult, result); + } + } + + + @Test + public void GetRelevanceMaskTest() { + // Setup a larger 50x50 image with known pixel values + int width = 50; + int height = 50; + + // Initialize the imageArray and localStds with some sample data (mock values for illustration) + float[] imageArray = new float[width * height]; + float[] localStds = new float[width * height]; + + // Set a seed for reproducibility + Random random = new Random(42); // Fixed seed + + // Fill the arrays with reproducible values + for (int i = 0; i < width * height; i++) { + imageArray[i] = (i % 50 == 0) ? 1.0f : 1.0f + random.nextFloat() * 2.0f; // Values between 1.0f and 3.0f + localStds[i] = (i % 50 == 0) ? 0.0f : random.nextFloat(); // Local standard deviations + } + + // Parameters for the relevance mask + int blockRadiusWidth = 2; // Adjusted radius + int blockRadiusHeight = 2; // Adjusted radius + float relevanceConstant = 2.0f; + + // Calculate expected noise mean variance based on mock data + float noiseMeanVariance = 0.25f; // Assuming pre-calculated based on input + + // Create expected relevance mask (for a large test case, exact expected values may vary) + float relevanceThreshold = noiseMeanVariance * relevanceConstant; + + // Execute the method + Utils.RelevanceMask relevanceMask = Utils.getRelevanceMask(imageArray, width, height, blockRadiusWidth, blockRadiusHeight, localStds, relevanceConstant); + + // Assertions to check if the relevance mask is correctly generated + assertEquals(relevanceConstant, relevanceMask.getRelevanceConstant()); + assertEquals(0.4694742262363434f, relevanceMask.getRelevanceThreshold()); + assertEquals(0.2347371131181717, relevanceMask.getNoiseMeanVariance()); + + // Optionally check some values in the mask for correctness (e.g., expected ranges or patterns) + // Example: Check some central part of the mask + for (int y = 20; y < 30; y++) { + for (int x = 20; x < 30; x++) { + assertTrue(relevanceMask.getRelevanceMask()[y * width + x] == 1.0f || relevanceMask.getRelevanceMask()[y * width + x] == 0.0f); + } + } + } + + + @Nested + class NormalizeImage2DTest { + + @Test + public void testNormalizeImageWithBinaryMask() { + // Setup: a small 4x4 image with known pixel values and a valid binary mask + int width = 4; + int height = 4; + int borderWidth = 1; + int borderHeight = 1; + + float[] imageArray = new float[] { + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f, + 9.0f, 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, 16.0f + }; + + // Binary mask (0.0f or 1.0f only) + float[] mask = new float[] { + 1.0f, 0.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 1.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + + // Expected normalized array with the mask applied (min = 1.0, max = 16.0) + float[] expectedArray = new float[] { + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 0.0f, 7.0f, 8.0f, + 9.0f, 10.0f, 1.0f, 12.0f, + 13.0f, 14.0f, 15.0f, 16.0f + }; + + // Run the normalization with a binary mask + float[] normalizedArray = Utils.normalizeImage2D(imageArray, width, height, borderWidth, borderHeight, mask); + + // Assertions: Check that the normalized array is as expected + assertArrayEquals(expectedArray, normalizedArray, 0.0001f); + } + + @Test + public void testNormalizeImageWithInvalidMask() { + // Setup: a small 4x4 image with known pixel values and an invalid mask (non-binary values) + int width = 4; + int height = 4; + int borderWidth = 0; + int borderHeight = 0; + + float[] imageArray = new float[] { + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f, + 9.0f, 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, 16.0f + }; + + // Invalid mask (contains values other than 0.0f or 1.0f) + float[] invalidMask = new float[] { + 1.0f, 0.5f, 1.0f, 0.0f, + 1.0f, 0.8f, 0.0f, 0.0f, + 1.0f, 0.0f, 1.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + + // Expect an IllegalArgumentException due to non-binary mask values + assertThrows(IllegalArgumentException.class, () -> { + Utils.normalizeImage2D(imageArray, width, height, borderWidth, borderHeight, invalidMask); + }); + } + } + + + // ----------------------------------------- // + // ---- METHODS FOR BLOCK REPETITION 3D ---- // + // ----------------------------------------- // + + @Nested + class GetReferenceBlock3DTest { + + @Test + public void testBlockRetrieval() { + // Create a 3D ImagePlus object (3x3x3 block) and assign it a window ID + int width = 3; + int height = 3; + int depth = 3; + ImageStack stack = new ImageStack(width, height); + + // Fill each slice with some values + for (int z = 0; z < depth; z++) { + FloatProcessor fp = new FloatProcessor(width, height); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + fp.setf(x, y, (z + 1) * (x + 1) + y); // Some arbitrary pattern + } + } + stack.addSlice(fp); + } + ImagePlus imagePlus = new ImagePlus("Test Image", stack); + imagePlus.show(); // This assigns it an ID in the WindowManager + + // Get the blockID (WindowManager uses 1-based indexing) + int blockID = imagePlus.getID(); + + // Run the method + Utils.ReferenceBlock3D refBlock = Utils.getReferenceBlock3D(blockID); + + // Assert that the block is not null + assertNotNull(refBlock); + + // Assert block dimensions + assertEquals(width, refBlock.getWidth()); + assertEquals(height, refBlock.getHeight()); + assertEquals(depth, refBlock.getDepth()); + + // Assert blockSize matches the count of pixels within the ellipsoid + assertTrue(refBlock.getSize() > 0); + + // Clean up ImageJ windows after the test + imagePlus.changes = false; // Prevents "Save changes?" prompt + imagePlus.close(); + } + + @Test + public void testOddDimensionValidation() { + // Create a 3D ImagePlus object with even dimensions (should fail) + int width = 4; // Even + int height = 3; // Odd + int depth = 3; // Odd + ImageStack stack = new ImageStack(width, height); + + // Fill each slice + for (int z = 0; z < depth; z++) { + FloatProcessor fp = new FloatProcessor(width, height); + stack.addSlice(fp); + } + ImagePlus imagePlus = new ImagePlus("Test Even Width Image", stack); + imagePlus.show(); + + // Run the method and expect an exception due to even dimensions + int blockID = imagePlus.getID(); + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + Utils.getReferenceBlock3D(blockID); + }); + assertEquals("Block dimensions must be odd.", exception.getMessage()); + + // Clean up ImageJ windows after the test + imagePlus.changes = false; + imagePlus.close(); + } + + @Test + public void testMinimumSlicesValidation() { + // Create a 3D ImagePlus object with fewer than 3 slices (should fail) + int width = 3; + int height = 3; + int depth = 1; // Less than 3 slices + ImageStack stack = new ImageStack(width, height); + + // Fill each slice + for (int z = 0; z < depth; z++) { + FloatProcessor fp = new FloatProcessor(width, height); + stack.addSlice(fp); + } + ImagePlus imagePlus = new ImagePlus("Test Few Slices Image", stack); + imagePlus.show(); + + // Run the method and expect an exception due to fewer than 3 slices + int blockID = imagePlus.getID(); + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + Utils.getReferenceBlock3D(blockID); + }); + assertEquals("Reference block must have at least 3 slices.", exception.getMessage()); + + // Clean up ImageJ windows after the test + imagePlus.changes = false; + imagePlus.close(); + } + + @Test + public void testNormalizationAndStatistics() { + // Create a 3x3x3 ImagePlus object for testing normalization and statistics + int width = 3; + int height = 3; + int depth = 3; + ImageStack stack = new ImageStack(width, height); + + // Fill each slice with a known pattern + float[][] slices = { + { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f }, + { 9.0f, 8.0f, 7.0f, 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f }, + { 1.0f, 3.0f, 5.0f, 7.0f, 9.0f, 2.0f, 4.0f, 6.0f, 8.0f } + }; + for (int z = 0; z < depth; z++) { + FloatProcessor fp = new FloatProcessor(width, height); + fp.setPixels(slices[z]); + stack.addSlice(fp); + } + ImagePlus imagePlus = new ImagePlus("Test Image for Stats", stack); + imagePlus.show(); + + // Get the blockID (WindowManager uses 1-based indexing) + int blockID = imagePlus.getID(); + + // Run the method + Utils.ReferenceBlock3D refBlock = Utils.getReferenceBlock3D(blockID); + + // Validate the statistics of the block + assertNotNull(refBlock); + + // Ensure that the mean and standard deviation have been computed + assertTrue(refBlock.getMean() != 0.0f); + assertTrue(refBlock.getStd() != 0.0f); + + // Clean up ImageJ windows after the test + imagePlus.changes = false; + imagePlus.close(); + } + } + + + @Nested + class GetInputImage3DTest { + + @Test + public void testImageRetrieval() { + // Create a 3D ImagePlus object (3x3x3 block) and assign it a window ID + int width = 3; + int height = 3; + int depth = 3; + ImageStack stack = new ImageStack(width, height); + + // Fill each slice with some values + for (int z = 0; z < depth; z++) { + FloatProcessor fp = new FloatProcessor(width, height); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + fp.setf(x, y, (z + 1) * (x + 1) + y); // Arbitrary values + } + } + stack.addSlice(fp); + } + ImagePlus imagePlus = new ImagePlus("Test Image", stack); + imagePlus.show(); // Assigns an ID in WindowManager + + // Get the imageID (WindowManager uses 1-based indexing) + int imageID = imagePlus.getID(); + + // Call the method (without stabilizing noise variance, but normalizing output) + Utils.InputImage3D inputImage = Utils.getInputImage3D(imageID, false, true); + + // Assertions + assertNotNull(inputImage, "Image should be retrieved successfully."); + assertEquals(width, inputImage.getWidth(), "Width should match."); + assertEquals(height, inputImage.getHeight(), "Height should match."); + assertEquals(depth, inputImage.getDepth(), "Depth should match."); + assertEquals(width * height * depth, inputImage.getSize(), "Size should match."); + + // Clean up ImageJ windows after the test + imagePlus.changes = false; // Prevents "Save changes?" prompt + imagePlus.close(); + } + + @Test + public void testImageNotFound() { + // Call method with an invalid image ID + int invalidImageID = 9999; // Assuming this ID does not exist + Utils.InputImage3D inputImage = Utils.getInputImage3D(invalidImageID, false, false); + + // Assert that the method returns null when the image is not found + assertNull(inputImage, "Image should not be found and the method should return null."); + } + + @Test + public void testInsufficientSlices() { + // Create an ImagePlus object with only 2 slices (should fail) + int width = 3; + int height = 3; + int depth = 2; // Less than 3 slices + ImageStack stack = new ImageStack(width, height); + + // Fill each slice + for (int z = 0; z < depth; z++) { + FloatProcessor fp = new FloatProcessor(width, height); + stack.addSlice(fp); + } + ImagePlus imagePlus = new ImagePlus("Test Few Slices Image", stack); + imagePlus.show(); // Assigns an ID in WindowManager + + // Get the imageID + int imageID = imagePlus.getID(); + + // Call the method (should return null due to insufficient slices) + Utils.InputImage3D inputImage = Utils.getInputImage3D(imageID, false, false); + + // Assert that the method returns null when there are fewer than 3 slices + assertNull(inputImage, "Image must have at least 3 slices, method should return null."); + + // Clean up ImageJ windows after the test + imagePlus.changes = false; + imagePlus.close(); + } + + @Test + public void testNormalization() { + // Create a 3x3x3 ImagePlus object for testing normalization + int width = 3; + int height = 3; + int depth = 3; + ImageStack stack = new ImageStack(width, height); + + // Fill each slice with a known pattern + float[][] slices = { + { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f }, // Slice 1 + { 9.0f, 8.0f, 7.0f, 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f }, // Slice 2 + { 1.0f, 3.0f, 5.0f, 7.0f, 9.0f, 2.0f, 4.0f, 6.0f, 8.0f } // Slice 3 + }; + for (int z = 0; z < depth; z++) { + FloatProcessor fp = new FloatProcessor(width, height); + fp.setPixels(slices[z]); + stack.addSlice(fp); + } + ImagePlus imagePlus = new ImagePlus("Test Image for Normalization", stack); + imagePlus.show(); // Assigns an ID in WindowManager + + // Get the imageID + int imageID = imagePlus.getID(); + + // Call the method with normalization enabled + Utils.InputImage3D inputImage = Utils.getInputImage3D(imageID, false, true); + + // Assertions + assertNotNull(inputImage, "Image should be retrieved successfully."); + float[] normalizedArray = inputImage.getImageArray(); + assertNotNull(normalizedArray, "Array should not be null after normalization."); + assertEquals(width * height * depth, normalizedArray.length, "Array length should match image size."); + + // Ensure array values are within the normalized range (0.0f - 1.0f) + for (float value : normalizedArray) { + assertTrue(value >= 0.0f && value <= 1.0f, "All values should be in the range [0.0, 1.0]."); + } + + // Clean up ImageJ windows after the test + imagePlus.changes = false; + imagePlus.close(); + } + + @Test + public void testNoiseVarianceStabilization() { + // Create a 3x3x3 ImagePlus object for testing noise variance stabilization + int width = 3; + int height = 3; + int depth = 3; + ImageStack stack = new ImageStack(width, height); + + // Fill each slice with arbitrary values + for (int z = 0; z < depth; z++) { + FloatProcessor fp = new FloatProcessor(width, height); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + fp.setf(x, y, (float) (Math.random() * 100)); // Random values + } + } + stack.addSlice(fp); + } + ImagePlus imagePlus = new ImagePlus("Test Image for Noise Variance", stack); + imagePlus.show(); // Assigns an ID in WindowManager + + // Get the imageID + int imageID = imagePlus.getID(); + + // Call the method with noise variance stabilization enabled + Utils.InputImage3D inputImage = Utils.getInputImage3D(imageID, true, false); + + // Assertions + assertNotNull(inputImage, "Image should be retrieved successfully."); + assertEquals(width, inputImage.getWidth(), "Width should match the input image."); + assertEquals(height, inputImage.getHeight(), "Height should match the input image."); + assertEquals(depth, inputImage.getDepth(), "Depth should match the input image."); + assertDoesNotThrow(() -> { + float gain = inputImage.getGain(); + float sigma = inputImage.getSigma(); + float offset = inputImage.getOffset(); + }, "Gain, sigma, and offset calculations should not throw exceptions."); + + // Clean up ImageJ windows after the test + imagePlus.changes = false; + imagePlus.close(); + } + } + + + @Nested + class GetInputImage3DOverloadTest { + + @Test + void testGetInputImage3DWithoutNoiseStabilizationOrNormalization() { + // Setup: 3D image data (2x2x3) + int width = 2; + int height = 2; + int depth = 3; + float[] imageArray = new float[] { + 1.0f, 2.0f, 3.0f, 4.0f, // Slice 1 + 5.0f, 6.0f, 7.0f, 8.0f, // Slice 2 + 9.0f, 10.0f, 11.0f, 12.0f // Slice 3 + }; + + // Test without stabilization or normalization + boolean stabiliseNoiseVariance = false; + boolean normalizeOutput = false; + + // Run the method + Utils.InputImage3D inputImage = Utils.getInputImage3D(imageArray, width, height, depth, stabiliseNoiseVariance, normalizeOutput); + + // Assert that the image was returned correctly + assertNotNull(inputImage, "Image should be retrieved successfully."); + assertEquals(width, inputImage.getWidth()); + assertEquals(height, inputImage.getHeight()); + assertEquals(depth, inputImage.getDepth()); + + // Check that gain, sigma, and offset are still 0.0f + assertEquals(0.0f, inputImage.getGain(), 0.0001f); + assertEquals(0.0f, inputImage.getSigma(), 0.0001f); + assertEquals(0.0f, inputImage.getOffset(), 0.0001f); + } + + @Test + void testGetInputImage3DWithNormalization() { + // Setup: 3D image data (2x2x3) + int width = 2; + int height = 2; + int depth = 3; + float[] imageArray = new float[] { + 1.0f, 2.0f, 3.0f, 4.0f, // Slice 1 + 5.0f, 6.0f, 7.0f, 8.0f, // Slice 2 + 9.0f, 10.0f, 11.0f, 12.0f // Slice 3 + }; + + // Test with normalization but no noise stabilization + boolean stabiliseNoiseVariance = false; + boolean normalizeOutput = true; + + // Expected normalized array (min = 1.0, max = 12.0) + float[] expectedArray = new float[] { + 0.0f, 0.090f, 0.181f, 0.272f, + 0.363f, 0.454f, 0.545f, 0.636f, + 0.727f, 0.818f, 0.909f, 1.0f + }; + + // Run the method + Utils.InputImage3D inputImage = Utils.getInputImage3D(imageArray, width, height, depth, stabiliseNoiseVariance, normalizeOutput); + + // Assert that the image was returned correctly + assertNotNull(inputImage, "Image should be retrieved successfully."); + assertArrayEquals(expectedArray, inputImage.getImageArray(), 0.001f); + } + } + + + @Nested + class GetMeanAndVarBlock3DTest { + + @Test + void testGetMeanAndVarBlock3D() { + // Setup: create a 3D image with known values (4x4x4) + int imageWidth = 4; + int imageHeight = 4; + int depth = 4; + float[] pixels = new float[] { + // Slice 1 (z=0) + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f, + 9.0f, 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, 16.0f, + // Slice 2 (z=1) + 17.0f, 18.0f, 19.0f, 20.0f, + 21.0f, 22.0f, 23.0f, 24.0f, + 25.0f, 26.0f, 27.0f, 28.0f, + 29.0f, 30.0f, 31.0f, 32.0f, + // Slice 3 (z=2) + 33.0f, 34.0f, 35.0f, 36.0f, + 37.0f, 38.0f, 39.0f, 40.0f, + 41.0f, 42.0f, 43.0f, 44.0f, + 45.0f, 46.0f, 47.0f, 48.0f, + // Slice 4 (z=3) + 49.0f, 50.0f, 51.0f, 52.0f, + 53.0f, 54.0f, 55.0f, 56.0f, + 57.0f, 58.0f, 59.0f, 60.0f, + 61.0f, 62.0f, 63.0f, 64.0f + }; + + // Define block coordinates (xStart, yStart, zStart, xEnd, yEnd, zEnd) + int xStart = 1, yStart = 1, zStart = 1; + int xEnd = 3, yEnd = 3, zEnd = 3; // This block is a 2x2x2 block from slice 2-3 + + // Expected mean and variance for the 2x2x2 block + float expectedMean = (22.0f + 23.0f + 26.0f + 27.0f + 38.0f + 39.0f + 42.0f + 43.0f) / 8.0f; + float expectedVariance = (float) (Math.pow(22.0 - expectedMean, 2) + Math.pow(23.0 - expectedMean, 2) + + Math.pow(26.0 - expectedMean, 2) + Math.pow(27.0 - expectedMean, 2) + + Math.pow(38.0 - expectedMean, 2) + Math.pow(39.0 - expectedMean, 2) + + Math.pow(42.0 - expectedMean, 2) + Math.pow(43.0 - expectedMean, 2)) / 8.0f; + + // Run the method + float[] result = Utils.getMeanAndVarBlock3D(pixels, imageWidth, imageHeight, xStart, yStart, zStart, xEnd, yEnd, zEnd); + + // Assert the mean and variance are correct + assertEquals(expectedMean, result[0], 0.0001f, "Mean should match expected value."); + assertEquals(expectedVariance, result[1], 0.0001f, "Variance should match expected value."); + } + } + + + @Nested + class GetNoiseMeanVar3DTest { + + @Test + void testGetMeanNoiseVar3D() { + // Setup: define dimensions and localStds array + int imageWidth = 5; + int imageHeight = 5; + int imageDepth = 5; + int blockRadiusWidth = 1; + int blockRadiusHeight = 1; + int blockRadiusDepth = 1; + + // Create a known localStds array (5x5x5), representing standard deviations at each pixel + float[] localStds = new float[] { + // Slice 1 + 1.0f, 1.2f, 1.1f, 1.3f, 1.0f, + 1.1f, 1.3f, 1.2f, 1.3f, 1.0f, + 1.2f, 1.1f, 1.0f, 1.2f, 1.1f, + 1.3f, 1.1f, 1.3f, 1.2f, 1.0f, + 1.0f, 1.2f, 1.1f, 1.0f, 1.3f, + // Slice 2 + 1.0f, 1.1f, 1.2f, 1.3f, 1.1f, + 1.2f, 1.3f, 1.0f, 1.3f, 1.1f, + 1.3f, 1.2f, 1.1f, 1.2f, 1.3f, + 1.1f, 1.3f, 1.2f, 1.1f, 1.0f, + 1.2f, 1.1f, 1.3f, 1.1f, 1.2f, + // Slice 3 + 1.1f, 1.2f, 1.3f, 1.0f, 1.2f, + 1.3f, 1.2f, 1.1f, 1.0f, 1.3f, + 1.1f, 1.3f, 1.2f, 1.0f, 1.1f, + 1.2f, 1.0f, 1.1f, 1.2f, 1.3f, + 1.3f, 1.2f, 1.1f, 1.3f, 1.2f, + // Slice 4 + 1.1f, 1.3f, 1.2f, 1.0f, 1.2f, + 1.0f, 1.3f, 1.1f, 1.3f, 1.2f, + 1.3f, 1.1f, 1.2f, 1.1f, 1.0f, + 1.2f, 1.0f, 1.3f, 1.1f, 1.2f, + 1.1f, 1.2f, 1.0f, 1.2f, 1.3f, + // Slice 5 + 1.3f, 1.0f, 1.1f, 1.3f, 1.2f, + 1.0f, 1.1f, 1.2f, 1.1f, 1.3f, + 1.3f, 1.2f, 1.0f, 1.1f, 1.2f, + 1.0f, 1.3f, 1.1f, 1.2f, 1.1f, + 1.2f, 1.3f, 1.0f, 1.1f, 1.0f + }; + + float expectedMeanNoiseVar = 1.355f; // Example expected result + + // Run the method + float result = Utils.getMeanNoiseVar3D(imageWidth, imageHeight, imageDepth, blockRadiusWidth, blockRadiusHeight, blockRadiusDepth, localStds); + + // Assert the result is correct + assertEquals(expectedMeanNoiseVar, result, 0.001f, "Mean noise variance should match expected value."); + } + } + + + @Nested + class GetRelevanceMask3DTest { + + @Test + void testGetRelevanceMask3D() { + // Setup: define dimensions and input parameters + int imageWidth = 5; + int imageHeight = 5; + int imageDepth = 5; + int blockRadiusWidth = 1; + int blockRadiusHeight = 1; + int blockRadiusDepth = 1; + float relevanceConstant = 1.0f; + + // Create a known localStds array (5x5x5), representing standard deviations at each pixel + float[] localStds = new float[] { + // Slice 1 + 1.0f, 1.2f, 1.1f, 1.3f, 1.0f, + 1.1f, 1.3f, 1.2f, 1.3f, 1.0f, + 1.2f, 1.1f, 1.0f, 1.2f, 1.1f, + 1.3f, 1.1f, 1.3f, 1.2f, 1.0f, + 1.0f, 1.2f, 1.1f, 1.0f, 1.3f, + // Slice 2 + 1.0f, 1.1f, 1.2f, 1.3f, 1.1f, + 1.2f, 1.3f, 1.0f, 1.3f, 1.1f, + 1.3f, 1.2f, 1.1f, 1.2f, 1.3f, + 1.1f, 1.3f, 1.2f, 1.1f, 1.0f, + 1.2f, 1.1f, 1.3f, 1.1f, 1.2f, + // Slice 3 + 1.1f, 1.2f, 1.3f, 1.0f, 1.2f, + 1.3f, 1.2f, 1.1f, 1.0f, 1.3f, + 1.1f, 1.3f, 1.2f, 1.0f, 1.1f, + 1.2f, 1.0f, 1.1f, 1.2f, 1.3f, + 1.3f, 1.2f, 1.1f, 1.3f, 1.2f, + // Slice 4 + 1.1f, 1.3f, 1.2f, 1.0f, 1.2f, + 1.0f, 1.3f, 1.1f, 1.3f, 1.2f, + 1.3f, 1.1f, 1.2f, 1.1f, 1.0f, + 1.2f, 1.0f, 1.3f, 1.1f, 1.2f, + 1.1f, 1.2f, 1.0f, 1.2f, 1.3f, + // Slice 5 + 1.3f, 1.0f, 1.1f, 1.3f, 1.2f, + 1.0f, 1.1f, 1.2f, 1.1f, 1.3f, + 1.3f, 1.2f, 1.0f, 1.1f, 1.2f, + 1.0f, 1.3f, 1.1f, 1.2f, 1.1f, + 1.2f, 1.3f, 1.0f, 1.1f, 1.0f + }; + + // Calculate expected mean noise variance + float expectedMeanNoiseVariance = Utils.getMeanNoiseVar3D(imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, localStds); + + // Calculate the relevance mask + float[] relevanceMask = Utils.getRelevanceMask3D(imageWidth, imageHeight, imageDepth, blockRadiusWidth, + blockRadiusHeight, blockRadiusDepth, localStds, relevanceConstant); + + // Verify the mask values + for (int z = blockRadiusDepth; z < imageDepth - blockRadiusDepth; z++) { + for (int y = blockRadiusHeight; y < imageHeight - blockRadiusHeight; y++) { + for (int x = blockRadiusWidth; x < imageWidth - blockRadiusWidth; x++) { + int index = imageWidth * imageHeight * z + y * imageWidth + x; + float localVariance = localStds[index] * localStds[index]; + if (localVariance <= expectedMeanNoiseVariance * relevanceConstant) { + assertEquals(0.0f, relevanceMask[index], "Relevance mask value at (" + x + ", " + y + ", " + z + ") should be 0.0"); + } else { + assertEquals(1.0f, relevanceMask[index], "Relevance mask value at (" + x + ", " + y + ", " + z + ") should be 1.0"); + } + } + } + } + } + } + + + @Nested + class ApplyMask3DTest { + + @Test + void testApplyMask3D() { + // Setup: Define dimensions and initialize a test image and mask + int imageWidth = 3; + int imageHeight = 3; + int imageDepth = 3; + + // Create a sample 3D image (3x3x3) + float[] imageArray = new float[] { + // Slice 1 + 1.0f, 2.0f, 3.0f, + 4.0f, 5.0f, 6.0f, + 7.0f, 8.0f, 9.0f, + // Slice 2 + 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, + 16.0f, 17.0f, 18.0f, + // Slice 3 + 19.0f, 20.0f, 21.0f, + 22.0f, 23.0f, 24.0f, + 25.0f, 26.0f, 27.0f + }; + + // Create a mask (1.0f for keep, 0.0f for remove) + float[] mask = new float[] { + // Slice 1 + 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 1.0f, + // Slice 2 + 0.0f, 1.0f, 0.0f, + 1.0f, 0.0f, 1.0f, + 1.0f, 0.0f, 0.0f, + // Slice 3 + 1.0f, 0.0f, 1.0f, + 0.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 1.0f + }; + + // Expected result after applying mask + float[] expectedImageArray = new float[] { + // Slice 1 + 1.0f, 0.0f, 3.0f, + 4.0f, 5.0f, 0.0f, + 0.0f, 8.0f, 9.0f, + // Slice 2 + 0.0f, 11.0f, 0.0f, + 13.0f, 0.0f, 15.0f, + 16.0f, 0.0f, 0.0f, + // Slice 3 + 19.0f, 0.0f, 21.0f, + 0.0f, 23.0f, 0.0f, + 25.0f, 26.0f, 27.0f + }; + + // Apply the mask + float[] resultImageArray = Utils.applyMask3D(imageArray, imageWidth, imageHeight, imageDepth, mask); + + // Check that the result matches the expected result + assertArrayEquals(expectedImageArray, resultImageArray, "The masked image array does not match the expected output."); + } + } + + + @Nested + class NormalizeImage3DTest { + + + @Test + void testNormalizeImage3DWithoutMask() { + // Setup: Define dimensions and initialize a test image + int imageWidth = 3; + int imageHeight = 3; + int imageDepth = 3; + + // Create a sample 3D image (3x3x3) + float[] imageArray = new float[] { + // Slice 1 + 1.0f, 2.0f, 3.0f, + 4.0f, 5.0f, 6.0f, + 7.0f, 8.0f, 9.0f, + // Slice 2 + 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, + 16.0f, 17.0f, 18.0f, + // Slice 3 + 19.0f, 20.0f, 21.0f, + 22.0f, 23.0f, 24.0f, + 25.0f, 26.0f, 27.0f + }; + + // Define border dimensions + int borderWidth = 0; + int borderHeight = 0; + int borderDepth = 0; + + // Expected result after normalization (values remapped to [0, 1]) + float[] expectedNormalizedArray = new float[] { + // Slice 1 + 0.0f, 0.03846154f, 0.07692308f, + 0.115384616f, 0.15384616f, 0.1923077f, + 0.23076923f, 0.26923078f, 0.30769232f, + // Slice 2 + 0.34615386f, 0.3846154f, 0.42307693f, + 0.46153846f, 0.5f, 0.53846157f, + 0.5769231f, 0.61538464f, 0.65384614f, + // Slice 3 + 0.6923077f, 0.7307692f, 0.7692308f, + 0.8076923f, 0.84615386f, 0.88461536f, + 0.9230769f, 0.96153843f, 1.0f + }; + + // Normalize the image without mask + float[] normalizedImage = Utils.normalizeImage3D(imageArray, imageWidth, imageHeight, imageDepth, borderWidth, borderHeight, borderDepth, null); + + // Check that the result matches the expected result + assertArrayEquals(expectedNormalizedArray, normalizedImage, Utils.EPSILON, "The normalized image array does not match the expected output."); + } + + @Test + void testNormalizeImage3DWithMask() { + // Setup: Define dimensions and initialize a test image with a mask + int imageWidth = 3; + int imageHeight = 3; + int imageDepth = 3; + + // Create a sample 3D image (3x3x3) + float[] imageArray = new float[] { + // Slice 1 + 1.0f, 2.0f, 3.0f, + 4.0f, 5.0f, 6.0f, + 7.0f, 8.0f, 9.0f, + // Slice 2 + 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, + 16.0f, 17.0f, 18.0f, + // Slice 3 + 19.0f, 20.0f, 21.0f, + 22.0f, 23.0f, 24.0f, + 25.0f, 26.0f, 27.0f + }; + + // Create a mask (1.0f for keep, 0.0f for remove) + float[] mask = new float[] { + // Slice 1 + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + // Slice 2 + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + // Slice 3 + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f + }; + + // Define border dimensions + int borderWidth = 0; + int borderHeight = 0; + int borderDepth = 0; + + // Expected result after normalization (same as without mask, since all values are included) + float[] expectedNormalizedArray = new float[] { + // Slice 1 + 0.0f, 0.03846154f, 0.07692308f, + 0.115384616f, 0.15384616f, 0.1923077f, + 0.23076923f, 0.26923078f, 0.30769232f, + // Slice 2 + 0.34615386f, 0.3846154f, 0.42307693f, + 0.46153846f, 0.5f, 0.53846157f, + 0.5769231f, 0.61538464f, 0.65384614f, + // Slice 3 + 0.6923077f, 0.7307692f, 0.7692308f, + 0.8076923f, 0.84615386f, 0.88461536f, + 0.9230769f, 0.96153843f, 1.0f + }; + + // Normalize the image with mask + float[] normalizedImage = Utils.normalizeImage3D(imageArray, imageWidth, imageHeight, imageDepth, borderWidth, borderHeight, borderDepth, mask); + + // Check that the result matches the expected result + assertArrayEquals(expectedNormalizedArray, normalizedImage, Utils.EPSILON, "The normalized image array with mask does not match the expected output."); + } + + @Test + void testNormalizeImage3DWithMaskAndBorders() { + // Setup: Define dimensions and initialize a test image with a mask + int imageWidth = 3; + int imageHeight = 3; + int imageDepth = 3; + + // Create a sample 3D image (3x3x3) + float[] imageArray = new float[] { + // Slice 1 + 1.0f, 2.0f, 3.0f, + 4.0f, 5.0f, 6.0f, + 7.0f, 8.0f, 9.0f, + // Slice 2 + 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, + 16.0f, 17.0f, 18.0f, + // Slice 3 + 19.0f, 20.0f, 21.0f, + 22.0f, 23.0f, 24.0f, + 25.0f, 26.0f, 27.0f + }; + + // Create a mask (1.0f for keep, 0.0f for remove) + float[] mask = new float[] { + // Slice 1 + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + // Slice 2 + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + // Slice 3 + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f + }; + + // Define border dimensions + int borderWidth = 1; + int borderHeight = 1; + int borderDepth = 1; + + // Expected result after normalization (with borders ignored) + float[] expectedNormalizedArray = new float[] { + // Slice 1 + 1.0f, 2.0f, 3.0f, + 4.0f, 5.0f, 6.0f, + 7.0f, 8.0f, 9.0f, + // Slice 2 + 10.0f, 11.0f, 12.0f, + 13.0f, 0.0f, 15.0f, + 16.0f, 17.0f, 18.0f, + // Slice 3 + 19.0f, 20.0f, 21.0f, + 22.0f, 23.0f, 24.0f, + 25.0f, 26.0f, 27.0f + }; + + // Normalize the image with mask and borders + float[] normalizedImage = Utils.normalizeImage3D(imageArray, imageWidth, imageHeight, imageDepth, borderWidth, borderHeight, borderDepth, mask); + + // Check that the result matches the expected result + assertArrayEquals(expectedNormalizedArray, normalizedImage, Utils.EPSILON, "The normalized image array with mask and borders does not match the expected output."); + } + } + + + // -------------------------- // + // ---- UNSORTED METHODS ---- // + // -------------------------- // + + @Nested + class NormalizeArrayTest { + + @Test + void testNormalizeArray() { + // Setup: Create a sample array + float[] inputArray = new float[] {3.0f, 5.0f, 1.0f, 8.0f, 2.0f}; + + // Expected normalized output based on the min (1.0f) and max (8.0f) + float[] expectedNormalizedArray = new float[] { + (3.0f - 1.0f) / (8.0f - 1.0f + Utils.EPSILON), // ≈ 0.25 + (5.0f - 1.0f) / (8.0f - 1.0f + Utils.EPSILON), // ≈ 0.50 + (1.0f - 1.0f) / (8.0f - 1.0f + Utils.EPSILON), // 0.0 + (8.0f - 1.0f) / (8.0f - 1.0f + Utils.EPSILON), // 1.0 + (2.0f - 1.0f) / (8.0f - 1.0f + Utils.EPSILON) // ≈ 0.125 + }; + + // Normalize the array + float[] normalizedArray = Utils.normalizeArray(inputArray); + + // Check that the result matches the expected result + assertArrayEquals(expectedNormalizedArray, normalizedArray, Utils.EPSILON, "The normalized array does not match the expected output."); + } + + @Test + void testNormalizeArraySingleValue() { + // Test with an array of a single value + float[] inputArray = new float[] {5.0f}; + + // Normalizing a single value should return an array with the same value + float[] expectedNormalizedArray = new float[] {0.0f}; // Edge case + + // Normalize the array + float[] normalizedArray = Utils.normalizeArray(inputArray); + + // Check that the result matches the expected result + assertArrayEquals(expectedNormalizedArray, normalizedArray, Utils.EPSILON, "The normalized array with a single value does not match the expected output."); + } + + @Test + void testNormalizeArrayWithNegativeValues() { + // Setup: Create a sample array with negative values + float[] inputArray = new float[] {-3.0f, -5.0f, -1.0f, -8.0f, -2.0f}; + + // Find min and max + float arrMin = Float.MAX_VALUE; + float arrMax = -Float.MAX_VALUE; + for(int i=0; i< inputArray.length; i++){ + arrMin = min(arrMin, inputArray[i]); + arrMax = max(arrMax, inputArray[i]); + } + + // Calculate expected normalized array + float[] expectedNormalizedArray = { + (inputArray[0]-arrMin)/(arrMax-arrMin+Utils.EPSILON), + (inputArray[1]-arrMin)/(arrMax-arrMin+Utils.EPSILON), + (inputArray[2]-arrMin)/(arrMax-arrMin+Utils.EPSILON), + (inputArray[3]-arrMin)/(arrMax-arrMin+Utils.EPSILON), + (inputArray[4]-arrMin)/(arrMax-arrMin+Utils.EPSILON), + }; + + // Normalize the array + float[] normalizedArray = Utils.normalizeArray(inputArray); + + // Check that the result matches the expected result + assertArrayEquals(expectedNormalizedArray, normalizedArray, Utils.EPSILON, "The normalized array with negative values does not match the expected output."); + } + } + + + @Nested + class GetImageIDByTitleTest { + + @Test + void testGetImageIDByTitle() { + String[] titles = {"Image1", "Image2", "Image3"}; + int[] ids = {101, 102, 103}; + + // Test valid title + assertEquals(101, Utils.getImageIDByTitle(titles, ids, "Image1")); + assertEquals(102, Utils.getImageIDByTitle(titles, ids, "Image2")); + assertEquals(103, Utils.getImageIDByTitle(titles, ids, "Image3")); + } + + @Test + void testGetImageIDByTitleNotFound() { + String[] titles = {"Image1", "Image2", "Image3"}; + int[] ids = {101, 102, 103}; + + // Test title not found + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + Utils.getImageIDByTitle(titles, ids, "NonExistentImage"); + }); + + String expectedMessage = "Title not found: NonExistentImage"; + String actualMessage = exception.getMessage(); + assertTrue(actualMessage.contains(expectedMessage)); + } + } + + + @Nested + class DisplayResults2DTest { + + @Test + void testDisplayResults2D() { + // Prepare a mock InputImage2D + int width = 100; + int height = 100; + int size = width*height; + float[] repetitionMap = new float[size]; + + // Fill repetition map with sample values + for (int i = 0; i < repetitionMap.length; i++) { + repetitionMap[i] = (float) i; // Sample values for the test + } + Utils.InputImage2D inputImage = new Utils.InputImage2D(repetitionMap, width, height, size); + + // Call the method to display results + Utils.displayResults2D(inputImage, repetitionMap); + + // Check if the image is displayed + ImagePlus displayedImage = WindowManager.getCurrentImage(); + assertNotNull(displayedImage, "Image should be displayed."); + + // Verify the image properties + assertEquals(width, displayedImage.getWidth(), "Width should match the input image width."); + assertEquals(height, displayedImage.getHeight(), "Height should match the input image height."); + + // Check that the FloatProcessor is set up correctly + FloatProcessor fp = (FloatProcessor) displayedImage.getProcessor(); + assertEquals(repetitionMap.length, fp.getPixelCount(), "Pixel count should match the repetition map size."); + } + } + + + @Nested + class DisplayResults3DTest { + + @Test + public void testDisplayResults3D() { + // Prepare test data + int width = 5; + int height = 5; + int depth = 3; + float[] repetitionMap = new float[width * height * depth]; + for (int z = 0; z < depth; z++) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + repetitionMap[width * height * z + y * width + x] = (float) (Math.random() * 100); + } + } + } + + // Create calibration (as needed by InputImage3D) + Calibration calibration = new Calibration(); + // Set any necessary calibration properties here if needed + + // Create InputImage3D with all required parameters + Utils.InputImage3D inputImage = new Utils.InputImage3D(repetitionMap, width, height, depth, repetitionMap.length, calibration, 0.0f, 0.0f, 0.0f); + + // Call the method under test + Utils.displayResults3D(inputImage, repetitionMap); + + // Check if the ImagePlus was created and is not null + ImagePlus imp = WindowManager.getCurrentImage(); + assertNotNull(imp, "The ImagePlus should be created and not null."); + + // Check that the image stack has the correct dimensions + assertEquals(depth, imp.getStackSize(), "The image stack should have the correct depth."); + assertEquals(width, imp.getWidth(), "The image width should match the input width."); + assertEquals(height, imp.getHeight(), "The image height should match the input height."); + + // Check if the LUT was applied correctly (this is more complex to test, you might need to validate visually or check properties) + // Additional assertions regarding the LUT can be added based on your requirements + + // Clean up - close the image if you want to avoid clutter in your test suite + imp.close(); + } + } +}