Adding a bitmap vectorization based on distance between two points;

fixed a bug in the tracking of origin when generating a bitmap. #16.
This commit is contained in:
Rob Greene 2018-06-23 16:50:05 -05:00
parent 13a6270723
commit efbf0973bb
6 changed files with 163 additions and 33 deletions

View File

@ -34,7 +34,7 @@ public class BitmapShape implements Shape {
}
public void insertColumn() {
origin.y++;
origin.x++;
for (List<Boolean> row : grid) {
row.add(0, Boolean.FALSE);
}
@ -45,7 +45,7 @@ public class BitmapShape implements Shape {
}
}
public void insertRow() {
origin.x++;
origin.y++;
grid.add(0, newRow(getWidth()));
}
public void addRow() {
@ -61,7 +61,7 @@ public class BitmapShape implements Shape {
origin.y = grid.size();
};
for (char pixel : line.toCharArray()) {
switch (pixel) {
switch (Character.toLowerCase(pixel)) {
case '+':
setOrigin.run();
// fall through to '.'
@ -138,7 +138,8 @@ public class BitmapShape implements Shape {
new SweepVectorization(this, VectorCommand.MOVE_DOWN, VectorCommand.MOVE_RIGHT),
new SweepVectorization(this, VectorCommand.MOVE_DOWN, VectorCommand.MOVE_LEFT),
new SweepVectorization(this, VectorCommand.MOVE_UP, VectorCommand.MOVE_RIGHT),
new SweepVectorization(this, VectorCommand.MOVE_UP, VectorCommand.MOVE_LEFT)
new SweepVectorization(this, VectorCommand.MOVE_UP, VectorCommand.MOVE_LEFT),
new EuclidianDistanceVectorization(this)
);
int byteLength = Integer.MAX_VALUE;
@ -249,4 +250,95 @@ public class BitmapShape implements Shape {
}
}
}
/**
* Encode a bitmap shape by using the Euclidean distance between plotted points to determine
* which vectors take precedence. Most of the math is captured in the Point class, which makes
* this much more straight-forward.
*/
public static class EuclidianDistanceVectorization implements Supplier<VectorShape> {
private List<Point> points;
private BitmapShape bitmapShape;
private VectorShape vshape;
public EuclidianDistanceVectorization(BitmapShape bitmapShape) {
this.bitmapShape = bitmapShape;
this.points = new ArrayList<>();
this.vshape = new VectorShape();
// Collect vector targets
for (int y=0; y<bitmapShape.getHeight(); y++) {
for (int x=0; x<bitmapShape.getWidth(); x++) {
if (bitmapShape.get(x,y)) {
points.add(new Point(x,y));
}
}
}
}
@Override
public VectorShape get() {
Point point = new Point(bitmapShape.origin);
boolean plotFirst = false;
while (!points.isEmpty()) {
Point target = null;
double distance = Double.MAX_VALUE;
for (Point candidate : points) {
double candidateDistance = point.distance(candidate);
if (distance >= candidateDistance) {
distance = candidateDistance;
target = candidate;
}
}
moveTo(plotFirst, point, target);
points.remove(target);
point = target;
plotFirst = true;
}
// Need to plot that last pixel
vshape.plotUp();
return vshape;
}
/**
* Move from origin to target. General strategy is to work from distance between points and bias the one that "loses"
* each time while resetting the one that "won". Hopefully this draws more of a jagged line than something entirely
* straight. (The goal being to prevent a bunch of plot up's in the worst case that doesn't encode nicely.)
* @param plotFirst Indicates if the first vector needs to plot; otherwise all other vectors will be moves
* @param origin Is the point of origin (which can be a plotted pixel)
* @param target Is the target point
*/
public void moveTo(boolean plotFirst, Point origin, Point target) {
if (origin.equals(target)) return;
VectorCommand xvector = origin.x < target.x ? VectorCommand.MOVE_RIGHT : VectorCommand.MOVE_LEFT;
VectorCommand yvector = origin.y < target.y ? VectorCommand.MOVE_DOWN : VectorCommand.MOVE_UP;
if (plotFirst) {
xvector = xvector.plot();
yvector = yvector.plot();
}
Point point = new Point(origin);
int xdist = Math.abs(point.x - target.x);
int ydist = Math.abs(point.y - target.y);
while (!point.equals(target)) {
if (xdist > ydist) {
xdist = Math.abs(point.x - target.x);
ydist += 1;
if (point.x != target.x) {
point.translate(xvector.xmove, xvector.ymove);
vshape.append(xvector);
xvector = xvector.move();
yvector = yvector.move();
}
} else {
xdist += 1;
ydist = Math.abs(point.y - target.y);
if (point.y != target.y) {
point.translate(yvector.xmove, yvector.ymove);
vshape.append(yvector);
xvector = xvector.move();
yvector = yvector.move();
}
}
}
}
}
}

View File

@ -43,4 +43,10 @@ public enum VectorCommand {
int newDirection = this.ordinal() ^ 0b010;
return VectorCommand.values()[newDirection];
}
public VectorCommand plot() {
return VectorCommand.values()[this.ordinal() | 0b100];
}
public VectorCommand move() {
return VectorCommand.values()[this.ordinal() & 0b011];
}
}

View File

@ -42,16 +42,16 @@ public class VectorShape implements Shape {
public final List<VectorCommand> vectors = new ArrayList<>();
public VectorShape moveUp() { return add(VectorCommand.MOVE_UP); }
public VectorShape moveRight() { return add(VectorCommand.MOVE_RIGHT); }
public VectorShape moveDown() { return add(VectorCommand.MOVE_DOWN); }
public VectorShape moveLeft() { return add(VectorCommand.MOVE_LEFT); }
public VectorShape plotUp() { return add(VectorCommand.PLOT_UP); }
public VectorShape plotRight() { return add(VectorCommand.PLOT_RIGHT); }
public VectorShape plotDown() { return add(VectorCommand.PLOT_DOWN); }
public VectorShape plotLeft() { return add(VectorCommand.PLOT_LEFT); }
public VectorShape moveUp() { return append(VectorCommand.MOVE_UP); }
public VectorShape moveRight() { return append(VectorCommand.MOVE_RIGHT); }
public VectorShape moveDown() { return append(VectorCommand.MOVE_DOWN); }
public VectorShape moveLeft() { return append(VectorCommand.MOVE_LEFT); }
public VectorShape plotUp() { return append(VectorCommand.PLOT_UP); }
public VectorShape plotRight() { return append(VectorCommand.PLOT_RIGHT); }
public VectorShape plotDown() { return append(VectorCommand.PLOT_DOWN); }
public VectorShape plotLeft() { return append(VectorCommand.PLOT_LEFT); }
private VectorShape add(VectorCommand vectorCommand) {
public VectorShape append(VectorCommand vectorCommand) {
this.vectors.add(vectorCommand);
return this;
}

View File

@ -42,12 +42,7 @@ public class ShapeGeneratorTest {
+ "|.XXX.|\n"
+ "+-----+\n";
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ShapeExporter exp = ShapeExporter.text().asciiTextBorder().build();
exp.export(st.shapes.get(0), outputStream);
String actual = new String(outputStream.toByteArray());
assertEquals(expected, actual);
assertShapeMatches(expected, st.shapes.get(0));
}
public void assertShapeBoxVectors(ShapeTable st) {
@ -66,4 +61,41 @@ public class ShapeGeneratorTest {
assertNotNull(s);
assertEquals(expected.vectors, s.toVector().vectors);
}
@Test
public void testMouseShape() throws IOException {
final String mouse = "+--------------+\n"
+ "|..........*X..|\n"
+ "|....XXXX.XX...|\n"
+ "|...XXXXXXXX...|\n"
+ "|.XXXXXXXXXXX..|\n"
+ "|XX.XXXXXXX.XX.|\n"
+ "|X...XXXXXXXXXX|\n"
+ "|XX............|\n"
+ "|.XXX.XX.......|\n"
+ "|...XXX........|\n"
+ "+--------------+\n";
ShapeTable st = ShapeGenerator.generate(getClass().getResourceAsStream("/mouse-bitmap.st"));
assertNotNull(st);
assertEquals(1, st.shapes.size());
// Verify we read the shape correctly...
Shape shape = st.shapes.get(0);
assertNotNull(shape);
assertShapeMatches(mouse, shape);
// Run vector transform to be certain we're ok
Shape vectorShape = shape.toVector();
assertNotNull(vectorShape);
assertShapeMatches(mouse, vectorShape);
}
public void assertShapeMatches(final String expected, Shape shape) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ShapeExporter exp = ShapeExporter.text().asciiTextBorder().build();
exp.export(shape, outputStream);
String actual = new String(outputStream.toByteArray());
assertEquals(expected, actual);
}
}

View File

@ -1,7 +1,7 @@
package io.github.applecommander.bastools.api.shapes;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.io.ByteArrayOutputStream;
@ -91,19 +91,6 @@ public class ShapesTest {
VectorShape vectorShape = bitmapShape.toVector();
BitmapShape newBitmapShape = vectorShape.toBitmap();
ShapeExporter exp = ShapeExporter.text().asciiTextBorder().build();
System.out.println("Original/expected:");
exp.export(bitmapShape, System.out);
System.out.println("Transformed/actual:");
exp.export(newBitmapShape, System.out);
System.out.println("Transformed vectors:");
System.out.println(vectorShape.vectors);
System.out.println(vectorShape.vectors.size());
System.out.println("Optimized vectors:");
System.out.println(vectorShape.optimize().vectors);
System.out.println(vectorShape.optimize().vectors.size());
exp.export(vectorShape.optimize().toBitmap(), System.out);
assertEquals(bitmapShape.grid, newBitmapShape.grid);
}

View File

@ -0,0 +1,13 @@
; From Mouse Maze (ca. 1983)
; See https://github.com/a2geek/mouse-maze-2001/tree/master/doc/original
.bitmap
..........*X..
....XXXX.XX...
...XXXXXXXX...
.XXXXXXXXXXX..
XX.XXXXXXX.XX.
X...XXXXXXXXXX
XX............
.XXX.XX.......
...XXX........