Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/main/java/com/bobrust/generator/Model.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ private void addShape(Circle shape) {
}
}

/**
* Add a pre-defined shape to this model without running optimization.
* Used by MultiResModel to propagate shapes from lower to higher resolutions.
*/
public void addExternalShape(Circle shape) {
addShape(shape);
}

/** Returns the current model score. */
public float getScore() {
Comment on lines +85 to +90
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.

addExternalShape() and getScore() are public but (per repo search) are only used by MultiResModel / tests within the same package. Consider reducing visibility (package-private) or clearly documenting the API contract, since addExternalShape lets callers mutate Model state without optimization and could be misused by external consumers.

Suggested change
public void addExternalShape(Circle shape) {
addShape(shape);
}
/** Returns the current model score. */
public float getScore() {
void addExternalShape(Circle shape) {
addShape(shape);
}
/** Returns the current model score. */
float getScore() {

Copilot uses AI. Check for mistakes.
return score;
}

private static final int max_random_states = 1000;
private static final int age = 100;
private static final int times = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
Expand Down
161 changes: 161 additions & 0 deletions src/main/java/com/bobrust/generator/MultiResModel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package com.bobrust.generator;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;

/**
* Progressive multi-resolution model for shape generation.
*
* Uses a resolution pyramid:
* <ul>
* <li>Level 2: quarter resolution (first 10% of shapes)</li>
* <li>Level 1: half resolution (next 30% of shapes)</li>
* <li>Level 0: full resolution (remaining 60% of shapes)</li>
* </ul>
*
* Shapes generated at lower resolutions are scaled and propagated to all
* finer resolution levels, so the full-resolution model stays in sync.
*/
public class MultiResModel {
/** Models at each resolution level: [0]=full, [1]=half, [2]=quarter */
private final Model[] levels;

/** Dimensions at each level */
private final int[][] dims;

/** The full-resolution target image */
private final BorstImage fullTarget;

private final int backgroundRGB;
private final int alpha;
private int shapesAdded;

/**
* Create a multi-resolution model.
*
* @param target the full-resolution target image
* @param backgroundRGB background color
* @param alpha alpha value for blending
*/
public MultiResModel(BorstImage target, int backgroundRGB, int alpha) {
this.fullTarget = target;
this.backgroundRGB = backgroundRGB;
this.alpha = alpha;
this.shapesAdded = 0;

int fw = target.width;
int fh = target.height;

dims = new int[][] {
{ fw, fh }, // Level 0: full
{ Math.max(1, fw / 2), Math.max(1, fh / 2) }, // Level 1: half
{ Math.max(1, fw / 4), Math.max(1, fh / 4) }, // Level 2: quarter
};

levels = new Model[3];
levels[0] = new Model(target, backgroundRGB, alpha);
levels[1] = new Model(scaleImage(target, dims[1][0], dims[1][1]), backgroundRGB, alpha);
levels[2] = new Model(scaleImage(target, dims[2][0], dims[2][1]), backgroundRGB, alpha);
}

/**
* Process one shape at the appropriate resolution level.
*
* @param currentShape current shape index (0-based)
* @param maxShapes total number of shapes to generate
* @return the counter from the worker (number of energy evaluations)
*/
public int processStep(int currentShape, int maxShapes) {
float progress = (float) currentShape / maxShapes;
int level;
if (progress < 0.10f) {
level = 2; // Quarter resolution
} else if (progress < 0.40f) {
level = 1; // Half resolution
} else {
level = 0; // Full resolution
}

// Run generation at selected level
int n = levels[level].processStep();

// Get the shape that was just added
Circle shape = levels[level].shapes.get(levels[level].shapes.size() - 1);

// Propagate the shape to all finer levels
for (int i = level - 1; i >= 0; i--) {
Circle scaled = scaleCircle(shape, level, i);
levels[i].addExternalShape(scaled);
}

shapesAdded++;
return n;
}

/**
* Scale a circle from one resolution level to another.
*/
private Circle scaleCircle(Circle shape, int fromLevel, int toLevel) {
float scaleX = (float) dims[toLevel][0] / dims[fromLevel][0];
float scaleY = (float) dims[toLevel][1] / dims[fromLevel][1];

int newX = Math.round(shape.x * scaleX);
int newY = Math.round(shape.y * scaleY);

// Scale the radius and snap to nearest valid size
int scaledR = Math.round(shape.r * scaleX);
int newR = BorstUtils.getClosestSize(scaledR);

// Create a new circle in the target level's worker
// We need to access the worker through the model
return new Circle(getWorker(levels[toLevel]), newX, newY, newR);
}
Comment on lines +99 to +113
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.

In scaleCircle(), scaleY is computed but the circle size is scaled using only scaleX. Because your level dimensions are derived via integer division (fw/2, fw/4, etc.), scaleX and scaleY can differ slightly, which can skew the propagated circle size. Consider scaling the circle size using a symmetric factor (e.g., min/avg of scaleX & scaleY) and/or mapping by size index per level so large coarse-level circles don’t collapse to the max supported size when propagated.

Copilot uses AI. Check for mistakes.

/**
* Get the full-resolution model (level 0).
*/
public Model getFullResModel() {
return levels[0];
}

/**
* Get the model at a specific level.
*/
public Model getModel(int level) {
return levels[level];
}

/**
* Get the number of shapes added so far.
*/
public int getShapesAdded() {
return shapesAdded;
}

/**
* Scale a BorstImage to a new size.
*/
private static BorstImage scaleImage(BorstImage source, int newWidth, int newHeight) {
BufferedImage scaled = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = scaled.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(source.image, 0, 0, newWidth, newHeight, null);
g.dispose();
return new BorstImage(scaled);
}

/**
* Reflectively get the Worker from a Model. This is needed because Worker
* is package-private and we need it to create Circle instances.
*/
private static Worker getWorker(Model model) {
try {
var field = Model.class.getDeclaredField("worker");
field.setAccessible(true);
return (Worker) field.get(model);
} catch (Exception e) {
throw new RuntimeException("Failed to access Model.worker", e);
}
}
Comment on lines +148 to +160
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.

MultiResModel currently uses reflection to access the private Model.worker field. This is brittle (breaks with refactors/strong encapsulation), slower, and can be blocked in some runtime environments. Since MultiResModel is in the same package, consider adding a package-private accessor/factory on Model (e.g., to create a Circle for that model) so MultiResModel can propagate shapes without reflection.

Copilot uses AI. Check for mistakes.
}
4 changes: 4 additions & 0 deletions src/main/java/com/bobrust/util/data/AppConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public interface AppConstants {
// TSP cost function weights
float TSP_W_PALETTE = 3.0f; // Weight for palette change cost
float TSP_W_DISTANCE = 1.0f; // Weight for Euclidean distance cost

// When true, use progressive multi-resolution generation:
// first 10% shapes at quarter res, next 30% at half res, remaining 60% at full res
boolean USE_PROGRESSIVE_RESOLUTION = true;
Comment on lines +49 to +52
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_PROGRESSIVE_RESOLUTION is introduced as a feature flag, but there are currently no references to it in src/main/java (search shows only this definition). As-is, toggling it has no effect. Either wire it into the generation path (e.g., where Model is instantiated/stepped) or remove it until integration is in place to avoid misleading configuration.

Copilot uses AI. Check for mistakes.

// Average canvas colors. Used as default colors
Color CANVAS_AVERAGE = new Color(0xb3aba0);
Expand Down
Loading
Loading