
net.algart.matrices.skeletons.SkeletonScanner 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 java.util.Objects;
import static net.algart.matrices.skeletons.SkeletonPixelClassifier.*;
/**
* Scanner of skeletons (bit matrices, generated by skeletonization algorithms), allowing
* to scan a skeleton, find and traverse all its structural elements like nodes and branches.
* This class works on the base of results of classifying skeleton pixels, performed by
* {@link SkeletonPixelClassifier} class, and requires an object of that class for creating an instance.
*
* The nonoriented graph, formed by the skeleton
*
* The base goal of this class is building and scanning the skeleton nonoriented graph,
* formed by nodes and branches of the given skeleton (bit matrix).
* Below is the formal definition of this graph for some n-dimensional skeleton bit matrix
* (or, briefly, skeleton) and some skeleton pixel classifier: an instance of
* {@link SkeletonPixelClassifier} class, used by this class
* (it is returned by {@link #pixelClassifier()} method).
*
* Let's define a pixel with integer coordinates (x, y, z,...)
* as a set of points of the n-dimensional space with such coordinates
* (x', y', z',...) that
* x−0.5≤x'≤x+0.5,
* y−0.5≤y'≤y+0.5,
* z−0.5≤z'≤z+0.5, ...
* In other words, pixel is a hypercube with the center (x, y, z,...)
* and the edge 1.0 (in 2-dimensional case, it is a square 1x1).
* Every unit element of the skeleton matrix with {@link Matrix#index(long...) coordinates}
* (x, y, z,...) corresponds to a pixel (x, y, z,...)
* in the n-dimensional Euclidean space.
*
* The skeleton nonoriented graph is a set of n-dimensional points-nodes,
* connected by polylines-edges.
* Namely:
*
*
* - The nodes of the graph are geometrical centers of all pixels of the skeleton, classified as
* {@link SkeletonPixelClassifier#TYPE_USUAL_NODE nodes},
* {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch ends} and
* {@link SkeletonPixelClassifier#TYPE_ISOLATED isolated pixels} by the
* current skeleton pixel classifier.
* Such types of nodes have correspondingly ≥3, 1 and 0 incident edges.
*
*
* Warning: nodes of the graph and
* {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node pixel type} in the skeleton matrix are different
* concepts! Nodes of the graph are points of the n-dimensional Euclidean space, namely centers of pixels
* (centers of 1x1 squares in 2-dimensional case), while node pixel type describes matrix elements,
* corresponding to pixels in the space (1x1 squares in 2-dimensional case).
* And nodes of the graphs are formed not only by node pixel type, but also by
* {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch ends} and
* {@link SkeletonPixelClassifier#TYPE_ISOLATED isolated pixels}.
*
*
* - The edges of the graph are polylines
* E0E1...Em, m≥1,
* connecting some pairs of graph nodes E0 and Em.
* Every such edge consists of a sequences of segments Ei Ei+1
* with length 1, √2, ... or √n, connecting geometrical centers of 2 pixels
* Ei and Ei+1,
* corresponding to 2 neighbouring unit matrix elements
* (in straight-and-diagonal connectivity terms, see below).
* Both ends E0 and Em of each edge are always nodes of the graph.
*
*
* The sequence of skeleton pixels, corresponding to points
* E1, E2, .., .Em−1,
* is also called a skeleton branch. In a case m=1, we also say that
* the nodes of the graph E0 and Em are connected
* with a degenerated 0-pixel skeleton branch (or, briefly, a degenerated branch).
*
*
* - If an edge E0E1...Em
* consist of more than 1 segment (m≥2), then all points
* E1, E2, ..., Em−1
* are geometrical centers of the pixels of the skeleton, classified as
* {@link SkeletonPixelClassifier#TYPE_USUAL_BRANCH usual branch pixels} or
* {@link SkeletonPixelClassifier#isAttachableBranchEndPixelType(int) attachable branch ends}
* by the current skeleton pixel classifier.
* Only the first and the last among points E1 and Em−1
* can correspond to attachable branch ends (but also can be centers of usual branch pixels);
* all other points Ek, 2≤k≤m−2,
* always correspond to usual branch pixels.
*
*
* Every skeleton pixel, classified as a usual branch pixel or an attachable branch end
* by the current skeleton pixel classifier,
* is always an element of some skeleton branch, i.e. its center is a point
* Ek with 1≤k≤m−1 for some graph edge.
* There is the only exception from this rule, described below in the paragraph 7.
*
*
* - If m≥3 and the point E1 corresponds to a pixel, classified as
* an {@link SkeletonPixelClassifier#isAttachableBranchEndPixelType(int) attachable branch end},
* then E0 pixel is detected as the attached branch node A and
* E2 pixel is detected as an element of attaching branch B
* for this branch end E1 by the current skeleton pixel classifier
* — see {@link SkeletonPixelClassifier comments to SkeletonPixelClassifier},
* section "Pixel types", group 5. In other words, the main classifying method
* {@link SkeletonPixelClassifier#asPixelTypes(Matrix, SkeletonPixelClassifier.AttachmentInformation)
* SkeletonPixelClassifier.asPixelTypes}
* returns for pixel E1 an index of its neighbour E0,
* when it is called in the mode
* {@link SkeletonPixelClassifier.AttachmentInformation#NEIGHBOUR_INDEX_OF_ATTACHED_NODE
* NEIGHBOUR_INDEX_OF_ATTACHED_NODE},
* or an index of its neighbour E2,
* when it is called in the mode
* {@link SkeletonPixelClassifier.AttachmentInformation#NEIGHBOUR_INDEX_OF_ATTACHING_BRANCH
* NEIGHBOUR_INDEX_OF_ATTACHING_BRANCH}.
*
*
* Analogously, if m≥3 and the point Em−1
* corresponds to a pixel, classified as
* an {@link SkeletonPixelClassifier#isAttachableBranchEndPixelType(int) attachable branch end},
* then Em pixel is detected as the attached branch node A and
* Em−2 pixel is detected as an element of attaching branch B
* for this branch end Em−1 by the current skeleton pixel classifier.
*
*
* - If m=2 and the only "internal" point E1 corresponds to a pixel, classified as
* an {@link SkeletonPixelClassifier#isAttachableBranchEndPixelType(int) attachable branch end},
* then, for this branch end,
* either E0 pixel is detected as the attached branch node A and
* E2 pixel is detected as an element of attaching branch B,
* or E2 pixel is detected as the attached branch node A and
* E0 pixel is detected as an element of attaching branch B
* by the current skeleton pixel classifier
* — see {@link SkeletonPixelClassifier comments to SkeletonPixelClassifier},
* section "Pixel types", group 5.
*
*
* - In addition to the situations 4 and 5, this graph contains edges without "internal" points (m=1),
* corresponding to degenerated 0-pixel branches.
* Such edges connect:
*
* - any pairs of neighbouring graph nodes, where both nodes are the centers of pixels, classified as
* {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch ends}
* (it is a very simple case of a short 2-pixel branch);
* - any pairs of neighbouring graph nodes, where one node is the center of a pixel, classified as
* a {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node}, and another is the center of a pixel, classified as
* a {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end};
* - any pairs of neighbouring graph nodes, where both nodes are the centers of pixels, classified as
* {@link SkeletonPixelClassifier#TYPE_USUAL_NODE nodes},
* when the connecting them with a degenerated branch is not prohibited by
* {@link SkeletonPixelClassifier#markNeighbouringNodesNotConnectedViaDegeneratedBranches}
* method (and, so, when each from these 2 nodes appears in the list of neighbour indexes,
* returned by {@link #adjacentBranches()} method for the second node).
*
*
*
*
* - The skeleton can also contain specific kind of branches, called cyclic branches,
* consisting of {@link SkeletonPixelClassifier#TYPE_USUAL_BRANCH usual branch pixels} only and containing no
* nodes; such branches do not correspond to any elements of the graph and are recognized separately
* by this class.
*
*
* - We can conclude from statements 1, 2 and 3, that each connected component of the skeleton,
* excepting cyclic branches, corresponds to a connected component of the graph, and vice versa.
*
*
* All said above is correct only if the processed skeleton matrix does not contain
* "{@link SkeletonPixelClassifier#TYPE_ILLEGAL illegal}" unit elements. If it contains them,
* the skeleton nonoriented graph can be partially incorrect.
*
* It is easy to note, that this graph does not contain nodes with 2 incident edges and
* has no self-loops (though we could interpret the case 7 in this manner).
*
* This class contains methods, allowing to scan this graph, i.e. to find all branches, incident with
* some node of the graph, i.e.
* {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node} or
* {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end} pixel type,
* and to scan any branch from one its end (node of the graph) to another.
* Thus, you can use this class to vectorize a skeleton by transforming this graph into some
* geometrical vector model, consisting of points (nodes) and curves (lines connecting nodes).
* The main methods, which you need to use for full scanning the graph, are the following:
*
*
* - {@link #nextNodeOrBranch()} — you should call this method to start scanning
* the next part of the skeleton;
* - {@link #firstStep(int, boolean)} — starts scanning a branch from
* a {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node} (but not
* a {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end});
* - {@link #firstStepFromBranch(boolean)} — starts scanning a branch from a branch pixel
* (including {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end});
* - {@link #nextStep()} — continues scanning the current branch.
*
*
* See comments to {@link #nextNodeOrBranch()} about possible strategies of scanning, such as recursive
* depth-first search. See also comments to {@link #scanBranch(int, boolean, boolean)} and
* {@link #scanBranchFromBranch(boolean, boolean)} methods to understand, how to fully scan every branch.
* And see below a full example of breadth-first traversal algorithm.
*
* Skeleton matrix, pixel classifier, current position
*
* This class always processes some fixed bit matrix, which is called the scanned skeleton
* and can be retrieved by {@link #skeleton()} method, and uses some fixed
* {@link SkeletonPixelClassifier skeleton pixel classifier}, which has the same number of dimensions
* and can be retrieved by {@link #pixelClassifier()} method.
* Inside the scanned skeleton, this class always supports some current position:
* it is just some set of coordinates, which can be set by {@link #goTo(long...)} or
* retrieved by {@link #currentCoordinates()}.
* After creation or calling {@link #reset()} method, this object is considered to be not
* {@link #isInitialized() positioned}, that is the current position is considered to be not set yet and
* most methods throw IllegalStateException
.
*
* Remembering and lightweight skeleton scanners
*
* In addition to storing the current position, this class may support storing information about
* visiting some pixels (probably in a form of an internal bit matrix, allocated while instantiation of this object).
* Instances of this class, that support storing this information,
* are called remembering skeleton scanners;
* instances, that do not support this, are called lightweight skeleton scanners.
*
* Lightweight skeleton scanners do not occupy a lot of memory and can be created quickly,
* and their functionality is enough for scanning a concrete skeleton branch or for classifying
* all skeleton pixels by their types
* ({@link #asPixelTypes(SkeletonPixelClassifier.AttachmentInformation)} method).
* But they do not allow to scan the nonoriented graph, formed by the skeleton, without additional
* efforts for storing visited nodes and branches in some external data structures.
*
* Remembering skeleton scanners is what you should use in most cases.
* They allow to correctly build a nonoriented graph from nodes and branches, described above,
* while a single pass through the skeleton, for example, by a simple loop or by a breadth-first search,
* as shown in the example of usage below.
* The main feature of a remembering scanner is that it allocates additional memory
* (probably a bit matrix with the same sizes as the scanned skeleton), where it can store the fact of visiting
* some pixels by {@link #visit()} and {@link #visitPreviousBranchPixel()} methods,
* and this information is used by some other methods (see the following list of differences in behaviour
* of the methods).
*
* You can find out, is a skeleton scanner lightweight or remembering, by {@link #isRemembering()} method.
* Besides this, the following methods work differently in lightweight and remembering skeleton scanners:
*
*
* - in lightweight scanners, {@link #visit()} and {@link #visitPreviousBranchPixel()} do nothing;
* in remembering scanners, these methods mark the corresponding pixels as "visited" and {@link #reset()} method
* clears this information (makes all pixels "unvisited");
* - in lightweight scanners, {@link #pixelVisitRemembered()} and {@link #neighbourVisitRemembered(int)}
* always return
false
;
* in remembering scanners, these methods return true
if
* the corresponding pixels are marked as "visited" by previous calls of
* {@link #visit()} or {@link #visitPreviousBranchPixel()};
* - in lightweight scanners,
onlyToUnvisited
argument of
* {@link #firstStep(int, boolean)}, {@link #firstStepFromBranch(boolean)},
* {@link #scanBranch(int, boolean, boolean)}, {@link #scanBranchFromBranch(boolean, boolean)} methods,
* as well as withVisiting
argument of
* {@link #scanBranch(int, boolean, boolean)}, {@link #scanBranchFromBranch(boolean, boolean)} methods
* do not matter (as if they would be false
);
* in remembering scanners, these arguments affect the behaviour (see comments to those methods).
*
*
* Note, that in remembering scanners the only ways to mark some pixels as "visited" are explicit direct calls
* of the corresponding methods {@link #visit()} and {@link #visitPreviousBranchPixel()}.
* You should call these methods manually: this class does not try to mark pixels as "visited" indirectly,
* inside other methods, scanning the skeleton.
* The only exception from this is {@link #scanBranch scanBranch} / {@link #scanBranchFromBranch scanBranchFromBranch}
* methods, which calls {@link #visitPreviousBranchPixel()} when their withVisiting
argument
* is true
.
*
* Connectivity model (straight-and-diagonal) and "neighbour" term
*
* Note, that this class, as well as {@link SkeletonPixelClassifier}, supposes
* the straight-and-diagonal connectivity kind: see
* {@link net.algart.matrices.scanning.ConnectivityType#STRAIGHT_AND_DIAGONAL}. It means, that all skeletons
* are supposed to be connected in terms of this connectivity: every connected component of the skeleton matrix
* is a "carcass" or "skeleton" of some connected component of the original matrix, for which this skeleton
* was built.
*
* So, the term "neighbour" of some pixel (matrix element) in this class
* and in {@link SkeletonScanner} always means another pixel (matrix element), so that
*
* max (|ik−jk|)=1
*
* where i0, i1, ..., in-1
* are coordinates of the first pixel and
* j0, j1, ..., jn-1
* are coordinates of the second pixel (a neighbour of the first one). In 2-dimensional case,
* such connectivity kind is also called 8-connectivity.
*
* Example of usage
*
* Below is a complete example of code, using a remembering skeleton scanner ss
for breadth-first
* traversal of the non-oriented graph, formed by the skeleton.
*
*
* while (ss.{@link #nextNodeOrBranch()}) {
* ss.{@link #checkInterruption()};
* ss.{@link #updateProgress()};
* long saveBaseIndex = ss.{@link #currentIndexInArray()};
* if (ss.{@link #isUsualBranch()}) { // should check cyclic loops here
* ss.{@link #scanBranchFromBranch scanBranchFromBranch}(true, false);
* boolean cyclicBranch = ss.isUsualBranch(); // in particular, not TYPE_ILLEGAL
* if (cyclicBranch) {
* assert ss.currentIndexInArray() == saveBaseIndex; // should return to the same position
* if (ss.{@link #firstStepFromBranch
* firstStepFromBranch}(true)) { // can be false if this pixel or its neighbour was visited
* do {
* // some processing the cyclic branch segment
* // from ss.{@link #previousCoordinates()} to ss.{@link #currentCoordinates()}...
* ss.{@link #visitPreviousBranchPixel()};
* } while (ss.{@link #nextStep()});
* }
* continue;
* } // else we shall now start from the nearest node, but after all return to saveBaseIndex
* }
*
* if (ss.{@link #isNodeOrFreeBranchEnd()} // necessary check: it is also possible an attached or illegal pixel
* && !ss.{@link #pixelVisitRemembered()}) // for correct processing degenerated 0-pixel branches
* {
* Queue<Long> queue = new LinkedList<Long>();
* ss.{@link #visit()};
* queue.add(ss.{@link #currentIndexInArray()});
* while (!queue.isEmpty()) {
* long saveIndex = queue.poll();
* ss.{@link #goToIndexInArray(long) goToIndexInArray}(saveIndex);
* if (ss.{@link #isIllegal()}) {
* continue; // why not?
* }
* assert ss.isNodeOrFreeBranchEnd() : ss;
* int[] adjacentBranches = ss.{@link #isNode()} ? ss.{@link #adjacentBranches()} : new int[]{-1};
* for (int nb : adjacentBranches) {
* ss.{@link #goToIndexInArray(long) goToIndexInArray}(saveIndex);
* int dnb = nb == -1 ? ss.{@link #firstStepFromBranchNeighbourIndex
* firstStepFromBranchNeighbourIndex}(false) : nb;
* boolean degeneratedBranch = ss.{@link #isNeighbourNodeOrFreeBranchEnd
* isNeighbourNodeOrFreeBranchEnd}(dnb);
* // Special case: we cannot mark 0-pixel degenerated branches between 2 neighbouring nodes
* // as "visited", because they contain no internal pixels, and we can store visiting
* // information in the remembering scanner for pixels only.
* // However, we still need to process degenerated branches, and only 1 time for each branch.
* // Let's scan such a branch if and only if the neighbour:
* // 1) is not visited yet or
* // 2) is inside the queue.
* // Then every degenerated branch PQ will be processed strictly 1 time.
* // Proof.
* // Note that pixels are visited at the moment when they are added to the queue.
* // Let's suppose that P appears the first in this loop (as the pixel at "saveIndex").
* // At this moment, Q is either not visited, or added to the queue, but not removed
* // from it yet (because P appears here before it). So, we'll really process PQ branch.
* // On the other hand, when Q will appear in this loop (as the pixel at "saveIndex"),
* // P will be already removed from the queue, so we'll not process PQ branch twice.
* // This completes the proof.
* if (degeneratedBranch && (
* !ss.{@link #neighbourVisitRemembered(int)
* neighbourVisitRemembered}(dnb) || queue.contains(ss.{@link #neighbourIndexInArray(int)
* neighbourIndexInArray}(dnb))))
* {
* // some processing the degenerated branch
* // from ss.currentCoordinates() to ss.neighbourCoordinates(nb)...
* }
* if (!(nb == -1 ? ss.{@link #firstStepFromBranch
* firstStepFromBranch}(true) : ss.{@link #firstStep firstStep}(nb, true))) {
* continue;
* }
* if (!degeneratedBranch) {
* do {
* // some processing the branch segment
* // from ss.previousCoordinates() to ss.currentCoordinates()...
* ss.{@link #visitPreviousBranchPixel()};
* } while (ss.{@link #nextStep()});
* }
* if (!ss.{@link #pixelVisitRemembered()}) {
* ss.{@link #visit()};
* queue.add(ss.{@link #currentIndexInArray()});
* }
* }
* }
* }
* ss.{@link #goToIndexInArray goToIndexInArray}(saveBaseIndex);
* }
*
*
* Of course, you can use another order of graph traversal, for example, depth-first.
* Note that some applications do not need depth-first or breadth-first order at all:
* it can be enough to find and detect nodes and edges in any order.
* In this case, you can just remove all operations with the queue.
*
* Also note: you can process degenerated 0-pixel branches in a common way, just by assigning
* degeneratedBranch=false
instead of calling {@link #isNeighbourNodeOrFreeBranchEnd
* isNeighbourNodeOrFreeBranchEnd} method in the example above.
* In this case, breadth-first traversal algorithm will form slightly incorrect graph, namely, some degenerated
* branches will not be processed, and it can lead to appearing graph nodes with 2 incident edges
* (with low probability). However, the resulting graph will still be good enough, with connected components
* corresponding to connected components of the skeleton, maybe even better than the full correct graph
* with all degenerated branches in a role of edges.
*
* Creating instances of this class
*
* This class is designed for a case of any number of dimensions, though, of course, the most popular
* case is 2-dimensional. You can create an instance of this class by the following basic methods:
*
*
* - {@link #getRememberingInstance(ArrayContext, Matrix, SkeletonPixelClassifier)},
* - {@link #getLightweightInstance(ArrayContext, Matrix, SkeletonPixelClassifier)}
*
*
* and also by the following methods, designed for using 2-dimensional pixel classifier
* {@link BasicSkeletonPixelClassifier2D}:
*
*
* - {@link #getRememberingOctupleThinningInstance2D(ArrayContext, Matrix)},
* - {@link #getLightweightOctupleThinningInstance2D(ArrayContext, Matrix)},
* - {@link #getRememberingQuadruple3x5ThinningInstance2D(ArrayContext, Matrix)},
* - {@link #getLightweightQuadruple3x5ThinningInstance2D(ArrayContext, Matrix)},
* - {@link #getRememberingStrongQuadruple3x5ThinningInstance2D(ArrayContext, Matrix)},
* - {@link #getLightweightStrongQuadruple3x5ThinningInstance2D(ArrayContext, Matrix)}.
*
*
* Pseudo-cyclic continuation
*
* This class supposes that the processed matrix is infinitely pseudo-cyclically continued, as well
* {@link net.algart.arrays.Matrices#asShifted Matrices.asShifted} method supposes it.
* You can change this behavior by appending the source matrix with zero elements
* by calling {@link Matrix#subMatrix(long[], long[], Matrix.ContinuationMode)} method,
* where the dimensions of the "submatrix" are greater than dimensions of the source one by 1
* and the continuationMode
argument is {@link net.algart.arrays.Matrix.ContinuationMode#ZERO_CONSTANT}.
*
* Multithreading compatibility
*
* This class is not thread-safe, but is thread-compatible
* and can be synchronized manually, if multithreading access is necessary.
*
* @author Daniel Alievsky
* @see SkeletonPixelClassifier
*/
public final class SkeletonScanner implements ArrayProcessor {
/**
* Current execution context. It is returned by {@link #context()} method. Can be {@code null}.
*/
final ArrayContext context;
/**
* The memory model used by this instance for all operations.
* Equal to {@link #context}.{@link ArrayContext#getMemoryModel() getMemoryModel()} if
* {@link #context}!=null
, in other case equal to
* {@link SimpleMemoryModel#getInstance()}.
*/
final MemoryModel memoryModel;
/**
* Reference to the skeleton
matrix passed to the constructor.
*/
final Matrix extends BitArray> skeleton;
/**
* Reference to the pixelClassifier
passed to the constructor.
*/
final SkeletonPixelClassifier pixelClassifier;
private final int dimCount;
private final int numberOfNeighbours;
private final BitArray skeletonArray;
private final UpdatableBitArray visitedArray;
private final Matrix extends PIntegerArray> pixelTypesOrAttachingBranches;
private final Matrix extends PIntegerArray> pixelTypesOrAttachedNodes;
private final PIntegerArray pixelTypesOrAttachingBranchesArray;
private final PIntegerArray pixelTypesOrAttachedNodesArray;
private final long arrayLength;
private final long[] neighbourOffsetsInArray;
private long currentIndexInArray = -1;
private int previousBranchStepDirection = -1;
private long startIndexInArray = -1;
SkeletonScanner(
ArrayContext context,
Matrix extends BitArray> skeleton,
SkeletonPixelClassifier pixelClassifier, boolean rememberVisitedPixels) {
Objects.requireNonNull(skeleton, "Null skeleton matrix");
Objects.requireNonNull(pixelClassifier, "Null pixel classifier");
if (pixelClassifier.dimCount() != skeleton.dimCount()) {
throw new IllegalArgumentException("pixelClassifier has " + pixelClassifier.dimCount()
+ " dimensions, but the skeleton matrix has " + skeleton.dimCount() + " dimensions");
}
this.context = context;
this.memoryModel = context == null ? SimpleMemoryModel.getInstance() : context.getMemoryModel();
this.skeleton = skeleton;
this.skeletonArray = this.skeleton.array();
this.pixelClassifier = pixelClassifier;
this.dimCount = pixelClassifier.dimCount;
this.numberOfNeighbours = pixelClassifier.numberOfNeighbours;
this.neighbourOffsetsInArray = new long[this.numberOfNeighbours];
long[] no = new long[this.dimCount];
for (int k = 0; k < this.numberOfNeighbours; k++) {
pixelClassifier.neighbourOffset(no, k);
this.neighbourOffsetsInArray[k] = skeleton.pseudoCyclicIndex(no);
}
this.arrayLength = this.skeletonArray.length();
this.pixelTypesOrAttachingBranches = pixelClassifier.asPixelTypes(this.skeleton,
SkeletonPixelClassifier.AttachmentInformation.NEIGHBOUR_INDEX_OF_ATTACHING_BRANCH);
this.pixelTypesOrAttachingBranchesArray = this.pixelTypesOrAttachingBranches.array();
this.pixelTypesOrAttachedNodes = pixelClassifier.asPixelTypes(this.skeleton,
SkeletonPixelClassifier.AttachmentInformation.NEIGHBOUR_INDEX_OF_ATTACHED_NODE);
this.pixelTypesOrAttachedNodesArray = this.pixelTypesOrAttachedNodes.array();
if (rememberVisitedPixels) {
Matrix visitedMatrix = ErodingSkeleton.mm(this.memoryModel, skeleton, 1).
newBitMatrix(skeleton.dimensions());
if (!SimpleMemoryModel.isSimpleArray(visitedMatrix.array())) {
// anti-optimization for a case of little matrices, but can be necessary for huge matrices
visitedMatrix = visitedMatrix.structureLike(skeleton);
}
this.visitedArray = visitedMatrix.array();
} else {
this.visitedArray = null;
}
}
/*Repeat() remembering(\s+instance) ==> lightweight$1;;
Remembering ==> Lightweight;;
(pixelClassifier\,\s+)true(\)\;) ==> $1false$2 */
/**
* Creates new remembering skeleton scanner, which will process the given skeleton
matrix
* on the base of the given pixel classifier.
* See the {@link SkeletonScanner comments to this class} about remembering and lightweight skeleton scanners.
*
* The passed matrix is supposed to be a final result of some skeletonization algorithm, for example,
* provided by this package. If it is not so, the passed pixel classifier scanner will probably consider many
* unit pixels of this matrix as "{@link SkeletonPixelClassifier#TYPE_ILLEGAL illegal}", so
* the branches and nodes found by this class will not form a correct skeletal graph.
* The same situation is also possible if the passed pixel classifier does not match the algorithm,
* used for skeletonization.
*
* @param context the {@link #context() context} that will be used by this object;
* can be {@code null}, then it will be ignored.
* @param skeleton the {@link #skeleton() skeleton}: a bit matrix that should be processed by this scanner.
* @param pixelClassifier the {@link #pixelClassifier() pixel classifier}, which will be used by this scanner
* for detecting types of all pixels.
* @return new instance of this class.
* @throws NullPointerException if matrix
or pixelClassifier
* argument is {@code null}.
* @throws IllegalArgumentException if skeleton.{@link Matrix#dimCount()
* dimCount()}!=pixelClassifier.{@link SkeletonPixelClassifier#dimCount()
* dimCount()}
.
*/
public static SkeletonScanner getRememberingInstance(
ArrayContext context,
Matrix extends BitArray> skeleton,
SkeletonPixelClassifier pixelClassifier) {
return new SkeletonScanner(context, skeleton, pixelClassifier, true);
}
/**
* Creates new remembering skeleton scanner, which will process the given skeleton
matrix,
* supposed to be the final result of skeletonization by {@link OctupleThinningSkeleton2D} algorithm.
* Equivalent to
* {@link #getRememberingInstance getRememberingInstance}(context, skeleton,
* {@link BasicSkeletonPixelClassifier2D#getOctupleThinningInstance()})
.
*
* @param context the {@link #context() context} that will be used by this object;
* can be {@code null}, then it will be ignored.
* @param skeleton the {@link #skeleton() skeleton}: a bit matrix that should be processed by this scanner.
* @return new instance of this class.
* @throws NullPointerException if matrix
argument is {@code null}.
* @throws IllegalArgumentException if skeleton.{@link Matrix#dimCount() dimCount()}!=2
.
*/
public static SkeletonScanner getRememberingOctupleThinningInstance2D(
ArrayContext context,
Matrix extends BitArray> skeleton) {
return getRememberingInstance(context, skeleton,
BasicSkeletonPixelClassifier2D.getOctupleThinningInstance());
}
/**
* Creates new remembering skeleton scanner, which will process the given skeleton
matrix,
* supposed to be the final result of skeletonization by {@link Quadruple3x5ThinningSkeleton2D} algorithm.
* Equivalent to
* {@link #getRememberingInstance getRememberingInstance}(context, skeleton,
* {@link BasicSkeletonPixelClassifier2D#getQuadruple3x5ThinningInstance()})
.
*
* @param context the {@link #context() context} that will be used by this object;
* can be {@code null}, then it will be ignored.
* @param skeleton the {@link #skeleton() skeleton}: a bit matrix that should be processed by this scanner.
* @return new instance of this class.
* @throws NullPointerException if matrix
argument is {@code null}.
* @throws IllegalArgumentException if skeleton.{@link Matrix#dimCount() dimCount()}!=2
.
*/
public static SkeletonScanner getRememberingQuadruple3x5ThinningInstance2D(
ArrayContext context,
Matrix extends BitArray> skeleton) {
return getRememberingInstance(context, skeleton,
BasicSkeletonPixelClassifier2D.getQuadruple3x5ThinningInstance());
}
/**
* Creates new remembering skeleton scanner, which will process the given skeleton
matrix,
* supposed to be the final result of skeletonization by {@link StrongQuadruple3x5ThinningSkeleton2D} algorithm.
* Equivalent to
* {@link #getRememberingInstance getRememberingInstance}(context, skeleton,
* {@link BasicSkeletonPixelClassifier2D#getStrongQuadruple3x5ThinningInstance()})
.
*
* @param context the {@link #context() context} that will be used by this object;
* can be {@code null}, then it will be ignored.
* @param skeleton the {@link #skeleton() skeleton}: a bit matrix that should be processed by this scanner.
* @return new instance of this class.
* @throws NullPointerException if matrix
argument is {@code null}.
* @throws IllegalArgumentException if skeleton.{@link Matrix#dimCount() dimCount()}!=2
.
*/
public static SkeletonScanner getRememberingStrongQuadruple3x5ThinningInstance2D(
ArrayContext context,
Matrix extends BitArray> skeleton) {
return getRememberingInstance(context, skeleton,
BasicSkeletonPixelClassifier2D.getStrongQuadruple3x5ThinningInstance());
}
/*Repeat.AutoGeneratedStart !! Auto-generated: NOT EDIT !! */
/**
* Creates new remembering skeleton scanner, which will process the given skeleton
matrix
* on the base of the given pixel classifier.
* See the {@link SkeletonScanner comments to this class} about remembering and lightweight skeleton scanners.
*
*
The passed matrix is supposed to be a final result of some skeletonization algorithm, for example,
* provided by this package. If it is not so, the passed pixel classifier scanner will probably consider many
* unit pixels of this matrix as "{@link SkeletonPixelClassifier#TYPE_ILLEGAL illegal}", so
* the branches and nodes found by this class will not form a correct skeletal graph.
* The same situation is also possible if the passed pixel classifier does not match the algorithm,
* used for skeletonization.
*
* @param context the {@link #context() context} that will be used by this object;
* can be {@code null}, then it will be ignored.
* @param skeleton the {@link #skeleton() skeleton}: a bit matrix that should be processed by this scanner.
* @param pixelClassifier the {@link #pixelClassifier() pixel classifier}, which will be used by this scanner
* for detecting types of all pixels.
* @return new instance of this class.
* @throws NullPointerException if matrix
or pixelClassifier
* argument is {@code null}.
* @throws IllegalArgumentException if skeleton.{@link Matrix#dimCount()
* dimCount()}!=pixelClassifier.{@link SkeletonPixelClassifier#dimCount()
* dimCount()}
.
*/
public static SkeletonScanner getLightweightInstance(
ArrayContext context,
Matrix extends BitArray> skeleton,
SkeletonPixelClassifier pixelClassifier) {
return new SkeletonScanner(context, skeleton, pixelClassifier, false);
}
/**
* Creates new remembering skeleton scanner, which will process the given skeleton
matrix,
* supposed to be the final result of skeletonization by {@link OctupleThinningSkeleton2D} algorithm.
* Equivalent to
* {@link #getLightweightInstance getLightweightInstance}(context, skeleton,
* {@link BasicSkeletonPixelClassifier2D#getOctupleThinningInstance()})
.
*
* @param context the {@link #context() context} that will be used by this object;
* can be {@code null}, then it will be ignored.
* @param skeleton the {@link #skeleton() skeleton}: a bit matrix that should be processed by this scanner.
* @return new instance of this class.
* @throws NullPointerException if matrix
argument is {@code null}.
* @throws IllegalArgumentException if skeleton.{@link Matrix#dimCount() dimCount()}!=2
.
*/
public static SkeletonScanner getLightweightOctupleThinningInstance2D(
ArrayContext context,
Matrix extends BitArray> skeleton) {
return getLightweightInstance(context, skeleton,
BasicSkeletonPixelClassifier2D.getOctupleThinningInstance());
}
/**
* Creates new remembering skeleton scanner, which will process the given skeleton
matrix,
* supposed to be the final result of skeletonization by {@link Quadruple3x5ThinningSkeleton2D} algorithm.
* Equivalent to
* {@link #getLightweightInstance getLightweightInstance}(context, skeleton,
* {@link BasicSkeletonPixelClassifier2D#getQuadruple3x5ThinningInstance()})
.
*
* @param context the {@link #context() context} that will be used by this object;
* can be {@code null}, then it will be ignored.
* @param skeleton the {@link #skeleton() skeleton}: a bit matrix that should be processed by this scanner.
* @return new instance of this class.
* @throws NullPointerException if matrix
argument is {@code null}.
* @throws IllegalArgumentException if skeleton.{@link Matrix#dimCount() dimCount()}!=2
.
*/
public static SkeletonScanner getLightweightQuadruple3x5ThinningInstance2D(
ArrayContext context,
Matrix extends BitArray> skeleton) {
return getLightweightInstance(context, skeleton,
BasicSkeletonPixelClassifier2D.getQuadruple3x5ThinningInstance());
}
/**
* Creates new remembering skeleton scanner, which will process the given skeleton
matrix,
* supposed to be the final result of skeletonization by {@link StrongQuadruple3x5ThinningSkeleton2D} algorithm.
* Equivalent to
* {@link #getLightweightInstance getLightweightInstance}(context, skeleton,
* {@link BasicSkeletonPixelClassifier2D#getStrongQuadruple3x5ThinningInstance()})
.
*
* @param context the {@link #context() context} that will be used by this object;
* can be {@code null}, then it will be ignored.
* @param skeleton the {@link #skeleton() skeleton}: a bit matrix that should be processed by this scanner.
* @return new instance of this class.
* @throws NullPointerException if matrix
argument is {@code null}.
* @throws IllegalArgumentException if skeleton.{@link Matrix#dimCount() dimCount()}!=2
.
*/
public static SkeletonScanner getLightweightStrongQuadruple3x5ThinningInstance2D(
ArrayContext context,
Matrix extends BitArray> skeleton) {
return getLightweightInstance(context, skeleton,
BasicSkeletonPixelClassifier2D.getStrongQuadruple3x5ThinningInstance());
}
/*Repeat.AutoGeneratedEnd*/
/**
* Returns the current context used by this instance for some operations.
* This context is specified while creating an instance of this class.
* It is used, for example, in {@link #scanBranch(int, boolean, boolean)} and
* {@link #scanBranchFromBranch(boolean, boolean)} methods.
*
* @return the current context used by this instance; can be {@code null}.
*/
public ArrayContext context() {
return context;
}
/**
* Creates new instance of this class with the identical behaviour, excepting
* that the returned object is always remembering skeleton scanner (remembering visits of pixels).
* See the {@link SkeletonScanner comments to this class} about remembering and lightweight skeleton scanners.
*
*
The returned scanner is always newly created instance (in particular,
* not {@link #isInitialized() positioned}), even if this instance is remembering.
* So, this method usually leads to allocating necessary amount of memory.
*
* @return the remembering version of this scanner.
*/
public SkeletonScanner getCompatibleRememberingInstance() {
return new SkeletonScanner(context, skeleton, pixelClassifier, true);
}
/**
* Creates new instance of this class with the identical behaviour, excepting
* that the returned object is always lightweight skeleton scanner (not remembering visits of pixels).
* See the {@link SkeletonScanner comments to this class} about remembering and lightweight skeleton scanners.
*
*
The returned scanner is always newly created instance (in particular,
* not {@link #isInitialized() positioned}), even if this instance is lightweight.
*
* @return the lightweight version of this scanner.
*/
public SkeletonScanner getCompatibleLightweightInstance() {
return new SkeletonScanner(context, skeleton, pixelClassifier, false);
}
/**
* Returns a reference to the skeleton, scanned by this object.
* It is usually specified while creating an instance of this object.
*
* @return a reference to the skeleton, scanned by this object.
*/
public Matrix extends BitArray> skeleton() {
return this.skeleton;
}
/**
* Returns a reference to the pixel classifier, used by this object.
* It is usually specified while creating an instance of this object.
*
* @return a reference to the pixel classifier, used by this object.
*/
public SkeletonPixelClassifier pixelClassifier() {
return pixelClassifier;
}
/**
* Equivalent to {@link #skeleton()}.{@link Matrix#dimCount() dimCount()}
.
* Always equal to {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#dimCount() dimCount()}
.
*
* @return the number of dimensions of the matrices, which can be processed by this object.
*/
public int dimCount() {
return dimCount;
}
/**
* Equivalent to {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#numberOfNeighbours()
* numberOfNeighbours()}
.
*
* @return the number of neighbours of each element of a skeleton matrix.
*/
public int numberOfNeighbours() {
return numberOfNeighbours;
}
/**
* Reduced and more efficient version of {@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)} method of the {@link #pixelClassifier() pixel classifier},
* designed for indexing elements of the {@link Matrix#array() built-in AlgART array} of the skeleton matrix.
* This method is equivalent to
* {@link #skeleton() skeleton()}.{@link Matrix#pseudoCyclicIndex(long...)
* pseudoCyclicIndex}({@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset}(neighbourIndex))
,
* but usually works much faster (in particular, does not allocate any arrays).
*
* @param neighbourIndex an index if the neighbour of some central element of the matrix.
* @return non-negative increment of the index in the built-in AlgART array of the skeleton matrix,
* corresponding to the shift from the central element to this neighbour.
*/
public long neighbourOffsetInArray(int neighbourIndex) {
checkNeighbourIndex(neighbourIndex);
return this.neighbourOffsetsInArray[neighbourIndex];
}
/**
* Equivalent to
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#asPixelTypes
* asPixelTypes}({@link #skeleton()}, attachmentInformation)
,
* but probably works faster.
*
* @param attachmentInformation what should this method return for attachable pixels.
* @return the matrix of integer codes with the same sizes as the {@link #skeleton()
* scanned skeleton matrix}, describing the types of all skeleton pixels.
* @throws NullPointerException if attachmentInformation
is {@code null}.
*/
public Matrix extends PIntegerArray> asPixelTypes(
SkeletonPixelClassifier.AttachmentInformation attachmentInformation) {
Objects.requireNonNull(attachmentInformation, "Null attachmentInformation");
switch (attachmentInformation) {
case NEIGHBOUR_INDEX_OF_ATTACHING_BRANCH:
return this.pixelTypesOrAttachingBranches;
case NEIGHBOUR_INDEX_OF_ATTACHED_NODE:
return this.pixelTypesOrAttachedNodes;
default:
throw new AssertionError("Unknown attachmentInformation: " + attachmentInformation);
}
}
/**
* Returns true
if and only if this instance was positioned to some coordinates in the skeleton matrix.
* More precisely, returns false
if this instance was newly created and none from
* {@link #nextNodeOrBranch()}, {@link #nextNodeOrBranchPixelType()},
* {@link #goTo}, {@link #goToIndexInArray} methods were called yet,
* or true
in all other cases.
* If this object is not positioned, most of methods, processing pixels in the current position,
* throw IllegalStateException
.
*
* @return true
if and only if this instance was already positioned by
* {@link #nextNodeOrBranch()} / {@link #nextNodeOrBranchPixelType()}
* or {@link #goTo} / {@link #goToIndexInArray} methods.
*/
public boolean isInitialized() {
return this.currentIndexInArray != -1;
}
/**
* Returns the current coordinates (or throws IllegalStateException
if the scanner
* was not {@link #isInitialized() positioned yet}).
*
*
The returned array is always a newly allocated Java array.
* Its length is always equal to {@link #dimCount() dimCount()}.
* The returned coordinates are always in ranges
*
* 0 ≤ result[k] < {@link #skeleton() skeleton()}.{@link Matrix#dim(int) dim}(k),
*
where result[k]
is the element #k
in the returned array.
*
* @return the current coordinates in the skeleton matrix.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
* @see #goTo(long...)
* @see #currentIndexInArray()
*/
public long[] currentCoordinates() {
checkInitialized();
return skeleton.coordinates(this.currentIndexInArray, null);
}
/**
* Reduced and more efficient version of {@link #currentCoordinates()}, designed for indexing
* elements of the {@link Matrix#array() built-in AlgART array} of the skeleton matrix.
* This method is equivalent to
* {@link #skeleton() skeleton()}.{@link Matrix#index(long...)
* index}({@link #currentCoordinates() currentCoordinates()})
,
* but usually works much faster (in particular, does not allocate any arrays).
* It is very possible that the implementation really stores the current index, returned by this method,
* and does not store an array of current coordinates: so, this method just returns an internal field,
* but {@link #currentCoordinates()} calculates results on the base on this value.
*
* The result of this method is always in range
* 0..{@link #skeleton() skeleton()}.{@link Matrix#size() size()}-1
.
*
* @return the current index in the built-in AlgART array of the skeleton matrix.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
* @see #goToIndexInArray(long)
*/
public long currentIndexInArray() {
checkInitialized();
return this.currentIndexInArray;
}
/**
* Returns the value of the element of the skeleton matrix at
* the {@link #currentCoordinates() current coordinates}.
* Equivalent to {@link #skeleton
* skeleton()()}.{@link Matrix#array() array()}.{@link BitArray#getBit(long)
* getBit}({@link #currentIndexInArray() currentIndexInArray()})
.
*
* @return the value of the current element of the skeleton matrix.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public boolean currentPixelValue() {
checkInitialized();
return skeletonArray.getBit(currentIndexInArray);
}
/**
* Returns the type of the current pixel of the skeleton matrix or, if it is an attachable branch end,
* returns the index of its neighbour, which lies at the branch, to which this pixel should be attached.
* Equivalent to m.{@link Matrix#array() array()}.{@link PIntegerArray#getInt(long)
* getInt}({@link #currentIndexInArray() currentIndexInArray()})
, where
*
* m = {@link #asPixelTypes(SkeletonPixelClassifier.AttachmentInformation)
* asPixelTypes}({@link SkeletonPixelClassifier.AttachmentInformation#NEIGHBOUR_INDEX_OF_ATTACHING_BRANCH})
*
* but probably works faster.
* See comments to {@link SkeletonPixelClassifier#asPixelTypes asPixelTypes} method for more details.
*
* @return the type of the current pixel of the skeleton matrix or (for attachable branch end)
* the direction towards the branch, to which this pixel should be attached.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
* @see #currentPixelTypeOrAttachedNode()
* @see #neighbourTypeOrAttachingBranch(int)
* @see #neighbourTypeOrAttachedNode(int)
*/
public int currentPixelTypeOrAttachingBranch() {
checkInitialized();
return pixelTypesOrAttachingBranchesArray.getInt(currentIndexInArray);
}
/**
* Returns the type of the current pixel of the skeleton matrix or, if it is an attachable branch end,
* returns the index of its neighbour, which is a node, which is one of the ends of the branch.
* Equivalent to m.{@link Matrix#array() array()}.{@link PIntegerArray#getInt(long)
* getInt}({@link #currentIndexInArray() currentIndexInArray()})
, where
*
* m = {@link #asPixelTypes(SkeletonPixelClassifier.AttachmentInformation)
* asPixelTypes}({@link SkeletonPixelClassifier.AttachmentInformation#NEIGHBOUR_INDEX_OF_ATTACHED_NODE})
*
* but probably works faster.
* See comments to {@link SkeletonPixelClassifier#asPixelTypes asPixelTypes} method for more details.
*
* @return the type of the current pixel of the skeleton matrix or (for attachable branch end)
* the direction towards the node, which is one of the branch ends.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
* @see #currentPixelTypeOrAttachingBranch()
* @see #neighbourTypeOrAttachingBranch(int)
* @see #neighbourTypeOrAttachedNode(int)
*/
public int currentPixelTypeOrAttachedNode() {
checkInitialized();
return pixelTypesOrAttachedNodesArray.getInt(currentIndexInArray);
}
/**
* Returns the coordinates of the element of the skeleton matrix, which is a neighbour with the given index
* of the {@link #currentCoordinates() current element}.
* Equivalent to {@link #skeleton()}.{@link Matrix#coordinates(long, long[])
* coordinates}(index, null)
, where
*
* index = ({@link #currentIndexInArray() currentIndexInArray()} + {@link #neighbourOffsetInArray(int)
* neighbourOffsetInArray}(neighbourIndex)) % {@link #skeleton() skeleton()}.{@link Matrix#size() size()}
*
* Note, that we allow a situation when the neighbouring element is out of ranges of the matrix coordinates.
* This situation is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonScanner comments to this class}.
*
* The returned array is always a newly allocated Java array.
* Its length is always equal to {@link #dimCount() dimCount()}.
* The returned coordinates are always in ranges
*
* 0 ≤ result[k] < {@link #skeleton() skeleton()}.{@link Matrix#dim(int) dim}(k),
*
where result[k]
is the element #k
in the returned array.
*
* @param neighbourIndex the index of the neighbour, in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* @return the coordinates of the given neighbour of the current element of the skeleton matrix.
* @throws IndexOutOfBoundsException if neighbourIndex
is out of range
* 0..{@link #numberOfNeighbours() numberOfNeighbours()}-1
.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
* @see #goTo(long...)
* @see #neighbourIndexInArray(int)
*/
public long[] neighbourCoordinates(int neighbourIndex) {
return skeleton.coordinates(neighbourIndexInArray(neighbourIndex), null);
}
/**
* Reduced and more efficient version of {@link #neighbourCoordinates(int)}, designed for indexing
* elements of the {@link Matrix#array() built-in AlgART array} of the skeleton matrix.
* Equivalent to
*
* ({@link #currentIndexInArray() currentIndexInArray()} + {@link #neighbourOffsetInArray(int)
* neighbourOffsetInArray}(neighbourIndex)) % {@link #skeleton() skeleton()}.{@link Matrix#size() size()}
*
* Note, that we allow a situation when the neighbouring element is out of ranges of the matrix coordinates.
* This situation is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonScanner comments to this class}.
*
* The result of this method is always in range
* 0..{@link #skeleton() skeleton()}.{@link Matrix#size() size()}-1
.
*
* @param neighbourIndex the index of the neighbour, in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* @return the index in the built-in AlgART array of the given neighbour of the current element
* of the skeleton matrix.
* @throws IndexOutOfBoundsException if neighbourIndex
is out of range
* 0..{@link #numberOfNeighbours() numberOfNeighbours()}-1
.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
* @see #goToIndexInArray(long)
*/
public long neighbourIndexInArray(int neighbourIndex) {
checkInitialized();
checkNeighbourIndex(neighbourIndex);
long index = currentIndexInArray + neighbourOffsetInArray(neighbourIndex);
if (index >= arrayLength) {
index -= arrayLength;
}
assert index <= arrayLength : index + " > " + arrayLength
+ ", currentIndexInArray=" + currentIndexInArray + ": " + this;
return index;
}
/**
* Returns the value of the element of the skeleton matrix, which is a neighbour with the given index
* of the {@link #currentCoordinates() current element}.
* Equivalent to {@link #skeleton()}.{@link Matrix#array() array()}.{@link BitArray#getBit(long)
* getBit}(index)
, where
*
* index = ({@link #currentIndexInArray() currentIndexInArray()} + {@link #neighbourOffsetInArray(int)
* neighbourOffsetInArray}(neighbourIndex)) % {@link #skeleton() skeleton()}.{@link Matrix#size() size()}
*
* Note, that we allow a situation when the neighbouring element is out of ranges of the matrix coordinates.
* This situation is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonScanner comments to this class}.
*
* @param neighbourIndex the index of the neighbour, in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* @return the value of the given neighbour of the current element of the skeleton matrix.
* @throws IndexOutOfBoundsException if neighbourIndex
is out of range
* 0..{@link #numberOfNeighbours() numberOfNeighbours()}-1
.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public boolean neighbourValue(int neighbourIndex) {
return skeletonArray.getBit(neighbourIndexInArray(neighbourIndex));
}
/**
* Returns the type of the pixel of the skeleton matrix, which is a neighbour with the given index
* of the {@link #currentCoordinates() current element}, or, if it is an attachable branch end,
* returns the index of a neighbour of this neighbour, which lies at the branch,
* to which this neighbour should be attached.
* Equivalent to m.{@link Matrix#array() array()}.{@link PIntegerArray#getInt(long)
* getInt}(index)
, where
*
* m = {@link #asPixelTypes(SkeletonPixelClassifier.AttachmentInformation)
* asPixelTypes}({@link SkeletonPixelClassifier.AttachmentInformation#NEIGHBOUR_INDEX_OF_ATTACHING_BRANCH})
* index = ({@link #currentIndexInArray() currentIndexInArray()} + {@link #neighbourOffsetInArray(int)
* neighbourOffsetInArray}(neighbourIndex)) % {@link #skeleton() skeleton()}.{@link Matrix#size() size()}
*
* Note, that we allow a situation when the neighbouring element is out of ranges of the matrix coordinates.
* This situation is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonScanner comments to this class}.
*
* @param neighbourIndex the index of the neighbour, in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* @return the value of the given neighbour of the current element of the skeleton matrix.
* @throws IndexOutOfBoundsException if neighbourIndex
is out of range
* 0..{@link #numberOfNeighbours() numberOfNeighbours()}-1
.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
* @see #currentPixelTypeOrAttachingBranch()
* @see #currentPixelTypeOrAttachedNode()
* @see #neighbourTypeOrAttachedNode(int)
*/
public int neighbourTypeOrAttachingBranch(int neighbourIndex) {
return pixelTypesOrAttachingBranchesArray.getInt(neighbourIndexInArray(neighbourIndex));
}
/**
* Returns the type of the pixel of the skeleton matrix, which is a neighbour with the given index
* of the {@link #currentCoordinates() current element}, or, if it is an attachable branch end,
* returns the index of a neighbour of this neighbour, which is a node, which is one of the ends of the branch.
* Equivalent to m.{@link Matrix#array() array()}.{@link PIntegerArray#getInt(long)
* getInt}(index)
, where
*
* m = {@link #asPixelTypes(SkeletonPixelClassifier.AttachmentInformation)
* asPixelTypes}({@link SkeletonPixelClassifier.AttachmentInformation#NEIGHBOUR_INDEX_OF_ATTACHED_NODE})
* index = ({@link #currentIndexInArray() currentIndexInArray()} + {@link #neighbourOffsetInArray(int)
* neighbourOffsetInArray}(neighbourIndex)) % {@link #skeleton() skeleton()}.{@link Matrix#size() size()}
*
* Note, that we allow a situation when the neighbouring element is out of ranges of the matrix coordinates.
* This situation is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonScanner comments to this class}.
*
* @param neighbourIndex the index of the neighbour, in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* @return the value of the given neighbour of the current element of the skeleton matrix.
* @throws IndexOutOfBoundsException if neighbourIndex
is out of range
* 0..{@link #numberOfNeighbours() numberOfNeighbours()}-1
.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
* @see #currentPixelTypeOrAttachingBranch()
* @see #currentPixelTypeOrAttachedNode()
* @see #neighbourTypeOrAttachingBranch(int)
*/
public int neighbourTypeOrAttachedNode(int neighbourIndex) {
return pixelTypesOrAttachedNodesArray.getInt(neighbourIndexInArray(neighbourIndex));
}
/**
* Returns true
if the {@link #currentCoordinates() current element} of the skeleton
* is a {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node}, i.e. a unit element
* where 3 or more thin connected 1-pixel branches meet or, as a degenerated case,
* an {@link SkeletonPixelClassifier#TYPE_ISOLATED isolated pixel}:
* a unit element having no unit neighbours.
* Equivalent both to {@link SkeletonPixelClassifier#isNodePixelType(int)
* SkeletonPixelClassifier.isNodePixelType}({@link #currentPixelTypeOrAttachingBranch()
* currentPixelTypeOrAttachingBranch()})
* and to {@link SkeletonPixelClassifier#isNodePixelType(int)
* SkeletonPixelClassifier.isNodePixelType}({@link #currentPixelTypeOrAttachedNode()
* currentPixelTypeOrAttachedNode()})
.
*
* @return true
if the {@link #currentCoordinates() current element} of the skeleton is a node,
* including the degenerated case of an isolated pixel.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public boolean isNode() {
checkInitialized();
return isNodePixelType(pixelTypesOrAttachingBranchesArray.getInt(currentIndexInArray));
}
/**
* Returns true
if the {@link #currentCoordinates() current element} of the skeleton
* is a {@link SkeletonPixelClassifier#TYPE_USUAL_BRANCH usual branch pixel},
* i.e. a unit pixel having exactly 2 unit neighbours.
* Equivalent both to {@link SkeletonPixelClassifier#isUsualBranchPixelType(int)
* SkeletonPixelClassifier.isUsualBranchPixelType}({@link #currentPixelTypeOrAttachingBranch()
* currentPixelTypeOrAttachingBranch()})
* and to {@link SkeletonPixelClassifier#isUsualBranchPixelType(int)
* SkeletonPixelClassifier.isUsualBranchPixelType}({@link #currentPixelTypeOrAttachedNode()
* currentPixelTypeOrAttachedNode()})
.
*
* @return true
if the {@link #currentCoordinates() current element} of the skeleton is
* a usual (non-ending) branch pixel.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public boolean isUsualBranch() {
checkInitialized();
return isUsualBranchPixelType(pixelTypesOrAttachingBranchesArray.getInt(currentIndexInArray));
}
/**
* Returns true
if the {@link #currentCoordinates() current element} of the skeleton
* is a {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end},
* i.e. a unit pixel having exactly 1 unit neighbour.
* Equivalent both to {@link SkeletonPixelClassifier#isFreeBranchEndPixelType(int)
* SkeletonPixelClassifier.isFreeBranchEndPixelType}({@link #currentPixelTypeOrAttachingBranch()
* currentPixelTypeOrAttachingBranch()})
* and to {@link SkeletonPixelClassifier#isFreeBranchEndPixelType(int)
* SkeletonPixelClassifier.isFreeBranchEndPixelType}({@link #currentPixelTypeOrAttachedNode()
* currentPixelTypeOrAttachedNode()})
.
*
* @return true
if the {@link #currentCoordinates() current element} of the skeleton is
* a free branch end.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public boolean isFreeBranchEnd() {
checkInitialized();
return isFreeBranchEndPixelType(pixelTypesOrAttachingBranchesArray.getInt(currentIndexInArray));
}
/**
* Returns true
if the {@link #currentCoordinates() current element} of the skeleton
* is an {@link SkeletonPixelClassifier#isAttachableBranchEndPixelType(int) attachable branch end},
* i.e. a unit pixel having 3 or more unit neighbours,
* which this class considers to be not a node, but an ending pixel of some branch.
* Equivalent both to {@link SkeletonPixelClassifier#isAttachableBranchEndPixelType(int)
* SkeletonPixelClassifier.isAttachableBranchEndPixelType}({@link #currentPixelTypeOrAttachingBranch()
* currentPixelTypeOrAttachingBranch()})
* and to {@link SkeletonPixelClassifier#isAttachableBranchEndPixelType(int)
* SkeletonPixelClassifier.isAttachableBranchEndPixelType}({@link #currentPixelTypeOrAttachedNode()
* currentPixelTypeOrAttachedNode()})
.
*
* @return true
if the {@link #currentCoordinates() current element} of the skeleton is
* an attachable branch end.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public boolean isAttachableBranchEnd() {
checkInitialized();
return isAttachableBranchEndPixelType(pixelTypesOrAttachingBranchesArray.getInt(currentIndexInArray));
}
/**
* Returns true
if the {@link #currentCoordinates() current element} of the skeleton
* is a node
* ({@link #isNode() isNode()}
returns true
)
* or a free branch end
* ({@link #isFreeBranchEnd() isFreeBranchEnd()}
returns true
).
* Equivalent both to {@link SkeletonPixelClassifier#isNodeOrFreeBranchEndPixelType(int)
* SkeletonPixelClassifier.isNodeOrFreeBranchEndPixelType}({@link #currentPixelTypeOrAttachingBranch()
* currentPixelTypeOrAttachingBranch()})
* and to {@link SkeletonPixelClassifier#isNodeOrFreeBranchEndPixelType(int)
* SkeletonPixelClassifier.isNodeOrFreeBranchEndPixelType}({@link #currentPixelTypeOrAttachedNode()
* currentPixelTypeOrAttachedNode()})
.
*
* @return true
if the {@link #currentCoordinates() current element} of the skeleton is
* a node or a free branch end.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public boolean isNodeOrFreeBranchEnd() {
checkInitialized();
return isNodeOrFreeBranchEndPixelType(pixelTypesOrAttachingBranchesArray.getInt(currentIndexInArray));
}
/**
* Returns true
if the {@link #currentCoordinates() current element} of the skeleton
* is a branch element: usual
* (where {@link #isUsualBranch() isUsualBranch()}
* returns true
), free branch end
* (where {@link #isFreeBranchEnd() isFreeBranchEnd()}
returns true
)
* or attachable branch end
* (where {@link #isAttachableBranchEnd() isAttachableBranchEnd()}
* returns true
).
* Equivalent both to {@link SkeletonPixelClassifier#isBranchPixelType(int)
* SkeletonPixelClassifier.isBranchPixelType}({@link #currentPixelTypeOrAttachingBranch()
* currentPixelTypeOrAttachingBranch()})
* and to {@link SkeletonPixelClassifier#isBranchPixelType(int)
* SkeletonPixelClassifier.isBranchPixelType}({@link #currentPixelTypeOrAttachedNode()
* currentPixelTypeOrAttachedNode()})
.
*
* @return true
if the {@link #currentCoordinates() current element} of the skeleton is
* some element of a branch.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public boolean isBranch() {
checkInitialized();
return isBranchPixelType(pixelTypesOrAttachingBranchesArray.getInt(currentIndexInArray));
}
/**
* Returns true
if the {@link #currentCoordinates() current element} of the skeleton
* is "{@link SkeletonPixelClassifier#TYPE_ILLEGAL illtegal}", i.e. a center of
* an impossible configuration for a correct result of the given skeletonization algorithm.
* Equivalent both to {@link SkeletonPixelClassifier#isIllegalPixelType(int)
* SkeletonPixelClassifier.isIllegalPixelType}({@link #currentPixelTypeOrAttachingBranch()
* currentPixelTypeOrAttachingBranch()})
* and to {@link SkeletonPixelClassifier#isIllegalPixelType(int)
* SkeletonPixelClassifier.isIllegalPixelType}({@link #currentPixelTypeOrAttachedNode()
* currentPixelTypeOrAttachedNode()})
.
*
* @return true
if the {@link #currentCoordinates() current element} of the skeleton is
* a part of pixel configuration which is incorrect for the given type of skeleton.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public boolean isIllegal() {
checkInitialized();
return isIllegalPixelType(pixelTypesOrAttachingBranchesArray.getInt(currentIndexInArray));
}
/**
* Returns true
if the neighbour with the given index of
* the {@link #currentCoordinates() current element} of the skeleton
* is a {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node}
* or a {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end}.
* Equivalent both to {@link SkeletonPixelClassifier#isNodeOrFreeBranchEndPixelType(int)
* SkeletonPixelClassifier.isNodeOrFreeBranchEndPixelType}({@link #neighbourTypeOrAttachingBranch(int)
* neighbourTypeOrAttachingBranch(neighbourIndex)})
* and to {@link SkeletonPixelClassifier#isNodeOrFreeBranchEndPixelType(int)
* SkeletonPixelClassifier.isNodeOrFreeBranchEndPixelType}({@link #neighbourTypeOrAttachedNode(int)
* neighbourTypeOrAttachedNode(neighbourIndex)})
.
*
* This method is helpful when {@link #isNodeOrFreeBranchEnd()} method returns true
,
* i.e. the current position is a node or a free branch end (and corresponds to a node in the nonoriented graph,
* formed by the skeleton), and we need to check, is it connected via a degenerated 0-pixel branch with
* a possible neighbouring node or free branch end (also corresponding to a node in the nonoriented graph).
* It is necessary in algorithms which process degenerated 0-pixel branches in some special way,
* like an algorithm, given in the {@link SkeletonScanner comments to this class}.
*
* @param neighbourIndex the index of the neighbour, in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* @return true
if the given neighbour of the current element of the skeleton matrix is
* a node or a free branch end.
* @throws IndexOutOfBoundsException if neighbourIndex
is out of range
* 0..{@link #numberOfNeighbours() numberOfNeighbours()}-1
.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public boolean isNeighbourNodeOrFreeBranchEnd(int neighbourIndex) {
return isNodeOrFreeBranchEndPixelType(pixelTypesOrAttachingBranchesArray.getInt(
neighbourIndexInArray(neighbourIndex)));
}
/**
* Sets the current position in the skeleton matrix to the specified coordinates.
*
*
In simple applications, you do not need this method: it is enough to implement a loop of calls of
* {@link #nextNodeOrBranch()} method.
*
* @param newCurrentCoordinates new current coordinates: the {@link #currentCoordinates()} method will return
* an identical array after this call.
* @throws NullPointerException if newCurrentCoordinates
argument is {@code null}.
* @throws IllegalArgumentException if the length of newCurrentCoordinates
array is not equal to
* {@link #dimCount() dimCount()}.
* @throws IndexOutOfBoundsException if one of new coordinates newCurrentCoordinates[k]
* is out of range 0..{@link #skeleton() skeleton()}.{@link Matrix#dim(int)
* dim}(k)-1
.
* @see #currentCoordinates()
* @see #goToIndexInArray(long)
*/
public void goTo(long... newCurrentCoordinates) {
checkCoordinates(newCurrentCoordinates);
this.currentIndexInArray = skeleton.index(newCurrentCoordinates.clone());
// it checks that coordinates are inside the matrix
this.previousBranchStepDirection = -1;
}
/**
* Reduced and more efficient version of {@link #goTo(long...)}, designed for indexing
* elements of the {@link Matrix#array() built-in AlgART array} of the skeleton matrix.
* This method is equivalent to
* {@link #goTo(long...) goto}({@link #skeleton() skeleton()}.{@link Matrix#coordinates(long, long[])
* coordinates}(newIndexInArray, null))
,
* but usually works much faster (in particular, does not allocate any arrays).
*
* @param newIndexInArray new current index in the built-in AlgART array of the skeleton matrix.
* @throws IndexOutOfBoundsException if newIndexInArray
is out of range
* 0..{@link #skeleton() skeleton()}.{@link Matrix#size() size()}-1
.
* @see #currentIndexInArray()
*/
public void goToIndexInArray(long newIndexInArray) {
if (newIndexInArray < 0 || newIndexInArray >= arrayLength) {
throw new IndexOutOfBoundsException("Index in array " + newIndexInArray
+ " is out of range 0.." + arrayLength);
}
this.currentIndexInArray = newIndexInArray;
this.previousBranchStepDirection = -1;
}
/**
* Moves the current position in the skeleton matrix to the given neighbour of the current element.
* The neighbour index is specified in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* This method is equivalent to {@link #goToIndexInArray(long) goToIndexInArray(index)},
* where
*
* index = ({@link #currentIndexInArray() currentIndexInArray()} + {@link #neighbourOffsetInArray(int)
* neighbourOffsetInArray}(neighbourIndex)) % {@link #skeleton() skeleton()}.{@link Matrix#size() size()}
*
* Note, that we allow a situation when the neighbouring element is out of ranges of the matrix coordinates.
* This situation is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonScanner comments to this class}.
*
* @param neighbourIndex the index of the neighbour, in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* @throws IndexOutOfBoundsException if neighbourIndex
is out of range
* 0..{@link #numberOfNeighbours() numberOfNeighbours()}-1
.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public void goToNeighbour(int neighbourIndex) {
checkInitialized();
long index = currentIndexInArray + neighbourOffsetInArray(neighbourIndex);
if (index >= arrayLength) {
index -= arrayLength;
}
assert index <= arrayLength : index + " > " + arrayLength
+ ", currentIndexInArray=" + currentIndexInArray + ": " + this;
this.currentIndexInArray = index;
}
/**
* Finds the next unit element in the skeleton matrix (in natural order of elements) after
* the {@link #currentCoordinates() current position}, the type of which is
* {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node}, {@link SkeletonPixelClassifier#TYPE_ISOLATED isolated}
* or {@link #isBranch() some branch pixel}, and moves the current position to this element.
* If this scanner was not {@link #isInitialized() positioned yet}, finds the first such element.
* Returns true
if this method has successfully found the required element, or false
* if there is no required position, i.e. if the matrix scanning is finished. In the second case,
* the current position is not changed.
*
* More precisely, this method finds the minimal k, so that
* - k ≥
({@link #isInitialized()} ? 0 : {@link #currentIndexInArray()}+1)
* and
* {@link #asPixelTypes(SkeletonPixelClassifier.AttachmentInformation)
* asPixelTypes(...)}.{@link Matrix#array() array()}.{@link PIntegerArray#getInt
* getInt}(
k)
is not equal to
* {@link SkeletonPixelClassifier#TYPE_ZERO TYPE_ZERO} or
* {@link SkeletonPixelClassifier#TYPE_ILLEGAL TYPE_ILLEGAL} (the argument of
* {@link #asPixelTypes(SkeletonPixelClassifier.AttachmentInformation) asPixelTypes} is not important here).
*
*
* If this index k exists, this method performs
* {@link #goToIndexInArray(long) goToIndexInArray}(k)
* and returns true
,
* in other case doesn't change the state of this object and returns false
.
*
*
Note, that if this scanner was not {@link #isInitialized() positioned yet}, it becomes positioned
* if this method returns true
, but stays not positioned if it returns false
.
*
*
After successful call of this method, you can be sure that the current position corresponds:
*
* - either to a {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node} or, maybe,
* {@link SkeletonPixelClassifier#TYPE_ISOLATED isolated pixel} (as a degenerated case of the node),
* - or to a skeleton branch (including possible case of
* {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end}).
*
*
* In the first case, i.e. if {@link #isNode()}, you can find all branches, originating at this node,
* by {@link #adjacentBranches()} method, which returns indexes of all corresponding neighbours.
* Then you can scan all these branches in a loop, starting the scanning of each branch by
* {@link #firstStep(int neighbourIndex, boolean onlyToUnvisited)}
* method, where neighbourIndex
is an element of the array — result of {@link #adjacentBranches()}.
* If necessary, you can scan each branch until its end by a loop of {@link #nextStep()} calls,
* like in {@link #scanBranch(int, boolean, boolean)} method, and, if the end will be
* a {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node} again, for example,
* recursively process this node in the same manner (that means deapth-first graph traversal).
*
*
Note, that the recursion should be used only
* if you remember all visited nodes (to avoid infinite recursion), for example, by using
* a {@link #isRemembering() remembering} scanner with the argument onlyToUnvisited=true
.
* Also note, that even remembering scanner does not allow to remember a fact of visiting
* degenerated 0-pixel branches: you should keep this in mind and process degenerated branches,
* if necessary, by some other mechanism.
*
*
In the second case, i.e. if {@link #isBranch()}, you should start scanning the found branch by
* {@link #firstStepFromBranch(boolean onlyToUnvisited)}
* method. But here it is important to distinguish two situations:
* {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end}
* and all other variants (usual branch elements and attachable branch ends).
* The first situation is similar to the first case (a node): it is a node of the skeleton
* nonoriented graph (see the {@link SkeletonScanner comments to this class}), having only one incident edge (branch).
* In the second situation, you can try to move to some {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node}
* / {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end} (one of 2 ends of this branch),
* for example, by {@link #scanBranchFromBranch(boolean, boolean) scanBranchFromBranch} method with
* withVisiting=false
argument, but you should remember, that it can be also a cyclic branch
* (the case 7 in the {@link SkeletonScanner comments to this class}). This case can be identified
* via the current pixel type after {@link #scanBranchFromBranch(boolean, boolean) scanBranchFromBranch} call:
* it will be {@link #isUsualBranch() usual branch pixel} only in a case of a cyclic branch.
*
*
Generally speaking, we do not recommend using Java recursion for graph traversal,
* because the internal JVM stack can be not enough for processing complex skeletons.
* Another approach is using breadth-first algorithm, based on some form of queue.
*
*
A full example of implementation of the breadth-first algorithm,
* with correct processing degenerated 0-pixel branches and cyclic branches,
* is given in the {@link SkeletonScanner comments to this class}.
*
* @return true
if this method has successfully found new node or branch pixel.
* @see #nextNodeOrBranchPixelType()
*/
public boolean nextNodeOrBranch() {
return nextNodeOrBranchPixelType() != null;
}
/**
* Enhanced version of {@link #nextNodeOrBranch()}, which returns the type of the successfully found element
* in the result or return {@code null} if the required element is not found.
* More precisely, it is equivalent to
*
* {@link #nextNodeOrBranch()} ? Integer.valueOf({@link #currentPixelTypeOrAttachedNode()}) : null
*
*
* @return the type of the found pixel (or, for attachable branch end,
* the index of its neighbouring node, which is one of the ends of the branch),
* or {@code null} if this method does not found the required element.
*/
public Integer nextNodeOrBranchPixelType() {
long index = currentIndexInArray;
int pixelType;
do {
index = skeletonArray.indexOf(index + 1, arrayLength, true);
if (index == -1) {
return null;
}
pixelType = pixelTypesOrAttachedNodesArray.getInt(index);
} while (pixelType == TYPE_ILLEGAL);
this.currentIndexInArray = index;
this.previousBranchStepDirection = -1;
return pixelType;
}
/**
* On the assumption that the {@link #currentCoordinates() current pixel} is a {@link #isNode() node},
* returns indexes of all its neighbours, which are the starting pixels of branches, incident with this node.
* In particular, if some of neighbours of this node are also nodes, this method detects and returns
* in the result the indexes of such from them, which are connected with this node by degenerated
* branches (consisting of 0 pixels).
* If the current pixel is not a node (or isolated pixel), this method throws IllegalStateException
.
*
* More precisely, the neighbour index k is an element of the returned array, if and only if:
*
* - the
{@link #firstStep(int, boolean) firstStep}(k,false)
call,
* performed at this position,
* would be successful (would return true
and successfully move the position to that neighbour);
* - and, in a case when this neighbour is a {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node},
* this neighbour is not marked (set to
Integer.MIN_VALUE
) by
* {@link #pixelClassifier()
* pixelClassifier()}.{@link SkeletonPixelClassifier#markNeighbouringNodesNotConnectedViaDegeneratedBranches
* markNeighbouringNodesNotConnectedViaDegeneratedBranches}
method, called
* for an array of types of all neighbours of the current node.
*
*
* The returned array is always a newly allocated Java array.
* Its length is always not greater than {@link #numberOfNeighbours()};
* it can be also empty, if the current element is an {@link SkeletonPixelClassifier#TYPE_ISOLATED isolated pixel}.
* The returned indexes specify neighbours in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* If you need maximal performance, you can eliminate memory allocation in a scanning loop
* by using {@link #adjacentBranches(int[])} method.
*
* @return the list of indexes of neighbours of the current pixel (node), towards which this class supposes
* existence of a branch, originating from this node.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet} or
* if !{@link #isNode() isNode()}
.
*/
public int[] adjacentBranches() throws IllegalStateException {
int[] neighbourIndexes = new int[numberOfNeighbours];
int n = adjacentBranches(neighbourIndexes);
return JArrays.copyOfRange(neighbourIndexes, 0, n);
}
/**
* More efficient version of {@link #adjacentBranches()} method, which stores the results
* in the specified Java array instead of creating new Java array.
* This method is equivalent to calling that method and copying its result into
* the beginning of result
Java array, but does not allocate any arrays.
* It is a better solution if we need to calculate adjacent branches in a long loop,
* because allows to avoid allocating a lot of short arrays.
*
*
The length of the passed array must be greater than or equal to {@link #numberOfNeighbours()}.
*
* @param result Java array for storing the results.
* @return the number of found neighbours (which are the starting pixels of branches,
* incident with this node): after calling this method,
* you should use this number of first elements of the result
array.
* @throws NullPointerException if result
argument is {@code null}.
* @throws IllegalArgumentException if result.length<{@link #numberOfNeighbours()}
.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet} or
* if !{@link #isNode() isNode()}
.
*/
public int adjacentBranches(int[] result) throws IllegalStateException {
Objects.requireNonNull(result, "Null result argument");
if (result.length < numberOfNeighbours) {
throw new IllegalArgumentException("Length of result array "
+ " is less than the number of neighbours of every pixel " + numberOfNeighbours);
}
if (!isNode()) {
throw new IllegalStateException("adjacentBranches() must be called at nodes only: " + this);
}
for (int k = 0; k < numberOfNeighbours; k++) {
// using result as a work memory
result[k] = neighbourTypeOrAttachingBranch(k);
}
pixelClassifier.markNeighbouringNodesNotConnectedViaDegeneratedBranches(result);
int count = 0;
for (int k = 0; k < numberOfNeighbours; k++) {
int neighbourType = result[k];
if (isAttachableBranchEndPixelType(neighbourType)) { // it should be attached namely to this node
int reverseStepDirection = pixelClassifier.reverseNeighbourIndex(k);
if (neighbourType != reverseStepDirection && neighbourTypeOrAttachedNode(k) != reverseStepDirection) {
continue;
}
}
if (isBranchPixelType(neighbourType) || (isNodePixelType(neighbourType))) {
result[count++] = k;
}
}
return count;
}
/**
* On the assumption that the {@link #currentCoordinates() current pixel} is a
* {@link #isNode() node or isolated pixel},
* checks whether we have a skeleton branch, originating at this node and going towards its neighbour
* with the index neighbourIndex
, and, if so, moves the current position to this neighbour and
* returns true
, if not, does nothing and returns false
.
* The neighbour index is specified in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* If the current pixel is not a node (or isolated pixel), this method throws IllegalStateException
.
* The movement along the branch, started by this method, can be continued by a loop of {@link #nextStep()} calls:
* see an example in comments to {@link #scanBranch scanBranch} method.
*
*
Warning: unlike {@link #adjacentBranches()} and in violation of the definition of the nonoriented graph,
* formed by the skeleton, this method works as if every neighbouring node (when such nodes exist)
* is connected with this one via a degenerated 0-pixel branch.
* So, you should use it together with {@link #adjacentBranches()}.
*
*
More precisely, this method checks the type of the given neighbour:
* {@link #neighbourTypeOrAttachingBranch(int) neighbourTypeOrAttachingBranch(neighbourIndex)}.
*
* - If it is a {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node},
* this method always moves the current position to that node and returns
true
* (it can lead to extra degenerated branches, but you can use {@link #adjacentBranches()} method
* to avoid this);
*
* - If the type of the given neighbour is a
* {@link SkeletonPixelClassifier#TYPE_USUAL_BRANCH usual branch element}
* or a {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end},
* this method moves the current position to this neighbour and returns
true
.
*
* - If the given neighbour is {@link SkeletonPixelClassifier#isAttachableBranchEndPixelType(int)
* attachable branch end}, this method checks its attached node A (returned by
* {@link #neighbourTypeOrAttachedNode(int) neighbourTypeOrAttachedNode(neighbourIndex)})
* and the element of attaching branch B (returned by
* {@link #neighbourTypeOrAttachingBranch(int) neighbourTypeOrAttachingBranch(neighbourIndex)})
* — see the description of group 5 of pixel types in the
* {@link SkeletonPixelClassifier comments to SkeletonPixelClassifier}.
* If one of pixels A or B is the current node, this method moves the current position to
* this neighbour and returns
true
, in other case if does nothing and returns false
.
*
* - In all other situations (the given neighbour is zero or
* "{@link SkeletonPixelClassifier#TYPE_ILLEGAL illegal}" unit element),
* this method does nothing and returns
false
.
*
*
* The rules, listed above, are used as described if the argument onlyToUnvisited
* is false
.
* If it is true
and if this scanner is {@link #isRemembering() remembering}, this method also checks,
* whether the given neighbour was already visited, i.e. checks the result of
* {@link #neighbourVisitRemembered(int) neighbourVisitRemembered(neighbourIndex)} call.
* If that call returns true
, this method does nothing and returns false
,
* in other case it works as described above.
*
*
Note, that even if this scanner is {@link #isRemembering() remembering}, this method does not store
* information about visiting pixels. If you want, you should do this manually by
* {@link #visitPreviousBranchPixel()} method.
*
*
Note, that we allow a situation when the neighbouring elements are out of ranges of the matrix coordinates.
* This situation is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonScanner comments to this class}.
*
* @param neighbourIndex the index of the neighbour, in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* @param onlyToUnvisited whether this method should go only to neighbours, which were never be visited before
* (this argument affects only if this scanner is {@link #isRemembering() remembering}).
* @return true
if the current position has been successfully moved to the neighbour.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet} or
* if !{@link #isNode() isNode()}
.
* @throws IndexOutOfBoundsException if neighbourIndex
is out of range
* 0..{@link #numberOfNeighbours() numberOfNeighbours()}-1
.
* @see #scanBranch(int, boolean, boolean)
*/
public boolean firstStep(int neighbourIndex, boolean onlyToUnvisited) throws IllegalStateException {
if (!isNode()) {
throw new IllegalStateException("Cannot perform first branch step with direction (" + neighbourIndex
+ ") from a pixel of the type " + currentPixelTypeOrAttachingBranch()
+ " - it must be a node or isolated pixel: " + this);
}
if (onlyToUnvisited && neighbourVisitRemembered(neighbourIndex)) {
return false;
}
int neighbourType = neighbourTypeOrAttachingBranch(neighbourIndex);
if (isBranchPixelType(neighbourType)) {
if (isAttachableBranchEndPixelType(neighbourType)) { // it should be attached namely to this node
int reverseStepDirection = pixelClassifier.reverseNeighbourIndex(neighbourIndex);
if (neighbourType != reverseStepDirection
&& neighbourTypeOrAttachedNode(neighbourIndex) != reverseStepDirection) {
return false;
}
}
} else if (!isNodePixelType(neighbourType)) {
return false;
}
this.startIndexInArray = currentIndexInArray; // to be on the safe side
shiftAlongBranch(neighbourIndex);
return true;
}
/**
* On the assumption that the {@link #currentCoordinates() current pixel} is
* {@link #isBranch() some branch pixel},
* moves the current position to a neighbour along this skeleton branch and returns true
.
* If the current pixel is not a branch pixel, this method throws IllegalStateException
.
* In a case of success (this method successfully changes the position and returns true
),
* the previous current position is internally stored: it will be used in {@link #nextStep()} method
* to finish scanning a cyclic branch.
* The movement along the branch, started by this method, can be continued by a loop of {@link #nextStep()} calls:
* see an example in comments to {@link #scanBranchFromBranch scanBranchFromBranch} method.
*
*
More precisely:
*
* - if the current pixel is a {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end},
* this method moves the current position to its only unit neighbour Q;
*
* - if the current pixel is a {@link SkeletonPixelClassifier#TYPE_USUAL_BRANCH usual branch element},
* this method moves the current position to some of 2 its unit neighbours
* Q1 and Q2;
*
* - if the current pixel is a {@link #isAttachableBranchEnd() attachable branch end},
* this method moves the current position to some of 2 its neighbours
* Q1 and Q2, indexes of which
* are returned by {@link #currentPixelTypeOrAttachingBranch()} and
* {@link #currentPixelTypeOrAttachedNode()} methods.
*
*
* The rules, listed above, are used as described if the argument onlyToUnvisited
* is false
.
* If it is true
and if this scanner is {@link #isRemembering() remembering}, this method also checks,
* whether the neighbours were already visited, i.e. checks the result of
* {@link #neighbourVisitRemembered(int) neighbourVisitRemembered(...)} call
* for the one (case 1) or for both (cases 2 and 3) neighbours.
* If that call returns false
for the only neighbour Q (case 1) or for some of two neighbours
* Q1 and Q2 (cases 2 and 3),
* this method moves to that neighbour and returns true
.
* In other case it does nothing and returns false
.
*
*
Note, that even if this scanner is {@link #isRemembering() remembering}, this method does not store
* information about visiting pixels. If you want, you should do this manually by
* {@link #visitPreviousBranchPixel()} method.
*
*
Note, that it is undocumented, which of two neighbours Q1 and Q2
* is selected in cases 2 and 3 (if one of them is not disabled because onlyToUnvisited=true
* and it was already visited).
*
*
Note, that we allow a situation when the neighbouring elements are out of ranges of the matrix coordinates.
* This situation is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonScanner comments to this class}.
*
*
This method is implemented in the following way:
*
* int nextNeighbourIndex = {@link #firstStepFromBranchNeighbourIndex
* firstStepFromBranchNeighbourIndex}(onlyToUnvisited);
* if (nextNeighbourIndex == -1) {
* return false;
* }
* this.startIndexInArray = {@link #currentIndexInArray()};
* // - an internal field; it will be used in {@link #nextStep()} to finish scanning a cyclic branch
* {@link #goToNeighbour goToNeighbour}(nextNeighbourIndex);
* this.previousBranchStepDirection = nextNeighbourIndex;
* // - an internal field, returned by {@link #previousBranchStepDirection()} method
* return true;
*
*
* @param onlyToUnvisited whether this method should go only to neighbours, which were never be visited before
* (this argument affects only if this scanner is {@link #isRemembering() remembering}).
* @return true
if the current position has been successfully moved to the neighbour.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet} or
* if !{@link #isBranch() isBranch()}
.
* @see #scanBranchFromBranch(boolean, boolean)
*/
public boolean firstStepFromBranch(boolean onlyToUnvisited) throws IllegalStateException {
int nextNeighbourIndex = firstStepFromBranchNeighbourIndex(onlyToUnvisited);
if (nextNeighbourIndex == -1) {
return false;
}
this.startIndexInArray = currentIndexInArray; // necessary for TYPE_USUAL_BRANCH
shiftAlongBranch(nextNeighbourIndex);
return true;
}
/**
* Returns the index of the neighbour, to which {@link #firstStepFromBranch(boolean onlyToUnvisited)} moves
* when called with the same onlyToUnvisited
argument.
* If that method returns false
and does not move anywhere, this method returns -1
.
* If that method throws IllegalStateException
, this method also throws this exception.
* Unlike {@link #firstStepFromBranch}, this method does not change anything in the internal state
* of the object.
*
* See comments to {@link #firstStepFromBranch} method for more details.
*
* @param onlyToUnvisited whether this method should check only neighbours, which were never be visited before
* (this argument affects only if this scanner is {@link #isRemembering() remembering}).
* @return the index of the neighbour true
, to which {@link #firstStepFromBranch}
* will move if it will be called.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet} or
* if !{@link #isBranch() isBranch()}
.
*/
public int firstStepFromBranchNeighbourIndex(boolean onlyToUnvisited) throws IllegalStateException {
int pixelType = currentPixelTypeOrAttachedNode();
if (isAttachableBranchEndPixelType(pixelType)) {
if (!(onlyToUnvisited && neighbourVisitRemembered(pixelType))) {
return pixelType;
}
pixelType = currentPixelTypeOrAttachingBranch();
// onlyToUnvisited is true
if (!neighbourVisitRemembered(pixelType)) {
return pixelType;
}
return -1;
}
switch (pixelType) {
case TYPE_FREE_BRANCH_END:
case TYPE_USUAL_BRANCH:
int neighbourCount = 0;
for (int k = 0; k < numberOfNeighbours; k++) {
if (neighbourValue(k)) {
neighbourCount++;
if (!(onlyToUnvisited && neighbourVisitRemembered(k))) {
return k;
}
}
}
if (neighbourCount != (pixelType == TYPE_FREE_BRANCH_END ? 1 : 2)) {
throw new AssertionError("Illegal detection of "
+ pixelType + ": there are no neighbours in " + this);
}
return -1;
default:
throw new IllegalStateException("Cannot perform first branch step without direction from a pixel "
+ "of the type " + pixelType + " - it must be a branch element: " + this);
}
}
/**
* Continues movement along the skeleton branch, started by {@link #firstStep(int, boolean)} or
* {@link #firstStepFromBranch(boolean)} method, and returns true
,
* if the end of the current branch is not reached yet, or does nothing and returns false
* if we have reached the end of the branch (usually a {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node} or
* {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end}).
*
*
This method may be called only if the previous change of the current position was performed
* by {@link #firstStep(int, boolean)}, {@link #firstStepFromBranch(boolean)} or this method.
* In other case (for example, if the last change of the current position was performed by {@link #goTo(long...)}
* or {@link #nextNodeOrBranch()}), this method throws IllegalStateException
.
*
*
More precisely:
*
* - if the current position is equal to the position, stored in the beginning of the last
* {@link #firstStepFromBranch(boolean)} call, this method does nothing and returns
false
* (it means that we've finished scanning of this cyclic branch and returned to the original position);
*
* - if the current pixel is a {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end},
* this method does nothing and returns
false
* (it means that we've reached the free end of this branch);
*
* - if the current pixel is a {@link SkeletonPixelClassifier#TYPE_USUAL_BRANCH usual branch element},
* this method moves the current position to that from its 2 unit neighbours
* Q1 and Q2,
* which was not current before the previous change of the current position via
* {@link #firstStep(int, boolean)}, {@link #firstStepFromBranch(boolean)} or this method,
* and returns
true
;
*
* - if the current pixel is an {@link #isAttachableBranchEnd() attachable branch end},
* this method finds 2 its neighbours Q1 and Q2, indexes of which
* are returned by {@link #currentPixelTypeOrAttachingBranch()} and
* {@link #currentPixelTypeOrAttachedNode()} methods,
* and moves the current position to that from Q1 and Q2,
* which was not current before the previous change of the current position via
* {@link #firstStep(int, boolean)}, {@link #firstStepFromBranch(boolean)} or this method,
* and returns
true
;
*
* - if the current pixel is a {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node} or an
* "{@link SkeletonPixelClassifier#TYPE_ILLEGAL illegal}" pixel, this method does nothing
* and returns
false
(it means that we've reached the node at the end of this branch
* or we cannot continue scanning because this pixel cannot belong to a correct skeleton);
*
* - if the current element is {@link SkeletonPixelClassifier#TYPE_ZERO zero},
* this method throws
IllegalStateException
.
*
*
* Note, that this method does not use information about possible previous visits of the pixels, probably
* remembered if this scanner is {@link #isRemembering() remembering}.
*
*
Note, that we allow a situation when the neighbouring element is out of ranges of the matrix coordinates.
* This situation is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonScanner comments to this class}.
*
* @return true
if the current position has been successfully moved to the neighbour.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet},
* or if the previous change of the current position was performed not by
* this method and not by {@link #firstStep(int, boolean)} or
* {@link #firstStepFromBranch(boolean)}.
*/
public boolean nextStep() throws IllegalStateException {
checkInitialized();
if (this.previousBranchStepDirection == -1) {
throw new IllegalStateException("nextStep() must be called after "
+ "successful firstStep/firstStepFromBranch or another nextStep() only");
}
if (currentIndexInArray == startIndexInArray) {
return false; // we've returned to the start position: it is possible for a loop of TYPE_USUAL_BRANCH
}
int reverseStepDirection = pixelClassifier.reverseNeighbourIndex(previousBranchStepDirection);
int nextDirection = 157;
int neighbourCount = 1; // previous neighbour
for (int k = 0; k < numberOfNeighbours; k++) {
if (k != reverseStepDirection && neighbourValue(k)) {
nextDirection = k;
neighbourCount++;
}
}
if (neighbourCount == 2) { // TYPE_USUAL_BRANCH: we found this quickly without calculation of pixel type
shiftAlongBranch(nextDirection);
return true;
}
if (neighbourCount == 1) { // TYPE_FREE_BRANCH_END: we found this quickly without calculation of pixel type
return false;
}
int pixelType = currentPixelTypeOrAttachingBranch();
if (pixelType >= 0) {
shiftAlongBranch(pixelType != reverseStepDirection ? pixelType : currentPixelTypeOrAttachedNode());
return true;
}
switch (pixelType) {
case TYPE_USUAL_NODE:
return false;
case TYPE_ISOLATED:
throw new AssertionError("Illegal detection of an isolated pixel at a branch: " + this);
case TYPE_FREE_BRANCH_END:
throw new AssertionError("Illegal detection of TYPE_FREE_BRANCH_END: "
+ "here is at least " + neighbourCount + " neighbours in " + this);
case TYPE_USUAL_BRANCH:
throw new AssertionError("Illegal detection of TYPE_USUAL_BRANCH: "
+ "here is at least " + neighbourCount + " neighbours in " + this);
case TYPE_ILLEGAL:
return false;
case TYPE_ZERO:
throw new AssertionError("Illegal detection of a zero pixel at a branch: " + this);
default:
throw new AssertionError("Unknown pixel type " + pixelType + " detected at a branch: " + this);
}
}
/**
* On the assumption that the {@link #currentCoordinates() current pixel} is a
* {@link #isNode() node or isolated pixel},
* completely scans the branch, originating at this node and going towards its neighbour with the given index.
* If onlyToUnvisited
argument is true
, this method does not try to scan a branch,
* the first pixel of which was already visited.
* If withVisiting
argument is true
, this method marks all visited and left pixels
* (including the starting node, but excluding the finish pixel) by {@link #visitPreviousBranchPixel()} method.
* Both arguments onlyToUnvisited
and withVisiting
have no effect if this
* scanner is not {@link #isRemembering() remembering}.
*
*
More precisely, this method is equivalent to the following code:
*
* if ({@link #firstStep firstStep}(neighbourIndex, onlyToUnvisited)) {
* do {
* if (withVisiting) {
* {@link #visitPreviousBranchPixel()};
* }
* } while ({@link #nextStep()});
* }
*
* with the only addition that this method also calls
* {@link #context()}.{@link ArrayContext#checkInterruption() checkInterruption()}
* method from time to time (if {@link #context()}!=null
)
* to allow interruption of scanning very long branches.
* No other methods of the context are called.
*
* @param neighbourIndex the index of the neighbour, in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* @param onlyToUnvisited whether this method should go only to neighbours, which were never be visited before
* (this argument affects only if this scanner is {@link #isRemembering() remembering}).
* @param withVisiting whether this method should call {@link #visitPreviousBranchPixel()} after each step
* (this argument affects only if this scanner is {@link #isRemembering() remembering}).
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet} or
* if !{@link #isNode() isNode()}
.
* @throws IndexOutOfBoundsException if neighbourIndex
is out of range
* 0..{@link #numberOfNeighbours() numberOfNeighbours()}-1
.
*/
public void scanBranch(int neighbourIndex, boolean onlyToUnvisited, boolean withVisiting)
throws IllegalStateException {
if (firstStep(neighbourIndex, onlyToUnvisited)) {
long counter = 0;
do {
if (withVisiting) {
visitPreviousBranchPixel();
}
counter++;
if (context != null && (counter & 0xFFFF) == 0) {
context.checkInterruption();
}
} while (nextStep());
}
}
/**
* On the assumption that the {@link #currentCoordinates() current pixel} is
* {@link #isBranch() some branch pixel},
* scans the part of this branch towards one of the sides of the current pixel.
* If the current pixel is a {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end}
* or if this branch is cyclic (consists of {@link SkeletonPixelClassifier#TYPE_USUAL_BRANCH usual branch pixels}
* only), this method completely scans whole this branch.
* If onlyToUnvisited
argument is true
, this method does not try to scan a branch,
* if the first scanned pixel was already visited.
* If withVisiting
argument is true
, this method marks all visited and left pixels
* (including the starting pixel, but excluding the finish one) by {@link #visitPreviousBranchPixel()} method.
* Both arguments onlyToUnvisited
and withVisiting
have no effect if this
* scanner is not {@link #isRemembering() remembering}.
*
* More precisely, this method is equivalent to the following code:
*
* if ({@link #firstStepFromBranch firstStepFromBranch}(onlyToUnvisited)) {
* do {
* if (withVisiting) {
* {@link #visitPreviousBranchPixel()};
* }
* } while ({@link #nextStep()});
* }
*
* with the only addition that this method also calls
* {@link #context()}.{@link ArrayContext#checkInterruption() checkInterruption()}
* method from time to time (if {@link #context()}!=null
)
* to allow interruption of scanning very long branches.
* No other methods of the context are called.
*
* @param onlyToUnvisited whether this method should go only to neighbours, which were never be visited before
* (this argument affects only if this scanner is {@link #isRemembering() remembering}).
* @param withVisiting whether this method should call {@link #visitPreviousBranchPixel()} after each step
* (this argument affects only if this scanner is {@link #isRemembering() remembering}).
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet} or
* if !{@link #isBranch() isBranch()}
.
*/
public void scanBranchFromBranch(boolean onlyToUnvisited, boolean withVisiting) throws IllegalStateException {
if (firstStepFromBranch(onlyToUnvisited)) {
long counter = 0;
do {
if (withVisiting) {
visitPreviousBranchPixel();
}
counter++;
if (context != null && (counter & 0xFFFF) == 0) {
context.checkInterruption();
}
} while (nextStep());
}
}
/**
* Returns an index of the neighbour, towards which the current position was moved
* by the previous change of the current position via
* {@link #firstStep(int, boolean)}, {@link #firstStepFromBranch(boolean)} or {@link #nextStep()} method,
* or -1
if the previous change of the current position was performed by some other method
* like {@link #goTo(long...)} or {@link #nextNodeOrBranch()}.
* This neighbour index is specified in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
* method. So, if direction
is the result of this method and it is not -1
,
* then {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#reverseNeighbourIndex(int)
* reverseNeighbourIndex(direction)}
is an index
* of the neighbour of the current element, which was current before the last movement.
*
* @return the direction of the last movement of the current position along a branch,
* performed by {@link #firstStep(int, boolean)}, {@link #firstStepFromBranch(boolean)}
* or {@link #nextStep()} method, or -1
if the last movement was performed by another method.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public int previousBranchStepDirection() {
checkInitialized();
return this.previousBranchStepDirection;
}
/**
* Returns the coordinates of the neighbour of the current element, which was current before the last change
* of the current position, if this change was performed via
* {@link #firstStep(int, boolean)}, {@link #firstStepFromBranch(boolean)} or {@link #nextStep()} method,
* or throws IllegalStateException
if the previous change of the current position was performed
* by some other method like {@link #goTo(long...)} or {@link #nextNodeOrBranch()}.
*
* The returned array is always a newly allocated Java array.
* Its length is always equal to {@link #dimCount() dimCount()}.
* The returned coordinates are always in ranges
*
* 0 ≤ result[k] < {@link #skeleton() skeleton()}.{@link Matrix#dim(int) dim}(k),
*
where result[k]
is the element #k
in the returned array.
*
* @return the coordinates of the previous current pixel.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet},
* or if the previous change of the current position was performed not by
* not by {@link #firstStep(int, boolean)}, {@link #firstStepFromBranch(boolean)}
* or {@link #nextStep()}.
* @see #previousIndexInArray()
*/
public long[] previousCoordinates() {
checkInitialized();
if (this.previousBranchStepDirection == -1) {
throw new IllegalStateException("previousCoordinates() must be called after "
+ "successful firstStep/firstStepFromBranch or another nextStep() only");
}
return skeleton.coordinates(previousIndexInArray(), null);
}
/**
* Reduced and more efficient version of {@link #previousCoordinates()}, designed for indexing
* elements of the {@link Matrix#array() built-in AlgART array} of the skeleton matrix.
* This method is equivalent to
* {@link #skeleton() skeleton()}.{@link Matrix#index(long...)
* index}({@link #previousCoordinates() previousCoordinates()})
,
* but usually works much faster (in particular, does not allocate any arrays).
*
* The result of this method is always in range
* 0..{@link #skeleton() skeleton()}.{@link Matrix#size() size()}-1
.
*
* @return the previous current index in the built-in AlgART array of the skeleton matrix.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet},
* or if the previous change of the current position was performed not by
* not by {@link #firstStep(int, boolean)}, {@link #firstStepFromBranch(boolean)}
* or {@link #nextStep()}.
*/
public long previousIndexInArray() {
checkInitialized();
if (this.previousBranchStepDirection == -1) {
throw new IllegalStateException("previousIndexInArray() must be called after "
+ "successful firstStep/firstStepFromBranch or another nextStep() only");
}
long index = currentIndexInArray - neighbourOffsetInArray(previousBranchStepDirection);
if (index < 0) {
index += arrayLength;
}
return index;
}
/**
* Returns true
if this scanner is remembering or false
if it is lightweight.
* See the {@link SkeletonScanner comments to this class} about remembering and lightweight skeleton scanners.
*
* @return whether this class is remembering.
*/
public boolean isRemembering() {
return visitedArray != null;
}
/**
* Returns true
if this scanner is {@link #isRemembering() remembering} and
* the {@link #currentCoordinates() current element} was already visited by
* {@link #visit()} or {@link #visitPreviousBranchPixel()} method.
*
*
If this scanner is lightweight, this method always returns false
.
*
* @return whether the current pixel is marked as "visited".
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public boolean pixelVisitRemembered() {
checkInitialized();
return visitedArray != null && visitedArray.getBit(currentIndexInArray);
}
/**
* Returns true
if this scanner is {@link #isRemembering() remembering} and
* the neighbour of the {@link #currentCoordinates() current element} with the given index
* was already visited by {@link #visit()} or {@link #visitPreviousBranchPixel()} method.
* The result will be the same as if we would call
* {@link #goToNeighbour(int) goToNeighbour}(neighbourIndex)
and then call
* {@link #pixelVisitRemembered()}, but this method does not change the current position.
*
*
If this scanner is lightweight, this method always returns false
.
*
*
Note, that we allow a situation when the neighbouring element is out of ranges of the matrix coordinates.
* This situation is processed according to the model of infinite pseudo-cyclical continuation —
* see the end of the {@link SkeletonScanner comments to this class}.
*
* @param neighbourIndex the index of the neighbour, in terms of
* {@link #pixelClassifier()}.{@link SkeletonPixelClassifier#neighbourOffset(int)
* neighbourOffset(int)}
method.
* @return whether the given neighbour of the current pixel is marked as "visited".
* @throws IndexOutOfBoundsException if neighbourIndex
is out of range
* 0..{@link #numberOfNeighbours() numberOfNeighbours()}-1
.
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public boolean neighbourVisitRemembered(int neighbourIndex) {
checkInitialized();
checkNeighbourIndex(neighbourIndex);
long index = currentIndexInArray + neighbourOffsetInArray(neighbourIndex);
if (index >= arrayLength) {
index -= arrayLength;
}
assert index <= arrayLength : index + " > " + arrayLength
+ ", currentIndexInArray=" + currentIndexInArray + ": " + this;
return visitedArray != null && visitedArray.getBit(index);
}
/**
* In {@link #isRemembering() remembering} scanners,
* marks the {@link #currentCoordinates() current element} of the skeleton matrix as "visited".
* In lightweight scanners, this method does nothing.
*
*
Note that the only way to "unmark" the visited element (i.e. to change its state back to "unvisited")
* is calling {@link #reset()} method.
*
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet}.
*/
public void visit() {
checkInitialized();
if (visitedArray != null) {
visitedArray.setBit(currentIndexInArray);
}
}
/**
* In {@link #isRemembering() remembering} scanners,
* marks the {@link #previousCoordinates() previous visited element} of the skeleton matrix as "visited".
* In lightweight scanners, this method does nothing.
* The "previous visited element" means the neighbour of the current element, coordinates of which
* are returned by {@link #previousCoordinates()} method.
*
*
Note that the only way to "unmark" the visited element (i.e. to change its state back to "unvisited")
* is calling {@link #reset()} method.
*
*
This methods is useful in loops of scanning skeleton branches: see examples of such loops
* in comments to {@link #scanBranch(int, boolean, boolean)} and {@link #scanBranchFromBranch(boolean, boolean)}
* methods.
*
* @throws IllegalStateException if this scanner was not {@link #isInitialized() positioned yet},
* or if the previous change of the current position was performed not by
* not by {@link #firstStep(int, boolean)}, {@link #firstStepFromBranch(boolean)}
* or {@link #nextStep()}.
*/
public void visitPreviousBranchPixel() {
checkInitialized();
if (this.previousBranchStepDirection == -1) {
throw new IllegalStateException("visitPreviousBranchPixel() must be called after "
+ "successful firstStep/firstStepFromBranch or another nextStep() only");
}
if (visitedArray != null) {
long index = currentIndexInArray - neighbourOffsetInArray(previousBranchStepDirection);
if (index < 0) {
index += arrayLength;
}
visitedArray.setBit(index);
}
}
/**
* Clears the state of this scanner: resets the current position to {@link #isInitialized() not positioned} state
* and, in {@link #isRemembering() remembering} scanners, resets the state of all pixels to
* {@link #pixelVisitRemembered() unvisited}.
*/
public void reset() {
if (visitedArray != null && isInitialized()) {
visitedArray.fill(false);
}
this.currentIndexInArray = -1;
this.previousBranchStepDirection = -1;
}
/**
* Calls {@link #context()}.{@link ArrayContext#updateProgress updateProgress(event)}
* with an event, created by the following operator:
* new ArrayContext.Event(boolean.class, {@link #currentIndexInArray()
* currentIndexInArray()}, {@link #skeleton() skeleton()}.{@link Matrix#size() size()})
,
* or does nothing if {@link #context()}==null
.
*
*
The method can be useful while sequentially scanning the skeleton via a usual loop of
* {@link #nextNodeOrBranch()} calls.
*/
public void updateProgress() {
if (context != null) {
context.updateProgress(new ArrayContext.Event(boolean.class, currentIndexInArray(), skeleton.size()));
}
}
/**
* Calls {@link #context()}.{@link ArrayContext#checkInterruption() checkInterruption()}
* or does nothing if {@link #context()}==null
.
*
*
The method can be useful while sequentially scanning the skeleton via a usual loop of
* {@link #nextNodeOrBranch()} calls.
*/
public void checkInterruption() {
if (context != null) {
context.checkInterruption();
}
}
/**
* Returns a brief string description of this object.
*
*
The result of this method may depend on implementation and usually contains
* a short description of the current state of the scanner.
*
* @return a brief string description of this object.
*/
@Override
public String toString() {
return "skeleton scanner, "
+ (isInitialized() ? "position (" + JArrays.toString(currentCoordinates(), ",", 100)
+ ")" : "not initialized")
+ (previousBranchStepDirection >= 0 ? ", last branch step " + previousBranchStepDirection : "")
+ " for " + skeleton;
}
private void checkInitialized() {
if (!isInitialized()) {
throw new IllegalStateException("The skeleton scanner is not positioned yet");
}
}
private void checkCoordinates(long[] coordinates) {
Objects.requireNonNull(coordinates, "Null list of coordinates");
if (coordinates.length != dimCount) {
throw new IllegalArgumentException("Number of coordinates " + coordinates.length
+ " is not equal to the number of matrix dimensions " + dimCount);
}
}
private void checkNeighbourIndex(int neighbourIndex) {
if (neighbourIndex < 0 || neighbourIndex >= numberOfNeighbours) {
throw new IndexOutOfBoundsException("Illegal neighbourIndex = " + neighbourIndex
+ ": must be in 0.." + (numberOfNeighbours - 1) + " range");
}
}
private void shiftAlongBranch(int neighbourIndex) {
long index = currentIndexInArray + neighbourOffsetInArray(neighbourIndex);
if (index >= arrayLength) {
index -= arrayLength;
}
assert index <= arrayLength : index + " > " + arrayLength
+ ", currentIndexInArray=" + currentIndexInArray + ": " + this;
this.currentIndexInArray = index;
this.previousBranchStepDirection = neighbourIndex;
}
}