Conversation
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>
There was a problem hiding this comment.
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
ErrorMapwith full/incremental error computation and O(1) weighted sampling (Vose alias method). - Threaded error-map usage through
Model→HillClimbGenerator→Circle.randomize(ErrorMap)behind a feature flag. - Added
ErrorGuidedPlacementTestand committed visual comparison PNGs undertest-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.
| 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 + ")"); |
There was a problem hiding this comment.
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.
| @Test | ||
| void testVisualComparison() throws IOException { | ||
| String[] names = {"photo_detail", "nature", "edges"}; | ||
| BufferedImage[] images = { | ||
| TestImageGenerator.createPhotoDetail(), | ||
| TestImageGenerator.createNature(), | ||
| TestImageGenerator.createEdges(), | ||
| }; | ||
| int maxShapes = 200; |
There was a problem hiding this comment.
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.
| @@ -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; | |||
| } | |||
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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).
| 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; |
| 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}; |
There was a problem hiding this comment.
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).
| // When true, bias random circle placement toward high-error regions using importance sampling | ||
| boolean USE_ERROR_GUIDED_PLACEMENT = true; |
There was a problem hiding this comment.
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.
| // 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")); |
Summary
ErrorMapclass maintains a 32x32 error grid with Vose's alias method for O(1) weighted samplingUSE_ERROR_GUIDED_PLACEMENTin AppConstants (default:true)Changes
ErrorMap.java— spatial error grid with full compute, incremental update, and alias-table samplingCircle.randomize(ErrorMap)— new overload that accepts an error map for guided placementWorker— holds a reference to the error mapModel— initializes the error map, passes it through the pipeline, updates it after each shapeHillClimbGenerator— passes error map togetBestRandomStatefor guided randomizationAppConstants— newUSE_ERROR_GUIDED_PLACEMENTflagTest results
Visual comparison images (200 shapes each) are committed in
test-results/:test-results/photo_detail_target.pngtest-results/photo_detail_uniform_200shapes.pngtest-results/photo_detail_guided_200shapes.pngtest-results/photo_detail_diff.pngtest-results/nature_target.pngtest-results/nature_uniform_200shapes.pngtest-results/nature_guided_200shapes.pngtest-results/nature_diff.pngtest-results/edges_target.pngtest-results/edges_uniform_200shapes.pngtest-results/edges_guided_200shapes.pngtest-results/edges_diff.pngTest plan
ErrorMapcorrectness: verifies left-half-red vs white-background produces higher error on the red side./gradlew clean buildpasses🤖 Generated with Claude Code