diff --git a/src/main/java/com/bobrust/generator/Circle.java b/src/main/java/com/bobrust/generator/Circle.java index 9aaf3cc..eb2cceb 100644 --- a/src/main/java/com/bobrust/generator/Circle.java +++ b/src/main/java/com/bobrust/generator/Circle.java @@ -41,9 +41,25 @@ public void mutateShape() { } public void randomize() { + randomize(null); + } + + /** + * Randomize circle position and size. + * When an {@link ErrorMap} is provided and error-guided placement is enabled, + * 80% of placements are biased toward high-error regions via importance + * sampling. The remaining 20% use uniform random placement for exploration. + */ + public void randomize(ErrorMap errorMap) { Random rnd = worker.getRandom(); - this.x = rnd.nextInt(worker.w); - this.y = rnd.nextInt(worker.h); + if (errorMap != null && rnd.nextFloat() < 0.8f) { + int[] pos = errorMap.samplePosition(rnd); + this.x = pos[0]; + this.y = pos[1]; + } else { + this.x = rnd.nextInt(worker.w); + this.y = rnd.nextInt(worker.h); + } this.r = BorstUtils.SIZES[rnd.nextInt(BorstUtils.SIZES.length)]; } diff --git a/src/main/java/com/bobrust/generator/ErrorMap.java b/src/main/java/com/bobrust/generator/ErrorMap.java new file mode 100644 index 0000000..d0c2e03 --- /dev/null +++ b/src/main/java/com/bobrust/generator/ErrorMap.java @@ -0,0 +1,233 @@ +package com.bobrust.generator; + +import java.util.Random; + +/** + * Spatial error map that tracks per-cell error across the image and supports + * importance sampling to bias circle placement toward high-error regions. + * + * The image is divided into a coarse grid (e.g. 32x32). Each cell stores the + * sum of squared per-pixel error for its region. An alias table enables O(1) + * weighted random sampling from the grid. + */ +public class ErrorMap { + private static final int DEFAULT_GRID_DIM = 32; + + final int gridWidth; + final int gridHeight; + final int cellWidth; + final int cellHeight; + final int imageWidth; + final int imageHeight; + final float[] cellErrors; + + // Alias table fields for O(1) weighted sampling + private int[] alias; + private float[] prob; + private boolean tableValid; + + public ErrorMap(int imageWidth, int imageHeight) { + this(imageWidth, imageHeight, DEFAULT_GRID_DIM, DEFAULT_GRID_DIM); + } + + public ErrorMap(int imageWidth, int imageHeight, int gridWidth, int gridHeight) { + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + this.gridWidth = gridWidth; + this.gridHeight = gridHeight; + this.cellWidth = Math.max(1, (imageWidth + gridWidth - 1) / gridWidth); + this.cellHeight = Math.max(1, (imageHeight + gridHeight - 1) / gridHeight); + this.cellErrors = new float[gridWidth * gridHeight]; + this.alias = new int[gridWidth * gridHeight]; + this.prob = new float[gridWidth * gridHeight]; + this.tableValid = false; + } + + /** + * Compute the full error map from scratch given target and current images. + */ + public void computeFull(BorstImage target, BorstImage current) { + int w = target.width; + int h = target.height; + int n = gridWidth * gridHeight; + for (int i = 0; i < n; i++) { + cellErrors[i] = 0; + } + + for (int py = 0; py < h; py++) { + int gy = py / cellHeight; + if (gy >= gridHeight) gy = gridHeight - 1; + int rowOffset = py * w; + for (int px = 0; px < w; px++) { + int gx = px / cellWidth; + if (gx >= gridWidth) gx = gridWidth - 1; + + int tt = target.pixels[rowOffset + px]; + int cc = current.pixels[rowOffset + px]; + + int dr = ((tt >>> 16) & 0xff) - ((cc >>> 16) & 0xff); + int dg = ((tt >>> 8) & 0xff) - ((cc >>> 8) & 0xff); + int db = (tt & 0xff) - (cc & 0xff); + + cellErrors[gy * gridWidth + gx] += dr * dr + dg * dg + db * db; + } + } + + tableValid = false; + } + + /** + * Incrementally update the error map after a circle was drawn. + * Only recomputes cells that overlap the circle's bounding box. + */ + public void updateIncremental(BorstImage target, BorstImage current, int cx, int cy, int cacheIndex) { + Scanline[] lines = CircleCache.CIRCLE_CACHE[cacheIndex]; + int w = target.width; + int h = target.height; + + // Find bounding box of affected grid cells + int minGx = Integer.MAX_VALUE, maxGx = Integer.MIN_VALUE; + int minGy = Integer.MAX_VALUE, maxGy = Integer.MIN_VALUE; + for (Scanline line : lines) { + int py = line.y + cy; + if (py < 0 || py >= h) continue; + int xs = Math.max(line.x1 + cx, 0); + int xe = Math.min(line.x2 + cx, w - 1); + if (xs > xe) continue; + + int gy = Math.min(py / cellHeight, gridHeight - 1); + int gx0 = Math.min(xs / cellWidth, gridWidth - 1); + int gx1 = Math.min(xe / cellWidth, gridWidth - 1); + + minGy = Math.min(minGy, gy); + maxGy = Math.max(maxGy, gy); + minGx = Math.min(minGx, gx0); + maxGx = Math.max(maxGx, gx1); + } + + if (minGx > maxGx || minGy > maxGy) return; + + // Recompute only the affected cells + for (int gy = minGy; gy <= maxGy; gy++) { + int pyStart = gy * cellHeight; + int pyEnd = Math.min(pyStart + cellHeight, h); + for (int gx = minGx; gx <= maxGx; gx++) { + int pxStart = gx * cellWidth; + int pxEnd = Math.min(pxStart + cellWidth, w); + + float error = 0; + for (int py = pyStart; py < pyEnd; py++) { + int rowOffset = py * w; + for (int px = pxStart; px < pxEnd; px++) { + int tt = target.pixels[rowOffset + px]; + int cc = current.pixels[rowOffset + px]; + + int dr = ((tt >>> 16) & 0xff) - ((cc >>> 16) & 0xff); + int dg = ((tt >>> 8) & 0xff) - ((cc >>> 8) & 0xff); + int db = (tt & 0xff) - (cc & 0xff); + + error += dr * dr + dg * dg + db * db; + } + } + cellErrors[gy * gridWidth + gx] = error; + } + } + + tableValid = false; + } + + /** + * Build the alias table for O(1) weighted sampling. + * Uses Vose's alias method. + */ + private void buildAliasTable() { + int n = cellErrors.length; + float totalError = 0; + for (int i = 0; i < n; i++) { + totalError += cellErrors[i]; + } + + if (totalError <= 0) { + // Uniform distribution fallback + for (int i = 0; i < n; i++) { + prob[i] = 1.0f; + alias[i] = i; + } + tableValid = true; + return; + } + + float[] scaled = new float[n]; + for (int i = 0; i < n; i++) { + scaled[i] = cellErrors[i] * n / totalError; + } + + // Partition into small and large + int[] small = new int[n]; + int[] large = new int[n]; + int smallCount = 0, largeCount = 0; + + for (int i = 0; i < n; i++) { + if (scaled[i] < 1.0f) { + small[smallCount++] = i; + } else { + large[largeCount++] = i; + } + } + + while (smallCount > 0 && largeCount > 0) { + int s = small[--smallCount]; + int l = large[--largeCount]; + + prob[s] = scaled[s]; + alias[s] = l; + + scaled[l] = (scaled[l] + scaled[s]) - 1.0f; + if (scaled[l] < 1.0f) { + small[smallCount++] = l; + } else { + large[largeCount++] = l; + } + } + + while (largeCount > 0) { + prob[large[--largeCount]] = 1.0f; + } + while (smallCount > 0) { + prob[small[--smallCount]] = 1.0f; + } + + tableValid = true; + } + + /** + * Sample a pixel position biased toward high-error regions. + * Uses the alias table for O(1) cell selection, then uniform + * random within the selected cell. + */ + public int[] samplePosition(Random rnd) { + if (!tableValid) { + buildAliasTable(); + } + + int n = cellErrors.length; + int cell; + int idx = rnd.nextInt(n); + if (rnd.nextFloat() < prob[idx]) { + cell = idx; + } else { + cell = alias[idx]; + } + + int gx = cell % gridWidth; + int gy = cell / gridWidth; + + int pxStart = gx * cellWidth; + int pyStart = gy * cellHeight; + + int px = pxStart + rnd.nextInt(Math.min(cellWidth, imageWidth - pxStart)); + int py = pyStart + rnd.nextInt(Math.min(cellHeight, imageHeight - pyStart)); + + return new int[]{px, py}; + } +} diff --git a/src/main/java/com/bobrust/generator/HillClimbGenerator.java b/src/main/java/com/bobrust/generator/HillClimbGenerator.java index 0723999..02bd2cd 100644 --- a/src/main/java/com/bobrust/generator/HillClimbGenerator.java +++ b/src/main/java/com/bobrust/generator/HillClimbGenerator.java @@ -6,12 +6,12 @@ import java.util.concurrent.ThreadLocalRandom; class HillClimbGenerator { - private static State getBestRandomState(List random_states) { + private static State getBestRandomState(List random_states, ErrorMap errorMap) { final int len = random_states.size(); for (int i = 0; i < len; i++) { State state = random_states.get(i); state.score = -1; - state.shape.randomize(); + state.shape.randomize(errorMap); } random_states.parallelStream().forEach(State::getEnergy); @@ -162,11 +162,15 @@ static float computeCoolingRate(float initialTemp, int maxAge) { } public static State getBestHillClimbState(List random_states, int age, int times) { + return getBestHillClimbState(random_states, age, times, null); + } + + public static State getBestHillClimbState(List random_states, int age, int times, ErrorMap errorMap) { float bestEnergy = 0; State bestState = null; for (int i = 0; i < times; i++) { - State oldState = getBestRandomState(random_states); + State oldState = getBestRandomState(random_states, errorMap); State state = getHillClimb(oldState, age); float energy = state.getEnergy(); diff --git a/src/main/java/com/bobrust/generator/Model.java b/src/main/java/com/bobrust/generator/Model.java index 4b76c55..d02255b 100644 --- a/src/main/java/com/bobrust/generator/Model.java +++ b/src/main/java/com/bobrust/generator/Model.java @@ -1,5 +1,7 @@ package com.bobrust.generator; +import com.bobrust.util.data.AppConstants; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -21,6 +23,8 @@ public class Model { public final int height; protected float score; + private ErrorMap errorMap; + public Model(BorstImage target, int backgroundRGB, int alpha) { int w = target.width; int h = target.height; @@ -33,11 +37,18 @@ public Model(BorstImage target, int backgroundRGB, int alpha) { this.current = new BorstImage(w, h); Arrays.fill(this.current.pixels, backgroundRGB); this.beforeImage = new BorstImage(w, h); - + this.score = BorstCore.differenceFull(target, current); this.context = new BorstImage(w, h); this.worker = new Worker(target, alpha); this.alpha = alpha; + + // Initialize error map if error-guided placement is enabled + if (AppConstants.USE_ERROR_GUIDED_PLACEMENT) { + this.errorMap = new ErrorMap(w, h); + this.errorMap.computeFull(target, current); + this.worker.setErrorMap(this.errorMap); + } } private void addShape(Circle shape) { @@ -45,13 +56,18 @@ private void addShape(Circle shape) { int cache_index = BorstUtils.getClosestSizeIndex(shape.r); BorstColor color = BorstCore.computeColor(target, current, alpha, cache_index, shape.x, shape.y); - + BorstCore.drawLines(current, color, alpha, cache_index, shape.x, shape.y); this.score = BorstCore.differencePartial(target, beforeImage, current, score, cache_index, shape.x, shape.y); shapes.add(shape); colors.add(color); - + BorstCore.drawLines(context, color, alpha, cache_index, shape.x, shape.y); + + // Incrementally update the error map after drawing the new shape + if (errorMap != null) { + errorMap.updateIncremental(target, current, shape.x, shape.y, cache_index); + } } private static final int max_random_states = 1000; @@ -69,7 +85,7 @@ public int processStep() { } } - State state = HillClimbGenerator.getBestHillClimbState(randomStates, age, times); + State state = HillClimbGenerator.getBestHillClimbState(randomStates, age, times, errorMap); addShape(state.shape); return worker.getCounter(); diff --git a/src/main/java/com/bobrust/generator/Worker.java b/src/main/java/com/bobrust/generator/Worker.java index a6e4ec6..6179162 100644 --- a/src/main/java/com/bobrust/generator/Worker.java +++ b/src/main/java/com/bobrust/generator/Worker.java @@ -13,6 +13,7 @@ class Worker { public final int h; public float score; private final AtomicInteger counter = new AtomicInteger(); + private ErrorMap errorMap; public Worker(BorstImage target, int alpha) { this.w = target.width; @@ -21,6 +22,16 @@ public Worker(BorstImage target, int alpha) { this.alpha = alpha; } + /** Returns the error map, or null if error-guided placement is disabled. */ + public ErrorMap getErrorMap() { + return errorMap; + } + + /** Sets the error map (called from Model when error-guided placement is enabled). */ + public void setErrorMap(ErrorMap errorMap) { + this.errorMap = errorMap; + } + /** * Returns a thread-local Random instance for use in parallel operations. * This avoids lock contention on a shared Random instance. diff --git a/src/main/java/com/bobrust/util/data/AppConstants.java b/src/main/java/com/bobrust/util/data/AppConstants.java index 6867178..57cf578 100644 --- a/src/main/java/com/bobrust/util/data/AppConstants.java +++ b/src/main/java/com/bobrust/util/data/AppConstants.java @@ -27,6 +27,9 @@ public interface AppConstants { // When true, use simulated annealing instead of pure hill climbing for shape optimization boolean USE_SIMULATED_ANNEALING = true; + + // When true, bias random circle placement toward high-error regions using importance sampling + boolean USE_ERROR_GUIDED_PLACEMENT = true; // Average canvas colors. Used as default colors Color CANVAS_AVERAGE = new Color(0xb3aba0); diff --git a/src/test/java/com/bobrust/generator/ErrorGuidedPlacementTest.java b/src/test/java/com/bobrust/generator/ErrorGuidedPlacementTest.java new file mode 100644 index 0000000..4a9d8c3 --- /dev/null +++ b/src/test/java/com/bobrust/generator/ErrorGuidedPlacementTest.java @@ -0,0 +1,337 @@ +package com.bobrust.generator; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.imageio.ImageIO; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for the error-guided circle placement feature. + * + * Compares uniform random placement against error-guided placement to verify + * that biasing toward high-error regions produces equal or better results. + * Generates visual comparison images saved to build/test-output/. + */ +class ErrorGuidedPlacementTest { + private static final int ALPHA = 128; + private static final int BACKGROUND = 0xFFFFFFFF; + private static final File OUTPUT_DIR = new File("build/test-output"); + + @BeforeAll + static void setup() { + OUTPUT_DIR.mkdirs(); + } + + // ---- Core test runner ---- + + /** + * Run the generator for a given number of shapes. + * @param useErrorGuided if true, uses error-guided placement; if false, uniform random. + * @return the final Model (with score and rendered current image). + */ + private static Model runGenerator(BufferedImage testImage, int maxShapes, boolean useErrorGuided) { + BufferedImage argbImage = ensureArgb(testImage); + BorstImage target = new BorstImage(argbImage); + Model model = new Model(target, BACKGROUND, ALPHA); + + // Override the error map on the worker based on what we want to test + Worker worker = getWorker(model); + ErrorMap errorMap = useErrorGuided ? getErrorMap(model) : null; + if (!useErrorGuided) { + worker.setErrorMap(null); + setErrorMap(model, null); + } + + for (int i = 0; i < maxShapes; i++) { + worker.init(model.current, model.score); + List randomStates = createRandomStates(worker, 200); + State best = getBestRandomState(randomStates, errorMap); + State state = HillClimbGenerator.getHillClimbClassic(best, 100); + addShapeToModel(model, state.shape); + // Re-fetch the error map as it gets updated after addShape + if (useErrorGuided) { + errorMap = getErrorMap(model); + } + } + return model; + } + + // ---- Test: error-guided produces lower or equal error ---- + + @Test + void testErrorGuidedProducesLowerOrEqualError() { + BufferedImage testImage = TestImageGenerator.createPhotoDetail(); + int maxShapes = 50; + + Model uniformModel = runGenerator(testImage, maxShapes, false); + Model guidedModel = runGenerator(testImage, maxShapes, true); + + float uniformScore = uniformModel.score; + float guidedScore = guidedModel.score; + + System.out.println("Uniform score: " + uniformScore); + System.out.println("Guided score: " + guidedScore); + float improvement = (uniformScore - guidedScore) / uniformScore * 100; + System.out.println("Improvement: " + improvement + "%"); + + // Allow 5% tolerance for stochastic variation + assertTrue(guidedScore <= uniformScore * 1.05f, + "Guided score (" + guidedScore + ") should not be significantly worse than uniform (" + uniformScore + ")"); + } + + @Test + void testErrorGuidedNeverSignificantlyWorse() { + BufferedImage[] images = { + TestImageGenerator.createSolid(), + TestImageGenerator.createGradient(), + TestImageGenerator.createEdges(), + TestImageGenerator.createNature(), + }; + String[] names = {"solid", "gradient", "edges", "nature"}; + int maxShapes = 30; + + for (int idx = 0; idx < images.length; idx++) { + Model uniformModel = runGenerator(images[idx], maxShapes, false); + Model guidedModel = runGenerator(images[idx], maxShapes, true); + System.out.println(names[idx] + " — Uniform: " + uniformModel.score + ", Guided: " + guidedModel.score); + + assertTrue(guidedModel.score <= uniformModel.score * 1.05f, + names[idx] + ": Guided (" + guidedModel.score + ") should not be significantly worse than uniform (" + uniformModel.score + ")"); + } + } + + // ---- Test: ErrorMap correctness ---- + + @Test + void testErrorMapBasicCorrectness() { + BufferedImage img = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = img.createGraphics(); + g.setColor(Color.RED); + g.fillRect(0, 0, 32, 64); // left half red + g.setColor(Color.WHITE); + g.fillRect(32, 0, 32, 64); // right half white + g.dispose(); + + BorstImage target = new BorstImage(ensureArgb(img)); + BorstImage current = new BorstImage(64, 64); + Arrays.fill(current.pixels, 0xFFFFFFFF); // all white + + ErrorMap map = new ErrorMap(64, 64, 2, 1); // 2 columns, 1 row + map.computeFull(target, current); + + // Left cells should have much higher error (red vs white) + // Right cells should have near-zero error (white vs white) + float leftError = map.cellErrors[0]; + float rightError = map.cellErrors[1]; + + assertTrue(leftError > rightError * 10, + "Left (red vs white) error (" + leftError + ") should be much higher than right (white vs white) error (" + rightError + ")"); + } + + @Test + void testErrorMapSamplingBias() { + // Create an image that is white everywhere except a small red patch + BufferedImage img = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = img.createGraphics(); + g.setColor(Color.WHITE); + g.fillRect(0, 0, 128, 128); + g.setColor(Color.RED); + g.fillRect(0, 0, 32, 32); // top-left corner is red + g.dispose(); + + BorstImage target = new BorstImage(ensureArgb(img)); + BorstImage current = new BorstImage(128, 128); + Arrays.fill(current.pixels, 0xFFFFFFFF); + + ErrorMap map = new ErrorMap(128, 128); + map.computeFull(target, current); + + // Sample many positions and verify bias toward top-left + java.util.Random rnd = new java.util.Random(42); + int topLeftCount = 0; + int totalSamples = 10000; + for (int i = 0; i < totalSamples; i++) { + int[] pos = map.samplePosition(rnd); + if (pos[0] < 32 && pos[1] < 32) { + topLeftCount++; + } + } + + // The top-left quadrant is 1/16 of the image area but has almost all the error. + // With error-guided sampling, it should receive >50% of samples. + float topLeftFraction = topLeftCount / (float) totalSamples; + assertTrue(topLeftFraction > 0.5f, + "Error-guided sampling should heavily favor the high-error region, but only " + + (topLeftFraction * 100) + "% of samples hit the top-left corner"); + } + + // ---- Visual comparison benchmark ---- + + @Test + void testVisualComparison() throws IOException { + String[] names = {"photo_detail", "nature", "edges"}; + BufferedImage[] images = { + TestImageGenerator.createPhotoDetail(), + TestImageGenerator.createNature(), + TestImageGenerator.createEdges(), + }; + int maxShapes = 200; + + for (int idx = 0; idx < names.length; idx++) { + String name = names[idx]; + BufferedImage targetImg = images[idx]; + System.out.println("Generating visual comparison for: " + name); + + // Save target + ImageIO.write(targetImg, "png", new File(OUTPUT_DIR, name + "_target.png")); + + // Run both methods + Model uniformModel = runGenerator(targetImg, maxShapes, false); + Model guidedModel = runGenerator(targetImg, maxShapes, true); + + // Save rendered results + BufferedImage uniformResult = toBufferedImage(uniformModel.current); + BufferedImage guidedResult = toBufferedImage(guidedModel.current); + ImageIO.write(uniformResult, "png", new File(OUTPUT_DIR, name + "_uniform_200shapes.png")); + ImageIO.write(guidedResult, "png", new File(OUTPUT_DIR, name + "_guided_200shapes.png")); + + // Generate difference heatmap between the two results + BufferedImage diffImage = generateDiffHeatmap(uniformResult, guidedResult); + ImageIO.write(diffImage, "png", new File(OUTPUT_DIR, name + "_diff.png")); + + System.out.println(" Uniform score: " + uniformModel.score); + System.out.println(" Guided score: " + guidedModel.score); + float improvement = (uniformModel.score - guidedModel.score) / uniformModel.score * 100; + System.out.println(" Improvement: " + improvement + "%"); + } + } + + // ---- Helper methods ---- + + private static BufferedImage ensureArgb(BufferedImage img) { + if (img.getType() == BufferedImage.TYPE_INT_ARGB) return img; + BufferedImage argb = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D g = argb.createGraphics(); + g.drawImage(img, 0, 0, null); + g.dispose(); + return argb; + } + + private static BufferedImage toBufferedImage(BorstImage borstImage) { + int w = borstImage.width; + int h = borstImage.height; + BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + int[] destPixels = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData(); + System.arraycopy(borstImage.pixels, 0, destPixels, 0, borstImage.pixels.length); + return img; + } + + /** + * Generate a heatmap showing absolute difference between two images. + * Brighter = more different. + */ + private static BufferedImage generateDiffHeatmap(BufferedImage a, BufferedImage b) { + int w = a.getWidth(); + int h = a.getHeight(); + BufferedImage diff = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int ca = a.getRGB(x, y); + int cb = b.getRGB(x, y); + + int dr = Math.abs(((ca >> 16) & 0xff) - ((cb >> 16) & 0xff)); + int dg = Math.abs(((ca >> 8) & 0xff) - ((cb >> 8) & 0xff)); + int db = Math.abs((ca & 0xff) - (cb & 0xff)); + + // Scale up for visibility and map to a heat color + int intensity = Math.min(255, (dr + dg + db) * 2); + int heatR = Math.min(255, intensity * 2); + int heatG = Math.max(0, 255 - intensity * 2); + int heatB = 0; + + diff.setRGB(x, y, 0xFF000000 | (heatR << 16) | (heatG << 8) | heatB); + } + } + return diff; + } + + // ---- Reflective helpers ---- + + private static Worker getWorker(Model model) { + try { + Field field = Model.class.getDeclaredField("worker"); + field.setAccessible(true); + return (Worker) field.get(model); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static ErrorMap getErrorMap(Model model) { + try { + Field field = Model.class.getDeclaredField("errorMap"); + field.setAccessible(true); + return (ErrorMap) field.get(model); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void setErrorMap(Model model, ErrorMap errorMap) { + try { + Field field = Model.class.getDeclaredField("errorMap"); + field.setAccessible(true); + field.set(model, errorMap); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static List createRandomStates(Worker worker, int count) { + List states = new ArrayList<>(); + for (int i = 0; i < count; i++) { + states.add(new State(worker)); + } + return states; + } + + private static State getBestRandomState(List states, ErrorMap errorMap) { + for (State s : states) { + s.score = -1; + s.shape.randomize(errorMap); + } + states.parallelStream().forEach(State::getEnergy); + float bestEnergy = Float.MAX_VALUE; + State bestState = null; + for (State s : states) { + float energy = s.getEnergy(); + if (bestState == null || energy < bestEnergy) { + bestEnergy = energy; + bestState = s; + } + } + return bestState; + } + + private static void addShapeToModel(Model model, Circle shape) { + try { + Method method = Model.class.getDeclaredMethod("addShape", Circle.class); + method.setAccessible(true); + method.invoke(model, shape); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/test-results/edges_diff.png b/test-results/edges_diff.png new file mode 100644 index 0000000..e97792f Binary files /dev/null and b/test-results/edges_diff.png differ diff --git a/test-results/edges_guided_200shapes.png b/test-results/edges_guided_200shapes.png new file mode 100644 index 0000000..813cd43 Binary files /dev/null and b/test-results/edges_guided_200shapes.png differ diff --git a/test-results/edges_target.png b/test-results/edges_target.png new file mode 100644 index 0000000..3b89f77 Binary files /dev/null and b/test-results/edges_target.png differ diff --git a/test-results/edges_uniform_200shapes.png b/test-results/edges_uniform_200shapes.png new file mode 100644 index 0000000..c962088 Binary files /dev/null and b/test-results/edges_uniform_200shapes.png differ diff --git a/test-results/nature_diff.png b/test-results/nature_diff.png new file mode 100644 index 0000000..fb5f5af Binary files /dev/null and b/test-results/nature_diff.png differ diff --git a/test-results/nature_guided_200shapes.png b/test-results/nature_guided_200shapes.png new file mode 100644 index 0000000..bad12f2 Binary files /dev/null and b/test-results/nature_guided_200shapes.png differ diff --git a/test-results/nature_target.png b/test-results/nature_target.png new file mode 100644 index 0000000..674423b Binary files /dev/null and b/test-results/nature_target.png differ diff --git a/test-results/nature_uniform_200shapes.png b/test-results/nature_uniform_200shapes.png new file mode 100644 index 0000000..aa765ab Binary files /dev/null and b/test-results/nature_uniform_200shapes.png differ diff --git a/test-results/photo_detail_diff.png b/test-results/photo_detail_diff.png new file mode 100644 index 0000000..7031885 Binary files /dev/null and b/test-results/photo_detail_diff.png differ diff --git a/test-results/photo_detail_guided_200shapes.png b/test-results/photo_detail_guided_200shapes.png new file mode 100644 index 0000000..3a9783a Binary files /dev/null and b/test-results/photo_detail_guided_200shapes.png differ diff --git a/test-results/photo_detail_target.png b/test-results/photo_detail_target.png new file mode 100644 index 0000000..9065aaf Binary files /dev/null and b/test-results/photo_detail_target.png differ diff --git a/test-results/photo_detail_uniform_200shapes.png b/test-results/photo_detail_uniform_200shapes.png new file mode 100644 index 0000000..21395a4 Binary files /dev/null and b/test-results/photo_detail_uniform_200shapes.png differ