Skip to content

Feature: Adaptive size selection based on local detail - #3

Merged
VoX merged 1 commit into
mainfrom
feature/adaptive-size-selection
Apr 4, 2026
Merged

Feature: Adaptive size selection based on local detail#3
VoX merged 1 commit into
mainfrom
feature/adaptive-size-selection

Conversation

@VoX

@VoXVoX commented Apr 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • New GradientMap class computes Sobel gradient magnitude of the target image, downsampled to a 32x32 grid, normalized to [0,1]. High values = edges/detail, low values = smooth regions.
  • Circle.randomize() now uses gradient-weighted size selection when USE_ADAPTIVE_SIZE is enabled: small circles near edges, large circles in smooth areas. Uses exp(-4 * |sizeNorm - (1 - gradient)|) weighting.
  • Circle.mutateShape() scales position perturbation by gradient (fine-tune near edges, explore broadly in smooth areas) and uses gradient-biased size selection during mutation.
  • Wired into Worker and Model — gradient map computed once from target image at initialization.
  • USE_ADAPTIVE_SIZE feature flag in AppConstants (default true).

Test Results

Visual comparisons for 200 shapes on three test images are committed to test-results/proposal3/:

ImageTargetUniformAdaptiveGradient MapDiff
photo_detailtargetuniformadaptivegradientdiff
naturetargetuniformadaptivegradientdiff
edgestargetuniformadaptivegradientdiff

Test Plan

  • Gradient map produces high values at edges, low/zero in smooth areas
  • Checkerboard image produces high gradient across most cells
  • Size selection near edges favors small circles vs smooth areas
  • Adaptive sizing produces lower or equal final error vs uniform (within 5% tolerance)
  • No regression across gradient, edges, nature, and photo_detail test images
  • ./gradlew clean build passes

Implements Proposal 3 from PROPOSALS.md.

Use Sobel gradient magnitude to bias circle size selection: small circles
near edges/detail, large circles in smooth areas. Mutation perturbation
is also scaled by local gradient for fine-tuning near edges.
- New GradientMap class: computes and normalizes Sobel gradient per grid cell
- Circle.randomize() uses gradient-weighted size selection when enabled
- Circle.mutateShape() scales position perturbation and uses gradient-biased
size selection near edges
- GradientMap wired into Worker and Model (computed once from target image)
- USE_ADAPTIVE_SIZE feature flag in AppConstants (default true)
- Comprehensive tests verifying gradient correctness and adaptive vs uniform
- Visual comparison images in test-results/proposal3/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings April 4, 2026 21:25

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds “Proposal 3” adaptive circle sizing driven by a precomputed Sobel gradient map of the target image, aiming to use smaller circles near detailed/edge regions and larger circles in smooth areas.

Changes:

  • Introduces GradientMap to compute a normalized, coarse-grid Sobel magnitude map and provide gradient-weighted size selection + mutation scaling.
  • Wires adaptive sizing into Model initialization and Worker/Circle shape randomization & mutation under a new USE_ADAPTIVE_SIZE flag.
  • Adds an end-to-end JUnit test suite and commits visual benchmark artifacts under test-results/proposal3/.

Reviewed changes

Copilot reviewed 7 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
test-results/proposal3/photo_detail_uniform.pngAdds visual benchmark artifact for uniform sizing (photo_detail).
test-results/proposal3/photo_detail_target.pngAdds target image artifact for photo_detail benchmark.
test-results/proposal3/photo_detail_gradient.pngAdds gradient-map visualization artifact for photo_detail.
test-results/proposal3/photo_detail_diff.pngAdds diff heatmap artifact (uniform vs adaptive) for photo_detail.
test-results/proposal3/photo_detail_adaptive.pngAdds visual benchmark artifact for adaptive sizing (photo_detail).
test-results/proposal3/nature_uniform.pngAdds visual benchmark artifact for uniform sizing (nature).
test-results/proposal3/nature_target.pngAdds target image artifact for nature benchmark.
test-results/proposal3/nature_gradient.pngAdds gradient-map visualization artifact for nature.
test-results/proposal3/nature_diff.pngAdds diff heatmap artifact (uniform vs adaptive) for nature.
test-results/proposal3/nature_adaptive.pngAdds visual benchmark artifact for adaptive sizing (nature).
test-results/proposal3/edges_uniform.pngAdds visual benchmark artifact for uniform sizing (edges).
test-results/proposal3/edges_target.pngAdds target image artifact for edges benchmark.
test-results/proposal3/edges_gradient.pngAdds gradient-map visualization artifact for edges.
test-results/proposal3/edges_diff.pngAdds diff heatmap artifact (uniform vs adaptive) for edges.
test-results/proposal3/edges_adaptive.pngAdds visual benchmark artifact for adaptive sizing (edges).
src/test/java/com/bobrust/generator/AdaptiveSizeSelectionTest.javaAdds correctness + end-to-end regression tests and generates benchmark images.
src/main/resources/versionBumps app version to 0.6.80.
src/main/java/com/bobrust/util/data/AppConstants.javaAdds USE_ADAPTIVE_SIZE feature flag (default true).
src/main/java/com/bobrust/generator/Worker.javaStores and exposes a GradientMap for use during shape generation/mutation.
src/main/java/com/bobrust/generator/Model.javaComputes gradient map once at initialization and wires it into Worker.
src/main/java/com/bobrust/generator/GradientMap.javaNew Sobel-based gradient map with gradient-biased size selection and mutation scaling.
src/main/java/com/bobrust/generator/Circle.javaUses gradient map to bias size selection and scale position mutation near edges.

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

Comment on lines +129 to +143
float[] weights = new float[numSizes];
float totalWeight = 0;

for (int i = 0; i < numSizes; i++) {
float sizeNorm = (float) i / (numSizes - 1); // 0=smallest, 1=largest
// High gradient -> prefer small (low sizeNorm), low gradient -> prefer large
weights[i] = (float) Math.exp(-4.0 * Math.abs(sizeNorm - (1.0 - gradient)));
totalWeight += weights[i];
}

// Weighted random selection
float r = rnd.nextFloat() * totalWeight;
float cumulative = 0;
for (int i = 0; i < numSizes; i++) {
cumulative += weights[i];

CopilotAIApr 4, 2026

Copy link

Choose a reason for hiding this comment

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

selectSizeIndex allocates a new float[] weights on every call. This method is called frequently (e.g., from Circle.randomize() / mutateShape()), so the per-call allocation will create substantial GC pressure and slow down generation. Consider rewriting selection to avoid allocations (e.g., two-pass compute total then select without storing the array, or reuse a preallocated array via ThreadLocal).

Suggested change
float[] weights = newfloat[numSizes];
floattotalWeight = 0;
for (inti = 0; i < numSizes; i++) {
floatsizeNorm = (float) i / (numSizes - 1); // 0=smallest, 1=largest
// High gradient -> prefer small (low sizeNorm), low gradient -> prefer large
weights[i] = (float) Math.exp(-4.0 * Math.abs(sizeNorm - (1.0 - gradient)));
totalWeight += weights[i];
}
// Weighted random selection
floatr = rnd.nextFloat() * totalWeight;
floatcumulative= 0;
for (inti = 0;i< numSizes; i++) {
cumulative += weights[i];
floattotalWeight = 0;
floattargetSizeNorm = 1.0f - gradient;
for (inti = 0; i < numSizes; i++) {
floatsizeNorm = (float) i / (numSizes - 1); // 0=smallest, 1=largest
// High gradient -> prefer small (low sizeNorm), low gradient -> prefer large
totalWeight += (float) Math.exp(-4.0 * Math.abs(sizeNorm - targetSizeNorm));
}
// Weighted random selection without per-call array allocation
floatr = rnd.nextFloat() * totalWeight;
floatcumulative = 0;
for (inti= 0;i < numSizes; i++) {
floatsizeNorm = (float)i/ (numSizes - 1); // 0=smallest, 1=largest
cumulative += (float) Math.exp(-4.0 * Math.abs(sizeNorm - targetSizeNorm));

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +35
private static final int ALPHA = 128;
private static final int BACKGROUND = 0xFFFFFFFF;
private static final File OUTPUT_DIR = new File("test-results/proposal3");

@BeforeAll
static void setup() {
OUTPUT_DIR.mkdirs();
}

CopilotAIApr 4, 2026

Copy link

Choose a reason for hiding this comment

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

This test writes generated images into test-results/proposal3, which is not ignored by git and is also committed in this PR. Running the test will overwrite tracked files and/or leave a dirty working tree. Consider writing outputs under build/ (as ErrorGuidedPlacementTest does) or gating the visual-output portion behind a system property / @Disabled so regular test runs don’t mutate the repository.

Copilot uses AI. Check for mistakes.
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;

CopilotAIApr 4, 2026

Copy link

Choose a reason for hiding this comment

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

Unused import: java.util.Arrays is imported but not used in this test class. Removing it will keep the test clean and avoid IDE/compiler warnings.

Suggested change
import java.util.Arrays;

Copilot uses AI. Check for mistakes.
@VoX
VoX merged commit 7b503a9 into mainApr 4, 2026
4 checks passed
Sign up for freeto 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

@VoX