diff --git a/src/main/java/net/imglib2/img/sparse/SparseCSCImg.java b/src/main/java/net/imglib2/img/sparse/SparseCSCImg.java new file mode 100644 index 000000000..5916f5592 --- /dev/null +++ b/src/main/java/net/imglib2/img/sparse/SparseCSCImg.java @@ -0,0 +1,70 @@ +package net.imglib2.img.sparse; + +import net.imglib2.Cursor; +import net.imglib2.Interval; +import net.imglib2.RandomAccess; +import net.imglib2.img.Img; +import net.imglib2.img.ImgFactory; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.numeric.NumericType; + +public class SparseCSCImg< + D extends NumericType<D> & NativeType<D>, + I extends IntegerType<I> & NativeType<I>> extends SparseImg<D,I> { + + public SparseCSCImg(final long numCols, final long numRows, final Img<D> data, final Img<I> indices, final Img<I> indptr) { + super(numCols, numRows, data, indices, indptr); + } + + @Override + public RandomAccess<D> randomAccess() { + return new SparseRandomAccess<D, I>(this, 1); + } + + @Override + public Cursor<D> localizingCursor() { + return new SparseLocalizingCursor<>(this, 1, data.firstElement()); + } + + @Override + public SparseCSCImg<D,I> copy() { + Img<D> dataCopy = data.copy(); + Img<I> indicesCopy = indices.copy(); + Img<I> indptrCopy = indptr.copy(); + return new SparseCSCImg<>(dimension(0), dimension(1), dataCopy, indicesCopy, indptrCopy); + } + + @Override + public ImgFactory<D> factory() { + return new SparseImgFactory<>(data.getAt(0), indices.getAt(0), 1); + } + + @Override + public ColumnMajorIterationOrder2D iterationOrder() { + return new ColumnMajorIterationOrder2D(this); + } + + /** + * An iteration order that scans a 2D image in column-major order. + * I.e., cursors iterate column by column and row by row. For instance a + * sparse img ranging from <em>(0,0)</em> to <em>(1,1)</em> is iterated like + * <em>(0,0), (0,1), (1,0), (1,1)</em> + */ + public static class ColumnMajorIterationOrder2D { + + private final Interval interval; + public ColumnMajorIterationOrder2D(final Interval interval) { + this.interval = interval; + } + + @Override + public boolean equals(final Object obj) { + + if (!(obj instanceof SparseCSCImg.ColumnMajorIterationOrder2D)) + return false; + + return SparseImg.haveSameIterationSpace(interval, ((ColumnMajorIterationOrder2D) obj).interval); + } + } +} diff --git a/src/main/java/net/imglib2/img/sparse/SparseCSRImg.java b/src/main/java/net/imglib2/img/sparse/SparseCSRImg.java new file mode 100644 index 000000000..fcf155757 --- /dev/null +++ b/src/main/java/net/imglib2/img/sparse/SparseCSRImg.java @@ -0,0 +1,70 @@ +package net.imglib2.img.sparse; + +import net.imglib2.Cursor; +import net.imglib2.Interval; +import net.imglib2.RandomAccess; +import net.imglib2.img.Img; +import net.imglib2.img.ImgFactory; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.numeric.NumericType; + +public class SparseCSRImg< + D extends NumericType<D> & NativeType<D>, + I extends IntegerType<I> & NativeType<I>> extends SparseImg<D,I> { + + public SparseCSRImg(final long numCols, final long numRows, final Img<D> data, final Img<I> indices, final Img<I> indptr) { + super(numCols, numRows, data, indices, indptr); + } + + @Override + public RandomAccess<D> randomAccess() { + return new SparseRandomAccess<D, I>(this, 0); + } + + @Override + public Cursor<D> localizingCursor() { + return new SparseLocalizingCursor<>(this, 0, data.firstElement()); + } + + @Override + public RowMajorIterationOrder2D iterationOrder() { + return new RowMajorIterationOrder2D(this); + } + + @Override + public SparseCSRImg<D,I> copy() { + Img<D> dataCopy = data.copy(); + Img<I> indicesCopy = indices.copy(); + Img<I> indptrCopy = indptr.copy(); + return new SparseCSRImg<>(dimension(0), dimension(1), dataCopy, indicesCopy, indptrCopy); + } + + @Override + public ImgFactory<D> factory() { + return new SparseImgFactory<>(data.getAt(0), indices.getAt(0), 0); + } + + /** + * An iteration order that scans a 2D image in row-major order. + * I.e., cursors iterate row by row and column by column. For instance a + * sparse img ranging from <em>(0,0)</em> to <em>(1,1)</em> is iterated like + * <em>(0,0), (1,0), (0,1), (1,1)</em> + */ + public static class RowMajorIterationOrder2D { + + private final Interval interval; + public RowMajorIterationOrder2D(final Interval interval) { + this.interval = interval; + } + + @Override + public boolean equals(final Object obj) { + + if (!(obj instanceof RowMajorIterationOrder2D)) + return false; + + return SparseImg.haveSameIterationSpace(interval, ((RowMajorIterationOrder2D) obj).interval); + } + } +} diff --git a/src/main/java/net/imglib2/img/sparse/SparseImg.java b/src/main/java/net/imglib2/img/sparse/SparseImg.java new file mode 100644 index 000000000..6a1ee94ca --- /dev/null +++ b/src/main/java/net/imglib2/img/sparse/SparseImg.java @@ -0,0 +1,173 @@ +package net.imglib2.img.sparse; + +import net.imglib2.Cursor; +import net.imglib2.Interval; +import net.imglib2.RandomAccess; +import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImgFactory; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.numeric.NumericType; +import net.imglib2.type.numeric.integer.LongType; + +import java.util.ArrayList; +import java.util.List; + +abstract public class SparseImg< + D extends NumericType<D> & NativeType<D>, + I extends IntegerType<I> & NativeType<I>> implements Img<D> { + + protected final long[] max; + protected final Img<D> data; + protected final Img<I> indices; + protected final Img<I> indptr; + + public SparseImg(long numCols, long numRows, Img<D> data, Img<I> indices, Img<I> indptr) { + + this.data = data; + this.indices = indices; + this.indptr = indptr; + this.max = new long[]{numCols-1, numRows-1}; + + if (data.numDimensions() != 1 || indices.numDimensions() != 1 || indptr.numDimensions() != 1) + throw new IllegalArgumentException("Data, index, and indptr Img must be one dimensional."); + if (data.min(0) != 0 || indices.min(0) != 0 || indptr.min(0) != 0) + throw new IllegalArgumentException("Data, index, and indptr arrays must start from 0."); + if (data.max(0) != indices.max(0)) + throw new IllegalArgumentException("Data and index array must be of the same size."); + if (indptr.max(0) != max[0]+1 && indptr.max(0) != max[1]+1) + throw new IllegalArgumentException("Indptr array does not fit number of slices."); + } + + public static <T extends NumericType<T> & NativeType<T>> SparseImg<T, LongType> convertToSparse(Img<T> img) { + return convertToSparse(img, 0); // CSR per default + } + + public static <T extends NumericType<T> & NativeType<T>> SparseImg<T, LongType> convertToSparse(Img<T> img, int leadingDimension) { + if (leadingDimension != 0 && leadingDimension != 1) + throw new IllegalArgumentException("Leading dimension in sparse array must be 0 or 1."); + + T zeroValue = img.getAt(0, 0).copy(); + zeroValue.setZero(); + + int nnz = getNumberOfNonzeros(img); + int ptrDimension = 1 - leadingDimension; + Img<T> data = new ArrayImgFactory<>(zeroValue).create(nnz); + Img<LongType> indices = new ArrayImgFactory<>(new LongType()).create(nnz); + Img<LongType> indptr = new ArrayImgFactory<>(new LongType()).create(img.dimension(ptrDimension) + 1); + + long count = 0; + T actualValue; + RandomAccess<T> ra = img.randomAccess(); + RandomAccess<T> dataAccess = data.randomAccess(); + RandomAccess<LongType> indicesAccess = indices.randomAccess(); + RandomAccess<LongType> indptrAccess = indptr.randomAccess(); + indptrAccess.setPosition(0,0); + indptrAccess.get().setLong(0L); + + for (long j = 0; j < img.dimension(ptrDimension); j++) { + ra.setPosition(j, ptrDimension); + for (long i = 0; i < img.dimension(leadingDimension); i++) { + ra.setPosition(i, leadingDimension); + actualValue = ra.get(); + if (!actualValue.valueEquals(zeroValue)) { + dataAccess.setPosition(count, 0); + dataAccess.get().set(actualValue); + indicesAccess.setPosition(count, 0); + indicesAccess.get().setLong(i); + count++; + } + } + indptrAccess.fwd(0); + indptrAccess.get().setLong(count); + } + + return (leadingDimension == 0) ? new SparseCSRImg<>(img.dimension(0), img.dimension(1), data, indices, indptr) + : new SparseCSCImg<>(img.dimension(0), img.dimension(1), data, indices, indptr); + } + + public static <T extends NumericType<T>> int getNumberOfNonzeros(Img<T> img) { + T zeroValue = img.getAt(0, 0).copy(); + zeroValue.setZero(); + + int nnz = 0; + for (T pixel : img) + if (!pixel.valueEquals(zeroValue)) + ++nnz; + return nnz; + } + + @Override + public long min(int d) { + return 0L; + } + + @Override + public long max(int d) { + return max[d]; + } + + @Override + public int numDimensions() { + return 2; + } + + @Override + public RandomAccess<D> randomAccess(Interval interval) { + return randomAccess(); + } + + public Img<D> getDataArray() { + return data; + } + + public Img<I> getIndicesArray() { + return indices; + } + + public Img<I> getIndexPointerArray() { + return indptr; + } + + @Override + public Cursor<D> cursor() { + return localizingCursor(); + } + + @Override + public long size() { + return max[0] * max[1]; + } + + /** + * Checks if two intervals have the same iteration space. + * + * @param a One interval + * @param b Other interval + * @return true if both intervals have compatible non-singleton dimensions, false otherwise + */ + protected static boolean haveSameIterationSpace(Interval a, Interval b) { + List<Integer> nonSingletonDimA = nonSingletonDimensions(a); + List<Integer> nonSingletonDimB = nonSingletonDimensions(b); + + if (nonSingletonDimA.size() != nonSingletonDimB.size()) + return false; + + for (int i = 0; i < nonSingletonDimA.size(); i++) { + Integer dimA = nonSingletonDimA.get(i); + Integer dimB = nonSingletonDimB.get(i); + if (a.min(dimA) != b.min(dimB) || a.max(dimA) != b.max(dimB)) + return false; + } + + return true; + } + + protected static List<Integer> nonSingletonDimensions(Interval interval) { + List<Integer> nonSingletonDim = new ArrayList<>(); + for (int i = 0; i < interval.numDimensions(); i++) + if (interval.dimension(i) > 1) + nonSingletonDim.add(i); + return nonSingletonDim; + } +} diff --git a/src/main/java/net/imglib2/img/sparse/SparseImgFactory.java b/src/main/java/net/imglib2/img/sparse/SparseImgFactory.java new file mode 100644 index 000000000..50c96c42b --- /dev/null +++ b/src/main/java/net/imglib2/img/sparse/SparseImgFactory.java @@ -0,0 +1,60 @@ +package net.imglib2.img.sparse; + +import net.imglib2.Dimensions; +import net.imglib2.exception.IncompatibleTypeException; +import net.imglib2.img.Img; +import net.imglib2.img.ImgFactory; +import net.imglib2.img.array.ArrayImg; +import net.imglib2.img.array.ArrayImgFactory; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.numeric.NumericType; + +/** + * Factory for {@link SparseImg}s. + * @param <D> type of data + * @param <I> type of indices + */ +public class SparseImgFactory< + D extends NumericType<D> & NativeType<D>, + I extends IntegerType<I> & NativeType<I>> extends ImgFactory<D> { + + protected final int leadingDimension; + protected final I indexType; + + + protected SparseImgFactory(D type, I indexType, int leadingDimension) { + super(type); + this.leadingDimension = leadingDimension; + this.indexType = indexType; + } + + @Override + public SparseImg<D, I> create(long... dimensions) { + if (dimensions.length != 2) + throw new IllegalArgumentException("Only 2D images are supported"); + + Dimensions.verify(dimensions); + ArrayImg<D, ?> data = new ArrayImgFactory<>(type()).create(1); + ArrayImg<I, ?> indices = new ArrayImgFactory<>(indexType).create(1); + int secondaryDimension = 1 - leadingDimension; + ArrayImg<I, ?> indptr = new ArrayImgFactory<>(indexType).create(dimensions[secondaryDimension] + 1); + + return (leadingDimension == 0) ? new SparseCSRImg<>(dimensions[0], dimensions[1], data, indices, indptr) + : new SparseCSCImg<>(dimensions[0], dimensions[1], data, indices, indptr); + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public <S> ImgFactory<S> imgFactory(S type) throws IncompatibleTypeException { + if (type instanceof NumericType && type instanceof NativeType) + return new SparseImgFactory<>((NumericType & NativeType) type, indexType, leadingDimension); + else + throw new IncompatibleTypeException(this, type.getClass().getCanonicalName() + " does not implement NumericType & NativeType."); + } + + @Override + public Img<D> create(long[] dim, D type) { + return create(dim); + } +} diff --git a/src/main/java/net/imglib2/img/sparse/SparseLocalizingCursor.java b/src/main/java/net/imglib2/img/sparse/SparseLocalizingCursor.java new file mode 100644 index 000000000..1ad75ed70 --- /dev/null +++ b/src/main/java/net/imglib2/img/sparse/SparseLocalizingCursor.java @@ -0,0 +1,116 @@ +package net.imglib2.img.sparse; + +import net.imglib2.AbstractLocalizingCursor; +import net.imglib2.Cursor; +import net.imglib2.RandomAccess; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.numeric.NumericType; + +public class SparseLocalizingCursor<T extends NumericType<T> & NativeType<T>> extends AbstractLocalizingCursor<T> { + + private final long[] max; + private boolean isHit = false; + private final int leadingDim; + private final int secondaryDim; + private final T fillValue; + + private SparseImg<T,?> img = null; + private Cursor<T> dataCursor; + private Cursor<? extends IntegerType<?>> indicesCursor; + private RandomAccess<? extends IntegerType<?>> indptrAccess; + + + public SparseLocalizingCursor(int n) { + super(n); + if (n != 2) + throw new IllegalArgumentException("Only 2D images are supported"); + + max = new long[]{0L, 0L}; + img = null; + dataCursor = null; + indicesCursor = null; + indptrAccess = null; + fillValue = null; + leadingDim = 0; + secondaryDim = 0; + } + + public SparseLocalizingCursor(SparseImg<T,?> img, int leadingDimension, T fillValue) { + super(img.numDimensions()); + if (n != 2) + throw new IllegalArgumentException("Only 2D images are supported"); + + this.img = img; + max = new long[]{img.dimension(0)-1, img.dimension(1)-1}; + this.leadingDim = leadingDimension; + this.secondaryDim = 1 - leadingDimension; + + this.dataCursor = img.data.cursor(); + this.indicesCursor = img.indices.localizingCursor(); + this.indptrAccess = img.indptr.randomAccess(); + dataCursor.fwd(); + indicesCursor.fwd(); + indptrAccess.setPosition(0, 0); + + this.fillValue = fillValue.copy(); + this.fillValue.setZero(); + + } + + @Override + public T get() { + if (isHit) + return dataCursor.get(); + return fillValue; + } + + @Override + public SparseLocalizingCursor<T> copy() { + return new SparseLocalizingCursor<>(img, leadingDim, fillValue); + } + + @Override + public void fwd() { + if (isHit) + advanceToNextNonzeroElement(); + + // always: advance to next element in picture ... + if (position[leadingDim] < max[leadingDim]) { + ++position[leadingDim]; + } else { + position[leadingDim] = 0; + ++position[secondaryDim]; + } + + // ... and check if it is a hit + isHit = indicesCursor.get().getIntegerLong() == position[leadingDim] + && indptrAccess.getLongPosition(0) == position[secondaryDim]; + } + + protected void advanceToNextNonzeroElement() { + if (indicesCursor.hasNext()) { + dataCursor.fwd(); + indicesCursor.fwd(); + } + long currentIndexPosition = indicesCursor.getLongPosition(0); + indptrAccess.fwd(0); + while (indptrAccess.get().getIntegerLong() <= currentIndexPosition) + indptrAccess.fwd(0); + indptrAccess.bck(0); + } + + @Override + public void reset() { + position[leadingDim] = -1; + position[secondaryDim] = 0; + dataCursor.reset(); + indicesCursor.reset(); + indptrAccess.setPosition(0,0); + } + + @Override + public boolean hasNext() { + return (position[0] < max[0] || position[1] < max[1]); + } +} diff --git a/src/main/java/net/imglib2/img/sparse/SparseRandomAccess.java b/src/main/java/net/imglib2/img/sparse/SparseRandomAccess.java new file mode 100644 index 000000000..25071192a --- /dev/null +++ b/src/main/java/net/imglib2/img/sparse/SparseRandomAccess.java @@ -0,0 +1,158 @@ +package net.imglib2.img.sparse; + +import net.imglib2.AbstractLocalizable; +import net.imglib2.Localizable; +import net.imglib2.RandomAccess; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.numeric.NumericType; + +public class SparseRandomAccess< + D extends NumericType<D> & NativeType<D>, + I extends IntegerType<I> & NativeType<I>> extends AbstractLocalizable implements RandomAccess<D> { + + protected final SparseImg<D, I> rai; + protected final RandomAccess<D> dataAccess; + protected final RandomAccess<I> indicesAccess; + protected final RandomAccess<I> indptrAccess; + protected final int leadingDim; + protected final int secondaryDim; + protected final D fillValue; + + public SparseRandomAccess(SparseImg<D, I> rai, int leadingDim) { + super(rai.numDimensions()); + this.rai = rai; + this.dataAccess = rai.data.randomAccess(); + this.indicesAccess = rai.indices.randomAccess(); + this.indptrAccess = rai.indptr.randomAccess(); + this.leadingDim = leadingDim; + this.secondaryDim = 1 - leadingDim; + + this.fillValue = dataAccess.get().createVariable(); + this.fillValue.setZero(); + } + + public SparseRandomAccess(SparseRandomAccess<D, I> ra) { + super(ra.rai.numDimensions()); + + this.rai = ra.rai; + this.leadingDim = ra.leadingDim; + this.secondaryDim = ra.secondaryDim; + this.setPosition( ra ); + + // not implementing copy() methods here had no effect since only setPosition() is used + this.indicesAccess = ra.indicesAccess.copy(); + this.indptrAccess = ra.indptrAccess.copy(); + this.dataAccess = ra.dataAccess.copy(); + this.fillValue = ra.fillValue.createVariable(); + this.fillValue.setZero(); + } + + @Override + public void fwd(int d) { + ++position[d]; + } + + @Override + public void bck(int d) { + --position[d]; + } + + @Override + public void move(int distance, int d) { + position[d] += distance; + } + + @Override + public void move(long distance, int d) { + position[d] += distance; + } + + @Override + public void move(Localizable localizable) { + for (int d = 0; d < n; ++d) { + position[d] += localizable.getLongPosition(d); + } + } + + @Override + public void move(int[] distance) { + for (int d = 0; d < n; ++d) { + position[d] += distance[d]; + } + } + + @Override + public void move(long[] distance) { + for (int d = 0; d < n; ++d) { + position[d] += distance[d]; + } + } + + @Override + public void setPosition(Localizable localizable) { + for (int d = 0; d < n; ++d) { + position[d] = localizable.getLongPosition(d); + } + } + + @Override + public void setPosition(int[] position) { + for (int d = 0; d < n; ++d) { + this.position[d] = position[d]; + } + } + + @Override + public void setPosition(long[] position) { + for (int d = 0; d < n; ++d) { + this.position[d] = position[d]; + } + } + + @Override + public void setPosition(int position, int d) { + this.position[d] = position; + } + + @Override + public void setPosition(long position, int d) { + this.position[d] = position; + } + + @Override + public D get() { + + // determine range of indices to search + indptrAccess.setPosition(position[secondaryDim], 0); + long start = indptrAccess.get().getIntegerLong(); + indptrAccess.fwd(0); + long end = indptrAccess.get().getIntegerLong(); + + if (start == end) + return fillValue; + + long current, currentInd; + do { + current = (start + end) / 2L; + indicesAccess.setPosition(current, 0); + currentInd = indicesAccess.get().getIntegerLong(); + + if (currentInd == position[leadingDim]) { + dataAccess.setPosition(indicesAccess); + return dataAccess.get(); + } + if (currentInd < position[leadingDim]) + start = current; + if (currentInd > position[leadingDim]) + end = current; + } while (current != start || (end - start) > 1L); + + return fillValue; + } + + @Override + public RandomAccess<D> copy() { + return new SparseRandomAccess<>(this); + } +} diff --git a/src/test/java/net/imglib2/img/sparse/SparseImageTest.java b/src/test/java/net/imglib2/img/sparse/SparseImageTest.java new file mode 100644 index 000000000..cfb378de6 --- /dev/null +++ b/src/test/java/net/imglib2/img/sparse/SparseImageTest.java @@ -0,0 +1,113 @@ +package net.imglib2.img.sparse; + +import java.util.HashMap; +import java.util.Map; + +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.type.Type; +import net.imglib2.type.numeric.integer.LongType; +import net.imglib2.type.numeric.real.DoubleType; +import net.imglib2.view.Views; +import org.junit.BeforeClass; +import org.junit.Test; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + + +public class SparseImageTest { + + protected static Map<String, SparseImg<DoubleType, LongType>> sparseImgs; + + @Test + public void CsrSetupIsCorrect() { + SparseCSRImg<DoubleType, LongType> csr = setupCsr(); + assertEquals(2, csr.numDimensions()); + assertArrayEquals(new long[]{0, 0}, csr.minAsLongArray()); + assertArrayEquals(new long[]{9, 8}, csr.maxAsLongArray()); + } + + @Test + public void iterationOrderEqualityTestIsCorrect() { + SparseCSRImg<DoubleType, LongType> csr = setupCsr(); + SparseCSRImg<DoubleType, LongType> csr2 = csr.copy(); + SparseCSCImg<DoubleType, LongType> csc = setupCsc(); + + assertEquals(csr.iterationOrder(), csr.iterationOrder()); + assertEquals(csr.iterationOrder(), csr2.iterationOrder()); + assertNotEquals(csr.iterationOrder(), csc.iterationOrder()); + } + + @Test + public void CsrNonzeroEntriesAreCorrect() { + int[] x = new int[]{2, 5, 0, 6, 9}; + int[] y = new int[]{0, 1, 2, 8, 8}; + + for (int i = 0; i < x.length; i++) { + RandomAccess<DoubleType> ra = setupCsr().randomAccess(); + assertEquals("Mismatch for x=" + x[i] + ", y=" + y[i], 1.0, ra.setPositionAndGet(x[i],y[i]).getRealDouble(), 1e-6); + } + } + + @Test + public void sparseHasCorrectNumberOfNonzeros() { + for (Map.Entry<String, SparseImg<DoubleType, LongType>> entry : sparseImgs.entrySet()) { + assertEquals("Mismatch for " + entry.getKey(), 5, SparseImg.getNumberOfNonzeros(entry.getValue())); + } + } + + @Test + public void conversionToSparseIsCorrect() { + for (SparseImg<DoubleType, LongType> sparse : sparseImgs.values()) { + assertEquals(5, SparseImg.getNumberOfNonzeros(sparse)); + SparseImg<DoubleType, LongType> newCsr = SparseImg.convertToSparse(sparse, 0); + assertTrue(newCsr instanceof SparseCSRImg); + assert2DRaiEquals(sparse, newCsr); + SparseImg<DoubleType, LongType> newCsc = SparseImg.convertToSparse(sparse, 1); + assertTrue(newCsc instanceof SparseCSCImg); + assert2DRaiEquals(sparse, newCsc); + } + } + + @Test + public void CscIsCsrTransposed() { + SparseCSRImg<DoubleType, LongType> csr = setupCsr(); + SparseCSCImg<DoubleType, LongType> csc = setupCsc(); + assert2DRaiEquals(csr, Views.permute(csc, 0, 1)); + } + + protected SparseCSRImg<DoubleType, LongType> setupCsr() { + return (SparseCSRImg<DoubleType, LongType>) sparseImgs.get("CSR"); + } + + protected SparseCSCImg<DoubleType, LongType> setupCsc() { + return (SparseCSCImg<DoubleType, LongType>) sparseImgs.get("CSC"); + } + + @BeforeClass + public static void setupSparseImages() { + Img<DoubleType> data = ArrayImgs.doubles(new double[]{1.0, 1.0, 1.0, 1.0, 1.0}, 5); + Img<LongType> indices = ArrayImgs.longs(new long[]{2L, 5L, 0L, 6L, 9L}, 5); + Img<LongType> indptr = ArrayImgs.longs(new long[]{0L, 1L, 2L, 3L, 3L, 3L, 3L, 3L, 3L, 5L}, 10); + + sparseImgs = new HashMap<>(); + sparseImgs.put("CSR", new SparseCSRImg<>(10, 9, data, indices, indptr)); + sparseImgs.put("CSC", new SparseCSCImg<>(9, 10, data, indices, indptr)); + } + + protected static <T extends Type<T>> void assert2DRaiEquals(RandomAccessibleInterval<T> expected, RandomAccessibleInterval<T> actual) { + assertEquals("Number of columns not the same.", expected.dimension(0), actual.dimension(0)); + assertEquals("Number of rows not the same.", expected.dimension(1), actual.dimension(1)); + + RandomAccess<T> raExpected = expected.randomAccess(); + RandomAccess<T> raActual = actual.randomAccess(); + for (int i = 0; i < expected.dimension(0); ++i) + for (int j = 0; j < expected.dimension(1); ++j) + assertEquals("Rai's differ on entry (" + i + "," + j +")", raExpected.setPositionAndGet(i, j), raActual.setPositionAndGet(i, j)); + } +} \ No newline at end of file