mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
co-authored by
stirling-image
parent
a8c7b92ca5
commit
58cdbe50b4
@@ -13,7 +13,7 @@ import { resize } from "./operations/resize.js";
|
||||
import { rotate } from "./operations/rotate.js";
|
||||
import { saturation } from "./operations/saturation.js";
|
||||
import { sepia } from "./operations/sepia.js";
|
||||
import { sharpen } from "./operations/sharpen.js";
|
||||
import { sharpen, sharpenAdvanced } from "./operations/sharpen.js";
|
||||
import { stripMetadata } from "./operations/strip-metadata.js";
|
||||
import type {
|
||||
BrightnessOptions,
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
RotateOptions,
|
||||
SaturationOptions,
|
||||
Sharp,
|
||||
SharpenAdvancedOptions,
|
||||
SharpenOptions,
|
||||
StripMetadataOptions,
|
||||
} from "./types.js";
|
||||
@@ -58,6 +59,8 @@ const OPERATION_MAP: Record<
|
||||
grayscale: (img) => grayscale(img),
|
||||
sepia: (img) => sepia(img),
|
||||
sharpen: (img, opts) => sharpen(img, opts as unknown as SharpenOptions),
|
||||
"sharpen-advanced": (img, opts) =>
|
||||
sharpenAdvanced(img, opts as unknown as SharpenAdvancedOptions),
|
||||
invert: (img) => invert(img),
|
||||
"edit-metadata": (img, opts) => editMetadata(img, opts as unknown as EditMetadataOptions),
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export { resize } from "./operations/resize.js";
|
||||
export { rotate } from "./operations/rotate.js";
|
||||
export { saturation } from "./operations/saturation.js";
|
||||
export { sepia } from "./operations/sepia.js";
|
||||
export { sharpen } from "./operations/sharpen.js";
|
||||
export { sharpen, sharpenAdvanced } from "./operations/sharpen.js";
|
||||
export { stripMetadata } from "./operations/strip-metadata.js";
|
||||
export * from "./types.js";
|
||||
export * from "./utils/metadata.js";
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -109,6 +109,28 @@ export interface SharpenOptions {
|
||||
value: number; // 0 to 100
|
||||
}
|
||||
|
||||
export type SharpenMethod = "adaptive" | "unsharp-mask" | "high-pass";
|
||||
|
||||
export interface SharpenAdvancedOptions {
|
||||
method: SharpenMethod;
|
||||
// Adaptive method params
|
||||
sigma?: number; // 0.5-10, Gaussian blur radius
|
||||
m1?: number; // 0-10, flat area sharpening
|
||||
m2?: number; // 0-20, textured area sharpening
|
||||
x1?: number; // 0-10, flat/jagged threshold
|
||||
y2?: number; // 0-50, max brightening (halo clamp)
|
||||
y3?: number; // 0-50, max darkening (halo clamp)
|
||||
// Unsharp mask params
|
||||
amount?: number; // 0-500, intensity percentage
|
||||
radius?: number; // 0.1-5.0, blur radius
|
||||
threshold?: number; // 0-255, minimum edge brightness
|
||||
// High-pass params
|
||||
strength?: number; // 0-100, blend strength
|
||||
kernelSize?: 3 | 5; // 3x3 or 5x5 kernel
|
||||
// Noise reduction
|
||||
denoise?: "off" | "light" | "medium" | "strong";
|
||||
}
|
||||
|
||||
export type EnhancementMode = "auto" | "portrait" | "landscape" | "low-light" | "food" | "document";
|
||||
|
||||
export interface AnalysisScores {
|
||||
|
||||
@@ -104,6 +104,14 @@ export const TOOLS: Tool[] = [
|
||||
icon: "SlidersHorizontal",
|
||||
route: "/adjust-colors",
|
||||
},
|
||||
{
|
||||
id: "sharpening",
|
||||
name: "Sharpening",
|
||||
description: "Adaptive, unsharp mask, and high-pass sharpening with presets",
|
||||
category: "adjustments",
|
||||
icon: "Focus",
|
||||
route: "/sharpening",
|
||||
},
|
||||
{
|
||||
id: "replace-color",
|
||||
name: "Replace & Invert Color",
|
||||
|
||||
@@ -49,6 +49,10 @@ export const en = {
|
||||
description:
|
||||
"Brightness, contrast, exposure, saturation, temperature, sharpness, and effects",
|
||||
},
|
||||
sharpening: {
|
||||
name: "Sharpening",
|
||||
description: "Adaptive, unsharp mask, and high-pass sharpening with presets",
|
||||
},
|
||||
"replace-color": {
|
||||
name: "Replace & Invert Color",
|
||||
description: "Replace specific colors or invert",
|
||||
|
||||
Reference in New Issue
Block a user