Skip to content

Commit

Permalink
Release 0.5 (#2)
Browse files Browse the repository at this point in the history
* Added perspective, projection and rubber sheet transform tools

* Bumped documentation version to 0.0.5
  • Loading branch information
defano authored Jul 2, 2017
1 parent 676d0a4 commit af8eb36
Show file tree
Hide file tree
Showing 25 changed files with 527 additions and 141 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@

.idea/*
*.iml
target/*
target/*
*.bin
.gradle/*
27 changes: 12 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ Javadocs [are available here](https://defano.github.io/jmonet/docs/).

* Offers a standard suite of paint tools with common modifier-key constraints (e.g., hold shift to snap lines to nearest 15-degree angle).
* Painting canvas supports undo and redo operations on all paint tool changes.
* Includes affine transform tools including flip, quadrant rotate, free-rotate, and shear.
* Includes affine and non-affine transform tools including flip, rotate, shear, perspective and projection.
* Painted images are scalable (displayed within a scrollable pane) and tools can be snapped to a grid.
* Lightweight toolkit integrates easily into Swing and JavaFX applications and has no transitive dependencies.
* Backed by a standard, Java `BufferedImage`; easy to import existing images or save changes.
* Lightweight toolkit integrates easily into Swing and JavaFX applications.
* All operations are backed by a standard Java `BufferedImage`; easy to import existing images and save changes.

## Paint Tools

Expand Down Expand Up @@ -42,15 +42,18 @@ Icon | Tool | Description
Tool | Description
----------------| -------------
Magnifier | Zoom in (scale the canvas) at the location clicked; hold `shift` to zoom out or `ctrl` to restore normal zoom.
Rotate | Define a selection outline, then use the drag handle to free-rotate the selected graphic around its center.
Slant | Define a selection rectangle, then use the drag handles to apply an affine shear transform to the selected graphic.
Scale | Define a selection rectangle, then stretch or shrink the selected image by dragging a handle.
Rotate | Define a selection, then use the drag handle to free-rotate the selected graphic around its center.
Slant | Define a selection, then use the drag handles to apply an affine shear transform to the selected graphic.
Scale | Define a selection, then expand or shrink the selected image by dragging a handle.
Perspective | Define a selection, then use the drag handles to warp the image onto an isosceles trapezoid, providing the effect of the left or right side of the image appearing nearer or farther from the viewer.
Projection | Define a selection, then use the drag handles to project the image onto the geometry of an arbitrary quadrilateral.
Rubber Sheet | Similar to the projection transform, but utilizes a "rubber sheet" algorithm that preserves relative position over linearity.

#### Static transforms

Selected images can be flipped horizontally, vertically or rotated 90 degrees clockwise or counterclockwise via the Selection or Lasso tools.

Once a selection has been made, invoke one of the following methods on the SelectionTool object:
Once a selection has been made, invoke one of the following methods on the `SelectionTool` object:

```
void rotateLeft();
Expand All @@ -69,7 +72,7 @@ JMonet is published to Maven Central; include the library in your Maven project'
<dependency>
<groupId>com.defano.jmonet</groupId>
<artifactId>jmonet</artifactId>
<version>0.0.4</version>
<version>0.0.5</version>
</dependency>
```

Expand All @@ -81,7 +84,7 @@ repositories {
}
dependencies {
compile 'com.defano.jmonet:jmonet:0.0.4'
compile 'com.defano.jmonet:jmonet:0.0.5'
}
```

Expand Down Expand Up @@ -221,9 +224,3 @@ Place the canvas(es) and/or other UI components in a `LayeredPane`. Use the `Lay
#### What about vector graphic tools (i.e., "draw" apps)?

Sorry, not the intent of this library. That said, many pieces of this library could be leveraged for such a tool...

#### Do you have tools for perspective, distort or other non-affine transforms?

Not yet. The Java Advanced Imaging libraries that provide the underlying algorithms for more complex transformations are licensed separately from the Java JDK and cannot automatically be "pulled in" to a project via build tools like Maven or Gradle (at least not legally). Thus, including such tools would make distributing and using this library a real PITA.

I might someday offer an "extension pack" JAR that has these tools but requires users to download and install Oracle's JAI library in order to compile them.
4 changes: 2 additions & 2 deletions docs/com/defano/jmonet/tools/util/FloodFill.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ <h2 title="Class FloodFill" class="title">Class FloodFill</h2>
<li>java.lang.Object</li>
<li>
<ul class="inheritance">
<li>com.defano.jmonet.tools.util.FloodFill</li>
<li>com.defano.jmonet.algo.FloodFill</li>
</ul>
</li>
</ul>
Expand Down Expand Up @@ -148,7 +148,7 @@ <h3>Method Summary</h3>
</tr>
<tr id="i0" class="altColor">
<td class="colFirst"><code>static void</code></td>
<td class="colLast"><code><span class="memberNameLink"><a href="../../../../../com/defano/jmonet/tools/util/FloodFill.html#floodFill-int-int-java.awt.Rectangle-com.defano.jmonet.tools.util.FillFunction-java.util.function.Predicate-">floodFill</a></span>(int&nbsp;x,
<td class="colLast"><code><span class="memberNameLink"><a href="../../../../../com/defano/jmonet/tools/util/FloodFill.html#floodFill-int-int-java.awt.Rectangle-com.defano.jmonet.algo.FillFunction-java.util.function.Predicate-">floodFill</a></span>(int&nbsp;x,
int&nbsp;y,
java.awt.Rectangle&nbsp;bounds,
<a href="../../../../../com/defano/jmonet/tools/util/FillFunction.html" title="interface in com.defano.jmonet.tools.util">FillFunction</a>&nbsp;fill,
Expand Down
2 changes: 1 addition & 1 deletion docs/com/defano/jmonet/tools/util/Transform.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ <h2 title="Class Transform" class="title">Class Transform</h2>
<li>java.lang.Object</li>
<li>
<ul class="inheritance">
<li>com.defano.jmonet.tools.util.Transform</li>
<li>com.defano.jmonet.algo.Transform</li>
</ul>
</li>
</ul>
Expand Down
2 changes: 1 addition & 1 deletion docs/index-files/index-6.html
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ <h2 class="title">F</h2>
</dd>
<dt><span class="memberNameLink"><a href="../com/defano/jmonet/tools/util/FloodFill.html#FloodFill--">FloodFill()</a></span> - Constructor for class com.defano.jmonet.tools.util.<a href="../com/defano/jmonet/tools/util/FloodFill.html" title="class in com.defano.jmonet.tools.util">FloodFill</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="../com/defano/jmonet/tools/util/FloodFill.html#floodFill-int-int-java.awt.Rectangle-com.defano.jmonet.tools.util.FillFunction-java.util.function.Predicate-">floodFill(int, int, Rectangle, FillFunction, Predicate&lt;Point&gt;)</a></span> - Static method in class com.defano.jmonet.tools.util.<a href="../com/defano/jmonet/tools/util/FloodFill.html" title="class in com.defano.jmonet.tools.util">FloodFill</a></dt>
<dt><span class="memberNameLink"><a href="../com/defano/jmonet/tools/util/FloodFill.html#floodFill-int-int-java.awt.Rectangle-com.defano.jmonet.algo.FillFunction-java.util.function.Predicate-">floodFill(int, int, Rectangle, FillFunction, Predicate&lt;Point&gt;)</a></span> - Static method in class com.defano.jmonet.tools.util.<a href="../com/defano/jmonet/tools/util/FloodFill.html" title="class in com.defano.jmonet.tools.util">FloodFill</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="../com/defano/jmonet/model/ImmutableProvider.html#from-com.defano.jmonet.model.Provider-">from(Provider&lt;T&gt;)</a></span> - Static method in class com.defano.jmonet.model.<a href="../com/defano/jmonet/model/ImmutableProvider.html" title="class in com.defano.jmonet.model">ImmutableProvider</a></dt>
<dd>&nbsp;</dd>
Expand Down
4 changes: 2 additions & 2 deletions javadoc/com/defano/jmonet/tools/util/FloodFill.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ <h2 title="Class FloodFill" class="title">Class FloodFill</h2>
<li>java.lang.Object</li>
<li>
<ul class="inheritance">
<li>com.defano.jmonet.tools.util.FloodFill</li>
<li>com.defano.jmonet.algo.FloodFill</li>
</ul>
</li>
</ul>
Expand Down Expand Up @@ -148,7 +148,7 @@ <h3>Method Summary</h3>
</tr>
<tr id="i0" class="altColor">
<td class="colFirst"><code>static void</code></td>
<td class="colLast"><code><span class="memberNameLink"><a href="../../../../../com/defano/jmonet/tools/util/FloodFill.html#floodFill-int-int-java.awt.Rectangle-com.defano.jmonet.tools.util.FillFunction-java.util.function.Predicate-">floodFill</a></span>(int&nbsp;x,
<td class="colLast"><code><span class="memberNameLink"><a href="../../../../../com/defano/jmonet/tools/util/FloodFill.html#floodFill-int-int-java.awt.Rectangle-com.defano.jmonet.algo.FillFunction-java.util.function.Predicate-">floodFill</a></span>(int&nbsp;x,
int&nbsp;y,
java.awt.Rectangle&nbsp;bounds,
<a href="../../../../../com/defano/jmonet/tools/util/FillFunction.html" title="interface in com.defano.jmonet.tools.util">FillFunction</a>&nbsp;fill,
Expand Down
2 changes: 1 addition & 1 deletion javadoc/com/defano/jmonet/tools/util/Transform.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ <h2 title="Class Transform" class="title">Class Transform</h2>
<li>java.lang.Object</li>
<li>
<ul class="inheritance">
<li>com.defano.jmonet.tools.util.Transform</li>
<li>com.defano.jmonet.algo.Transform</li>
</ul>
</li>
</ul>
Expand Down
2 changes: 1 addition & 1 deletion javadoc/index-files/index-6.html
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ <h2 class="title">F</h2>
</dd>
<dt><span class="memberNameLink"><a href="../com/defano/jmonet/tools/util/FloodFill.html#FloodFill--">FloodFill()</a></span> - Constructor for class com.defano.jmonet.tools.util.<a href="../com/defano/jmonet/tools/util/FloodFill.html" title="class in com.defano.jmonet.tools.util">FloodFill</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="../com/defano/jmonet/tools/util/FloodFill.html#floodFill-int-int-java.awt.Rectangle-com.defano.jmonet.tools.util.FillFunction-java.util.function.Predicate-">floodFill(int, int, Rectangle, FillFunction, Predicate&lt;Point&gt;)</a></span> - Static method in class com.defano.jmonet.tools.util.<a href="../com/defano/jmonet/tools/util/FloodFill.html" title="class in com.defano.jmonet.tools.util">FloodFill</a></dt>
<dt><span class="memberNameLink"><a href="../com/defano/jmonet/tools/util/FloodFill.html#floodFill-int-int-java.awt.Rectangle-com.defano.jmonet.algo.FillFunction-java.util.function.Predicate-">floodFill(int, int, Rectangle, FillFunction, Predicate&lt;Point&gt;)</a></span> - Static method in class com.defano.jmonet.tools.util.<a href="../com/defano/jmonet/tools/util/FloodFill.html" title="class in com.defano.jmonet.tools.util">FloodFill</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="../com/defano/jmonet/model/ImmutableProvider.html#from-com.defano.jmonet.model.Provider-">from(Provider&lt;T&gt;)</a></span> - Static method in class com.defano.jmonet.model.<a href="../com/defano/jmonet/model/ImmutableProvider.html" title="class in com.defano.jmonet.model">ImmutableProvider</a></dt>
<dd>&nbsp;</dd>
Expand Down
10 changes: 9 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,22 @@
<packaging>jar</packaging>
<groupId>com.defano.jmonet</groupId>
<artifactId>jmonet</artifactId>
<version>0.0.4</version>
<version>0.0.5</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>gov.nist.math</groupId>
<artifactId>jama</artifactId>
<version>1.0.3</version>
</dependency>
</dependencies>

<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.defano.jmonet.tools.util;
package com.defano.jmonet.algo;

import java.awt.*;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.defano.jmonet.tools.util;
package com.defano.jmonet.algo;

import java.awt.*;
import java.util.Stack;
Expand Down
175 changes: 175 additions & 0 deletions src/main/java/com/defano/jmonet/algo/Projection.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.defano.jmonet.algo;

import Jama.Matrix;
import com.defano.jmonet.model.FlexQuadrilateral;

import java.awt.image.BufferedImage;

public class Projection {

/**
* Performs a homography projection of the source image onto an arbitrary quadrilateral. This method assumes that
* the source image is the same dimensions as the bounds of the quadrilateral. Additional geometric limitations
* of this algorithm exist which have been accounted for in {@link FlexQuadrilateral} (i.e., top-left bounds
* must remain to the left of top-right or bottom-right points).
*
* This magical incantation of linear algebra is as described by CorrMap:
* http://www.corrmap.com/features/homography_transformation.php
*
* @param source The source image to be projected
* @param projection The geometry on which to project the image
* @return The transformed image
*/
public static BufferedImage project(BufferedImage source, FlexQuadrilateral projection) {

double x1, y1, x2, y2, x3, y3, x4, y4, X1, Y1, X2, Y2, X3, Y3, X4, Y4;

int imageWidth = projection.width();
int imageHeight = projection.height();

// Destination image geometry (defined by projection)
x1 = Math.abs(projection.getTopLeft().getX());
y1 = Math.abs(projection.getTopLeft().getY());
x2 = Math.abs(projection.getTopRight().getX());
y2 = Math.abs(projection.getTopRight().getY());
x3 = Math.abs(projection.getBottomRight().getX());
y3 = Math.abs(projection.getBottomRight().getY());
x4 = Math.abs(projection.getBottomLeft().getX());
y4 = Math.abs(projection.getBottomLeft().getY());

// Source image geometry (assumed to be the bounds rect of projection)
X1 = 0;
Y1 = 0;
X2 = imageWidth - 1;
Y2 = 0;
X3 = imageWidth - 1;
Y3 = imageHeight - 1;
X4 = 0;
Y4 = imageHeight - 1;

double M_a[][] =
{ {x1, y1, 1, 0, 0, 0, -x1 * X1, -y1 * X1},
{x2, y2, 1, 0, 0, 0, -x2 * X2, -y2 * X2},
{x3, y3, 1, 0, 0, 0, -x3 * X3, -y3 * X3},
{x4, y4, 1, 0, 0, 0, -x4 * X4, -y4 * X4},
{0, 0, 0, x1, y1, 1, -x1 * Y1, -y1 * Y1},
{0, 0, 0, x2, y2, 1, -x2 * Y2, -y2 * Y2},
{0, 0, 0, x3, y3, 1, -x3 * Y3, -y3 * Y3},
{0, 0, 0, x4, y4, 1, -x4 * Y4, -y4 * Y4}
};

double M_b[][] = {{X1}, {X2}, {X3}, {X4}, {Y1}, {Y2}, {Y3}, {Y4}};

Matrix A = new Matrix(M_a);
Matrix B = new Matrix(M_b);
Matrix C = A.solve(B);

double a = C.get(0, 0); // fixed scale factor in X direction with scale Y unchanged
double b = C.get(1, 0); // scale factor in X direction proportional to Y distance from origin
double c = C.get(2, 0); // origin translation in X direction
double d = C.get(3, 0); // scale factor in Y direction proportional to X distance from origin
double e = C.get(4, 0); // fixed scale factor in Y direction with scale X unchanged
double f = C.get(5, 0); // origin translation in Y direction
double g = C.get(6, 0); // proportional scale factors X and Y in function of X
double h = C.get(7, 0); // proportional scale factors X and Y in function of Y

BufferedImage output = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);

for (int i = 0; i < imageWidth; i++) {
for (int j = 0; j < imageHeight; j++) {
int x = (int) (((a * i) + (b * j) + c) / ((g * i) + (h * j) + 1));
int y = (int) (((d * i) + (e * j) + f) / ((g * i) + (h * j) + 1));

if (x > 0 && x < source.getWidth() && y > 0 && y < source.getHeight() && i > 0 && i < output.getWidth() && j > 0 && j< output.getHeight()) {
int p = source.getRGB(x, y);
output.setRGB(i, j, p);
}
}
}

return output;
}

/**
* Performs a rubber sheet projection of the source image onto an arbitrary quadrilateral. This method assumes that
* the source image is the same dimensions as the bounds of the quadrilateral. Additional geometric limitations
* of this algorithm exist which have been accounted for in {@link FlexQuadrilateral} (i.e., top-left bounds
* must remain to the left of top-right or bottom-right points).
*
* This magical incantation of linear algebra is as described by CorrMap:
* http://www.corrmap.com/features/rubber-sheeting_transformation.php
*
* @param source The source image to be projected
* @param projection The geometry on which to project the image
* @return The transformed image
*/
public static BufferedImage rubberSheet(BufferedImage source, FlexQuadrilateral projection) {

double x1, y1, x2, y2, x3, y3, x4, y4, X1, Y1, X2, Y2, X3, Y3, X4, Y4;

int sourceWidth = projection.width();
int sourceHeight = projection.height();

// Destination image geometry (defined by projection)
x1 = Math.abs(projection.getTopLeft().getX());
y1 = Math.abs(projection.getTopLeft().getY());
x2 = Math.abs(projection.getTopRight().getX());
y2 = Math.abs(projection.getTopRight().getY());
x3 = Math.abs(projection.getBottomRight().getX());
y3 = Math.abs(projection.getBottomRight().getY());
x4 = Math.abs(projection.getBottomLeft().getX());
y4 = Math.abs(projection.getBottomLeft().getY());

// Source image geometry (assumed to be the bounds rect of projection)
X1 = 0;
Y1 = 0;
X2 = sourceWidth - 1;
Y2 = 0;
X3 = sourceWidth - 1;
Y3 = sourceHeight - 1;
X4 = 0;
Y4 = sourceHeight - 1;

double M_a[][] =
{ {x1 * y1, x1, y1, 1, 0, 0, 0, 0},
{x2 * y2, x2, y2, 1, 0, 0, 0, 0},
{x3 * y3, x3, y3, 1, 0, 0, 0, 0},
{x4 * y4, x4, y4, 1, 0, 0, 0, 0},
{0, 0, 0, 0, x1 * y1, x1, y1, 1},
{0, 0, 0, 0, x2 * y2, x2, y2, 1},
{0, 0, 0, 0, x3 * y3, x3, y3, 1},
{0, 0, 0, 0, x4 * y4, x4, y4, 1}
};

double M_b[][] = {{X1}, {X2}, {X3}, {X4}, {Y1}, {Y2}, {Y3}, {Y4}};

Matrix A = new Matrix(M_a);
Matrix B = new Matrix(M_b);
Matrix C = A.solve(B);

double a = C.get(0, 0); // scale factor in X direction proportional to the multiplication X * Y
double b = C.get(1, 0); // fixed scale factor in X direction with scale Y unchanged
double c = C.get(2, 0); // scale factor in X direction proportional to Y distance from origin
double d = C.get(3, 0); // origin translation in X direction
double e = C.get(4, 0); // scale factor in Y direction proportional to the multiplication X * Y
double f = C.get(5, 0); // fixed scale factor in Y direction with scale X unchanged
double g = C.get(6, 0); // scale factor in Y direction proportional to X distance from origin
double h = C.get(7, 0); // origin translation in Y direction

BufferedImage output = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_INT_ARGB);

for (int i = 0; i < sourceWidth; i++) {
for (int j = 0; j < sourceHeight; j++) {
int x = (int) ((a * i * j) + (b * i) + (c * j) + d);
int y = (int) ((e * i * j) + (f * i) + (g * j) + h);

if (x > 0 && x < source.getWidth() && y > 0 && y < source.getHeight() && i > 0 && i < output.getWidth() && j > 0 && j< output.getHeight()) {
int p = source.getRGB(x, y);
output.setRGB(i, j, p);
}
}
}

return output;
}
}
Loading

0 comments on commit af8eb36

Please sign in to comment.