All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.algart.matrices.skeletons.SkeletonScanner Maven / Gradle / Ivy

Go to download

Open-source Java libraries, supporting generalized smart arrays and matrices with elements of any types, including a wide set of 2D-, 3D- and multidimensional image processing and other algorithms, working with arrays and matrices.

There is a newer version: 1.4.23
Show newest version
/*
 * 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 (xyz,...) * 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 (xyz,...) * 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} * (xyz,...) corresponds to a pixel (xyz,...) * in the n-dimensional Euclidean space.

* *

The skeleton nonoriented graph is a set of n-dimensional points-nodes, * connected by polylines-edges. * Namely:

* *
    *
  1. 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}. *
     
  2. * *
  3. 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). *
     
  4. * *
  5. 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≤km−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≤km−1 for some graph edge. * There is the only exception from this rule, described below in the paragraph 7. *
     
  6. * *
  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. *
     
  8. * *
  9. 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. *
     
  10. * *
  11. 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). *
       
    • *
    *
  12. * *
  13. 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. *
     
  14. * *
  15. 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.
  16. *
* *

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 (|ikjk|)=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 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 pixelTypesOrAttachingBranches; private final Matrix 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 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 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 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 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 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 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 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 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 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 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 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: *

    *
  1. either to a {@link SkeletonPixelClassifier#TYPE_USUAL_NODE node} or, maybe, * {@link SkeletonPixelClassifier#TYPE_ISOLATED isolated pixel} (as a degenerated case of the node),
  2. *
  3. or to a skeleton branch (including possible case of * {@link SkeletonPixelClassifier#TYPE_FREE_BRANCH_END free branch end}).
  4. *
* *

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: *

    *
  1. 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);
  2. *
  3. 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.
  4. *
* *

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)}. *

    *
  1. 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);
  2. * *
  3. 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.
  4. * *
  5. 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.
  6. * *
  7. In all other situations (the given neighbour is zero or * "{@link SkeletonPixelClassifier#TYPE_ILLEGAL illegal}" unit element), * this method does nothing and returns false.
  8. *
* *

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: *

    *
  1. 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;
  2. * *
  3. 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;
  4. * *
  5. 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.
  6. *
* *

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: *

    *
  1. 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);
  2. * *
  3. 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);
  4. * *
  5. 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;
  6. * *
  7. 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;
  8. * *
  9. 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);
  10. * *
  11. if the current element is {@link SkeletonPixelClassifier#TYPE_ZERO zero}, * this method throws IllegalStateException.
  12. *
* *

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; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy