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

net.algart.matrices.skeletons.OctupleThinningSkeleton2D 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.

The 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 static net.algart.matrices.skeletons.ThinningTools.*;

/**
 * 

Algorithm of 2-dimensional skeletonization of binary matrices based on 8 thinning steps, * corresponding to 8 directions with the step 45 degree, based on analysis of 3x3 aperture.

* *

More precisely, this class is an implementation of {@link ThinningSkeleton} interface, * iteratively processing some bit matrix ({@link Matrix}({@link UpdatableBitArray})), named * result and passed to the {@link #getInstance getInstance} method. * In this implementation:

* *
    *
  • {@link #performIteration(ArrayContext)} method sequentially calls * {@link #asThinning(int directionIndex)} method and copies its result to * the result matrix for directionIndex=0,1,2,3,4,5,6,7. * It means, that all "objects" in the matrix (areas filled by 1 elements) * are "thinned" 8 times: from left direction, from left-top diagonal direction, etc. * Depending on the argument of an instantiation method, {@link #performIteration(ArrayContext)} may skip * calling {@link #asThinning asThinning} for odd directions (1,3,5,7). *
  • * *
  • {@link #done()} method returns true if the last iteration was unable to change the matrix: * all "objects" are already "thin".
  • * *
  • {@link #result()} method always returns the reference to the source matrix, passed to * {@link #getInstance getInstance} method.
  • *
* *

The algorithm, implemented by this class, guarantees that 8-connected "objects" * (areas filled by 1 elements) always stay 8-connected; * see {@link ThinningSkeleton} interface about the precise sense of this state. * The resulting "skeleton" are usually "thin" enough (1-pixel lines), * but some little not "thin" areas are possible. * An example of resulting skeleton:

* *
 * . . . . . . . .
 * . 1 1 . . . . .
 * . . . 1 . . . .
 * . . . 1 . . . .
 * . . . . 1 . . .
 * . . . . . 1 . .
 * . . . . . . 1 .
 * . . . . . . . .
 * 
* *

Examples of the result in the "bad" cases, when some areas cannot be "thinned" by this algorithm:

* *
 * . . . . . . . . .     . . . . . . . .     . . . . . . . .     . . . 1 . . . .
 * . . . 1 . 1 . . .     . . . . . . . .     . . . 1 . . . .     1 . . 1 . . 1 .
 * . . 1 . 1 . 1 . .     . . . . . . . .     1 . . 1 . . 1 .     . 1 . 1 . 1 . .
 * . 1 . 1 1 1 . 1 .     . . 1 . . 1 . .     . 1 . 1 . 1 . .     . . 1 1 1 . . .
 * . . 1 1 1 1 1 . .     . . . 1 1 . . .     . . 1 1 1 . . .     1 1 1 1 1 1 1 .
 * . 1 . 1 1 1 . 1 .     . . . 1 1 . . .     . . 1 1 1 . . .     . . 1 1 1 . . .
 * . . 1 . 1 . 1 . .     . . 1 . . 1 . .     . 1 . 1 . 1 . .     . 1 . 1 . 1 . .
 * . . . 1 . 1 . . .     . . . . . . . .     1 . . 1 . . 1 .     1 . . 1 . . 1 .
 * . . . . . . . . .     . . . . . . . .     . . . 1 . . . .     . . . 1 . . . .
 * 
* *

The left example can have any size: it is possible to construct very large area filled by 1, which cannot * be skeletonized. But it can be excluded by little * {@link net.algart.matrices.morphology.Morphology#closing(Matrix, * net.algart.math.patterns.Pattern, net.algart.matrices.morphology.Morphology.SubtractionMode) closing} * of the source matrix by the rectangle 2x1 before running skeletonization. * As an alternative, the left case can be processed by {@link Quadruple3x5ThinningSkeleton2D} algorithm, * performed after this skeletonization.

* *

This class is based on {@link Matrices#asShifted Matrices.asShifted} method * with some elementwise logical operations (AND, OR, NOT). * So, the matrix is supposed to be infinitely pseudo-cyclically continued, as well * {@link 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}.

* *

This class may be applied to a matrix with any number of dimensions, * but it is designed for 2-dimensional case: all other dimensions will be ignored.

* *

This class is not thread-safe, but is thread-compatible * and can be synchronized manually, if multithreading access is necessary.

* * @author Daniel Alievsky * @see WeakOctupleThinningSkeleton2D */ public class OctupleThinningSkeleton2D extends AbstractThinningSkeleton2D implements ThinningSkeleton { private static final int[][] SKELETON_XP = { {1, 0}, // 0: -A2 {-1, 0}, // 1: -C2 {1, 1}, // 2: -A1 {0, 1}, // 3: -B1 {1, -1}, // 4: -A3 {0, -1}, // 5: -B3 {0, 1, 0, -1, -1, 1, -1, -1}, // 6: -B1,-B3,-C1,-C3 }; private static final int[][] SKELETON_YP = rotate90(SKELETON_XP); private static final int[][] SKELETON_XM = rotate180(SKELETON_XP); private static final int[][] SKELETON_YM = rotate270(SKELETON_XP); private static final int[][][] QUADRUPLE_SKELETON = { SKELETON_XP, SKELETON_YP, SKELETON_XM, SKELETON_YM }; private static final int[][] SKELETON_XPYP = { {1, 0, 1, 1, 0, 1}, // 0: -A2,-A1,-B1 {0, -1, -1, 0}, // 1: -B3,-C2 {1, -1, 1, 0, 1, 1, 0, 1, -1, 1}, // 2: -A3,-A2,-A1,-B1,-C1 }; private static final int[][] SKELETON_XMYP = rotate90(SKELETON_XPYP); private static final int[][] SKELETON_XMYM = rotate180(SKELETON_XPYP); private static final int[][] SKELETON_XPYM = rotate270(SKELETON_XPYP); private static final int[][][] DIAGONAL_SKELETON = { SKELETON_XPYP, SKELETON_XMYP, SKELETON_XMYM, SKELETON_XPYM }; private final boolean topological; private OctupleThinningSkeleton2D(ArrayContext arrayContext, Matrix matrix, boolean diagonalThinning, boolean topological) { super(arrayContext, matrix, true, diagonalThinning); // false may be passed here for debugging this.topological = topological; } /** * Creates new instance of this class. * *

If the diagonalThinning argument is false, the algorithm will skip thinning * along diagonal directions (directionIndex=1,3,5,7). * The result will be still correct, but the lines of the skeleton will be not so even. * *

If the topological is true, the algorithm doesn't stop when all objects in the matrix * become "thin" (1-pixel thickness), but continues shortening all "free ends" of all skeleton lines, * while there is at least one "free end". As a result, objects that have no "holes" will be removed at all, * objects that have 1 hole will be transformed into 1-pixel closed line ("ring"), etc. * This mode essentially slows down the algorithm. * * @param context the {@link #context() context} that will be used by this object; * can be {@code null}, then it will be ignored. * @param matrix the bit matrix that should be processed and returned by {@link #result()} method. * @param diagonalThinning whether the algorithm will perform diagonal thinning; usually true. * @param topological whether the algorithm will shorten isolated thin lines with "free ends". * @return new instance of this class. * @throws NullPointerException if matrix argument is {@code null}. */ public static OctupleThinningSkeleton2D getInstance(ArrayContext context, Matrix matrix, boolean diagonalThinning, boolean topological) { return new OctupleThinningSkeleton2D(context, matrix, diagonalThinning, topological); } /** * Creates new instance of this class. * Equivalent to {@link #getInstance(ArrayContext, Matrix, boolean, boolean) * getInstance(context, matrix, true, false)}. * * @param context the {@link #context() context} that will be used by this object; * can be {@code null}, then it will be ignored. * @param matrix the bit matrix that should be processed and returned by {@link #result()} method. * @return new instance of this class. * @throws NullPointerException if matrix argument is {@code null}. */ public static OctupleThinningSkeleton2D getInstance(ArrayContext context, Matrix matrix) { return new OctupleThinningSkeleton2D(context, matrix, true, false); } @Override public long estimatedNumberOfIterations() { return ThinningTools.estimatedNumberOfIterations(result, topological); } // WARNING: SUN BUG IN javadoc UTILITY (1.6.0_04, 1.7.0-ea)! // Below we cannot write "{@link #result()}" - it leads to ClassCastException in javadoc. /** * Returns current {@link IterativeArrayProcessor#result() result()} matrix thinned along the given direction. * The result is "lazy": it is only a view of the current matrix. * *

The precise algorithm of thinning is not documented. * Generally speaking, the "thinning" means removing elements * from the boundary of any "object" (area of the matrix filled by 1). * directionIndex specifies the "eroded side" of objects, * or the direction of thinning:

    *
  • 0 means removing elements from the left, i.e. from the side (x−1,y),
  • *
  • 1 means "diagonal" removal from the side (x−1,y−1),
  • *
  • 2 means removal from the side (x,y−1),
  • *
  • 3 means "diagonal" removal from the side (x+1,y−1),
  • *
  • 4 means removal from the right, i.e. from the side (x+1,y),
  • *
  • 5 means "diagonal" removal from the side (x+1,y+1),
  • *
  • 6 means removal from the side (x,y+1),
  • *
  • 7 means "diagonal" removal from the side (x−1,y+1).
  • *
* *

Though the algorithm is not documented, there are the following guarantees: *

    *
  • this algorithm never sets zero elements to unit: if the element of the current matrix * with some coordinates (x0, y0) is 0, * then the element with the same coordinates in the returned matrix is also 0;
  • * *
  • each element of the returned matrix with coordinates * (x0, y0) * depends only on the elements in 3x3 aperture * x0−1≤xx0+1, * y0−1≤yy0+1 * of the current matrix;
  • * *
  • if all elements of the current matrix in 3x3 aperture * x0−1≤xx0+1, * y0−1≤yy0+1 * are inside the matrix (i.e. 1≤x0dimX−2, * 1≤y0dimY−2, * dimX and dimY are dimensions of the matrix) * and all they are equal to 1, then the element (x0, y0) * in the returned matrix will be equal to 1.
  • *
* * @param directionIndex the direction of thinning, from 0 to 7. * @return the thinned view if the current {@link #result()} matrix. * @throws IllegalArgumentException if directionIndex is not in 0..7 range. */ @Override public Matrix asThinning(int directionIndex) { if (directionIndex < 0 || directionIndex > 7) throw new IllegalArgumentException("Illegal directionIndex = " + directionIndex + " (must be 0..7)"); if (directionIndex % 2 == 0) { return asQuadrupleThinning(result, directionIndex / 2, topological); } else { return asDiagonalThinning(result, directionIndex / 2, topological); } } /** * Returns a brief string description of this object. * * @return a brief string description of this object. */ @Override public String toString() { return "octuple thinning 2D skeletonizer, " + (diagonalThinning ? "8 steps" : "4 steps") + (topological ? ", topological mode" : ""); } private static Matrix asQuadrupleThinning(Matrix matrix, int directionIndex, boolean topological) { int[][] points = QUADRUPLE_SKELETON[directionIndex]; // Comments below suppose the case SKELETON_XP (directionIndex = 0) // We consider the 3x3 aperture of bit (0 or 1) elements: // A1 B1 C1 // A2 1 C2 // A3 B3 C3 // The central element can be supposed to be 1, because the last operation of the method is "and" // (if the central element is 0, all other calculations have no effect). // We need to clear the central element to 0 in some "thinning" situation. Matrix internal = shift(matrix, points[0]); // -A2 Matrix internalOrThin = or(internal, not(shift(matrix, points[1]))); // -C2 // internalOrThin is 0 if ~A2 & C2 (it is a left boundary and not both A2 and C2 are 0): candidate for removal // If internalOrThin is 1, the central element must never be cleared. // From this moment, we may suppose that it is 0, i.e. A2=0 and C2=1: // if not, other calculations have no effect due to the last "or" operation. Matrix topArticulation = andNot(shift(matrix, points[2]), shift(matrix, points[3])); // points[2] = -A1, points[3] = -B1 // topArticulation = 1 if A1 & ~B1: a local articulation point Matrix bottomArticulation = andNot(shift(matrix, points[4]), shift(matrix, points[5])); // points[4] = -A3, points[5] = -B3 // bottomArticulation = 1 if A3 & ~B3: a local articulation point Matrix notRemoved; if (topological) { notRemoved = or(internalOrThin, topArticulation, bottomArticulation); } else { Matrix dilation4Points = or(shifts(matrix, points[6])); // points[6] = {-B1,-B3,-C1,-C3} // dilation4Points is 0 if B1=B3=C1=C3=0: we have a "free end" of a horizontal line notRemoved = or(internalOrThin, topArticulation, bottomArticulation, not(dilation4Points)); } return and(matrix, notRemoved); } static Matrix asDiagonalThinning(Matrix matrix, int directionIndex, boolean topological) { int[][] points = DIAGONAL_SKELETON[directionIndex]; // Comments below suppose the case SKELETON_XPYP (directionIndex = 0) // We consider the 3x3 aperture of bit (0 or 1) elements: // A1 B1 C1 // A2 1 C2 // A3 B3 C3 // The central element can be supposed to be 1, because the last operation of the method is "and" // (if the central element is 0, all other calculations have no effect). // We need to clear the central element to 0 in some "thinning" situation. Matrix dilationInvCorner3Points = or(shifts(matrix, points[0])); // points[0] = {-A2,-A1,-B1} // dilationInvCorner3Points is 0 if all corner A2,A1,B1 is 0: we are at the left-top boundary. // From this moment, we may suppose that it is true (A2=A1=B1=0) // if not (dilationY3Points=1), other calculations of weakErosion // have no effect due to the "or" operation. Matrix erosionCorner3Points = and(shifts(matrix, points[1])); // points[1] = {-B3,-C2} // erosionCorner3Points is 1 if there is a corner 2x2 (0,0;1,0;0,1) filled by 1 and containing the center. // Because we already know that the center is 1 and A2=B1=0, then erosionCorner3Points=B3&C2. // We allow to clear the center only if this opening =1 (we are not on a thin diagonal line A3..C1). Matrix weakErosion = or(dilationInvCorner3Points, not(erosionCorner3Points)); // Removing the center, if dilationInvCorner3Points=0 (A2=A1=B1=0) and erosionCorner3Points=1 (B3=C2=1). if (topological) { Matrix dilation5Points = or(shifts(matrix, points[2])); // points[2] = {-A3,-A2,-A1,-B1,-C1} return and(matrix, weakErosion, dilation5Points); // In addition, removing the center if these 5 points are 0: a "free end" of the diagonal line A1..C3. } else { return and(matrix, weakErosion); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy