mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: replace enhancement pipeline with CLAHE + normalise + gamma
CLAHE provides adaptive local contrast, normalise stretches the histogram, and gamma adjusts exposure perceptually. Replaces the old modulate/linear pipeline that compounded errors and darkened images. Preset multipliers now include clahe and normalise entries. Key fixes beyond the spec: - maxSlope rounded to integer (Sharp requirement) - White balance uses linear() instead of recomb() to avoid float-cast that breaks CLAHE in the libvips pipeline - CLAHE tile size adapts to image dimensions (1x1 for tiny images) - Gamma clamped to Sharp's valid range (1.0-3.0) - Normalise lower/upper correctly mapped to percentile cutoffs
This commit is contained in:
@@ -205,3 +205,79 @@ describe("applyCorrections", () => {
|
||||
expect(enhancedMeta.height).toBe(originalMeta.height);
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyCorrections pipeline (CLAHE + normalise + gamma)", () => {
|
||||
it("does not darken a well-exposed image", async () => {
|
||||
const analysis = await analyzeImage(PNG_200x150);
|
||||
const image = sharp(PNG_200x150);
|
||||
const enhanced = applyCorrections(image, analysis.corrections, "auto", 50, {});
|
||||
const enhancedBuf = await enhanced.toBuffer();
|
||||
|
||||
const origStats = await sharp(PNG_200x150).stats();
|
||||
const enhStats = await sharp(enhancedBuf).stats();
|
||||
|
||||
const origLum =
|
||||
origStats.channels[0].mean * 0.299 +
|
||||
origStats.channels[1].mean * 0.587 +
|
||||
origStats.channels[2].mean * 0.114;
|
||||
const enhLum =
|
||||
enhStats.channels[0].mean * 0.299 +
|
||||
enhStats.channels[1].mean * 0.587 +
|
||||
enhStats.channels[2].mean * 0.114;
|
||||
|
||||
// Enhanced image should not be more than 5% darker
|
||||
expect(enhLum).toBeGreaterThan(origLum * 0.95);
|
||||
});
|
||||
|
||||
it("brightens a dark image", async () => {
|
||||
const darkBuffer = await sharp({
|
||||
create: { width: 100, height: 100, channels: 3, background: { r: 30, g: 30, b: 30 } },
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const analysis = await analyzeImage(darkBuffer);
|
||||
const enhanced = applyCorrections(sharp(darkBuffer), analysis.corrections, "auto", 50, {});
|
||||
const enhancedBuf = await enhanced.toBuffer();
|
||||
|
||||
const origStats = await sharp(darkBuffer).stats();
|
||||
const enhStats = await sharp(enhancedBuf).stats();
|
||||
|
||||
const origLum =
|
||||
origStats.channels[0].mean * 0.299 +
|
||||
origStats.channels[1].mean * 0.587 +
|
||||
origStats.channels[2].mean * 0.114;
|
||||
const enhLum =
|
||||
enhStats.channels[0].mean * 0.299 +
|
||||
enhStats.channels[1].mean * 0.587 +
|
||||
enhStats.channels[2].mean * 0.114;
|
||||
|
||||
expect(enhLum).toBeGreaterThan(origLum * 1.1);
|
||||
});
|
||||
|
||||
it("applies CLAHE at intensity 0 with no visible effect", async () => {
|
||||
const image = sharp(PNG_200x150);
|
||||
const corrections = {
|
||||
brightness: 0,
|
||||
contrast: 0,
|
||||
temperature: 0,
|
||||
saturation: 0,
|
||||
sharpness: 0,
|
||||
denoise: 0,
|
||||
};
|
||||
const enhanced = applyCorrections(image, corrections, "auto", 0, {});
|
||||
const enhancedBuf = await enhanced.toBuffer();
|
||||
|
||||
const origStats = await sharp(PNG_200x150).stats();
|
||||
const enhStats = await sharp(enhancedBuf).stats();
|
||||
const origLum =
|
||||
origStats.channels[0].mean * 0.299 +
|
||||
origStats.channels[1].mean * 0.587 +
|
||||
origStats.channels[2].mean * 0.114;
|
||||
const enhLum =
|
||||
enhStats.channels[0].mean * 0.299 +
|
||||
enhStats.channels[1].mean * 0.587 +
|
||||
enhStats.channels[2].mean * 0.114;
|
||||
expect(Math.abs(enhLum - origLum)).toBeLessThan(origLum * 0.15);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user