feat: SOTA sharpening tool with 3 methods and 7 presets (#56)

* feat(sharpening): add SharpenAdvancedOptions type for multi-method sharpening

* feat(sharpening): implement 3-method sharpen engine (adaptive, USM, high-pass)

* feat(sharpening): add dedicated API route with Zod validation

* feat(sharpening): register tool in constants, i18n, and suggested tools

* feat(sharpening): add settings UI with presets, methods, and advanced controls

* feat(sharpening): register tool in frontend registry with before-after display

---------

Co-authored-by: stirling-image <stirling-image@users.noreply.github.com>
This commit is contained in:
stirling-image
2026-04-13 19:12:09 +08:00
committed by GitHub
co-authored by stirling-image
parent a8c7b92ca5
commit 58cdbe50b4
11 changed files with 646 additions and 3 deletions
@@ -1,4 +1,4 @@
import type { Sharp, SharpenOptions } from "../types.js";
import type { Sharp, SharpenAdvancedOptions, SharpenOptions } from "../types.js";
export async function sharpen(image: Sharp, options: SharpenOptions): Promise<Sharp> {
const { value } = options;
@@ -13,3 +13,95 @@ export async function sharpen(image: Sharp, options: SharpenOptions): Promise<Sh
return image.sharpen({ sigma });
}
const DENOISE_KERNEL: Record<string, number> = {
light: 3,
medium: 5,
strong: 7,
};
export async function sharpenAdvanced(
image: Sharp,
options: SharpenAdvancedOptions,
): Promise<Sharp> {
const { method, denoise } = options;
// Optional noise reduction pre-pass
if (denoise && denoise !== "off") {
const kernelSize = DENOISE_KERNEL[denoise];
if (kernelSize) {
image = image.median(kernelSize);
}
}
switch (method) {
case "adaptive":
return sharpenAdaptive(image, options);
case "unsharp-mask":
return sharpenUnsharpMask(image, options);
case "high-pass":
return sharpenHighPass(image, options);
default:
throw new Error(`Unknown sharpening method: ${method}`);
}
}
function sharpenAdaptive(image: Sharp, options: SharpenAdvancedOptions): Sharp {
const sigma = options.sigma ?? 1.0;
const m1 = options.m1 ?? 1.0;
const m2 = options.m2 ?? 3.0;
const x1 = options.x1 ?? 2.0;
const y2 = options.y2 ?? 12;
const y3 = options.y3 ?? 20;
return image.sharpen({ sigma, m1, m2, x1, y2, y3 });
}
function sharpenUnsharpMask(image: Sharp, options: SharpenAdvancedOptions): Sharp {
const amount = options.amount ?? 100;
const radius = options.radius ?? 1.0;
const threshold = options.threshold ?? 0;
const sigma = radius;
const intensity = amount / 100;
const x1 = (threshold / 255) * 10;
return image.sharpen({ sigma, m1: intensity, m2: intensity, x1, y2: 15, y3: 25 });
}
function sharpenHighPass(image: Sharp, options: SharpenAdvancedOptions): Sharp {
const strength = options.strength ?? 50;
const kernelSize = options.kernelSize ?? 3;
const s = strength / 100;
if (kernelSize === 5) {
const k = [
0,
-s,
-s,
-s,
0,
-s,
s,
s * 2,
s,
-s,
-s,
s * 2,
1 + s * 8,
s * 2,
-s,
-s,
s,
s * 2,
s,
-s,
0,
-s,
-s,
-s,
0,
];
return image.convolve({ width: 5, height: 5, kernel: k });
}
const k = [0, -s, 0, -s, 1 + 4 * s, -s, 0, -s, 0];
return image.convolve({ width: 3, height: 3, kernel: k });
}