Skip to content

Commit

Permalink
add align()
Browse files Browse the repository at this point in the history
  • Loading branch information
micycle1 committed Jul 27, 2023
1 parent 3ce7aa5 commit f4bf271
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* `toCentroidDualGraph()` to `PGS_Conversion`. Converts a mesh-like PShape into its centroid-based undirected dual-graph.
* `isValid()` to `PGS_ShapePredicates`. Checks if a PShape is valid, and reports the validation error if it is invalid.
* `obstaclePack()` to `PGS_CirclePacking`. Packs circles of varying radii within a given shape, whilst respecting pointal obstacles.
* `align()` to `PGS_Transformation`. Aligns one polygon shape to another, by finding the optimal transformation.

### Changed
* Reimplemented `PGS_Processing.equalParition()`. New algorithm is ~2x faster. Also removed `precise` parameter from method signature (no longer necessary).
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,19 @@ Much of the functionality (but by no means all) is demonstrated below:
<td align="center" valign="center"><b>Resize</td>
<td align="center" valign="center"><b>Homothetic Transformation</td>
<td align="center" valign="center"><b>Shear</td>
<td align="center" valign="center"><b>Align</td>
</tr>
<tr>
<td valign="top" width="25%"><img src="resources/transform/resize.gif"></td>
<td valign="top" width="25%"><img src="resources/transform/homothetic.gif"></td>
<td valign="top" width="25%"><img src="resources/transform/shear.gif"></td>
<td valign="top" width="25%"><img src="resources/transform/align.gif"></td>
</tr>
<tr>
<td align="center" valign="center"></td>
<td align="center" valign="center">Projection-transform a shape with respect to a fixed point.</td>
<td align="center" valign="center"></td>
<td align="center" valign="center">Maximum-overlap alignment.</td>
</tr>
</table>

Expand Down
Binary file added resources/transform/align.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions src/main/java/micycle/pgs/PGS_Transformation.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.locationtech.jts.geom.util.AffineTransformation;
import org.locationtech.jts.operation.distance.IndexedFacetDistance;

import micycle.pgs.commons.ProcrustesAlignment;
import processing.core.PShape;
import processing.core.PVector;

Expand Down Expand Up @@ -308,6 +309,9 @@ public static PShape translateTo(PShape shape, double x, double y) {
*/
public static PShape translateCentroidTo(PShape shape, double x, double y) {
Geometry g = fromPShape(shape);
if (g.getNumPoints() == 0) {
return shape;
}
Point c = g.getCentroid();
double translateX = x - c.getX();
double translateY = y - c.getY();
Expand All @@ -332,6 +336,9 @@ public static PShape translateCentroidTo(PShape shape, double x, double y) {
*/
public static PShape translateEnvelopeTo(PShape shape, double x, double y) {
Geometry g = fromPShape(shape);
if (g.getNumPoints() == 0) {
return shape;
}
Point c = g.getEnvelope().getCentroid();
double translateX = x - c.getX();
double translateY = y - c.getY();
Expand Down Expand Up @@ -415,6 +422,73 @@ public static PShape homotheticTransformation(PShape shape, PVector center, doub
return toPShape(PGS.GEOM_FACTORY.createPolygon(lr, holes));
}

/**
* Aligns one polygon shape to another, using Procrustes analysis to find the
* optimal transformation. The transformation includes translation, rotation and
* scaling to maximize overlap between the two shapes.
*
* @param alignShape the polygon shape to be transformed and aligned to the
* other shape.
* @param baseShape the shape that the other shape will be aligned to.
* @return a new PShape that is the transformed and aligned version of
* sourceShape.
* @since 1.3.1
*/
public static PShape align(PShape sourceShape, PShape transformShape) {
return align(sourceShape, transformShape, 1);
}

/**
* Aligns one polygon shape to another, using Procrustes analysis to find the
* optimal transformation. The transformation includes translation, rotation and
* scaling to maximize overlap between the two shapes.
* <p>
* This method signature aligns the shape according to a provided ratio,
* indicating how much alignment transformation to apply.
*
* @param alignShape the polygon shape to be transformed and aligned to the
* other shape.
* @param baseShape the shape that the other shape will be aligned to.
* @param alignmentRatio a value in [0,1] indicating how much to transform the
* shape from its original position to its most aligned
* position. 0 means no transformation, 1 means maximum
* alignment.
* @return a new PShape that is the transformed and aligned version of
* sourceShape.
* @since 1.3.1
*/
public static PShape align(PShape alignShape, PShape baseShape, double alignmentRatio) {
final Geometry g1 = fromPShape(alignShape);
final Geometry g2 = fromPShape(baseShape);
if (g1.getGeometryType() != Geometry.TYPENAME_POLYGON || g2.getGeometryType() != Geometry.TYPENAME_POLYGON) {
throw new IllegalArgumentException("Inputs to align() must be polygons.");
}
if (((Polygon) g1).getNumInteriorRing() > 0 || ((Polygon) g2).getNumInteriorRing() > 0) {
throw new IllegalArgumentException("Polygon inputs to align() must be holeless.");
}

// both shapes need same vertex quantity
final int vertices = Math.min(alignShape.getVertexCount(), baseShape.getVertexCount()) - 1;
PShape sourceShapeT = alignShape;
PShape transformShapeT = baseShape;
if (alignShape.getVertexCount() > vertices) {
sourceShapeT = PGS_Morphology.simplifyDCE(alignShape, vertices);
}
if (baseShape.getVertexCount() > vertices) {
transformShapeT = PGS_Morphology.simplifyDCE(baseShape, vertices);
}

double[] m = ProcrustesAlignment.transform((Polygon) fromPShape(sourceShapeT), (Polygon) fromPShape(transformShapeT));

Coordinate c = g2.getCentroid().getCoordinate();
double scale = 1 + (m[2] - 1) * alignmentRatio;
AffineTransformation transform = AffineTransformation.scaleInstance(scale, scale, c.x, c.y).rotate(m[3] * alignmentRatio, c.x, c.y)
.translate(m[0] * alignmentRatio, m[1] * alignmentRatio);
Geometry aligned = transform.transform(g2);

return toPShape(aligned);
}

/**
* Rotates a shape around a given point.
*
Expand Down
175 changes: 175 additions & 0 deletions src/main/java/micycle/pgs/commons/ProcrustesAlignment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package micycle.pgs.commons;

import org.apache.commons.math3.linear.MatrixUtils;
import org.apache.commons.math3.linear.RealMatrix;
import org.apache.commons.math3.linear.SingularValueDecomposition;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Polygon;

/**
*
* The ProcrustesAlignment class provides methods for performing
* ProcrustesAlignment analysis, which is a technique for aligning and comparing
* geometric shapes. ProcrustesAlignment analysis aims to find the best
* transformation (translation, rotation, and scaling) that minimizes the
* differences between corresponding points of two shapes.
* <p>
* This class is particularly useful in shape matching and analysis tasks where
* the only permitted deformation modes are uniform scaling, rotation, and
* translation. It is commonly used in various fields such as computer vision,
* image processing, pattern recognition, and bioinformatics.
*
* @author Michael Carleton
*/
public class ProcrustesAlignment {

private ProcrustesAlignment() {
}

/**
* Performs <i>ProcrustesAlignment Analysis</i> to align two polygons.
* <p>
* Finds the optimal scaling, translation and rotation to best align
* <code>transformPolygon</code> with respect to <code>sourcePolygon</code>.
* <p>
* Note: the polygons should have the same number of vertices.
*
* @param sourcePolygon the first polygon
* @param transformPolygon the polygon to transform/align
* @return an array containing the optimal translation (x, y), scale, and
* rotation angle (radians, clockwise) to align transform polygon to
* source polygon.
*/
public static double[] transform(final Polygon sourcePolygon, final Polygon transformPolygon) {
final Coordinate[] coordsA = sourcePolygon.getExteriorRing().getCoordinates();
final Coordinate[] coordsB = transformPolygon.getExteriorRing().getCoordinates();

if (coordsA.length != coordsB.length) {
throw new IllegalArgumentException("Polygon exterior rings are different lengths!");
}

// Find optimal translation
Coordinate t = findTranslation(sourcePolygon, transformPolygon);

// Shift to origin (required for scaling & rotation)
Coordinate ca = sourcePolygon.getCentroid().getCoordinate();
Coordinate cb = transformPolygon.getCentroid().getCoordinate();
translate(coordsA, -ca.x, -ca.y);
translate(coordsB, -cb.x, -cb.y);

// Find optimal scale
double scale = findScale(coordsA, coordsB);

// Find optimal rotation
double theta = findRotation(coordsA, coordsB);

// Return transformation parameters
return new double[] { t.x, t.y, scale, theta };
}

/**
* Translates an array of coordinates by dx and dy.
*
* @param coords the array of coordinates to translate
* @param dx the translation in x
* @param dy the translation in y
*/
private static void translate(final Coordinate[] coords, final double dx, final double dy) {
for (Coordinate c : coords) {
c.x += dx;
c.y += dy;
}
}

/**
* Finds the optimal translation vector between two polygons.
*
* @param a the first polygon
* @param b the second polygon
* @return the translation vector as a Coordinate
*/
private static Coordinate findTranslation(Polygon a, Polygon b) {
Coordinate ca = a.getCentroid().getCoordinate();
Coordinate cb = b.getCentroid().getCoordinate();

return new Coordinate(ca.x - cb.x, ca.y - cb.y);

}

/**
* Finds the optimal scale factor to match the size of two coordinate arrays.
*
* @param aCoords the first coordinate array
* @param bCoords the second coordinate array
* @return the scale factor
*/
private static double findScale(final Coordinate[] aCoords, final Coordinate[] bCoords) {
double rmsd1 = getRMSD(aCoords);
double rmsd2 = getRMSD(bCoords);

return rmsd1 / rmsd2;
}

/**
* Calculates the RMSD (root mean square deviation) of a coordinate array.
*
* @param coords the array of coordinates
* @return the RMSD
*/
private static double getRMSD(final Coordinate[] coords) {
double sum = 0;

for (Coordinate c : coords) {
sum += c.x * c.x + c.y * c.y;
}

return Math.sqrt(sum / coords.length);
}

/**
* Finds the optimal rotation angle to align two coordinate arrays.
*
* @param c1 the first coordinate array
* @param c2 the second coordinate array
* @return the rotation angle in radians
*/
private static double findRotation(final Coordinate[] c1, final Coordinate[] c2) {
RealMatrix m = computeOptimalRotationMatrix(c1, c2);
double cosTheta = m.getEntry(0, 0);
double sinTheta = m.getEntry(1, 0);

double theta = Math.atan2(sinTheta, cosTheta);
return -theta; // NOTE negate for CW rotation
}

/**
* Computes the optimal rotation matrix to align two coordinate arrays using
* Kabsch algorithm.
* <p>
* Each array must be translated first, such that its centroid coincides with
* (0, 0).
*
* @param c1 the first coordinate array
* @param c2 the second coordinate array
* @return the 2x2 rotation matrix
*/
private static RealMatrix computeOptimalRotationMatrix(final Coordinate[] c1, final Coordinate[] c2) {
final double[][] covarianceMatrix = new double[2][2];
for (int i = 0; i < c1.length; i++) {
final double[] translatedSourcePoint = { c1[i].x, c1[i].y };
final double[] translatedTargetPoint = { c2[i].x, c2[i].y };

covarianceMatrix[0][0] += translatedSourcePoint[0] * translatedTargetPoint[0];
covarianceMatrix[0][1] += translatedSourcePoint[0] * translatedTargetPoint[1];
covarianceMatrix[1][0] += translatedSourcePoint[1] * translatedTargetPoint[0];
covarianceMatrix[1][1] += translatedSourcePoint[1] * translatedTargetPoint[1];
}

final RealMatrix covarianceMatrixMatrix = MatrixUtils.createRealMatrix(covarianceMatrix);

final SingularValueDecomposition svd = new SingularValueDecomposition(covarianceMatrixMatrix);
final RealMatrix rotationMatrix = svd.getV().multiply(svd.getUT());
return rotationMatrix;
}

}

0 comments on commit f4bf271

Please sign in to comment.