From 2df9740b75a686724dcf5c3683743fc86646efb8 Mon Sep 17 00:00:00 2001 From: Thomas <thomas.musset@pasteur.fr> Date: Sun, 7 Jul 2024 00:38:32 +0200 Subject: [PATCH] updated pom to v2.0.0-a.1, fix classes accordingly to new architecture, added icon, updated .gitignore --- .gitignore | 41 ++ SLIC/.gitignore | 10 - SLIC/pom.xml | 31 - .../algorithms/danyfel80/islic/CIELab.java | 192 ------ .../algorithms/danyfel80/islic/SLICTask.java | 629 ------------------ .../plugins/danyfel80/islic/LABToRGB.java | 50 -- .../plugins/danyfel80/islic/RGBToLAB.java | 51 -- .../java/plugins/danyfel80/islic/SLIC.java | 135 ---- pom.xml | 35 + .../algorithms/danyfel80/islic/CIELab.java | 225 +++++++ .../algorithms/danyfel80/islic/SLICTask.java | 601 +++++++++++++++++ .../plugins/danyfel80/islic/LABToRGB.java | 74 +++ .../plugins/danyfel80/islic/RGBToLAB.java | 75 +++ .../java/plugins/danyfel80/islic/SLIC.java | 134 ++++ src/main/resources/slic.png | Bin 0 -> 9879 bytes 15 files changed, 1185 insertions(+), 1098 deletions(-) create mode 100644 .gitignore delete mode 100644 SLIC/.gitignore delete mode 100644 SLIC/pom.xml delete mode 100644 SLIC/src/main/java/algorithms/danyfel80/islic/CIELab.java delete mode 100644 SLIC/src/main/java/algorithms/danyfel80/islic/SLICTask.java delete mode 100644 SLIC/src/main/java/plugins/danyfel80/islic/LABToRGB.java delete mode 100644 SLIC/src/main/java/plugins/danyfel80/islic/RGBToLAB.java delete mode 100644 SLIC/src/main/java/plugins/danyfel80/islic/SLIC.java create mode 100644 pom.xml create mode 100644 src/main/java/algorithms/danyfel80/islic/CIELab.java create mode 100644 src/main/java/algorithms/danyfel80/islic/SLICTask.java create mode 100644 src/main/java/plugins/danyfel80/islic/LABToRGB.java create mode 100644 src/main/java/plugins/danyfel80/islic/RGBToLAB.java create mode 100644 src/main/java/plugins/danyfel80/islic/SLIC.java create mode 100644 src/main/resources/slic.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57f16fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +/build* +/workspace +setting.xml +release/ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ +icy.log + +### IntelliJ IDEA ### +.idea/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +**/.DS_Store +Icon? \ No newline at end of file diff --git a/SLIC/.gitignore b/SLIC/.gitignore deleted file mode 100644 index 95724b8..0000000 --- a/SLIC/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -/.gradle/ -/.settings/ -/bin/ -/build/ -/ecbuild/ -/target/ -/workspace/ -/.classpath -/.project -/setting.xml diff --git a/SLIC/pom.xml b/SLIC/pom.xml deleted file mode 100644 index 52f1eb1..0000000 --- a/SLIC/pom.xml +++ /dev/null @@ -1,31 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - <modelVersion>4.0.0</modelVersion> - <parent> - <groupId>org.bioimageanalysis.icy</groupId> - <artifactId>parent-pom-plugin</artifactId> - <version>1.0.4</version> - </parent> - <artifactId>slic</artifactId> - <version>1.0.1</version> - <name>SLIC - Superpixels</name> - <description/> - <build> - </build> - <dependencies> - <dependency> - <groupId>org.bioimageanalysis.icy</groupId> - <artifactId>ezplug</artifactId> - </dependency> - <dependency> - <groupId>org.bioimageanalysis.icy</groupId> - <artifactId>protocols</artifactId> - </dependency> - </dependencies> - <repositories> - <repository> - <id>icy</id> - <url>https://icy-nexus.pasteur.fr/repository/Icy/</url> - </repository> - </repositories> -</project> \ No newline at end of file diff --git a/SLIC/src/main/java/algorithms/danyfel80/islic/CIELab.java b/SLIC/src/main/java/algorithms/danyfel80/islic/CIELab.java deleted file mode 100644 index 4c6a7e1..0000000 --- a/SLIC/src/main/java/algorithms/danyfel80/islic/CIELab.java +++ /dev/null @@ -1,192 +0,0 @@ -package algorithms.danyfel80.islic; - -public class CIELab { - - public static class XYZ { - - private static final double[][] M = { { 0.4124, 0.3576, 0.1805 }, { 0.2126, 0.7152, 0.0722 }, - { 0.0193, 0.1192, 0.9505 } }; - private static final double[][] Mi = { { 3.2406, -1.5372, -0.4986 }, { -0.9689, 1.8758, 0.0415 }, - { 0.0557, -0.2040, 1.0570 } }; - - public static double[] fromRGB(int[] rgb) { - return fromRGB(rgb[0], rgb[1], rgb[2]); - } - - public static double[] fromRGB(int R, int G, int B) { - double[] result = new double[3]; - - // convert 0..255 into 0..1 - double r = R / 255.0; - double g = G / 255.0; - double b = B / 255.0; - - // assume sRGB - if (r <= 0.04045) { - r = r / 12.92; - } else { - r = Math.pow(((r + 0.055) / 1.055), 2.4); - } - if (g <= 0.04045) { - g = g / 12.92; - } else { - g = Math.pow(((g + 0.055) / 1.055), 2.4); - } - if (b <= 0.04045) { - b = b / 12.92; - } else { - b = Math.pow(((b + 0.055) / 1.055), 2.4); - } - - r *= 100.0; - g *= 100.0; - b *= 100.0; - - // [X Y Z] = [r g b][M] - result[0] = (r * M[0][0]) + (g * M[0][1]) + (b * M[0][2]); - result[1] = (r * M[1][0]) + (g * M[1][1]) + (b * M[1][2]); - result[2] = (r * M[2][0]) + (g * M[2][1]) + (b * M[2][2]); - - return result; - } - - public static int[] toRGB(double[] xyz) { - return toRGB(xyz[0], xyz[1], xyz[2]); - } - - public static int[] toRGB(double X, double Y, double Z) { - int[] result = new int[3]; - - double x = X / 100.0; - double y = Y / 100.0; - double z = Z / 100.0; - - // [r g b] = [X Y Z][Mi] - double r = (x * Mi[0][0]) + (y * Mi[0][1]) + (z * Mi[0][2]); - double g = (x * Mi[1][0]) + (y * Mi[1][1]) + (z * Mi[1][2]); - double b = (x * Mi[2][0]) + (y * Mi[2][1]) + (z * Mi[2][2]); - - // assume sRGB - if (r > 0.0031308) { - r = ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055); - } else { - r = (r * 12.92); - } - if (g > 0.0031308) { - g = ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055); - } else { - g = (g * 12.92); - } - if (b > 0.0031308) { - b = ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055); - } else { - b = (b * 12.92); - } - - r = (r < 0) ? 0 : r; - g = (g < 0) ? 0 : g; - b = (b < 0) ? 0 : b; - - // convert 0..1 into 0..255 - result[0] = (int) Math.round(r * 255); - result[1] = (int) Math.round(g * 255); - result[2] = (int) Math.round(b * 255); - - return result; - } - - } - - private static final double[] whitePoint = { 95.0429, 100.0, 108.8900 }; // D65 - - public static double[] fromRGB(int[] rgb) { - return fromXYZ(XYZ.fromRGB(rgb)); - } - - public static double[] fromRGB(int r, int g, int b) { - return fromXYZ(XYZ.fromRGB(r, g, b)); - } - - private static double[] fromXYZ(double[] xyz) { - return fromXYZ(xyz[0], xyz[1], xyz[2]); - } - - public static double[] fromXYZ(double X, double Y, double Z) { - double x = X / whitePoint[0]; - double y = Y / whitePoint[1]; - double z = Z / whitePoint[2]; - - if (x > 0.008856) { - x = Math.pow(x, 1.0 / 3.0); - } - else { - x = (7.787 * x) + (16.0 / 116.0); - } - if (y > 0.008856) { - y = Math.pow(y, 1.0 / 3.0); - } - else { - y = (7.787 * y) + (16.0 / 116.0); - } - if (z > 0.008856) { - z = Math.pow(z, 1.0 / 3.0); - } - else { - z = (7.787 * z) + (16.0 / 116.0); - } - - double[] result = new double[3]; - - result[0] = (116.0 * y) - 16.0; - result[1] = 500.0 * (x - y); - result[2] = 200.0 * (y - z); - - return result; - } - - public static int[] toRGB(double[] lab) { - return XYZ.toRGB(toXYZ(lab)); - } - - public static int[] toRGB(double L, double a, double b) { - return XYZ.toRGB(toXYZ(L, a, b)); - } - - public static double[] toXYZ(double[] lab) { - return toXYZ(lab[0], lab[1], lab[2]); - } - - public static double[] toXYZ(double L, double a, double b) { - double[] result = new double[3]; - - double y = (L + 16.0) / 116.0; - double y3 = Math.pow(y, 3.0); - double x = (a / 500.0) + y; - double x3 = Math.pow(x, 3.0); - double z = y - (b / 200.0); - double z3 = Math.pow(z, 3.0); - - if (y3 > 0.008856) { - y = y3; - } else { - y = (y - (16.0 / 116.0)) / 7.787; - } - if (x3 > 0.008856) { - x = x3; - } else { - x = (x - (16.0 / 116.0)) / 7.787; - } - if (z3 > 0.008856) { - z = z3; - } else { - z = (z - (16.0 / 116.0)) / 7.787; - } - - // results in 0...100 - result[0] = x * whitePoint[0]; - result[1] = y * whitePoint[1]; - result[2] = z * whitePoint[2]; - - return result; - } -} diff --git a/SLIC/src/main/java/algorithms/danyfel80/islic/SLICTask.java b/SLIC/src/main/java/algorithms/danyfel80/islic/SLICTask.java deleted file mode 100644 index cf4420d..0000000 --- a/SLIC/src/main/java/algorithms/danyfel80/islic/SLICTask.java +++ /dev/null @@ -1,629 +0,0 @@ -/* - * Copyright 2010-2016 Institut Pasteur. - * - * This file is part of Icy. - * - * Icy is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Icy is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Icy. If not, see <http://www.gnu.org/licenses/>. - */ -package algorithms.danyfel80.islic; - -import java.awt.Color; -import java.awt.Point; -import java.awt.Rectangle; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import icy.image.IcyBufferedImage; -import icy.roi.BooleanMask2D; -import icy.roi.ROI; -import icy.sequence.Sequence; -import icy.type.DataType; -import icy.type.collection.array.Array2DUtil; -import icy.type.point.Point3D; -import plugins.kernel.roi.roi2d.ROI2DArea; - -/** - * @author Daniel Felipe Gonzalez Obando - */ -public class SLICTask -{ - - private static final double EPSILON = 0.25; - static int MAX_ITERATIONS = 10; - static double ERROR_THRESHOLD = 1e-2; - - private Sequence sequence; - private int S; - private int Sx; - private int Sy; - private int SPnum; - private double rigidity; - - private boolean computeROIs; - private List<ROI> rois; - private Sequence superPixelsResult; - - private Map<Point3D.Integer, double[]> colorLUT; - private Map<Point, Double> distanceLUT; - - public SLICTask(Sequence sequence, int SPSize, double rigidity, boolean computeROIs) - { - this.sequence = sequence; - this.S = SPSize; - this.Sx = (sequence.getWidth() + S / 2 - 1) / S; - this.Sy = (sequence.getHeight() + S / 2 - 1) / S; - this.SPnum = Sx * Sy; - this.rigidity = rigidity; - - this.computeROIs = computeROIs; - - colorLUT = new HashMap<>(); - distanceLUT = new HashMap<>(); - } - - public void execute() - { - - double[] ls = new double[SPnum]; - double[] as = new double[SPnum]; - double[] bs = new double[SPnum]; - - double[] cxs = new double[SPnum]; - double[] cys = new double[SPnum]; - - double[][] sequenceData = Array2DUtil.arrayToDoubleArray(sequence.getDataXYC(0, 0), - sequence.isSignedDataType()); - - initialize(ls, as, bs, cxs, cys, sequenceData); - - int[] clusters = new int[sequence.getWidth() * sequence.getHeight()]; - double[] distances = Arrays.stream(clusters).mapToDouble(i -> Double.MAX_VALUE).toArray(); - - int iterations = 0; - double error = 0; - do - { - assignClusters(sequenceData, clusters, distances, ls, as, bs, cxs, cys); - error = updateClusters(sequenceData, clusters, ls, as, bs, cxs, cys); - iterations++; - System.out.format("Iteration=%d, Error=%f%n", iterations, error); - } - while (error > ERROR_THRESHOLD && iterations < MAX_ITERATIONS); - System.out.format("End of iterations%n"); - - computeSuperpixels(sequenceData, clusters, ls, as, bs, cxs, cys); - } - - private void initialize(double[] ls, double[] as, double[] bs, double[] cxs, double[] cys, double[][] sequenceData) - { - - IntStream.range(0, SPnum).forEach(sp -> { - int x, y, yOff, bestx, besty, bestyOff, i, j, yjOff; - double bestGradient, candidateGradient; - - int spx = sp % Sx; - int spy = sp / Sx; - - x = S / 2 + (spx) * S; - y = S / 2 + (spy) * S; - yOff = y * sequence.getWidth(); - - cxs[sp] = bestx = x; - cys[sp] = besty = y; - bestyOff = yOff; - bestGradient = Double.MAX_VALUE; - - for (j = -1; j <= 1; j++) - { - if (y + j >= 0 && y + j < sequence.getHeight()) - { - yjOff = (y + j) * sequence.getWidth(); - for (i = -1; i <= 1; i++) - { - if (x >= 0 && x < sequence.getWidth() && x + i >= 0 && x + i < sequence.getWidth()) - { - candidateGradient = getGradient(sequenceData, x + yOff, (x + i) + yjOff); - if (candidateGradient < bestGradient) - { - bestx = x + i; - besty = y + j; - bestyOff = yjOff; - bestGradient = candidateGradient; - } - } - } - } - } - - cxs[sp] = bestx; - cys[sp] = besty; - int bestPos = bestx + bestyOff; - - double[] bestLAB = getCIELab(sequenceData, bestPos); - ls[sp] = bestLAB[0]; - as[sp] = bestLAB[1]; - bs[sp] = bestLAB[2]; - - }); - - } - - private double getGradient(double[][] data, int pos1, int pos2) - { - int[] valRGB1 = new int[3]; - int[] valRGB2 = new int[3]; - IntStream.range(0, 3).forEach(c -> { - try - { - valRGB1[c] = (int) Math.round(255 * (data[c % sequence.getSizeC()][pos1] / sequence.getDataTypeMax())); - valRGB2[c] = (int) Math.round(255 * (data[c % sequence.getSizeC()][pos2] / sequence.getDataTypeMax())); - } - catch (Exception e) - { - throw e; - } - }); - - double[] val1 = CIELab.fromRGB(valRGB1); - double[] val2 = CIELab.fromRGB(valRGB2); - - double avgGrad = IntStream.range(0, 3).mapToDouble(c -> val2[c] - val1[c]).average().getAsDouble(); - - return avgGrad; - } - - private double[] getCIELab(double[][] sequenceData, int pos) - { - - int[] rgbVal = new int[3]; - IntStream.range(0, 3).forEach(c -> { - rgbVal[c] = (int) Math - .round(255 * (sequenceData[c % sequence.getSizeC()][pos] / sequence.getDataTypeMax())); - }); - Point3D.Integer rgbPoint = new Point3D.Integer(rgbVal); - - double[] LAB = colorLUT.get(rgbPoint); - if (LAB == null) - { - int[] RGB = new int[3]; - IntStream.range(0, 3).forEach(c -> { - RGB[c] = (int) Math - .round(255 * (sequenceData[c % sequence.getSizeC()][pos] / sequence.getDataTypeMax())); - }); - - LAB = CIELab.fromRGB(RGB); - colorLUT.put(rgbPoint, LAB); - } - return LAB; - } - - private void assignClusters(double[][] sequenceData, int[] clusters, double[] distances, double[] ls, double[] as, - double[] bs, double[] cxs, double[] cys) - { - // Each cluster - IntStream.range(0, SPnum).forEach(k -> { - double ckx = cxs[k], cky = cys[k], lk = ls[k], ak = as[k], bk = bs[k]; - int posk = ((int) ckx) + ((int) cky) * sequence.getWidth(); - distances[posk] = 0; - clusters[posk] = k; - - // 2Sx2S window assignment - int j, i, yOff; - for (j = -S; j < S; j++) - { - int ciy = (int) (cky + j); - if (ciy >= 0 && ciy < sequence.getHeight()) - { - yOff = ciy * sequence.getWidth(); - for (i = -S; i < S; i++) - { - int cix = (int) (ckx + i); - if (cix >= 0 && cix < sequence.getWidth()) - { - int posi = cix + yOff; - double[] LABi = getCIELab(sequenceData, posi); - double li = LABi[0]; - double ai = LABi[1]; - double bi = LABi[2]; - - double di = getDistance(ckx, cky, lk, ak, bk, i, j, li, ai, bi); - if (di < distances[posi]) - { - distances[posi] = di; - clusters[posi] = clusters[posk]; - } - } - } - } - } - }); - } - - private double getDistance(double ckx, double cky, double lk, double ak, double bk, int dx, int dy, double li, - double ai, double bi) - { - - double diffl, diffa, diffb; - diffl = li - lk; - diffa = ai - ak; - diffb = bi - bk; - - double dc = Math.sqrt(diffl * diffl + diffa * diffa + diffb * diffb); - Point dPt = new Point((int) Math.min(dx, dy), (int) Math.max(dx, dy)); - Double ds = distanceLUT.get(dPt); - if (ds == null) - { - ds = Math.sqrt(dx * dx + dy * dy); - distanceLUT.put(dPt, ds); - } - return Math.sqrt(dc * dc + (ds * ds) * (rigidity * rigidity * S)); - } - - private double updateClusters(double[][] sequenceData, int[] clusters, double[] ls, double[] as, double[] bs, - double[] cxs, double[] cys) - { - - double[] newls = new double[SPnum]; - double[] newas = new double[SPnum]; - double[] newbs = new double[SPnum]; - double[] newcxs = new double[SPnum]; - double[] newcys = new double[SPnum]; - int[] cants = new int[SPnum]; - - // int blockSize = 500; - // IntStream.iterate(0, sy -> sy+blockSize).limit((int)Math.ceil(sequence.getHeight()/blockSize)).forEach(y0->{ - // int y1 = Math.min(y0+blockSize, sequence.getHeight() - y0); - // IntStream.iterate(0, sx -> sx+blockSize).limit((int)Math.ceil(sequence.getWidth()/blockSize)).forEach(x0->{ - // int x1 = Math.min(x0+blockSize, sequence.getWidth() - x0); - // }); - // }); - for (int y = 0, yOff; y < sequence.getHeight(); y++) - { - yOff = y * sequence.getWidth(); - for (int x = 0, pos; x < sequence.getWidth(); x++) - { - pos = x + yOff; - int sp = clusters[pos]; - double[] lab = getCIELab(sequenceData, pos); - - cants[sp]++; - - newls[sp] += (lab[0] - newls[sp]) / cants[sp]; - newas[sp] += (lab[1] - newas[sp]) / cants[sp]; - newbs[sp] += (lab[2] - newbs[sp]) / cants[sp]; - newcxs[sp] += (x - newcxs[sp]) / cants[sp]; - newcys[sp] += (y - newcys[sp]) / cants[sp]; - } - } - - double error = IntStream.range(0, SPnum).mapToDouble(sp -> { - // double diffl = ls[sp] - newls[sp]; - // double diffa = as[sp] - newas[sp]; - // double diffb = bs[sp] - newbs[sp]; - double diffx = cxs[sp] - newcxs[sp]; - double diffy = cys[sp] - newcys[sp]; - - ls[sp] = newls[sp]; - as[sp] = newas[sp]; - bs[sp] = newbs[sp]; - cxs[sp] = newcxs[sp]; - cys[sp] = newcys[sp]; - - return Math.sqrt(diffx * diffx + diffy * diffy/* + diffl * diffl + diffa * diffa + diffb * diffb */); - }).sum() / SPnum; - - return error; - } - - private void computeSuperpixels(double[][] sequenceData, int[] clusters, double[] ls, double[] as, double[] bs, - double[] cxs, double[] cys) - { - - boolean[] visited = new boolean[clusters.length]; - int[] finalClusters = new int[clusters.length]; - - List<Point3D.Double> labs = new ArrayList<>(SPnum); - List<Double> areas = new ArrayList<>(SPnum); - List<Point> firstPoints = new ArrayList<>(SPnum); - - // fill known clusters - AtomicInteger usedLabels = new AtomicInteger(0); - IntStream.range(0, SPnum).forEach(i -> { - Point3D.Double labCenter = new Point3D.Double(); - AtomicInteger area = new AtomicInteger(0); - Point p = new Point((int) Math.round(cxs[i]), (int) Math.round(cys[i])); - int pPos = p.x + p.y * sequence.getWidth(); - - if (clusters[pPos] == i && !visited[pPos]) - { - findAreaAndColor(sequenceData, clusters, finalClusters, visited, p, labCenter, area, - usedLabels.getAndIncrement()); - labs.add(labCenter); - areas.add(area.get() / (double) (S * S)); - firstPoints.add(p); - } - - }); - - int firstUnknownCluster = usedLabels.get(); - - // fill independent clusters - for (int y = 0; y < sequence.getHeight(); y++) - { - int yOff = y * sequence.getWidth(); - for (int x = 0; x < sequence.getWidth(); x++) - { - int pos = x + yOff; - if (!visited[pos]) - { - Point3D.Double labCenter = new Point3D.Double(); - AtomicInteger area = new AtomicInteger(0); - Point p = new Point(x, y); - - findAreaAndColor(sequenceData, clusters, finalClusters, visited, p, labCenter, area, - usedLabels.getAndIncrement()); - - labs.add(labCenter); - areas.add(area.get() / (double) (S * S)); - firstPoints.add(p); - } - } - } - - // System.out.println("unvisited = " + IntStream.range(0, visited.length).filter(i -> visited[i] == false).sum()); - - // find neighbors and merge independent clusters - boolean[] mergedClusters = new boolean[usedLabels.get()]; - int[] mergedRefs = IntStream.range(0, usedLabels.get()).toArray(); - IntStream.range(firstUnknownCluster, usedLabels.get()).forEach(i -> { - List<Integer> neighbors = findNeighbors(finalClusters, visited, firstPoints.get(i)); - if (neighbors.size() > 0) - { - int bestNeighbour = neighbors.get(0); - double bestL = Double.MAX_VALUE; - - // boolean found = false; - for (Integer j : neighbors) - { - if (j < i) - { - // found = true; - double l = computeL(labs, areas, i, j); - if (l < bestL) - { - bestNeighbour = j; - bestL = l; - } - } - } - /* - * if (!found) { for (Integer j: neighbors) { double l = computeL(labs, - * areas, i, j); if (l < bestL) { bestNeighbour = j; bestL = l; } } } - * - * if (found) { - */ - double rArea = areas.get(i); - double relSPSize = rArea / 4; - relSPSize *= relSPSize; - - double coeff = relSPSize * (1.0 + bestL); - if (coeff < EPSILON) - { - mergedClusters[i] = true; - mergedRefs[i] = bestNeighbour; - } - // } else { - // mergedClusters[i] = true; - // mergedRefs[i] = bestNeighbour; - // } - } - else - { - System.err.format("Cluster at (%d, %d) has no neighbors", firstPoints.get(i).x, firstPoints.get(i).y); - } - }); - IntStream.range(firstUnknownCluster, usedLabels.get()).forEach(i -> { - if (mergedClusters[i]) - { - int appliedLabel = i; - while (appliedLabel != mergedRefs[appliedLabel]) - { - appliedLabel = mergedRefs[appliedLabel]; - } - findAreaAndColor(sequenceData, clusters, finalClusters, visited, firstPoints.get(i), - new Point3D.Double(), new AtomicInteger(0), appliedLabel); - } - - }); - - if (this.computeROIs) - { - // Create and add ROIs to sequence - this.rois = new ArrayList<>(); - IntStream.range(0, usedLabels.get()).forEach(i -> { - if (!mergedClusters[i]) - { - ROI2DArea roi = defineROI(finalClusters, firstPoints.get(i), labs.get(i)); - rois.add(roi); - } - }); - sequence.addROIs(rois, false); - } - else - { - List<int[]> rgbs = labs.stream().map(lab -> CIELab.toRGB(lab.x, lab.y, lab.z)).collect(Collectors.toList()); - superPixelsResult = new Sequence( - new IcyBufferedImage(sequence.getWidth(), sequence.getHeight(), 3, DataType.UBYTE)); - - superPixelsResult.setPixelSizeX(sequence.getPixelSizeX()); - superPixelsResult.setPixelSizeY(sequence.getPixelSizeY()); - superPixelsResult.setPixelSizeZ(sequence.getPixelSizeZ()); - superPixelsResult.setPositionX(sequence.getPositionX()); - superPixelsResult.setPositionY(sequence.getPositionY()); - superPixelsResult.setPositionZ(sequence.getPositionZ()); - - superPixelsResult.beginUpdate(); - double[][] spData = Array2DUtil.arrayToDoubleArray(superPixelsResult.getDataXYC(0, 0), - superPixelsResult.isSignedDataType()); - for (int y = 0, yOff; y < sequence.getHeight(); y++) - { - yOff = y * sequence.getWidth(); - for (int x = 0; x < sequence.getWidth(); x++) - { - final int pos = x + yOff; - int[] rgbVal = rgbs.get(finalClusters[pos]); - - IntStream.range(0, 3).forEach(c -> { - spData[c][pos] = rgbVal[c]; - }); - - } - } - Array2DUtil.doubleArrayToArray(spData, superPixelsResult.getDataXYC(0, 0)); - superPixelsResult.dataChanged(); - superPixelsResult.endUpdate(); - } - } - - private void findAreaAndColor(double[][] sequenceData, int[] clusters, int[] newClusters, boolean[] visited, - Point p, Point3D.Double labCenter, AtomicInteger area, int label) - { - int posp = p.x + p.y * sequence.getWidth(); - area.set(0); - labCenter.x = 0d; - labCenter.y = 0d; - labCenter.z = 0d; - - Deque<Point> q = new LinkedList<>(); - int val = clusters[posp]; - - visited[posp] = true; - q.add(p); - - while (!q.isEmpty()) - { - Point pti = q.pop(); - int posi = pti.x + pti.y * sequence.getWidth(); - - newClusters[posi] = label; - area.getAndIncrement(); - double[] labi = getCIELab(sequenceData, posi); - labCenter.x += (labi[0] - labCenter.x) / area.get(); - labCenter.y += (labi[1] - labCenter.y) / area.get(); - labCenter.z += (labi[2] - labCenter.z) / area.get(); - - int[] ds = new int[] {0, -1, 0, 1, 0}; - for (int is = 1; is < ds.length; is++) - { - Point ptn = new Point(pti.x + ds[is - 1], pti.y + ds[is]); - int posn = ptn.x + ptn.y * sequence.getWidth(); - if (sequence.getBounds2D().contains(ptn.x, ptn.y) && !visited[posn] && clusters[posn] == val) - { - visited[posn] = true; - q.add(ptn); - } - } - - } - } - - private List<Integer> findNeighbors(int[] newClusters, boolean[] visited, Point p) - { - int posp = p.x + p.y * sequence.getWidth(); - - HashSet<Integer> neighs = new HashSet<>(); - - Deque<Point> q = new LinkedList<>(); - int val = newClusters[posp]; - - visited[posp] = false; - q.add(p); - - while (!q.isEmpty()) - { - Point pti = q.pop(); - - int[] ds = new int[] {0, -1, 0, 1, 0}; - for (int is = 1; is < ds.length; is++) - { - Point ptn = new Point(pti.x + ds[is - 1], pti.y + ds[is]); - int posn = ptn.x + ptn.y * sequence.getWidth(); - if (sequence.getBounds2D().contains(ptn.x, ptn.y)) - { - if (newClusters[posn] == val) - { - if (visited[posn]) - { - visited[posn] = false; - q.add(ptn); - } - } - else - { - neighs.add(newClusters[posn]); - } - } - } - - } - return new ArrayList<Integer>(neighs); - } - - private double computeL(List<Point3D.Double> labs, List<Double> areas, int i, Integer j) - { - Point3D.Double diffLab = new Point3D.Double(); - diffLab.x = labs.get(j).x - labs.get(i).x; - diffLab.y = labs.get(j).y - labs.get(i).y; - diffLab.z = labs.get(j).z - labs.get(i).z; - - try - { - return diffLab.length() / areas.get(j); - } - catch (Exception e) - { - throw e; - } - } - - private ROI2DArea defineROI(int[] newClusters, Point p, Point3D.Double labP) - { - int posp = p.x + p.y * sequence.getWidth(); - double[] lab = new double[] {labP.x, labP.y, labP.z}; - int[] rgb = CIELab.toRGB(lab); - - int val = newClusters[posp]; - Rectangle seqBounds = sequence.getBounds2D(); - ROI2DArea roi1 = new ROI2DArea(new BooleanMask2D()); - IntStream.range(0, newClusters.length).filter(pi -> newClusters[pi] == val) - .forEach(pi -> roi1.addPoint(pi % seqBounds.width, pi / seqBounds.width));// .forEach(pi -> bMask[pi] = true); - - roi1.setColor(new Color(rgb[0], rgb[1], rgb[2])); - return roi1; - } - - public Sequence getResultSequence() - { - return this.superPixelsResult; - } -} diff --git a/SLIC/src/main/java/plugins/danyfel80/islic/LABToRGB.java b/SLIC/src/main/java/plugins/danyfel80/islic/LABToRGB.java deleted file mode 100644 index 769ac46..0000000 --- a/SLIC/src/main/java/plugins/danyfel80/islic/LABToRGB.java +++ /dev/null @@ -1,50 +0,0 @@ -package plugins.danyfel80.islic; - -import java.util.stream.IntStream; - -import algorithms.danyfel80.islic.CIELab; -import icy.image.IcyBufferedImage; -import icy.sequence.Sequence; -import icy.type.DataType; -import icy.type.collection.array.Array2DUtil; -import plugins.adufour.ezplug.EzPlug; -import plugins.adufour.ezplug.EzVarSequence; - -public class LABToRGB extends EzPlug { - - EzVarSequence inLabSequence; - - @Override - protected void initialize() { - inLabSequence = new EzVarSequence("LAB sequence"); - addEzComponent(inLabSequence); - } - - @Override - protected void execute() { - Sequence labSequence = inLabSequence.getValue(); - Sequence rgbSequence = new Sequence(new IcyBufferedImage(labSequence.getWidth(), labSequence.getHeight(),3, DataType.UBYTE)); - - double[][] labIm = Array2DUtil.arrayToDoubleArray(labSequence.getDataXYC(0, 0), labSequence.isSignedDataType()); - - rgbSequence.beginUpdate(); - double[][] rgbIm = Array2DUtil.arrayToDoubleArray(rgbSequence.getDataXYC(0, 0), rgbSequence.isSignedDataType()); - IntStream.range(0, rgbIm[0].length).forEach(pos->{ - double[] lab = IntStream.range(0, 3).mapToDouble(c->labIm[c][pos]).toArray(); - int[] rgb = CIELab.toRGB(lab); - IntStream.range(0, 3).forEach(c->rgbIm[c][pos] = rgb[c]); - }); - Array2DUtil.doubleArrayToArray(rgbIm, rgbSequence.getDataXYC(0, 0)); - rgbSequence.dataChanged(); - rgbSequence.endUpdate(); - - - addSequence(rgbSequence); - } - - @Override - public void clean() { - // TODO Auto-generated method stub - } - -} diff --git a/SLIC/src/main/java/plugins/danyfel80/islic/RGBToLAB.java b/SLIC/src/main/java/plugins/danyfel80/islic/RGBToLAB.java deleted file mode 100644 index ac6139a..0000000 --- a/SLIC/src/main/java/plugins/danyfel80/islic/RGBToLAB.java +++ /dev/null @@ -1,51 +0,0 @@ -package plugins.danyfel80.islic; - -import java.util.stream.IntStream; - -import algorithms.danyfel80.islic.CIELab; -import icy.image.IcyBufferedImage; -import icy.sequence.Sequence; -import icy.type.DataType; -import icy.type.collection.array.Array2DUtil; -import plugins.adufour.ezplug.EzPlug; -import plugins.adufour.ezplug.EzVarSequence; - -public class RGBToLAB extends EzPlug { - - EzVarSequence inRgbSequence; - - @Override - protected void initialize() { - inRgbSequence = new EzVarSequence("RGB sequence"); - addEzComponent(inRgbSequence); - } - - @Override - protected void execute() { - - Sequence rgbSequence = inRgbSequence.getValue(); - Sequence labSequence = new Sequence( - new IcyBufferedImage(rgbSequence.getWidth(), rgbSequence.getHeight(), 3, DataType.DOUBLE)); - - double[][] rgbIm = Array2DUtil.arrayToDoubleArray(rgbSequence.getDataXYC(0, 0), rgbSequence.isSignedDataType()); - - labSequence.beginUpdate(); - double[][] labIm = Array2DUtil.arrayToDoubleArray(labSequence.getDataXYC(0, 0), labSequence.isSignedDataType()); - IntStream.range(0, labIm[0].length).forEach(pos -> { - int[] rgb = IntStream.range(0, 3).map(c -> (int) Math.round(255 * (rgbIm[c][pos] / rgbSequence.getDataTypeMax()))) - .toArray(); - double[] lab = CIELab.fromRGB(rgb); - IntStream.range(0, 3).forEach(c -> labIm[c][pos] = lab[c]); - }); - Array2DUtil.doubleArrayToArray(labIm, labSequence.getDataXYC(0, 0)); - labSequence.dataChanged(); - labSequence.endUpdate(); - - labSequence.setName(rgbSequence.getName() + "_LAB"); - addSequence(labSequence); - } - - @Override - public void clean() {} - -} diff --git a/SLIC/src/main/java/plugins/danyfel80/islic/SLIC.java b/SLIC/src/main/java/plugins/danyfel80/islic/SLIC.java deleted file mode 100644 index ea1dc51..0000000 --- a/SLIC/src/main/java/plugins/danyfel80/islic/SLIC.java +++ /dev/null @@ -1,135 +0,0 @@ -package plugins.danyfel80.islic; - -import algorithms.danyfel80.islic.SLICTask; -import icy.gui.dialog.MessageDialog; -import icy.main.Icy; -import icy.plugin.PluginLauncher; -import icy.plugin.PluginLoader; -import plugins.adufour.blocks.lang.Block; -import plugins.adufour.blocks.util.VarList; -import plugins.adufour.ezplug.EzPlug; -import plugins.adufour.ezplug.EzVarBoolean; -import plugins.adufour.ezplug.EzVarDouble; -import plugins.adufour.ezplug.EzVarInteger; -import plugins.adufour.ezplug.EzVarSequence; - -public class SLIC extends EzPlug implements Block -{ - - private EzVarSequence inSequence; - private EzVarInteger inSPSize; - private EzVarDouble inSPReg; - private EzVarBoolean inIsROIOutput; - - private EzVarSequence outSequence; - - @Override - protected void initialize() - { - inSequence = new EzVarSequence("Sequence"); - inSPSize = new EzVarInteger("Superpixel size"); - inSPReg = new EzVarDouble("Superpixels regularity"); - inIsROIOutput = new EzVarBoolean("Output as ROIs", false); - - inSPSize.setValue(30); - inSPReg.setValue(0.2); - - addEzComponent(inSequence); - addEzComponent(inSPSize); - addEzComponent(inSPReg); - addEzComponent(inIsROIOutput); - } - - @Override - public void declareInput(VarList inputMap) - { - inSequence = new EzVarSequence("Sequence"); - inSPSize = new EzVarInteger("Superpixel size"); - inSPReg = new EzVarDouble("Superpixels regularity"); - inIsROIOutput = new EzVarBoolean("Output as ROIs", false); - - inSPSize.setValue(30); - inSPReg.setValue(0.2); - - inputMap.add(inSequence.name, inSequence.getVariable()); - inputMap.add(inSPSize.name, inSPSize.getVariable()); - inputMap.add(inSPReg.name, inSPReg.getVariable()); - inputMap.add(inIsROIOutput.name, inIsROIOutput.getVariable()); - } - - @Override - public void declareOutput(VarList outputMap) - { - outSequence = new EzVarSequence("Result"); - - outputMap.add(outSequence.name, outSequence.getVariable()); - } - - @Override - protected void execute() - { - long procTime, startTime, endTime; - SLICTask task; - try - { - task = new SLICTask(inSequence.getValue(), inSPSize.getValue(), inSPReg.getValue(), - inIsROIOutput.getValue()); - } - catch (Exception e) - { - e.printStackTrace(); - if (!this.isHeadLess()) - { - MessageDialog.showDialog("Initialization Error", - String.format("SLIC could not start properly: " + e.getMessage()), MessageDialog.ERROR_MESSAGE); - } - return; - } - startTime = System.currentTimeMillis(); - try - { - task.execute(); - } - catch (Exception e) - { - e.printStackTrace(); - if (!this.isHeadLess()) - { - MessageDialog.showDialog("Runtime Error", - String.format("SLIC could not run properly: " + e.getMessage()), MessageDialog.ERROR_MESSAGE); - } - return; - } - - endTime = System.currentTimeMillis(); - procTime = endTime - startTime; - System.out.println(String.format("SLIC finished in %d milliseconds", procTime)); - if (!this.inIsROIOutput.getValue()) - { - task.getResultSequence().setName(inSequence.getValue().getName() + String.format("_SLIC(size=%s,reg=%.2f)", - inSPSize.getValue().intValue(), inSPReg.getValue().doubleValue())); - } - if (!this.isHeadLess()) - { - MessageDialog.showDialog(String.format("SLIC finished in %d milliseconds", procTime)); - addSequence(task.getResultSequence()); - } - else - { - outSequence.setValue(task.getResultSequence()); - } - - } - - @Override - public void clean() - { - // TODO Auto-generated method stub - } - - public static void main(String[] args) - { - Icy.main(args); - PluginLauncher.start(PluginLoader.getPlugin(SLIC.class.getName())); - } -} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..324627d --- /dev/null +++ b/pom.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>pom-icy</artifactId> + <version>3.0.0-a.1</version> + </parent> + + <artifactId>slic</artifactId> + <version>2.0.0-a.1</version> + + <name>SLIC - Superpixels</name> + + <dependencies> + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>ezplug</artifactId> + </dependency> + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>protocols</artifactId> + </dependency> + </dependencies> + + <repositories> + <repository> + <id>icy</id> + <url>https://nexus-icy.pasteur.cloud/repository/icy/</url> + </repository> + </repositories> +</project> \ No newline at end of file diff --git a/src/main/java/algorithms/danyfel80/islic/CIELab.java b/src/main/java/algorithms/danyfel80/islic/CIELab.java new file mode 100644 index 0000000..020c402 --- /dev/null +++ b/src/main/java/algorithms/danyfel80/islic/CIELab.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2010-2024. Institut Pasteur. + * + * This file is part of Icy. + * Icy is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Icy is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Icy. If not, see <https://www.gnu.org/licenses/>. + */ + +package algorithms.danyfel80.islic; + +import org.jetbrains.annotations.NotNull; + +public class CIELab { + public static class XYZ { + private static final double[][] M = { + {0.4124, 0.3576, 0.1805}, + {0.2126, 0.7152, 0.0722}, + {0.0193, 0.1192, 0.9505} + }; + private static final double[][] Mi = { + {3.2406, -1.5372, -0.4986}, + {-0.9689, 1.8758, 0.0415}, + {0.0557, -0.2040, 1.0570} + }; + + public static double @NotNull [] fromRGB(final int @NotNull [] rgb) { + return fromRGB(rgb[0], rgb[1], rgb[2]); + } + + public static double @NotNull [] fromRGB(final int R, final int G, final int B) { + final double[] result = new double[3]; + + // convert 0..255 into 0..1 + double r = R / 255.0; + double g = G / 255.0; + double b = B / 255.0; + + // assume sRGB + if (r <= 0.04045) { + r = r / 12.92; + } + else { + r = Math.pow(((r + 0.055) / 1.055), 2.4); + } + if (g <= 0.04045) { + g = g / 12.92; + } + else { + g = Math.pow(((g + 0.055) / 1.055), 2.4); + } + if (b <= 0.04045) { + b = b / 12.92; + } + else { + b = Math.pow(((b + 0.055) / 1.055), 2.4); + } + + r *= 100.0; + g *= 100.0; + b *= 100.0; + + // [X Y Z] = [r g b][M] + result[0] = (r * M[0][0]) + (g * M[0][1]) + (b * M[0][2]); + result[1] = (r * M[1][0]) + (g * M[1][1]) + (b * M[1][2]); + result[2] = (r * M[2][0]) + (g * M[2][1]) + (b * M[2][2]); + + return result; + } + + public static int @NotNull [] toRGB(final double @NotNull [] xyz) { + return toRGB(xyz[0], xyz[1], xyz[2]); + } + + public static int @NotNull [] toRGB(final double X, final double Y, final double Z) { + final int[] result = new int[3]; + + final double x = X / 100.0; + final double y = Y / 100.0; + final double z = Z / 100.0; + + // [r g b] = [X Y Z][Mi] + double r = (x * Mi[0][0]) + (y * Mi[0][1]) + (z * Mi[0][2]); + double g = (x * Mi[1][0]) + (y * Mi[1][1]) + (z * Mi[1][2]); + double b = (x * Mi[2][0]) + (y * Mi[2][1]) + (z * Mi[2][2]); + + // assume sRGB + if (r > 0.0031308) { + r = ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055); + } + else { + r = (r * 12.92); + } + if (g > 0.0031308) { + g = ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055); + } + else { + g = (g * 12.92); + } + if (b > 0.0031308) { + b = ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055); + } + else { + b = (b * 12.92); + } + + r = (r < 0) ? 0 : r; + g = (g < 0) ? 0 : g; + b = (b < 0) ? 0 : b; + + // convert 0..1 into 0..255 + result[0] = (int) Math.round(r * 255); + result[1] = (int) Math.round(g * 255); + result[2] = (int) Math.round(b * 255); + + return result; + } + + } + + private static final double[] whitePoint = {95.0429, 100.0, 108.8900}; // D65 + + public static double @NotNull [] fromRGB(final int[] rgb) { + return fromXYZ(XYZ.fromRGB(rgb)); + } + + public static double @NotNull [] fromRGB(final int r, final int g, final int b) { + return fromXYZ(XYZ.fromRGB(r, g, b)); + } + + private static double @NotNull [] fromXYZ(final double @NotNull [] xyz) { + return fromXYZ(xyz[0], xyz[1], xyz[2]); + } + + public static double @NotNull [] fromXYZ(final double X, final double Y, final double Z) { + double x = X / whitePoint[0]; + double y = Y / whitePoint[1]; + double z = Z / whitePoint[2]; + + if (x > 0.008856) { + x = Math.pow(x, 1.0 / 3.0); + } + else { + x = (7.787 * x) + (16.0 / 116.0); + } + if (y > 0.008856) { + y = Math.pow(y, 1.0 / 3.0); + } + else { + y = (7.787 * y) + (16.0 / 116.0); + } + if (z > 0.008856) { + z = Math.pow(z, 1.0 / 3.0); + } + else { + z = (7.787 * z) + (16.0 / 116.0); + } + + final double[] result = new double[3]; + + result[0] = (116.0 * y) - 16.0; + result[1] = 500.0 * (x - y); + result[2] = 200.0 * (y - z); + + return result; + } + + public static int @NotNull [] toRGB(final double[] lab) { + return XYZ.toRGB(toXYZ(lab)); + } + + public static int @NotNull [] toRGB(final double L, final double a, final double b) { + return XYZ.toRGB(toXYZ(L, a, b)); + } + + public static double @NotNull [] toXYZ(final double @NotNull [] lab) { + return toXYZ(lab[0], lab[1], lab[2]); + } + + public static double @NotNull [] toXYZ(final double L, final double a, final double b) { + final double[] result = new double[3]; + + double y = (L + 16.0) / 116.0; + final double y3 = Math.pow(y, 3.0); + double x = (a / 500.0) + y; + final double x3 = Math.pow(x, 3.0); + double z = y - (b / 200.0); + final double z3 = Math.pow(z, 3.0); + + if (y3 > 0.008856) { + y = y3; + } + else { + y = (y - (16.0 / 116.0)) / 7.787; + } + if (x3 > 0.008856) { + x = x3; + } + else { + x = (x - (16.0 / 116.0)) / 7.787; + } + if (z3 > 0.008856) { + z = z3; + } + else { + z = (z - (16.0 / 116.0)) / 7.787; + } + + // results in 0...100 + result[0] = x * whitePoint[0]; + result[1] = y * whitePoint[1]; + result[2] = z * whitePoint[2]; + + return result; + } +} diff --git a/src/main/java/algorithms/danyfel80/islic/SLICTask.java b/src/main/java/algorithms/danyfel80/islic/SLICTask.java new file mode 100644 index 0000000..254b508 --- /dev/null +++ b/src/main/java/algorithms/danyfel80/islic/SLICTask.java @@ -0,0 +1,601 @@ +/* + * Copyright (c) 2010-2024. Institut Pasteur. + * + * This file is part of Icy. + * Icy is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Icy is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Icy. If not, see <https://www.gnu.org/licenses/>. + */ + +package algorithms.danyfel80.islic; + +import org.bioimageanalysis.extension.kernel.roi.roi2d.ROI2DArea; +import org.bioimageanalysis.icy.common.collection.array.Array2DUtil; +import org.bioimageanalysis.icy.common.geom.point.Point3D; +import org.bioimageanalysis.icy.common.type.DataType; +import org.bioimageanalysis.icy.model.image.IcyBufferedImage; +import org.bioimageanalysis.icy.model.roi.ROI; +import org.bioimageanalysis.icy.model.roi.mask.BooleanMask2D; +import org.bioimageanalysis.icy.model.sequence.Sequence; +import org.bioimageanalysis.icy.system.logging.IcyLogger; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.util.List; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +/** + * @author Daniel Felipe Gonzalez Obando + */ +public class SLICTask { + private static final double EPSILON = 0.25; + static int MAX_ITERATIONS = 10; + static double ERROR_THRESHOLD = 1e-2; + + private final Sequence sequence; + private final int S; + private final int Sx; + private final int Sy; + private final int SPnum; + private final double rigidity; + + private final boolean computeROIs; + private List<ROI> rois; + private Sequence superPixelsResult; + + private final Map<Point3D.Integer, double[]> colorLUT; + private final Map<Point, Double> distanceLUT; + + public SLICTask(@NotNull final Sequence sequence, final int SPSize, final double rigidity, final boolean computeROIs) { + this.sequence = sequence; + this.S = SPSize; + this.Sx = (sequence.getWidth() + S / 2 - 1) / S; + this.Sy = (sequence.getHeight() + S / 2 - 1) / S; + this.SPnum = Sx * Sy; + this.rigidity = rigidity; + + this.computeROIs = computeROIs; + + colorLUT = new HashMap<>(); + distanceLUT = new HashMap<>(); + } + + public void execute() { + final double[] ls = new double[SPnum]; + final double[] as = new double[SPnum]; + final double[] bs = new double[SPnum]; + + final double[] cxs = new double[SPnum]; + final double[] cys = new double[SPnum]; + + final double[][] sequenceData = Array2DUtil.arrayToDoubleArray( + sequence.getDataXYC(0, 0), + sequence.isSignedDataType() + ); + + initialize(ls, as, bs, cxs, cys, sequenceData); + + final int[] clusters = new int[sequence.getWidth() * sequence.getHeight()]; + final double[] distances = Arrays.stream(clusters).mapToDouble(i -> Double.MAX_VALUE).toArray(); + + int iterations = 0; + double error; // = 0; + do { + assignClusters(sequenceData, clusters, distances, ls, as, bs, cxs, cys); + error = updateClusters(sequenceData, clusters, ls, as, bs, cxs, cys); + iterations++; + IcyLogger.trace(this.getClass(), String.format("Iteration=%d, Error=%f", iterations, error)); + } + while (error > ERROR_THRESHOLD && iterations < MAX_ITERATIONS); + IcyLogger.trace(this.getClass(), "End of iterations%n"); + + computeSuperpixels(sequenceData, clusters, ls, as, bs, cxs, cys); + } + + private void initialize(final double[] ls, final double[] as, final double[] bs, final double[] cxs, final double[] cys, final double[][] sequenceData) { + IntStream.range(0, SPnum).forEach(sp -> { + final int x; + final int y; + final int yOff; + int bestx; + int besty; + int bestyOff; + int i; + int j; + int yjOff; + double bestGradient, candidateGradient; + + final int spx = sp % Sx; + final int spy = sp / Sx; + + x = S / 2 + (spx) * S; + y = S / 2 + (spy) * S; + yOff = y * sequence.getWidth(); + + cxs[sp] = bestx = x; + cys[sp] = besty = y; + bestyOff = yOff; + bestGradient = Double.MAX_VALUE; + + for (j = -1; j <= 1; j++) { + if (y + j >= 0 && y + j < sequence.getHeight()) { + yjOff = (y + j) * sequence.getWidth(); + for (i = -1; i <= 1; i++) { + if (x >= 0 && x < sequence.getWidth() && x + i >= 0 && x + i < sequence.getWidth()) { + candidateGradient = getGradient(sequenceData, x + yOff, (x + i) + yjOff); + if (candidateGradient < bestGradient) { + bestx = x + i; + besty = y + j; + bestyOff = yjOff; + bestGradient = candidateGradient; + } + } + } + } + } + + cxs[sp] = bestx; + cys[sp] = besty; + final int bestPos = bestx + bestyOff; + + final double[] bestLAB = getCIELab(sequenceData, bestPos); + ls[sp] = bestLAB[0]; + as[sp] = bestLAB[1]; + bs[sp] = bestLAB[2]; + }); + } + + private double getGradient(final double[][] data, final int pos1, final int pos2) { + final int[] valRGB1 = new int[3]; + final int[] valRGB2 = new int[3]; + IntStream.range(0, 3).forEach(c -> { + try { + valRGB1[c] = (int) Math.round(255 * (data[c % sequence.getSizeC()][pos1] / sequence.getDataTypeMax())); + valRGB2[c] = (int) Math.round(255 * (data[c % sequence.getSizeC()][pos2] / sequence.getDataTypeMax())); + } + catch (final Exception e) { + IcyLogger.error(this.getClass(), e, "Error while getting gradient."); + throw e; + } + }); + + final double[] val1 = CIELab.fromRGB(valRGB1); + final double[] val2 = CIELab.fromRGB(valRGB2); + + return IntStream.range(0, 3).mapToDouble(c -> val2[c] - val1[c]).average().orElseThrow(); + } + + private double @NotNull [] getCIELab(final double[][] sequenceData, final int pos) { + final int[] rgbVal = new int[3]; + IntStream.range(0, 3).forEach(c -> rgbVal[c] = (int) Math + .round(255 * (sequenceData[c % sequence.getSizeC()][pos] / sequence.getDataTypeMax()))); + final Point3D.Integer rgbPoint = new Point3D.Integer(rgbVal); + + double[] LAB = colorLUT.get(rgbPoint); + if (LAB == null) { + final int[] RGB = new int[3]; + IntStream.range(0, 3).forEach(c -> RGB[c] = (int) Math + .round(255 * (sequenceData[c % sequence.getSizeC()][pos] / sequence.getDataTypeMax()))); + + LAB = CIELab.fromRGB(RGB); + colorLUT.put(rgbPoint, LAB); + } + return LAB; + } + + private void assignClusters( + final double[][] sequenceData, + final int[] clusters, + final double[] distances, + final double[] ls, + final double[] as, + final double[] bs, + final double[] cxs, + final double[] cys + ) { + // Each cluster + IntStream.range(0, SPnum).forEach(k -> { + final double ckx = cxs[k]; + final double cky = cys[k]; + final double lk = ls[k]; + final double ak = as[k]; + final double bk = bs[k]; + final int posk = ((int) ckx) + ((int) cky) * sequence.getWidth(); + distances[posk] = 0; + clusters[posk] = k; + + // 2Sx2S window assignment + int j, i, yOff; + for (j = -S; j < S; j++) { + final int ciy = (int) (cky + j); + if (ciy >= 0 && ciy < sequence.getHeight()) { + yOff = ciy * sequence.getWidth(); + for (i = -S; i < S; i++) { + final int cix = (int) (ckx + i); + if (cix >= 0 && cix < sequence.getWidth()) { + final int posi = cix + yOff; + final double[] LABi = getCIELab(sequenceData, posi); + final double li = LABi[0]; + final double ai = LABi[1]; + final double bi = LABi[2]; + + final double di = getDistance(ckx, cky, lk, ak, bk, i, j, li, ai, bi); + if (di < distances[posi]) { + distances[posi] = di; + clusters[posi] = clusters[posk]; + } + } + } + } + } + }); + } + + private double getDistance( + final double ckx, + final double cky, + final double lk, + final double ak, + final double bk, + final int dx, + final int dy, + final double li, + final double ai, + final double bi + ) { + final double diffl; + final double diffa; + final double diffb; + diffl = li - lk; + diffa = ai - ak; + diffb = bi - bk; + + final double dc = Math.sqrt(diffl * diffl + diffa * diffa + diffb * diffb); + final Point dPt = new Point(Math.min(dx, dy), Math.max(dx, dy)); + final Double ds = distanceLUT.computeIfAbsent(dPt, k -> Math.sqrt(dx * dx + dy * dy)); + return Math.sqrt(dc * dc + (ds * ds) * (rigidity * rigidity * S)); + } + + private double updateClusters( + final double[][] sequenceData, + final int[] clusters, + final double[] ls, + final double[] as, + final double[] bs, + final double[] cxs, + final double[] cys + ) { + final double[] newls = new double[SPnum]; + final double[] newas = new double[SPnum]; + final double[] newbs = new double[SPnum]; + final double[] newcxs = new double[SPnum]; + final double[] newcys = new double[SPnum]; + final int[] cants = new int[SPnum]; + + // int blockSize = 500; + // IntStream.iterate(0, sy -> sy+blockSize).limit((int)Math.ceil(sequence.getHeight()/blockSize)).forEach(y0->{ + // int y1 = Math.min(y0+blockSize, sequence.getHeight() - y0); + // IntStream.iterate(0, sx -> sx+blockSize).limit((int)Math.ceil(sequence.getWidth()/blockSize)).forEach(x0->{ + // int x1 = Math.min(x0+blockSize, sequence.getWidth() - x0); + // }); + // }); + for (int y = 0, yOff; y < sequence.getHeight(); y++) { + yOff = y * sequence.getWidth(); + for (int x = 0, pos; x < sequence.getWidth(); x++) { + pos = x + yOff; + final int sp = clusters[pos]; + final double[] lab = getCIELab(sequenceData, pos); + + cants[sp]++; + + newls[sp] += (lab[0] - newls[sp]) / cants[sp]; + newas[sp] += (lab[1] - newas[sp]) / cants[sp]; + newbs[sp] += (lab[2] - newbs[sp]) / cants[sp]; + newcxs[sp] += (x - newcxs[sp]) / cants[sp]; + newcys[sp] += (y - newcys[sp]) / cants[sp]; + } + } + + return IntStream.range(0, SPnum).mapToDouble(sp -> { + // double diffl = ls[sp] - newls[sp]; + // double diffa = as[sp] - newas[sp]; + // double diffb = bs[sp] - newbs[sp]; + final double diffx = cxs[sp] - newcxs[sp]; + final double diffy = cys[sp] - newcys[sp]; + + ls[sp] = newls[sp]; + as[sp] = newas[sp]; + bs[sp] = newbs[sp]; + cxs[sp] = newcxs[sp]; + cys[sp] = newcys[sp]; + + return Math.sqrt(diffx * diffx + diffy * diffy/* + diffl * diffl + diffa * diffa + diffb * diffb */); + }).sum() / SPnum; + } + + private void computeSuperpixels( + final double[][] sequenceData, + final int @NotNull [] clusters, + final double[] ls, + final double[] as, + final double[] bs, + final double[] cxs, + final double[] cys + ) { + final boolean[] visited = new boolean[clusters.length]; + final int[] finalClusters = new int[clusters.length]; + + final List<Point3D.Double> labs = new ArrayList<>(SPnum); + final List<Double> areas = new ArrayList<>(SPnum); + final List<Point> firstPoints = new ArrayList<>(SPnum); + + // fill known clusters + final AtomicInteger usedLabels = new AtomicInteger(0); + IntStream.range(0, SPnum).forEach(i -> { + final Point3D.Double labCenter = new Point3D.Double(); + final AtomicInteger area = new AtomicInteger(0); + final Point p = new Point((int) Math.round(cxs[i]), (int) Math.round(cys[i])); + final int pPos = p.x + p.y * sequence.getWidth(); + + if (clusters[pPos] == i && !visited[pPos]) { + findAreaAndColor(sequenceData, clusters, finalClusters, visited, p, labCenter, area, + usedLabels.getAndIncrement()); + labs.add(labCenter); + areas.add(area.get() / (double) (S * S)); + firstPoints.add(p); + } + }); + + final int firstUnknownCluster = usedLabels.get(); + + // fill independent clusters + for (int y = 0; y < sequence.getHeight(); y++) { + final int yOff = y * sequence.getWidth(); + for (int x = 0; x < sequence.getWidth(); x++) { + final int pos = x + yOff; + if (!visited[pos]) { + final Point3D.Double labCenter = new Point3D.Double(); + final AtomicInteger area = new AtomicInteger(0); + final Point p = new Point(x, y); + + findAreaAndColor(sequenceData, clusters, finalClusters, visited, p, labCenter, area, + usedLabels.getAndIncrement()); + + labs.add(labCenter); + areas.add(area.get() / (double) (S * S)); + firstPoints.add(p); + } + } + } + + // System.out.println("unvisited = " + IntStream.range(0, visited.length).filter(i -> visited[i] == false).sum()); + + // find neighbors and merge independent clusters + final boolean[] mergedClusters = new boolean[usedLabels.get()]; + final int[] mergedRefs = IntStream.range(0, usedLabels.get()).toArray(); + IntStream.range(firstUnknownCluster, usedLabels.get()).forEach(i -> { + final List<Integer> neighbors = findNeighbors(finalClusters, visited, firstPoints.get(i)); + if (!neighbors.isEmpty()) { + int bestNeighbour = neighbors.getFirst(); + double bestL = Double.MAX_VALUE; + + // boolean found = false; + for (final Integer j : neighbors) { + if (j < i) { + // found = true; + final double l = computeL(labs, areas, i, j); + if (l < bestL) { + bestNeighbour = j; + bestL = l; + } + } + } + /* + * if (!found) { for (Integer j: neighbors) { double l = computeL(labs, + * areas, i, j); if (l < bestL) { bestNeighbour = j; bestL = l; } } } + * + * if (found) { + */ + final double rArea = areas.get(i); + double relSPSize = rArea / 4; + relSPSize *= relSPSize; + + final double coeff = relSPSize * (1.0 + bestL); + if (coeff < EPSILON) { + mergedClusters[i] = true; + mergedRefs[i] = bestNeighbour; + } + // } else { + // mergedClusters[i] = true; + // mergedRefs[i] = bestNeighbour; + // } + } + else { + IcyLogger.error( + this.getClass(), + String.format("Cluster at (%d, %d) has no neighbors.", firstPoints.get(i).x, firstPoints.get(i).y) + ); + } + }); + IntStream.range(firstUnknownCluster, usedLabels.get()).forEach(i -> { + if (mergedClusters[i]) { + int appliedLabel = i; + while (appliedLabel != mergedRefs[appliedLabel]) { + appliedLabel = mergedRefs[appliedLabel]; + } + findAreaAndColor(sequenceData, clusters, finalClusters, visited, firstPoints.get(i), + new Point3D.Double(), new AtomicInteger(0), appliedLabel); + } + + }); + + if (this.computeROIs) { + // Create and add ROIs to sequence + this.rois = new ArrayList<>(); + IntStream.range(0, usedLabels.get()).forEach(i -> { + if (!mergedClusters[i]) { + final ROI2DArea roi = defineROI(finalClusters, firstPoints.get(i), labs.get(i)); + rois.add(roi); + } + }); + sequence.addROIs(rois, false); + } + else { + final List<int[]> rgbs = labs.stream().map(lab -> CIELab.toRGB(lab.x, lab.y, lab.z)).toList(); + superPixelsResult = new Sequence(new IcyBufferedImage(sequence.getWidth(), sequence.getHeight(), 3, DataType.UBYTE)); + + superPixelsResult.setPixelSizeX(sequence.getPixelSizeX()); + superPixelsResult.setPixelSizeY(sequence.getPixelSizeY()); + superPixelsResult.setPixelSizeZ(sequence.getPixelSizeZ()); + superPixelsResult.setPositionX(sequence.getPositionX()); + superPixelsResult.setPositionY(sequence.getPositionY()); + superPixelsResult.setPositionZ(sequence.getPositionZ()); + + superPixelsResult.beginUpdate(); + final double[][] spData = Array2DUtil.arrayToDoubleArray(superPixelsResult.getDataXYC(0, 0), + superPixelsResult.isSignedDataType()); + for (int y = 0, yOff; y < sequence.getHeight(); y++) { + yOff = y * sequence.getWidth(); + for (int x = 0; x < sequence.getWidth(); x++) { + final int pos = x + yOff; + final int[] rgbVal = rgbs.get(finalClusters[pos]); + + IntStream.range(0, 3).forEach(c -> spData[c][pos] = rgbVal[c]); + + } + } + Array2DUtil.doubleArrayToArray(spData, superPixelsResult.getDataXYC(0, 0)); + superPixelsResult.dataChanged(); + superPixelsResult.endUpdate(); + } + } + + private void findAreaAndColor( + final double[][] sequenceData, + final int @NotNull [] clusters, + final int[] newClusters, + final boolean @NotNull [] visited, + @NotNull final Point p, + final Point3D.@NotNull Double labCenter, + @NotNull final AtomicInteger area, + final int label + ) { + final int posp = p.x + p.y * sequence.getWidth(); + area.set(0); + labCenter.x = 0d; + labCenter.y = 0d; + labCenter.z = 0d; + + final Deque<Point> q = new LinkedList<>(); + final int val = clusters[posp]; + + visited[posp] = true; + q.add(p); + + while (!q.isEmpty()) { + final Point pti = q.pop(); + final int posi = pti.x + pti.y * sequence.getWidth(); + + newClusters[posi] = label; + area.getAndIncrement(); + final double[] labi = getCIELab(sequenceData, posi); + labCenter.x += (labi[0] - labCenter.x) / area.get(); + labCenter.y += (labi[1] - labCenter.y) / area.get(); + labCenter.z += (labi[2] - labCenter.z) / area.get(); + + final int[] ds = new int[]{0, -1, 0, 1, 0}; + for (int is = 1; is < ds.length; is++) { + final Point ptn = new Point(pti.x + ds[is - 1], pti.y + ds[is]); + final int posn = ptn.x + ptn.y * sequence.getWidth(); + if (sequence.getBounds2D().contains(ptn.x, ptn.y) && !visited[posn] && clusters[posn] == val) { + visited[posn] = true; + q.add(ptn); + } + } + + } + } + + @Contract("_, _, _ -> new") + private @NotNull List<Integer> findNeighbors(final int @NotNull [] newClusters, final boolean @NotNull [] visited, @NotNull final Point p) { + final int posp = p.x + p.y * sequence.getWidth(); + + final HashSet<Integer> neighs = new HashSet<>(); + + final Deque<Point> q = new LinkedList<>(); + final int val = newClusters[posp]; + + visited[posp] = false; + q.add(p); + + while (!q.isEmpty()) { + final Point pti = q.pop(); + + final int[] ds = new int[]{0, -1, 0, 1, 0}; + for (int is = 1; is < ds.length; is++) { + final Point ptn = new Point(pti.x + ds[is - 1], pti.y + ds[is]); + final int posn = ptn.x + ptn.y * sequence.getWidth(); + if (sequence.getBounds2D().contains(ptn.x, ptn.y)) { + if (newClusters[posn] == val) { + if (visited[posn]) { + visited[posn] = false; + q.add(ptn); + } + } + else { + neighs.add(newClusters[posn]); + } + } + } + + } + return new ArrayList<>(neighs); + } + + private double computeL(@NotNull final List<Point3D.Double> labs, final List<Double> areas, final int i, final Integer j) { + final Point3D.Double diffLab = new Point3D.Double(); + diffLab.x = labs.get(j).x - labs.get(i).x; + diffLab.y = labs.get(j).y - labs.get(i).y; + diffLab.z = labs.get(j).z - labs.get(i).z; + + try { + return diffLab.length() / areas.get(j); + } + catch (final Exception e) { + IcyLogger.error(this.getClass(), e); + throw e; + } + } + + private @NotNull ROI2DArea defineROI(final int @NotNull [] newClusters, @NotNull final Point p, final Point3D.@NotNull Double labP) { + final int posp = p.x + p.y * sequence.getWidth(); + final double[] lab = new double[]{labP.x, labP.y, labP.z}; + final int[] rgb = CIELab.toRGB(lab); + + final int val = newClusters[posp]; + final Rectangle seqBounds = sequence.getBounds2D(); + final ROI2DArea roi1 = new ROI2DArea(new BooleanMask2D()); + IntStream.range(0, newClusters.length) + .filter(pi -> newClusters[pi] == val) + .forEach(pi -> roi1.addPoint(pi % seqBounds.width, pi / seqBounds.width));// .forEach(pi -> bMask[pi] = true); + + roi1.setColor(new Color(rgb[0], rgb[1], rgb[2])); + return roi1; + } + + public Sequence getResultSequence() { + return this.superPixelsResult; + } +} diff --git a/src/main/java/plugins/danyfel80/islic/LABToRGB.java b/src/main/java/plugins/danyfel80/islic/LABToRGB.java new file mode 100644 index 0000000..48ebd3d --- /dev/null +++ b/src/main/java/plugins/danyfel80/islic/LABToRGB.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2010-2024. Institut Pasteur. + * + * This file is part of Icy. + * Icy is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Icy is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Icy. If not, see <https://www.gnu.org/licenses/>. + */ + +package plugins.danyfel80.islic; + +import algorithms.danyfel80.islic.CIELab; +import org.bioimageanalysis.icy.common.collection.array.Array2DUtil; +import org.bioimageanalysis.icy.common.type.DataType; +import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginIcon; +import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginName; +import org.bioimageanalysis.icy.model.image.IcyBufferedImage; +import org.bioimageanalysis.icy.model.sequence.Sequence; +import plugins.adufour.ezplug.EzPlug; +import plugins.adufour.ezplug.EzVarSequence; + +import java.util.stream.IntStream; + +@IcyPluginName("LAB to RGB") +@IcyPluginIcon(path = "/slic.png") +public class LABToRGB extends EzPlug { + EzVarSequence inLabSequence; + + @Override + protected void initialize() { + inLabSequence = new EzVarSequence("LAB sequence"); + addEzComponent(inLabSequence); + } + + @Override + protected void execute() { + final Sequence labSequence = inLabSequence.getValue(); + final Sequence rgbSequence = new Sequence(new IcyBufferedImage(labSequence.getWidth(), + labSequence.getHeight(), + 3, + DataType.UBYTE + )); + + final double[][] labIm = Array2DUtil.arrayToDoubleArray(labSequence.getDataXYC(0, 0), labSequence.isSignedDataType()); + + rgbSequence.beginUpdate(); + final double[][] rgbIm = Array2DUtil.arrayToDoubleArray(rgbSequence.getDataXYC(0, 0), rgbSequence.isSignedDataType()); + IntStream.range(0, rgbIm[0].length).forEach(pos -> { + final double[] lab = IntStream.range(0, 3).mapToDouble(c -> labIm[c][pos]).toArray(); + final int[] rgb = CIELab.toRGB(lab); + IntStream.range(0, 3).forEach(c -> rgbIm[c][pos] = rgb[c]); + }); + Array2DUtil.doubleArrayToArray(rgbIm, rgbSequence.getDataXYC(0, 0)); + rgbSequence.dataChanged(); + rgbSequence.endUpdate(); + + + addSequence(rgbSequence); + } + + @Override + public void clean() { + // + } +} diff --git a/src/main/java/plugins/danyfel80/islic/RGBToLAB.java b/src/main/java/plugins/danyfel80/islic/RGBToLAB.java new file mode 100644 index 0000000..f9ceee5 --- /dev/null +++ b/src/main/java/plugins/danyfel80/islic/RGBToLAB.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2010-2024. Institut Pasteur. + * + * This file is part of Icy. + * Icy is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Icy is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Icy. If not, see <https://www.gnu.org/licenses/>. + */ + +package plugins.danyfel80.islic; + +import algorithms.danyfel80.islic.CIELab; +import org.bioimageanalysis.icy.common.collection.array.Array2DUtil; +import org.bioimageanalysis.icy.common.type.DataType; +import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginIcon; +import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginName; +import org.bioimageanalysis.icy.model.image.IcyBufferedImage; +import org.bioimageanalysis.icy.model.sequence.Sequence; +import plugins.adufour.ezplug.EzPlug; +import plugins.adufour.ezplug.EzVarSequence; + +import java.util.stream.IntStream; + +@IcyPluginName("RGB to LAB") +@IcyPluginIcon(path = "/slic.png") +public class RGBToLAB extends EzPlug { + EzVarSequence inRgbSequence; + + @Override + protected void initialize() { + inRgbSequence = new EzVarSequence("RGB sequence"); + addEzComponent(inRgbSequence); + } + + @Override + protected void execute() { + final Sequence rgbSequence = inRgbSequence.getValue(); + final Sequence labSequence = new Sequence(new IcyBufferedImage( + rgbSequence.getWidth(), + rgbSequence.getHeight(), + 3, + DataType.DOUBLE + )); + + final double[][] rgbIm = Array2DUtil.arrayToDoubleArray(rgbSequence.getDataXYC(0, 0), rgbSequence.isSignedDataType()); + + labSequence.beginUpdate(); + final double[][] labIm = Array2DUtil.arrayToDoubleArray(labSequence.getDataXYC(0, 0), labSequence.isSignedDataType()); + IntStream.range(0, labIm[0].length).forEach(pos -> { + final int[] rgb = IntStream.range(0, 3).map(c -> (int) Math.round(255 * (rgbIm[c][pos] / rgbSequence.getDataTypeMax()))).toArray(); + final double[] lab = CIELab.fromRGB(rgb); + IntStream.range(0, 3).forEach(c -> labIm[c][pos] = lab[c]); + }); + Array2DUtil.doubleArrayToArray(labIm, labSequence.getDataXYC(0, 0)); + labSequence.dataChanged(); + labSequence.endUpdate(); + + labSequence.setName(rgbSequence.getName() + "_LAB"); + addSequence(labSequence); + } + + @Override + public void clean() { + + } +} diff --git a/src/main/java/plugins/danyfel80/islic/SLIC.java b/src/main/java/plugins/danyfel80/islic/SLIC.java new file mode 100644 index 0000000..9bc58bd --- /dev/null +++ b/src/main/java/plugins/danyfel80/islic/SLIC.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2010-2024. Institut Pasteur. + * + * This file is part of Icy. + * Icy is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Icy is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Icy. If not, see <https://www.gnu.org/licenses/>. + */ + +package plugins.danyfel80.islic; + +import algorithms.danyfel80.islic.SLICTask; +import org.bioimageanalysis.icy.Icy; +import org.bioimageanalysis.icy.extension.plugin.PluginLauncher; +import org.bioimageanalysis.icy.extension.plugin.PluginLoader; +import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginIcon; +import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginName; +import org.bioimageanalysis.icy.system.logging.IcyLogger; +import org.jetbrains.annotations.NotNull; +import plugins.adufour.blocks.lang.Block; +import plugins.adufour.blocks.util.VarList; +import plugins.adufour.ezplug.*; + +@IcyPluginName("SLIC") +@IcyPluginIcon(path = "/slic.png") +public class SLIC extends EzPlug implements Block { + private EzVarSequence inSequence; + private EzVarInteger inSPSize; + private EzVarDouble inSPReg; + private EzVarBoolean inIsROIOutput; + + private EzVarSequence outSequence; + + @Override + protected void initialize() { + inSequence = new EzVarSequence("Sequence"); + inSPSize = new EzVarInteger("Superpixel size"); + inSPReg = new EzVarDouble("Superpixels regularity"); + inIsROIOutput = new EzVarBoolean("Output as ROIs", false); + + inSPSize.setValue(30); + inSPReg.setValue(0.2); + + addEzComponent(inSequence); + addEzComponent(inSPSize); + addEzComponent(inSPReg); + addEzComponent(inIsROIOutput); + } + + @Override + public void declareInput(final @NotNull VarList inputMap) { + inSequence = new EzVarSequence("Sequence"); + inSPSize = new EzVarInteger("Superpixel size"); + inSPReg = new EzVarDouble("Superpixels regularity"); + inIsROIOutput = new EzVarBoolean("Output as ROIs", false); + + inSPSize.setValue(30); + inSPReg.setValue(0.2); + + inputMap.add(inSequence.name, inSequence.getVariable()); + inputMap.add(inSPSize.name, inSPSize.getVariable()); + inputMap.add(inSPReg.name, inSPReg.getVariable()); + inputMap.add(inIsROIOutput.name, inIsROIOutput.getVariable()); + } + + @Override + public void declareOutput(final @NotNull VarList outputMap) { + outSequence = new EzVarSequence("Result"); + + outputMap.add(outSequence.name, outSequence.getVariable()); + } + + @Override + protected void execute() { + final long procTime; + final long startTime; + final long endTime; + final SLICTask task; + try { + task = new SLICTask(inSequence.getValue(), inSPSize.getValue(), inSPReg.getValue(), + inIsROIOutput.getValue()); + } + catch (final Exception e) { + IcyLogger.error(this.getClass(), e, "SLIC could not start properly."); + return; + } + startTime = System.currentTimeMillis(); + try { + task.execute(); + } + catch (final Exception e) { + IcyLogger.error(this.getClass(), e, "SLIC could not run properly."); + return; + } + + endTime = System.currentTimeMillis(); + procTime = endTime - startTime; + IcyLogger.info(this.getClass(), String.format("SLIC finished in %d milliseconds", procTime)); + if (!this.inIsROIOutput.getValue()) { + task.getResultSequence().setName(inSequence.getValue().getName() + String.format( + "_SLIC(size=%s,reg=%.2f)", + inSPSize.getValue(), + inSPReg.getValue()) + ); + } + if (!this.isHeadLess()) { + addSequence(task.getResultSequence()); + } + else { + outSequence.setValue(task.getResultSequence()); + } + + } + + @Override + public void clean() { + // + } + + @SuppressWarnings("resource") + public static void main(final String[] args) { + Icy.main(args); + PluginLauncher.start(PluginLoader.getPlugin(SLIC.class.getName())); + } +} diff --git a/src/main/resources/slic.png b/src/main/resources/slic.png new file mode 100644 index 0000000000000000000000000000000000000000..99e74d99539e812d1a3d6c21386d66f2f83d194a GIT binary patch literal 9879 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 aj_9ZpYh29V@jg7OA8M-`sMV<0hW;O3JsQ;j literal 0 HcmV?d00001 -- GitLab