Feature: Calibration tool for measuring actual Rust paint values#10
Feature: Calibration tool for measuring actual Rust paint values#10
Conversation
Provides a CalibrationPatternGenerator that creates a 6x6 reference grid (sizes x alphas) and a ScreenshotAnalyzer that measures painted circles from Rust screenshots to detect mismatches with hardcoded SIZES and ALPHAS constants. Includes grid auto-detection via brightness projections, shape diff image generation, and a copy-paste Java snippet for corrected values. Also makes CircleCache and Scanline public so the calibration package can access the scanline masks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a calibration workflow to generate a known size/opacity grid pattern and analyze Rust screenshots to measure actual brush diameters, effective alpha, and circle shape fidelity vs the current scanline masks.
Changes:
- Introduces
CalibrationPatternGeneratorto generate a size×opacity grid reference image. - Introduces
ScreenshotAnalyzerto auto-detect the grid in a screenshot, measure per-cell diameter/alpha/shape match, and emit a diff image + suggested constants. - Adds
CalibrationRoundTripTestto validate round-trip behavior (including 2x scaling) and exposes generator internals needed by the calibration code.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/test/java/com/bobrust/calibration/CalibrationRoundTripTest.java | Adds round-trip + scaling tests for the calibration toolchain |
| src/main/java/com/bobrust/generator/Scanline.java | Makes Scanline public for cross-package calibration usage |
| src/main/java/com/bobrust/generator/CircleCache.java | Makes CircleCache public so calibration code can access scanline masks |
| src/main/java/com/bobrust/calibration/ScreenshotAnalyzer.java | New screenshot analysis CLI + measurement and diff-image generation |
| src/main/java/com/bobrust/calibration/CalibrationPatternGenerator.java | New reference-pattern generator CLI (grid of size/opacity circles) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * <li>Shape match percentage against Bob-Rust's scanline masks</li> | ||
| * </ul> | ||
| * | ||
| * Usage: java -cp ... com.bobrust.calibration.ScreenshotAnalyzer screenshot.png [reference.png] |
There was a problem hiding this comment.
The class-level Javadoc says the CLI usage is screenshot.png [reference.png], but main actually treats the optional second argument as the diff output path. Please update the Javadoc usage string to match the implemented arguments (or implement the reference-image parameter if that was intended).
| * Usage: java -cp ... com.bobrust.calibration.ScreenshotAnalyzer screenshot.png [reference.png] | |
| * Usage: java -cp ... com.bobrust.calibration.ScreenshotAnalyzer screenshot.png [diff.png] |
| // Find the total energy to set a threshold | ||
| long totalEnergy = 0; | ||
| for (long v : proj) totalEnergy += v; | ||
| if (totalEnergy == 0) return null; | ||
|
|
There was a problem hiding this comment.
The comment says totalEnergy is used to set a threshold, but the code only uses it to early-return when the projection is empty. Either add the intended thresholding (to avoid noise peaks) or update the comment to reflect the actual behavior.
| int totalChecked = 0; | ||
| for (int dy = -halfSize; dy <= halfSize; dy++) { | ||
| for (int dx = -halfSize; dx <= halfSize; dx++) { | ||
| int px = cx + dx; | ||
| int py = cy + dy; | ||
| if (px < 0 || px >= imgW || py < 0 || py >= imgH) continue; | ||
| totalChecked++; |
There was a problem hiding this comment.
totalChecked is incremented but never used. Please remove it (or use it to report/normalize something) to avoid dead code and confusion about what the loop is measuring.
| int totalChecked = 0; | |
| for (int dy = -halfSize; dy <= halfSize; dy++) { | |
| for (int dx = -halfSize; dx <= halfSize; dx++) { | |
| int px = cx + dx; | |
| int py = cy + dy; | |
| if (px < 0 || px >= imgW || py < 0 || py >= imgH) continue; | |
| totalChecked++; | |
| for (int dy = -halfSize; dy <= halfSize; dy++) { | |
| for (int dx = -halfSize; dx <= halfSize; dx++) { | |
| int px = cx + dx; | |
| int py = cy + dy; | |
| if (px < 0 || px >= imgW || py < 0 || py >= imgH) continue; |
| if (totalExpected == 0) return 0.0; | ||
| // F1-like score: penalize both misses and false positives | ||
| int errors = (totalExpected - matched) + falsePositives; | ||
| int possible = totalExpected + falsePositives; | ||
| return possible > 0 ? 100.0 * (possible - errors) / possible : 100.0; |
There was a problem hiding this comment.
The score labeled as "F1-like" is effectively matched / (totalExpected + falsePositives) (IoU/Jaccard-style), not an F1 score. Either rename the metric/comment to what it is, or change the formula to a true F1 (2TP/(2TP+FP+FN)) so the reported percentage matches the description.
| // White with given alpha, composited onto black background: | ||
| // result = alpha/255 * 255 = alpha |
There was a problem hiding this comment.
This comment describes drawing "white with given alpha" composited onto black, but the code actually writes an opaque pixel with RGB set to alpha (gray). Please update the comment to match what is written, since this affects how consumers interpret the generated pattern (alpha channel vs brightness).
| // White with given alpha, composited onto black background: | |
| // result = alpha/255 * 255 = alpha | |
| // Store the composited-on-black result directly as an opaque grayscale pixel: | |
| // RGB = alpha, with a fully opaque alpha channel. |
| import org.apache.logging.log4j.Logger; | ||
|
|
||
| class CircleCache { | ||
| public class CircleCache { | ||
| private static final Logger LOGGER = LogManager.getLogger(BobRustPainter.class); | ||
|
|
There was a problem hiding this comment.
Making this class public exposes CIRCLE_CACHE/CIRCLE_CACHE_LENGTH as part of the public API, and those arrays remain mutable from outside the package. If this is only needed for calibration tooling, consider keeping the class/package internals non-public and exposing a narrow accessor (or returning defensive copies / unmodifiable views) to avoid accidental mutation by external callers.
| public int y; | ||
| public int x1; | ||
| public int x2; // inclusive |
There was a problem hiding this comment.
Changing Scanline to public exposes a mutable data type (public fields) as API surface. Consider making the fields final (or using an immutable type like a record) so callers can’t mutate cached scanlines and corrupt shape masks at runtime.
| public int y; | |
| public int x1; | |
| public int x2; // inclusive | |
| public final int y; | |
| public final int x1; | |
| public final int x2; // inclusive |
| // All bottom-row detected cells should have correct alpha | ||
| int bottomRowDetected = 0; | ||
| for (int col = 0; col < numSizes; col++) { | ||
| if (detected[numAlphas - 1][col]) bottomRowDetected++; | ||
| } | ||
| assertTrue(alphaMatches >= bottomRowDetected * 0.8, | ||
| "Too few alpha matches in bottom row: " + alphaMatches + "/" + bottomRowDetected); |
There was a problem hiding this comment.
The comment says "All bottom-row detected cells should have correct alpha", but the assertion only requires 80% (alphaMatches >= bottomRowDetected * 0.8). Please align the comment/message with the actual tolerance, or tighten the assertion if the intent is truly 100% for the round-trip case.
| // All bottom-row detected cells should have correct alpha | |
| int bottomRowDetected = 0; | |
| for (int col = 0; col < numSizes; col++) { | |
| if (detected[numAlphas - 1][col]) bottomRowDetected++; | |
| } | |
| assertTrue(alphaMatches >= bottomRowDetected * 0.8, | |
| "Too few alpha matches in bottom row: " + alphaMatches + "/" + bottomRowDetected); | |
| // At least 80% of bottom-row detected cells should have correct alpha | |
| int bottomRowDetected = 0; | |
| for (int col = 0; col < numSizes; col++) { | |
| if (detected[numAlphas - 1][col]) bottomRowDetected++; | |
| } | |
| assertTrue(alphaMatches >= bottomRowDetected * 0.8, | |
| "Too few alpha matches in bottom row (expected at least 80%): " | |
| + alphaMatches + "/" + bottomRowDetected); |
Summary
Calibration tool to measure the actual circle sizes, alpha values, and circle shapes from Rust game screenshots and compare against Bob-Rust's hardcoded assumptions.
New files
CalibrationPatternGenerator— generates a 6x6 calibration grid (size x opacity), white on blackScreenshotAnalyzer— analyzes a screenshot of the painted pattern, auto-detects grid, measures diameter/alpha/shape match per cellCalibrationRoundTripTest— 5 tests including round-trip verification and 2x scale detectionUsage
CalibrationPatternGeneratorto create reference patternScreenshotAnalyzer path/to/screenshot.pngOutput includes
All 41 tests pass.