mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: recalibrate enhancement scoring with dead zones
Contrast score now uses linear stdevLum/1.2 centered at 50 (was miscalibrated 25-75 range centered at 75). Corrections use dead zones (score 40-60 = zero) so well-exposed images get near-zero adjustments instead of being darkened.
This commit is contained in:
@@ -119,9 +119,8 @@ function computeScores(
|
||||
): AnalysisScores {
|
||||
const exposureScore = clamp(Math.round((meanLum / 255) * 100), 0, 100);
|
||||
|
||||
const idealStdev = 60;
|
||||
const contrastDeviation = Math.abs(stdevLum - idealStdev) / idealStdev;
|
||||
const contrastScore = clamp(Math.round((1 - contrastDeviation) * 50 + 25), 0, 100);
|
||||
// Linear mapping: stdev ~60 = score 50. Higher stdev = higher contrast.
|
||||
const contrastScore = clamp(Math.round(stdevLum / 1.2), 0, 100);
|
||||
|
||||
const meanR = rCh.mean;
|
||||
const meanG = gCh.mean;
|
||||
@@ -146,9 +145,9 @@ function computeScores(
|
||||
}
|
||||
|
||||
function computeCorrections(scores: AnalysisScores): CorrectionParams {
|
||||
const brightness = clamp(Math.round((50 - scores.exposure) * 1.2), -60, 60);
|
||||
const contrast = clamp(Math.round((50 - scores.contrast) * 0.8), -40, 40);
|
||||
const temperature = clamp(Math.round((50 - scores.whiteBalance) * 0.5), -30, 30);
|
||||
const brightness = deadZoneCorrection(scores.exposure, 40, 60, 0.8);
|
||||
const contrast = deadZoneCorrection(scores.contrast, 40, 60, 0.6);
|
||||
const temperature = deadZoneCorrection(scores.whiteBalance, 40, 60, 0.5);
|
||||
|
||||
const saturation =
|
||||
scores.saturation < 40
|
||||
@@ -165,6 +164,16 @@ function computeCorrections(scores: AnalysisScores): CorrectionParams {
|
||||
return { brightness, contrast, temperature, saturation, sharpness, denoise };
|
||||
}
|
||||
|
||||
/**
|
||||
* Scores inside [lo, hi] produce zero correction.
|
||||
* Scores outside scale from the dead zone edge, not from 50.
|
||||
*/
|
||||
function deadZoneCorrection(score: number, lo: number, hi: number, factor: number): number {
|
||||
if (score >= lo && score <= hi) return 0;
|
||||
if (score < lo) return clamp(Math.round((lo - score) * factor), 0, 60);
|
||||
return clamp(Math.round((hi - score) * factor), -60, 0);
|
||||
}
|
||||
|
||||
function detectIssues(scores: AnalysisScores): string[] {
|
||||
const issues: string[] = [];
|
||||
if (scores.exposure < 35) issues.push("underexposed");
|
||||
|
||||
@@ -84,6 +84,40 @@ describe("analyzeImage", () => {
|
||||
const result = await analyzeImage(darkBuffer);
|
||||
expect(result.suggestedMode).toBe("low-light");
|
||||
});
|
||||
|
||||
it("produces contrast score near 50 for a typical well-exposed image", async () => {
|
||||
// A gradient image has stdev ~60, which should score ~50
|
||||
const gradientBuffer = await sharp(
|
||||
Buffer.from(
|
||||
Array.from({ length: 100 * 100 * 3 }, (_, i) => Math.floor(((i % 300) * 255) / 300)),
|
||||
),
|
||||
{ raw: { width: 100, height: 100, channels: 3 } },
|
||||
)
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const result = await analyzeImage(gradientBuffer);
|
||||
expect(result.scores.contrast).toBeGreaterThanOrEqual(35);
|
||||
expect(result.scores.contrast).toBeLessThanOrEqual(65);
|
||||
});
|
||||
|
||||
it("produces near-zero corrections for well-exposed images (dead zone)", async () => {
|
||||
// Wide spread centered at 128 gives stdev ~60 (contrast ~50) and mean ~128 (exposure ~50).
|
||||
// Both scores land inside the [40,60] dead zone, so corrections should be zero.
|
||||
const midGrayBuffer = await sharp(
|
||||
Buffer.from(
|
||||
Array.from({ length: 100 * 100 * 3 }, (_, i) => 24 + Math.floor(Math.random() * 208)),
|
||||
),
|
||||
{ raw: { width: 100, height: 100, channels: 3 } },
|
||||
)
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const result = await analyzeImage(midGrayBuffer);
|
||||
// Corrections should be zero or near-zero in the dead zone
|
||||
expect(Math.abs(result.corrections.brightness)).toBeLessThanOrEqual(5);
|
||||
expect(Math.abs(result.corrections.contrast)).toBeLessThanOrEqual(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scaleCorrections", () => {
|
||||
|
||||
Reference in New Issue
Block a user