| Server IP : 217.154.3.148 / Your IP : 216.73.216.90 Web Server : Apache System : Linux blissful-wright.217-154-3-148.plesk.page 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64 User : gracious-boyd_hfhh1h8vo9n ( 10000) PHP Version : 8.3.31 Disable Function : opcache_get_status MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /var/www/vhosts/gracious-boyd.217-154-3-148.plesk.page/nextjs/lib/mask/ |
Upload File : |
import {
otsuThreshold,
percentile,
clipRescale,
binarize,
morphOpen,
morphClose,
morphDilate,
morphErode,
sobel,
removeBorderTouching,
keepLargest,
invert255,
largestBorderConnected,
dropSmall,
subtractEdges,
maskOr,
maskAnd,
maskMinus,
stripBorder,
gateByDistance,
geodesicGrow,
suppressGridEdges,
suppressGridEdgesAdaptive,
suppressThinLines,
localStd,
hysteresisEdges,
meanStdMasked,
illuminationNormalize,
} from '@/utils/maskOps'
export type MorphOp = {
op: 'open' | 'close' | 'dilate' | 'erode'
radius: number
}
export type EdgeGuardConfig = {
enabled: boolean
threshold?: number
dilate?: number
}
export type EdgeFillConfig = {
enabled: boolean
threshold: number
dilate: number
}
export type NormalizeOptions = {
invertMode: 'auto' | 'true' | 'false' | boolean
thresholdMode: 'otsu' | 'value'
thresholdValue: number
percentileLow: number
percentileHigh: number
morphOps: MorphOp[]
keepLargest: number
minArea: number
removeBorder: boolean
edgeGuard: EdgeGuardConfig
edgeFill: EdgeFillConfig
kDark: number
kLight: number
seedBorder: number
seedGrow: number
gridKill: string
gridVFrac: number
gridHFrac: number
gridPad: number
gridSigma: number
thinWidth: number
thinLengthFrac: number
carveRadius: number
edgeLowFrac: number
localStdWin: number
localStdK: number
illuminationRadius: number
illuminationAmount: number
fallbackHigh: number
fallbackLow: number
bgMode: 'largest' | 'classic'
}
export type NormalizedStats = {
originalCoverage: number
originalMean: number
percentileLow: number
percentileHigh: number
thresholdMode: string
invertMode: string | boolean
inverted: boolean
clip?: { low: number; high: number }
morphApplied: string[]
backgroundMode?: string
background?: { mode: string; coverage: number; fallback?: boolean; skipped?: boolean; occluderCoverage?: number }
borderMinArea?: number
dropSmall?: number
keepLargest?: number
edgeGuard?: { threshold: number; dilate: number }
edgeFill?: { threshold?: number; low?: number; lowFrac?: number; dilate?: number; enabled?: boolean }
gridKillMode?: { mode: string; sigma?: number; vFrac?: number; hFrac?: number; pad?: number }
hybrid?: {
kDark: number
kLight: number
thrDark: number
thrLight: number
std: number
stdClamped?: number
seedCoverage: number
seedMask?: number
borderCull?: number
carveRadius?: number
seedGrow?: number
gateDist?: number
gridKill?: string
gridSigma?: number
thinWidth?: number
thinLength?: number
textureMask?: number
localStdWin?: number
localStdK?: number
localStdThresh?: number
edgeLowFrac?: number
illuminationRadius?: number
illuminationAmount?: number
}
seedFallback?: boolean
fallbackReason?: string | null
highCoverage?: number
}
export type NormalizedResult = {
data: Uint8Array
coverage: number
inverted: boolean
threshold: number
original: number
fallback: boolean
stats: NormalizedStats
}
export type MaskPresetName = 'tiles-default' | 'tiles-detail'
const defaultMorphOps: MorphOp[] = [
{ op: 'open', radius: 1 },
{ op: 'close', radius: 2 },
]
export const baseNormalizeOptions = (): NormalizeOptions => ({
invertMode: 'auto',
thresholdMode: 'otsu',
thresholdValue: 127,
percentileLow: 1,
percentileHigh: 99,
morphOps: [...defaultMorphOps],
keepLargest: 0,
minArea: 900,
removeBorder: true,
edgeGuard: { enabled: false, threshold: 48, dilate: 1 },
edgeFill: { enabled: true, threshold: 96, dilate: 1 },
kDark: 1.25,
kLight: 0.5,
seedBorder: 6,
seedGrow: 6,
gridKill: 'auto',
gridVFrac: 0.55,
gridHFrac: 0.55,
gridPad: 1,
gridSigma: 2.5,
thinWidth: 3,
thinLengthFrac: 0.25,
carveRadius: 2,
edgeLowFrac: 0.5,
localStdWin: 7,
localStdK: 1.2,
illuminationRadius: 0,
illuminationAmount: 0,
fallbackHigh: 0.98,
fallbackLow: 0.02,
bgMode: 'largest',
})
export function applyPreset(config: NormalizeOptions, presetName: MaskPresetName): NormalizeOptions {
const next = { ...config }
const ensureEdgeFill = (threshold: number) => {
const prev = next.edgeFill || { enabled: true, threshold, dilate: 1 }
next.edgeFill = {
enabled: true,
threshold,
dilate: Number.isFinite(prev.dilate) ? Math.max(0, Math.round(prev.dilate!)) : 1,
}
}
switch (presetName) {
case 'tiles-default':
next.kDark = 0.40
next.kLight = 0.16
ensureEdgeFill(84)
next.edgeLowFrac = 0.30
next.seedBorder = 2
next.seedGrow = 28
next.carveRadius = 6
next.gridKill = 'auto'
next.gridSigma = 2.8
next.gridPad = 3
next.thinWidth = 2
next.thinLengthFrac = 0.30
next.minArea = 400
next.edgeGuard = { enabled: false, threshold: 48, dilate: 1 }
next.localStdWin = 7
next.localStdK = 1.2
next.illuminationRadius = 32
next.illuminationAmount = 0.75
return next
case 'tiles-detail':
next.kDark = 0.45
next.kLight = 0.18
ensureEdgeFill(80)
next.edgeLowFrac = 0.30
next.seedBorder = 3
next.seedGrow = 32
next.carveRadius = 5
next.gridKill = 'auto'
next.gridSigma = 3.0
next.gridPad = 3
next.thinWidth = 3
next.thinLengthFrac = 0.25
next.minArea = 600
next.edgeGuard = { enabled: false, threshold: 48, dilate: 1 }
next.localStdWin = 7
next.localStdK = 1.2
next.illuminationRadius = 28
next.illuminationAmount = 0.7
return next
default:
return next
}
}
export function resolvePresetName(preset?: string | null): MaskPresetName {
if (!preset) return 'tiles-default'
const lower = preset.trim().toLowerCase()
return lower === 'tiles-detail' ? 'tiles-detail' : 'tiles-default'
}
export function normalizeMaskBinary(
mask: Uint8Array,
width: number,
height: number,
options: NormalizeOptions,
graySource?: Uint8Array,
): NormalizedResult {
const total = mask.length
if (total === 0) {
return {
data: mask,
coverage: 0,
inverted: false,
threshold: 0,
original: 0,
fallback: false,
stats: {
originalCoverage: 0,
originalMean: 0,
percentileLow: options.percentileLow,
percentileHigh: options.percentileHigh,
thresholdMode: options.thresholdMode,
invertMode: options.invertMode,
inverted: false,
morphApplied: [],
backgroundMode: options.bgMode,
},
}
}
const gray = Uint8Array.from(mask)
let solid = 0
let sum = 0
for (let i = 0; i < total; i++) {
if (mask[i] >= 128) solid += 1
sum += mask[i]
}
const originalCoverage = solid / total
const originalMean = sum / (total * 255)
const cfg: NormalizeOptions = {
invertMode: options.invertMode,
thresholdMode: options.thresholdMode,
thresholdValue: options.thresholdValue,
percentileLow: options.percentileLow,
percentileHigh: options.percentileHigh,
morphOps: options.morphOps,
keepLargest: options.keepLargest,
minArea: options.minArea,
edgeGuard: options.edgeGuard,
edgeFill: options.edgeFill ?? { enabled: true, threshold: 96, dilate: 1 },
kDark: Number.isFinite(options.kDark) ? options.kDark : 1.25,
kLight: Number.isFinite(options.kLight) ? options.kLight : 0.5,
seedBorder: Number.isFinite(options.seedBorder) ? options.seedBorder : 6,
seedGrow: Number.isFinite(options.seedGrow) ? options.seedGrow : 6,
gridKill: options.gridKill ?? 'auto',
gridVFrac: Number.isFinite(options.gridVFrac) ? options.gridVFrac : 0.55,
gridHFrac: Number.isFinite(options.gridHFrac) ? options.gridHFrac : 0.55,
gridPad: Number.isFinite(options.gridPad) ? options.gridPad : 1,
gridSigma: Number.isFinite(options.gridSigma) ? options.gridSigma : 2.5,
thinWidth: Number.isFinite(options.thinWidth) ? Math.max(0, options.thinWidth) : 3,
thinLengthFrac: Number.isFinite(options.thinLengthFrac)
? Math.max(0, Math.min(1, options.thinLengthFrac))
: 0.25,
carveRadius: Number.isFinite(options.carveRadius)
? Math.max(0, Math.round(options.carveRadius))
: 2,
edgeLowFrac: Number.isFinite(options.edgeLowFrac) ? Math.max(0, Math.min(1, options.edgeLowFrac)) : 0.5,
localStdWin: Number.isFinite(options.localStdWin) ? Math.max(1, Math.round(options.localStdWin)) : 7,
localStdK: Number.isFinite(options.localStdK) ? options.localStdK : 1.2,
illuminationRadius: Number.isFinite(options.illuminationRadius)
? Math.max(0, options.illuminationRadius)
: 0,
illuminationAmount: Number.isFinite(options.illuminationAmount)
? Math.max(0, Math.min(1, options.illuminationAmount))
: 0,
fallbackHigh: options.fallbackHigh,
fallbackLow: options.fallbackLow,
bgMode: options.bgMode,
}
const imageGray = graySource && graySource.length === total ? graySource : undefined
const baseIntensity = imageGray ?? gray
const intensity =
cfg.illuminationRadius > 0 && cfg.illuminationAmount > 0
? illuminationNormalize(
baseIntensity,
width,
height,
cfg.illuminationRadius,
cfg.illuminationAmount,
)
: baseIntensity
const coverageOf = (bin: Uint8Array | null | undefined): number => {
if (!bin || bin.length === 0) return 0
let filled = 0
for (let i = 0; i < Math.min(bin.length, total); i++) if (bin[i]) filled += 1
return filled / total
}
const stats: NormalizedStats = {
originalCoverage,
originalMean,
percentileLow: cfg.percentileLow,
percentileHigh: cfg.percentileHigh,
thresholdMode: cfg.thresholdMode,
invertMode: cfg.invertMode,
inverted: false,
morphApplied: [],
backgroundMode: cfg.bgMode,
}
let fallback = false
let fallbackReason: string | null = null
let highCoverageFlag = false
if (originalCoverage > cfg.fallbackHigh) {
highCoverageFlag = true
} else if (originalCoverage < cfg.fallbackLow) {
fallback = true
fallbackReason = 'coverage-low'
}
let working: Uint8Array = gray
if (!fallback) {
const lo = percentile(working, cfg.percentileLow)
const hi = percentile(working, cfg.percentileHigh)
if (hi > lo) {
working = clipRescale(working, lo, hi) as Uint8Array
stats.clip = { low: lo, high: hi }
}
}
let threshold = cfg.thresholdMode === 'otsu' ? otsuThreshold(working) : cfg.thresholdValue
if (!Number.isFinite(threshold)) threshold = 127
threshold = Math.max(0, Math.min(255, Math.round(threshold)))
let inverted: boolean
if (cfg.invertMode === 'auto') {
inverted = originalMean < 0.5 && originalCoverage < 0.8
} else if (cfg.invertMode === 'true') {
inverted = true
} else if (cfg.invertMode === 'false') {
inverted = false
} else {
inverted = Boolean(cfg.invertMode)
}
stats.inverted = inverted
const fgBase = binarize(working, threshold, inverted) as Uint8Array
stats.originalCoverage = originalCoverage
stats.originalMean = originalMean
let fg = fgBase
if (!fallback) {
const morphApplied: string[] = []
for (const entry of cfg.morphOps || []) {
const radius = Math.max(1, Math.round(entry.radius || 1))
switch (entry.op) {
case 'open':
fg = morphOpen(fg, width, height, radius) as Uint8Array
morphApplied.push(`open:${radius}`)
break
case 'close':
fg = morphClose(fg, width, height, radius) as Uint8Array
morphApplied.push(`close:${radius}`)
break
case 'dilate':
fg = morphDilate(fg, width, height, radius) as Uint8Array
morphApplied.push(`dilate:${radius}`)
break
case 'erode':
fg = morphErode(fg, width, height, radius) as Uint8Array
morphApplied.push(`erode:${radius}`)
break
default:
break
}
}
if (morphApplied.length) stats.morphApplied = morphApplied
if (cfg.bgMode === 'largest') {
const background = largestBorderConnected(invert255(fg), width, height)
const bgCoverage = coverageOf(background)
const occluderBase = invert255(background)
if (bgCoverage > 0.01) {
const statsMasked = meanStdMasked(intensity, background)
const { mean, std } = statsMasked
const stdClamped = Math.min(std, 32)
const kDark = Math.max(0, cfg.kDark)
const kLight = Math.max(0, cfg.kLight)
const thrDark = Math.max(0, Math.min(255, mean - stdClamped * kDark))
const thrLight = Math.max(0, Math.min(255, mean + stdClamped * kLight))
const borderCull = cfg.seedBorder > 0 ? cfg.seedBorder : 0
const mDark = new Uint8Array(total)
const mLight = new Uint8Array(total)
for (let i = 0; i < total; i++) {
const v = intensity[i]
if (v <= thrDark) mDark[i] = 255
if (v >= thrLight) mLight[i] = 255
}
const localStdWin = Number.isFinite(cfg.localStdWin) ? Math.max(1, Math.round(cfg.localStdWin)) : 7
const localStdK = Number.isFinite(cfg.localStdK) ? cfg.localStdK : 1.2
let textureMask = new Uint8Array(total)
let localStdThresh = 0
if (localStdK > 0 && localStdWin > 0) {
const stdMap = localStd(intensity, width, height, localStdWin)
localStdThresh = localStdK * std
for (let i = 0; i < total; i++) {
if (stdMap[i] >= localStdThresh) textureMask[i] = 255
}
}
const textureCoverage = coverageOf(textureMask)
const seedBase = maskOr(maskOr(mDark, mLight), textureMask) as Uint8Array
let seed: Uint8Array = (borderCull > 0
? (stripBorder(seedBase, width, height, cfg.seedBorder) as Uint8Array)
: seedBase) as Uint8Array
seed = morphClose(seed, width, height, 2) as Uint8Array
const notBackground = occluderBase
const gateDist = cfg.seedBorder > 0 ? cfg.seedBorder : 8
let edges: Uint8Array = new Uint8Array(seed.length)
if (cfg.edgeFill.enabled) {
const edgeThr = Number.isFinite(cfg.edgeFill.threshold) ? cfg.edgeFill.threshold : 96
const edgeDilate = Math.max(0, Math.round(cfg.edgeFill.dilate || 0))
const grad = sobel(intensity, width, height)
const lowFrac = Number.isFinite(cfg.edgeLowFrac) ? cfg.edgeLowFrac : 0.5
const lowThr = Math.max(0, Math.round(edgeThr * Math.max(0, Math.min(1, lowFrac))))
edges = hysteresisEdges(grad, width, height, edgeThr, lowThr) as Uint8Array
if (cfg.gridKill !== 'off') {
const pad = Number.isFinite(cfg.gridPad) ? cfg.gridPad : 1
if (cfg.gridKill === 'auto' || cfg.gridKill === 'adaptive') {
const sigma = Number.isFinite(cfg.gridSigma) ? cfg.gridSigma : 2.5
edges = suppressGridEdgesAdaptive(edges, width, height, sigma, pad) as Uint8Array
stats.gridKillMode = { mode: 'adaptive', sigma: Number(sigma.toFixed(2)), pad }
} else {
const vFrac = Number.isFinite(cfg.gridVFrac) ? cfg.gridVFrac : 0.55
const hFrac = Number.isFinite(cfg.gridHFrac) ? cfg.gridHFrac : 0.55
edges = suppressGridEdges(edges, width, height, vFrac, hFrac, pad) as Uint8Array
stats.gridKillMode = { mode: cfg.gridKill, vFrac, hFrac, pad }
}
} else {
stats.gridKillMode = { mode: 'off' }
}
if (edgeDilate > 0) edges = morphDilate(edges, width, height, edgeDilate) as Uint8Array
edges = gateByDistance(edges, seed, width, height, gateDist) as Uint8Array
stats.edgeFill = {
enabled: true,
threshold: edgeThr,
low: lowThr,
lowFrac: Number(lowFrac.toFixed(2)),
dilate: edgeDilate,
}
} else {
stats.edgeFill = { enabled: false }
stats.gridKillMode = { mode: 'skipped' }
}
const edgeGrowMask: Uint8Array = edges.length
? (morphDilate(edges, width, height, 1) as Uint8Array)
: edges
const growMask = maskOr(notBackground, edgeGrowMask) as Uint8Array
seed = maskAnd(seed, growMask) as Uint8Array
let fgSeed: Uint8Array = maskOr(seed, edges) as Uint8Array
if (cfg.seedGrow > 0) {
fgSeed = geodesicGrow(fgSeed, growMask, width, height, cfg.seedGrow) as Uint8Array
} else {
fgSeed = maskAnd(fgSeed, growMask) as Uint8Array
}
const seedOnBackground = maskAnd(fgSeed, background) as Uint8Array
const seedBgCoverage = coverageOf(seedOnBackground)
const carveRadius = Math.max(0, Number.isFinite(cfg.carveRadius) ? Math.round(cfg.carveRadius) : 0)
const carveMask =
carveRadius > 0
? (morphDilate(fgSeed, width, height, carveRadius) as Uint8Array)
: fgSeed
const bgCarved = maskMinus(background, carveMask) as Uint8Array
let fgCandidate: Uint8Array = invert255(bgCarved) as Uint8Array
fgCandidate = maskAnd(fgCandidate, growMask) as Uint8Array
if (cfg.thinWidth > 0 && width > 0 && height > 0) {
const thinWidth = Math.max(1, Math.round(cfg.thinWidth))
const thinLength = Math.max(0, Math.min(1, Number(cfg.thinLengthFrac)))
fgCandidate = suppressThinLines(fgCandidate, width, height, thinWidth, thinLength)
}
fgCandidate = dropSmall(fgCandidate, width, height, cfg.minArea > 0 ? cfg.minArea : 1) as Uint8Array
let seedCoverage = coverageOf(fgCandidate)
if (cfg.keepLargest > 0) {
fgCandidate = keepLargest(fgCandidate, width, height, cfg.keepLargest) as Uint8Array
seedCoverage = coverageOf(fgCandidate)
}
if (seedCoverage === 0) {
stats.seedFallback = true
fgCandidate = dropSmall(notBackground, width, height, cfg.minArea > 0 ? cfg.minArea : 1) as Uint8Array
if (cfg.keepLargest > 0)
fgCandidate = keepLargest(fgCandidate, width, height, cfg.keepLargest) as Uint8Array
seedCoverage = coverageOf(fgCandidate)
}
const bgFinalCoverage = coverageOf(bgCarved)
const occluderCoverage = coverageOf(fgCandidate)
stats.background = { mode: cfg.bgMode, coverage: bgFinalCoverage, occluderCoverage }
stats.hybrid = {
kDark,
kLight,
thrDark: Number(thrDark.toFixed(1)),
thrLight: Number(thrLight.toFixed(1)),
std: Number(std.toFixed(2)),
stdClamped: Number(stdClamped.toFixed(2)),
seedCoverage: Number(seedCoverage.toFixed(4)),
seedMask: Number(seedBgCoverage.toFixed(4)),
borderCull,
carveRadius,
seedGrow: cfg.seedGrow,
gateDist,
gridKill: cfg.gridKill,
gridSigma: Number.isFinite(cfg.gridSigma) ? Number(cfg.gridSigma.toFixed(2)) : undefined,
thinWidth: cfg.thinWidth,
thinLength: Number(cfg.thinLengthFrac.toFixed(3)),
textureMask: Number(textureCoverage.toFixed(4)),
localStdWin,
localStdK: Number(localStdK.toFixed(3)),
localStdThresh: Number(localStdThresh.toFixed(2)),
edgeLowFrac: Number((Number.isFinite(cfg.edgeLowFrac) ? cfg.edgeLowFrac : 0.5).toFixed(2)),
illuminationRadius: cfg.illuminationRadius,
illuminationAmount: Number(cfg.illuminationAmount.toFixed(2)),
}
if (cfg.minArea > 0) stats.dropSmall = cfg.minArea
if (cfg.keepLargest > 0) stats.keepLargest = cfg.keepLargest
if (highCoverageFlag) stats.highCoverage = originalCoverage
fg = fgCandidate
} else {
stats.background = { mode: cfg.bgMode, coverage: bgCoverage, fallback: true }
fg = dropSmall(fg, width, height, cfg.minArea > 0 ? cfg.minArea : 1)
if (cfg.keepLargest > 0) fg = keepLargest(fg, width, height, cfg.keepLargest)
if (cfg.minArea > 0) stats.dropSmall = cfg.minArea
if (cfg.keepLargest > 0) stats.keepLargest = cfg.keepLargest
}
} else {
if (cfg.minArea > 0 && cfg.removeBorder !== false) {
fg = removeBorderTouching(fg, width, height, cfg.minArea, true)
stats.borderMinArea = cfg.minArea
}
if (cfg.keepLargest > 0) {
fg = keepLargest(fg, width, height, cfg.keepLargest)
stats.keepLargest = cfg.keepLargest
}
}
if (cfg.edgeGuard.enabled) {
const thresholdRaw =
typeof cfg.edgeGuard.threshold === 'number' && Number.isFinite(cfg.edgeGuard.threshold)
? cfg.edgeGuard.threshold
: 48
const edgeThreshold = Math.max(0, Math.min(255, Math.round(thresholdRaw)))
const dilateRadius = Math.max(0, Math.round(cfg.edgeGuard.dilate || 0))
fg = subtractEdges(fg, intensity, width, height, { thr: edgeThreshold, dilate: dilateRadius })
stats.edgeGuard = { threshold: edgeThreshold, dilate: dilateRadius }
}
} else if (fallback) {
stats.fallbackReason = fallbackReason
stats.morphApplied = []
stats.background = { mode: cfg.bgMode, coverage: 0, skipped: true }
} else if (highCoverageFlag) {
stats.highCoverage = originalCoverage
}
const coverage = coverageOf(fg)
return {
data: fg,
coverage,
threshold,
inverted,
original: originalCoverage,
fallback,
stats,
}
}
export type NormalizeMaskRequest = {
mask: Uint8Array
width: number
height: number
preset?: string | null
gray?: Uint8Array
overrides?: Partial<NormalizeOptions>
}
export type NormalizeMaskResponse = {
preset: MaskPresetName
options: NormalizeOptions
result: NormalizedResult
}
export function normalizeWithPreset(request: NormalizeMaskRequest): NormalizeMaskResponse {
const base = baseNormalizeOptions()
const preset = resolvePresetName(request.preset)
const config = applyPreset(base, preset)
if (request.overrides) {
const { overrides } = request
if (overrides.invertMode !== undefined) config.invertMode = overrides.invertMode
if (overrides.thresholdMode !== undefined) config.thresholdMode = overrides.thresholdMode
if (overrides.thresholdValue !== undefined) config.thresholdValue = overrides.thresholdValue
if (overrides.percentileLow !== undefined) config.percentileLow = overrides.percentileLow
if (overrides.percentileHigh !== undefined) config.percentileHigh = overrides.percentileHigh
if (overrides.keepLargest !== undefined) config.keepLargest = overrides.keepLargest
if (overrides.minArea !== undefined) config.minArea = overrides.minArea
if (overrides.removeBorder !== undefined) config.removeBorder = overrides.removeBorder
if (overrides.edgeGuard !== undefined) config.edgeGuard = overrides.edgeGuard
if (overrides.edgeFill !== undefined) config.edgeFill = overrides.edgeFill
if (overrides.kDark !== undefined) config.kDark = overrides.kDark
if (overrides.kLight !== undefined) config.kLight = overrides.kLight
if (overrides.seedBorder !== undefined) config.seedBorder = overrides.seedBorder
if (overrides.seedGrow !== undefined) config.seedGrow = overrides.seedGrow
if (overrides.gridKill !== undefined) config.gridKill = overrides.gridKill
if (overrides.gridVFrac !== undefined) config.gridVFrac = overrides.gridVFrac
if (overrides.gridHFrac !== undefined) config.gridHFrac = overrides.gridHFrac
if (overrides.gridPad !== undefined) config.gridPad = overrides.gridPad
if (overrides.gridSigma !== undefined) config.gridSigma = overrides.gridSigma
if (overrides.thinWidth !== undefined) config.thinWidth = overrides.thinWidth
if (overrides.thinLengthFrac !== undefined) config.thinLengthFrac = overrides.thinLengthFrac
if (overrides.carveRadius !== undefined) config.carveRadius = overrides.carveRadius
if (overrides.edgeLowFrac !== undefined) config.edgeLowFrac = overrides.edgeLowFrac
if (overrides.localStdWin !== undefined) config.localStdWin = overrides.localStdWin
if (overrides.localStdK !== undefined) config.localStdK = overrides.localStdK
if (overrides.illuminationRadius !== undefined) config.illuminationRadius = overrides.illuminationRadius
if (overrides.illuminationAmount !== undefined) config.illuminationAmount = overrides.illuminationAmount
if (overrides.fallbackHigh !== undefined) config.fallbackHigh = overrides.fallbackHigh
if (overrides.fallbackLow !== undefined) config.fallbackLow = overrides.fallbackLow
if (overrides.bgMode !== undefined) config.bgMode = overrides.bgMode
if (overrides.morphOps !== undefined) config.morphOps = overrides.morphOps
}
const result = normalizeMaskBinary(request.mask, request.width, request.height, config, request.gray)
return { preset, options: config, result }
}