net.algart.matrices.skeletons.ApertureBasedSkeletonPixelClassifier Maven / Gradle / Ivy
Show all versions of algart Show documentation
/*
* The MIT License (MIT)
*
* Copyright (c) 2007-2024 Daniel Alievsky, AlgART Laboratory (http://algart.net)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.algart.matrices.skeletons;
import net.algart.arrays.*;
import net.algart.math.functions.AbstractFunc;
import net.algart.math.functions.Func;
import net.algart.math.functions.LinearFunc;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* A skeletal implementation of the {@link SkeletonPixelClassifier} abstract class,
* minimizing the effort required to implement its abstract methods.
*
* Namely, the main {@link #asPixelTypes asPixelTypes} method is implemented in this class
* via the following 2 abstract methods:
*
*
* - {@link #pixelTypeOrAttachingBranch(int)},
* - {@link #pixelTypeOrAttachedNode(int)}.
*
*
* The methods {@link #neighbourOffset(long[], int)} and {@link #reverseNeighbourIndex(int)}
* are implemented on the base of the array of offsets of all neighbours of a random element.
* This array should be passed to the {@link #ApertureBasedSkeletonPixelClassifier(int, long[][]) constructor}.
* The constructor analyses the passed array, checks that it really contains
* offsets of all neighbours, copies this array into an internal field, which is used
* by {@link #neighbourOffset(long[], int)}, and also automatically finds,
* for every neighbour, the reverse neighbour index, which will be returned by {@link #reverseNeighbourIndex(int)}.
*
*
The method {@link #markNeighbouringNodesNotConnectedViaDegeneratedBranches(int[])} is implemented here
* in some reasonable way for 2-dimensional case, as specified in comments to this method.
* For other number of dimensions, this method does nothing.
* It is a good solution for the degenerated case {@link #dimCount() dimCount()}=1;
* for 3-dimensional case, this method probably should be overridden.
*
* So, it is enough to implement {@link #pixelTypeOrAttachingBranch(int)} and {@link #pixelTypeOrAttachedNode(int)}
* methods and, maybe, override {@link #markNeighbouringNodesNotConnectedViaDegeneratedBranches(int[])} method
* to create a full implementation of the skeleton pixel classifier on the base of this class.
*
* This class can be used in 1-, 2- and 3-dimensional cases only.
* One of the reasons of this restriction is that the argument of {@link #pixelTypeOrAttachingBranch(int)}
* and {@link #pixelTypeOrAttachedNode(int)} (int
type) can represent the values of, maximally, 32
* neighbours. It is enough for 3-dimensional case, where the number of neighbours is 33−1=26<32,
* but not enough already for 4-dimensional case, where the number of neighbours is 34−1=80
* (and even 64-bit long
type would have been insufficient).
*
* This class is immutable and thread-safe:
* there are no ways to modify settings of the created instance.
*
* @author Daniel Alievsky
*/
public abstract class ApertureBasedSkeletonPixelClassifier extends SkeletonPixelClassifier {
private final long[][] neighbourOffsets;
private final int[] reverseNeighbourIndexes;
/**
* Creates new instance of this class, allowing to process skeletons with the given number of dimensions,
* with the order of neighbours, specified in the second argument.
* The number of dimensions must be 1, 2 or 3.
*
* The argument neighbourOffsets
must contain offsets of all neighbours,
* in terms of the {@link net.algart.matrices.scanning.ConnectivityType#STRAIGHT_AND_DIAGONAL
* straight-and-diagonal connectivity kind}, of any matrix element, in some order.
* More precisely, this array must contain
* {@link #numberOfNeighbours() numberOfNeighbours()}=3dimCount-1
elements
* (2, 8 or 26 for 1-, 2-, 3-dimensional cases) in 3/3x3/3x3x3-aperture, and each its element
* neighbourOffsets[k]
must be equal to the result of
* {@link #neighbourOffset(int) neighbourOffset(k)} call.
*
*
The passed neighbourOffsets
array is deeply cloned by the constructor: no references to it
* or its elements are maintained by the created object.
*
* @param dimCount the number of dimensions, which will be returned by
* {@link #dimCount() dimCount()} method.
* @param neighbourOffsets offsets of all neighbours of any matrix element,
* in terms of {@link #neighbourOffset(int) neighbourOffset(int)} method.
* @throws NullPointerException if neighbourOffsets
or one of its elements is {@code null}.
* @throws IllegalArgumentException if dimCount
is not in 1..3
range,
* or if neighbourOffsets.length!=3dimCount-1
,
* or if neighbourOffsets[k].length!=dimCount
* for some k
,
* or if neighbourOffsets
does not contain, in some order,
* the offsets of all 3dimCount-1
neighbours
* (in particular, if some elements neighbourOffsets[k][j]
are
* not in -1..+1
range or if offsets of some neighbours are equal).
*/
protected ApertureBasedSkeletonPixelClassifier(int dimCount, long[][] neighbourOffsets) {
super(dimCount);
Objects.requireNonNull(neighbourOffsets, "Null neighbourOffsets array");
if (dimCount > 3) {
throw new IllegalArgumentException("This class " + getClass().getName() + " cannot process "
+ dimCount + "-dimensional apertures (maximum 3-dimensional ones are allowed)");
}
if (this.numberOfNeighbours != neighbourOffsets.length) {
throw new IllegalArgumentException("Number of passed neighbour offsets " + neighbourOffsets.length
+ " does not match the number of neighbours in 3x3x... aperture " + this.numberOfNeighbours);
}
if (this.numberOfNeighbours > 30) // bit #31 can be used for the central element of the aperture
{
throw new AssertionError("This class " + getClass().getName()
+ " cannot process more than 30 elements in the aperture (besides the central element)");
}
// We cannot use full 63-bit precision here, because double values cannot precisely store all long values
this.neighbourOffsets = new long[neighbourOffsets.length][dimCount];
for (int k = 0; k < neighbourOffsets.length; k++) {
long[] neighbourOffset = neighbourOffsets[k];
// creating a copy: necessary if another thread is modifying the argument now
Objects.requireNonNull(neighbourOffset, "Null neighbourOffsets[" + k + "]");
if (neighbourOffset.length != dimCount) {
throw new IllegalArgumentException("Illegal neighbourOffsets[" + k + "].length = "
+ neighbourOffset.length + ": does not match to the number of dimensions " + dimCount);
}
System.arraycopy(neighbourOffset, 0, this.neighbourOffsets[k], 0, dimCount);
}
// now this.neighbourOffsets is a deep copy of the argument, which cannot be destroyed by another thread
this.reverseNeighbourIndexes = new int[this.neighbourOffsets.length];
for (int k = 0; k < this.neighbourOffsets.length; k++) {
int reverseIndex = -1;
boolean allZero = true;
for (int j = 0; j < dimCount; j++) {
if (Math.abs(this.neighbourOffsets[k][j]) > 1) {
throw new IllegalArgumentException("Illegal neighbourOffsets: the offset #" + k
+ " (" + JArrays.toString(this.neighbourOffsets[k], ",", 1000)
+ " describes not a neighbour, because some of its components is not in -1..1 range");
}
allZero &= this.neighbourOffsets[k][j] == 0;
}
if (allZero) {
throw new IllegalArgumentException("Illegal neighbourOffsets: the offset #" + k + " is zero");
}
for (int i = 0; i < this.neighbourOffsets.length; i++) {
if (i == k) {
continue;
}
boolean matchThis = true;
for (int j = 0; j < dimCount; j++) {
matchThis &= this.neighbourOffsets[i][j] == this.neighbourOffsets[k][j];
}
if (matchThis) {
throw new IllegalArgumentException("Illegal neighbourOffsets: the offsets #" + k
+ " and # " + i + " are equal");
}
boolean matchNegative = true;
for (int j = 0; j < dimCount; j++) {
matchNegative &= this.neighbourOffsets[i][j] == -this.neighbourOffsets[k][j];
}
if (matchNegative) {
reverseIndex = i; break;
}
}
if (reverseIndex == -1) {
throw new IllegalArgumentException("Illegal neighbourOffsets: the offset #" + k
+ " (" + JArrays.toString(this.neighbourOffsets[k], ",", 1000)
+ ") has no corresponding reverse offset (the same but with negative sign)");
}
this.reverseNeighbourIndexes[k] = reverseIndex;
}
// We've checked 3^dimCount-1 offsets and all they are different non-zero vectors with -1..1 components,
// so, we can be sure that they are really the offsets of all elements of 3x3x... aperture, in some order.
}
@Override
public void neighbourOffset(long[] coordinateIncrements, int neighbourIndex) {
Objects.requireNonNull(coordinateIncrements, "Null list of coordinates");
if (coordinateIncrements.length != dimCount) {
throw new IllegalArgumentException("Number of coordinates " + coordinateIncrements.length
+ " is not equal to the number of matrix dimensions " + dimCount());
}
if (neighbourIndex < 0 || neighbourIndex >= numberOfNeighbours) {
throw new IndexOutOfBoundsException("Illegal neighbourIndex = " + neighbourIndex
+ ": must be in 0.." + (numberOfNeighbours - 1) + " range");
}
System.arraycopy(neighbourOffsets[neighbourIndex], 0, coordinateIncrements, 0, dimCount);
}
@Override
public int reverseNeighbourIndex(int neighbourIndex) {
return reverseNeighbourIndexes[neighbourIndex];
}
@Override
public Matrix extends PIntegerArray> asPixelTypes(
Matrix extends BitArray> skeleton,
AttachmentInformation attachmentInformation)
{
Objects.requireNonNull(attachmentInformation, "Null attachmentInformation");
Matrix extends PIntegerArray> packed = asNeighbourhoodBitMaps(skeleton);
switch (attachmentInformation) {
case NEIGHBOUR_INDEX_OF_ATTACHING_BRANCH:
return Matrices.asFuncMatrix(false, new AbstractFunc() {
@Override
public double get(double... x) {
return get(x[0]);
}
@Override
public double get(double x0) {
int apertureBits = (int) x0; // precise operations, because x0 is "int" 31-bit value
if ((apertureBits & 1) == 0) {
return TYPE_ZERO;
}
return pixelTypeOrAttachingBranch(apertureBits >>> 1);
}
}, IntArray.class, packed);
case NEIGHBOUR_INDEX_OF_ATTACHED_NODE:
return Matrices.asFuncMatrix(false, new AbstractFunc() {
@Override
public double get(double... x) {
return get(x[0]);
}
@Override
public double get(double x0) {
int apertureBits = (int) x0; // precise operations, because x0 is "int" 31-bit value
if ((apertureBits & 1) == 0) {
return TYPE_ZERO;
}
return pixelTypeOrAttachedNode(apertureBits >>> 1);
}
}, IntArray.class, packed);
default:
throw new AssertionError("Unknown attachmentInformation: " + attachmentInformation);
}
}
@Override
public void markNeighbouringNodesNotConnectedViaDegeneratedBranches(int[] pixelTypesOfAllNeighbours) {
if (pixelTypesOfAllNeighbours.length < numberOfNeighbours) {
throw new IllegalArgumentException("Too short pixelTypesOfAllNeighbours array");
}
if (dimCount != 2) {
return; // should be overridden for another number of dimensions
}
for (int neighbourIndex = 0; neighbourIndex < numberOfNeighbours; neighbourIndex++) {
if (neighbourIndex % 2 == 0) { // diagonal degenerated branch
if (pixelTypesOfAllNeighbours[(neighbourIndex + 1) & 7] == TYPE_USUAL_NODE
|| pixelTypesOfAllNeighbours[(neighbourIndex + 7) & 7] == TYPE_USUAL_NODE)
{
pixelTypesOfAllNeighbours[neighbourIndex] = Integer.MIN_VALUE;
}
}
}
}
/**
* Returns an immutable view of the passed skeleton matrix, where each element is an integer,
* containing, in its low bits, the bit values of the corresponding element
* C
of the source skeleton and of all its neighbours (in terms of the
* {@link net.algart.matrices.scanning.ConnectivityType#STRAIGHT_AND_DIAGONAL
* straight-and-diagonal connectivity kind}).
*
*
More precisely, each integer element w of the resulting matrix will contain:
*
* - in the bit #0 (in other words, w
&1
):
* the value of the corresponding element
* C
of the source skeleton bit matrix;
* - in the bit #k+1, 0≤k<{@link #numberOfNeighbours() numberOfNeighbours()}
* (in other words, (w
>>>(
k+1))&1
):
* the value of the neighbour #k of the central element C
,
* in terms of {@link #neighbourOffset(int) neighbourOffset(int)} method;
* - all other bits of the elements if the resulting matrix will be zero.
*
*
* In particular, in {@link BasicSkeletonPixelClassifier2D} implementation,
* the lower 9 bits in the elements of the returned matrix correspond to elements of 3x3 aperture
* of the source skeleton according the following diagram:
*
* 1 2 3
* 8 0 4
* 7 6 5
* (the x-axis is directed rightward, the y-axis is directed downward).
*
* The implementation of {@link #asPixelTypes asPixelTypes} method in this class is based on
* this method and {@link #pixelTypeOrAttachingBranch(int)} and {@link #pixelTypeOrAttachedNode(int)} methods:
* the results w, returned by this method for unit central elements C
* of the source skeleton, are shifted to the right and passed as
* apertureBits
=w>>>1
argument to
* {@link #pixelTypeOrAttachingBranch(int)} or {@link #pixelTypeOrAttachedNode(int)} to form the elements
* of the resulting matrix.
*
*
Note, that the situation, when the neighbouring elements are out of ranges of the matrix coordinates,
* is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonPixelClassifier comments to SkeletonPixelClassifier}.
*
* @param skeleton the skeleton matrix that should be processed.
* @return the matrix of integer values with the same sizes, containing the bit maps
* of the neighbourhoods of all skeleton pixels.
* @throws NullPointerException if skeleton
is {@code null}.
* @throws IllegalArgumentException if skeleton.dimCount()!={@link #dimCount()}
.
*/
public final Matrix extends PIntegerArray> asNeighbourhoodBitMaps(
Matrix extends BitArray> skeleton)
{
Objects.requireNonNull(skeleton, "Null skeleton");
if (skeleton.dimCount() != dimCount) {
throw new IllegalArgumentException("This object (" + this + ") can process "
+ dimCount + "-dimensional matrices only");
}
List> shifted = new ArrayList>();
shifted.add(skeleton);
long[] shift = new long[dimCount];
for (long[] apertureOffset : this.neighbourOffsets) {
for (int k = 0; k < dimCount; k++) {
shift[k] = -apertureOffset[k];
}
shifted.add(Matrices.asShifted(skeleton, shift).cast(PArray.class));
}
double[] weights = new double[shifted.size()];
assert weights.length <= 31;
for (int k = 0; k < weights.length; k++) {
weights[k] = 1L << k;
}
Func packingShiftedBits = LinearFunc.getInstance(0.0, weights);
return Matrices.asFuncMatrix(packingShiftedBits, IntArray.class, shifted);
}
/*Repeat() NEIGHBOUR_INDEX_OF_ATTACHING_BRANCH ==> NEIGHBOUR_INDEX_OF_ATTACHED_NODE;;
OrAttachingBranch ==> OrAttachedNode
*/
/**
* Calculates and returns the value of an element C'
* in the resulting matrix, produced by
* {@link #asPixelTypes asPixelTypes} method with
* {@link SkeletonPixelClassifier.AttachmentInformation#NEIGHBOUR_INDEX_OF_ATTACHING_BRANCH
* NEIGHBOUR_INDEX_OF_ATTACHING_BRANCH} value of attachmentInformation
argument,
* on the base of bit values of all neighbours (in terms of the
* {@link net.algart.matrices.scanning.ConnectivityType#STRAIGHT_AND_DIAGONAL
* straight-and-diagonal connectivity kind})
* of the corresponding unit element C
in the source skeleton bit matrix.
*
* More precisely, the bit values of the neighbours of this skeleton element C
* are passed via the low
* m={@link #numberOfNeighbours() numberOfNeighbours()} bits of apertureBits
argument.
* The bit #k of this argument, 0≤k<m (its value is
* (apertureBits>>>
k)&1
), is equal to the value
* of the neighbour #k in terms of {@link #neighbourOffset(int) neighbourOffset(int)} method.
* In particular, in {@link BasicSkeletonPixelClassifier2D} implementation,
* the order of neighbours is described by the following diagram:
*
* 0 1 2
* 7 C 3
* 6 5 4
* So, 8 low bits of apertureBits
contain the values of the corresponding neighbouring elements
* in anticlockwise order (the x-axis is directed rightward, the y-axis is directed downward).
*
*
It is supposed that the central element (C
) of the skeleton is unit
* (for zero elements ot the skeleton matrix, {@link #asPixelTypes asPixelTypes} method of
* this class returns {@link #TYPE_ZERO} without calling this method).
*
*
Note, that the situation, when the neighbouring elements are out of ranges of the matrix coordinates,
* is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonPixelClassifier comments to SkeletonPixelClassifier}.
*
* @param apertureBits the values of all 8 neighbours of the current unit element of the source skeleton
* bit matrix.
* @return the type of this pixel of the skeleton.
*/
protected abstract int pixelTypeOrAttachingBranch(int apertureBits);
/*Repeat.AutoGeneratedStart !! Auto-generated: NOT EDIT !! */
/**
* Calculates and returns the value of an element C'
* in the resulting matrix, produced by
* {@link #asPixelTypes asPixelTypes} method with
* {@link SkeletonPixelClassifier.AttachmentInformation#NEIGHBOUR_INDEX_OF_ATTACHED_NODE
* NEIGHBOUR_INDEX_OF_ATTACHED_NODE} value of attachmentInformation
argument,
* on the base of bit values of all neighbours (in terms of the
* {@link net.algart.matrices.scanning.ConnectivityType#STRAIGHT_AND_DIAGONAL
* straight-and-diagonal connectivity kind})
* of the corresponding unit element C
in the source skeleton bit matrix.
*
*
More precisely, the bit values of the neighbours of this skeleton element C
* are passed via the low
* m={@link #numberOfNeighbours() numberOfNeighbours()} bits of apertureBits
argument.
* The bit #k of this argument, 0≤k<m (its value is
* (apertureBits>>>
k)&1
), is equal to the value
* of the neighbour #k in terms of {@link #neighbourOffset(int) neighbourOffset(int)} method.
* In particular, in {@link BasicSkeletonPixelClassifier2D} implementation,
* the order of neighbours is described by the following diagram:
*
* 0 1 2
* 7 C 3
* 6 5 4
* So, 8 low bits of apertureBits
contain the values of the corresponding neighbouring elements
* in anticlockwise order (the x-axis is directed rightward, the y-axis is directed downward).
*
*
It is supposed that the central element (C
) of the skeleton is unit
* (for zero elements ot the skeleton matrix, {@link #asPixelTypes asPixelTypes} method of
* this class returns {@link #TYPE_ZERO} without calling this method).
*
*
Note, that the situation, when the neighbouring elements are out of ranges of the matrix coordinates,
* is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonPixelClassifier comments to SkeletonPixelClassifier}.
*
* @param apertureBits the values of all 8 neighbours of the current unit element of the source skeleton
* bit matrix.
* @return the type of this pixel of the skeleton.
*/
protected abstract int pixelTypeOrAttachedNode(int apertureBits);
/*Repeat.AutoGeneratedEnd*/
}