Skip to content

Commit

Permalink
fixup align params
Browse files Browse the repository at this point in the history
  • Loading branch information
micycle1 committed Aug 10, 2024
1 parent d4cff64 commit 1935eca
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 43 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed
* `urquhartFaces()`, `relativeNeighborFaces()`, `gabrielFaces()` and `spannerFaces()` from `PGS_Meshing` now preserve holes from the input.
* The `from` and `to` arguments for `align()` were the wrong way round.
* The output of `PGS_Morphology.smoothGaussian()` is no longer (slightly) affected by the vertex ordering of the input.
* The `transform` and `reference` arguments for `PGS_Transformation.align()` were the wrong way round.

### Removed
* `simplifyDCE(shape, targetNumVertices)` and `simplifyDCE(shape, vertexRemovalFraction)` in favour a single method that accepts a user-defined termination callback that is supplied with the current vertex candidate's coordinate, relevance score, and the number of vertices remaining.
Expand Down
100 changes: 68 additions & 32 deletions src/main/java/micycle/pgs/PGS_Transformation.java
Original file line number Diff line number Diff line change
Expand Up @@ -445,15 +445,15 @@ public static PShape homotheticTransformation(PShape shape, PVector center, doub
* 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.
* @param shapeToAlign the polygon shape to be transformed and aligned to the
* other shape.
* @param referenceShape 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.4.0
*/
public static PShape align(PShape alignShape, PShape baseShape) {
return align(alignShape, baseShape, 1);
public static PShape align(PShape shapeToAlign, PShape referenceShape) {
return align(shapeToAlign, referenceShape, 1);
}

/**
Expand All @@ -464,47 +464,83 @@ public static PShape align(PShape alignShape, PShape baseShape) {
* 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.
* @param shapeToAlign the polygon shape to be transformed and aligned to the
* reference shape.
* @param referenceShape the shape that the other shape will be aligned to.
* @param alignmentRatio a value between 0 and 1 indicating the degree of
* alignment transformation to apply.
* @return a new PShape that is the transformed and aligned version of
* alignShape.
* @since 1.4.0
*/
public static PShape align(PShape alignShape, PShape baseShape, double alignmentRatio) {
final Geometry g1 = fromPShape(alignShape);
final Geometry g2 = fromPShape(baseShape);
public static PShape align(PShape shapeToAlign, PShape referenceShape, double alignmentRatio) {
return align(shapeToAlign, referenceShape, alignmentRatio, true, true, true);
}

/**
* Aligns one polygon shape to another, allowing for control over the
* application of scaling, translation, and rotation transformations
* individually.
*
* @param shapeToAlign the polygon shape to be aligned to the reference
* shape.
* @param referenceShape the reference shape to which the
* <code>shapeToAlign</code> will be aligned.
* @param alignmentRatio a value between 0 and 1 indicating the degree of
* alignment transformation to apply.
* @param applyScale if true, applies scaling alignment
* @param applyTranslation if true, applies transformation alignment
* @param applyRotation if true, applies rotation alignment
* @return a new PShape that is the transformed and aligned version of
* <code>shapeToAlign</code>.
* @since 2.0
*/
public static PShape align(PShape shapeToAlign, PShape referenceShape, double alignmentRatio, boolean applyScale,
boolean applyTranslation, boolean applyRotation) {
final double[] params = getProcrustesParams(shapeToAlign, referenceShape);

final Geometry g1 = fromPShape(shapeToAlign);
Coordinate c = g1.getCentroid().getCoordinate();

double scale = applyScale ? 1 + (params[2] - 1) * alignmentRatio : 1;
double rotation = applyRotation ? params[3] * alignmentRatio : 0;
double translateX = applyTranslation ? params[0] * alignmentRatio : 0;
double translateY = applyTranslation ? params[1] * alignmentRatio : 0;

AffineTransformation transform = AffineTransformation.scaleInstance(scale, scale, c.x, c.y).rotate(rotation, c.x, c.y)
.translate(translateX, translateY);

Geometry aligned = transform.transform(g1);

return toPShape(aligned);
}

/**
* @return an array having 4 values: the optimal translation (x, y), scale, and
* rotation angle (radians, clockwise).
*/
private static double[] getProcrustesParams(PShape shapeToAlign, PShape referenceShape) {
final Geometry g1 = fromPShape(shapeToAlign);
final Geometry g2 = fromPShape(referenceShape);
if (!g1.getGeometryType().equals(Geometry.TYPENAME_POLYGON) || !g2.getGeometryType().equals(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());
PShape sourceShapeT = alignShape;
PShape transformShapeT = baseShape;
if (alignShape.getVertexCount() > vertices) {
sourceShapeT = PGS_Morphology.simplifyDCE(alignShape, (v, r, verticesRemaining) -> verticesRemaining <= vertices);
// both shapes need same vertex quantity, simplify rather than densify
final int vertices = Math.min(shapeToAlign.getVertexCount(), referenceShape.getVertexCount());
PShape referenceShapeT = referenceShape;
PShape shapeToAlignT = shapeToAlign;
if (shapeToAlign.getVertexCount() > vertices) {
shapeToAlignT = PGS_Morphology.simplifyDCE(shapeToAlign, (v, r, verticesRemaining) -> verticesRemaining <= vertices);
}
if (baseShape.getVertexCount() > vertices) {
transformShapeT = PGS_Morphology.simplifyDCE(baseShape, (v, r, verticesRemaining) -> verticesRemaining <= vertices);
if (referenceShape.getVertexCount() > vertices) {
referenceShapeT = PGS_Morphology.simplifyDCE(referenceShape, (v, r, verticesRemaining) -> verticesRemaining <= 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);
return ProcrustesAlignment.transform((Polygon) fromPShape(referenceShapeT), (Polygon) fromPShape(shapeToAlignT));
}

/**
Expand Down
21 changes: 11 additions & 10 deletions src/main/java/micycle/pgs/commons/ProcrustesAlignment.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,30 @@ 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>.
* <code>transformPolygon</code> with respect to <code>referencePolygon</code>.
* <p>
* Note: the polygons should have the same number of vertices.
*
* @param sourcePolygon the first polygon
* @param referencePolygon 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.
* @return an array having 4 values: the optimal translation (x, y), scale, and
* rotation angle (radians, clockwise) to best align the transform
* polygon to the reference polygon.
*/
public static double[] transform(final Polygon sourcePolygon, final Polygon transformPolygon) {
final Coordinate[] coordsA = sourcePolygon.getExteriorRing().getCoordinates();
public static double[] transform(final Polygon referencePolygon, final Polygon transformPolygon) {
final Coordinate[] coordsA = referencePolygon.getExteriorRing().getCoordinates();
final Coordinate[] coordsB = transformPolygon.getExteriorRing().getCoordinates();

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

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

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

0 comments on commit 1935eca

Please sign in to comment.