Skip to content

Feature: Error-guided circle placement#2

Merged
VoX merged 1 commit intomainfrom
feature/error-guided-placement
Apr 4, 2026
Merged

Feature: Error-guided circle placement#2
VoX merged 1 commit intomainfrom
feature/error-guided-placement

Conversation

@VoX
Copy link
Copy Markdown
Owner

@VoX VoX commented Apr 4, 2026

Summary

  • Implements Proposal Feature: Error-guided circle placement #2: Spatial Error-Guided Circle Placement using importance sampling
  • New ErrorMap class maintains a 32x32 error grid with Vose's alias method for O(1) weighted sampling
  • 80% of circle placements are biased toward high-error regions; 20% remain uniform for exploration
  • Error map updates incrementally after each shape (only affected cells recomputed)
  • Feature flag USE_ERROR_GUIDED_PLACEMENT in AppConstants (default: true)

Changes

  • ErrorMap.java — spatial error grid with full compute, incremental update, and alias-table sampling
  • Circle.randomize(ErrorMap) — new overload that accepts an error map for guided placement
  • Worker — holds a reference to the error map
  • Model — initializes the error map, passes it through the pipeline, updates it after each shape
  • HillClimbGenerator — passes error map to getBestRandomState for guided randomization
  • AppConstants — new USE_ERROR_GUIDED_PLACEMENT flag

Test results

Visual comparison images (200 shapes each) are committed in test-results/:

Image Target Uniform Guided Diff
photo_detail test-results/photo_detail_target.png test-results/photo_detail_uniform_200shapes.png test-results/photo_detail_guided_200shapes.png test-results/photo_detail_diff.png
nature test-results/nature_target.png test-results/nature_uniform_200shapes.png test-results/nature_guided_200shapes.png test-results/nature_diff.png
edges test-results/edges_target.png test-results/edges_uniform_200shapes.png test-results/edges_guided_200shapes.png test-results/edges_diff.png

Test plan

  • ErrorMap correctness: verifies left-half-red vs white-background produces higher error on the red side
  • Sampling bias: confirms >50% of samples target a small high-error region (top-left 1/16th of image)
  • Error-guided never significantly worse than uniform across solid, gradient, edges, nature images
  • Visual comparison: 200-shape renders for photo_detail, nature, edges with both methods
  • ./gradlew clean build passes

🤖 Generated with Claude Code

Bias random circle placement toward high-error regions using a spatial
error map with alias-table sampling. 80% of placements target high-error
cells, 20% remain uniform for exploration. The error map updates
incrementally after each shape, keeping overhead minimal.

New files:
- ErrorMap.java: spatial error grid with Vose alias method for O(1) sampling
- ErrorGuidedPlacementTest.java: correctness tests and visual comparisons

Feature flag: USE_ERROR_GUIDED_PLACEMENT (default true) in AppConstants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 4, 2026 21:17
@VoX VoX merged commit 01e4f3b into main Apr 4, 2026
2 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements an error-guided circle placement strategy that biases random candidate generation toward high-error regions using a coarse error grid with alias-table sampling, and adds tests plus visual-result artifacts to validate the behavior.

Changes:

  • Added ErrorMap with full/incremental error computation and O(1) weighted sampling (Vose alias method).
  • Threaded error-map usage through ModelHillClimbGeneratorCircle.randomize(ErrorMap) behind a feature flag.
  • Added ErrorGuidedPlacementTest and committed visual comparison PNGs under test-results/.

Reviewed changes

Copilot reviewed 7 out of 19 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
test-results/photo_detail_uniform_200shapes.png Adds uniform-placement render artifact for visual comparison.
test-results/photo_detail_target.png Adds target image artifact for the photo_detail scenario.
test-results/photo_detail_guided_200shapes.png Adds guided-placement render artifact for visual comparison.
test-results/photo_detail_diff.png Adds diff heatmap artifact between uniform vs guided renders.
test-results/nature_uniform_200shapes.png Adds uniform-placement render artifact for the nature scenario.
test-results/nature_target.png Adds target image artifact for the nature scenario.
test-results/nature_guided_200shapes.png Adds guided-placement render artifact for the nature scenario.
test-results/nature_diff.png Adds diff heatmap artifact between uniform vs guided renders.
test-results/edges_uniform_200shapes.png Adds uniform-placement render artifact for the edges scenario.
test-results/edges_target.png Adds target image artifact for the edges scenario.
test-results/edges_guided_200shapes.png Adds guided-placement render artifact for the edges scenario.
test-results/edges_diff.png Adds diff heatmap artifact between uniform vs guided renders.
src/test/java/com/bobrust/generator/ErrorGuidedPlacementTest.java Adds correctness, bias, and (heavy) end-to-end comparison tests for guided placement.
src/main/java/com/bobrust/util/data/AppConstants.java Adds USE_ERROR_GUIDED_PLACEMENT feature flag (default enabled).
src/main/java/com/bobrust/generator/Worker.java Adds ErrorMap storage/accessors on Worker.
src/main/java/com/bobrust/generator/Model.java Initializes/updates ErrorMap and passes it into hill-climb candidate selection.
src/main/java/com/bobrust/generator/HillClimbGenerator.java Threads ErrorMap into random-state generation (getBestRandomState).
src/main/java/com/bobrust/generator/ErrorMap.java New spatial error grid with incremental updates and alias-table sampling.
src/main/java/com/bobrust/generator/Circle.java Adds randomize(ErrorMap) overload implementing 80/20 guided vs uniform placement.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +77 to +90
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 + ")");
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These score-comparison tests are non-deterministic because circle randomization uses ThreadLocalRandom (via Worker.getRandom()), so guided vs uniform can intermittently violate the 5% threshold and flake in CI. Consider injecting a seeded Random into Worker/Circle for tests (or controlling randomness via a test-only hook) and/or running multiple trials and asserting on an aggregate (median/mean) to make the test reliable.

Copilot uses AI. Check for mistakes.
Comment on lines +181 to +189
@Test
void testVisualComparison() throws IOException {
String[] names = {"photo_detail", "nature", "edges"};
BufferedImage[] images = {
TestImageGenerator.createPhotoDetail(),
TestImageGenerator.createNature(),
TestImageGenerator.createEdges(),
};
int maxShapes = 200;
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test performs long-running generation (200 shapes × multiple images) and writes PNGs to disk under build/test-output/ unconditionally. This is likely to be slow and brittle in automated builds; consider marking it @Disabled by default, gating it behind a system property/tag, or moving it to a benchmark/integration test suite.

Copilot uses AI. Check for mistakes.
Comment on lines 16 to +33
@@ -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;
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worker now stores an ErrorMap, but there are no production uses of getErrorMap()/setErrorMap() (only Model sets it). Since HillClimbGenerator takes ErrorMap explicitly, this field + accessors appear dead and can confuse the data flow; consider removing them or refactoring so the placement logic consistently sources the map from Worker.

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +72
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;
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorMap’s error metric sums only RGB squared error, but BorstCore.differenceFull/differencePartial include alpha-channel error (da*da). If targets/backgrounds can have non-opaque alpha, the sampling weights won’t match the generator’s actual score function; consider including alpha in the per-pixel error (or explicitly documenting/enforcing that alpha is always 255).

Suggested change
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;
int da = ((tt >>> 24) & 0xff) - ((cc >>> 24) & 0xff);
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] += da * da + dr * dr + dg * dg + db * db;

Copilot uses AI. Check for mistakes.
Comment on lines +208 to +231
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};
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

samplePosition() uses imageWidth/imageHeight captured at construction time, while computeFull()/updateIncremental() iterate using target.width/height. If an ErrorMap is ever computed from an image with different dimensions than the constructor args, sampling can return out-of-bounds coordinates relative to the images/worker. Consider validating dimensions in computeFull/updateIncremental (or deriving sampling bounds from the passed BorstImage dimensions).

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +32
// When true, bias random circle placement toward high-error regions using importance sampling
boolean USE_ERROR_GUIDED_PLACEMENT = true;
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

USE_ERROR_GUIDED_PLACEMENT defaults to true, which changes generator behavior and adds per-shape error-map maintenance overhead whenever Model is instantiated. If this is intended to be optional/experimental, consider defaulting to false or making it configurable at runtime (rather than a compile-time constant) so callers can opt in without rebuilding.

Suggested change
// When true, bias random circle placement toward high-error regions using importance sampling
boolean USE_ERROR_GUIDED_PLACEMENT = true;
// When true, bias random circle placement toward high-error regions using importance sampling.
// Runtime-configurable via -Dbobrust.useErrorGuidedPlacement=true and defaults to false.
boolean USE_ERROR_GUIDED_PLACEMENT = Boolean.parseBoolean(
System.getProperty("bobrust.useErrorGuidedPlacement", "false"));

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants