shadow-score 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.ts","../src/detector.ts"],"names":["sharp"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ShadowOptions } from './types';\n\n/**\n * Validates the image input parameter.\n */\nexport function validateInput(input: unknown): void {\n if (input === null || input === undefined) {\n throw new TypeError('Invalid image input: input must be a file path string, Buffer, or Uint8Array.');\n }\n\n if (typeof input === 'string') {\n if (input.trim().length === 0) {\n throw new Error('Invalid image input: file path string cannot be empty.');\n }\n return;\n }\n\n if (Buffer.isBuffer(input) || input instanceof Uint8Array) {\n if (input.length === 0) {\n throw new Error('Invalid image input: buffer cannot be empty.');\n }\n return;\n }\n\n throw new TypeError('Invalid image input: expected a file path string, Buffer, or Uint8Array.');\n}\n\n/**\n * Validates and normalizes user-provided shadow options with safe defaults.\n */\nexport function normalizeOptions(options?: ShadowOptions): Required<ShadowOptions> {\n const threshold = options?.threshold ?? 0.2;\n const downsampleWidth = options?.downsampleWidth ?? 256;\n const minIlluminationDelta = options?.minIlluminationDelta ?? 20;\n const smoothingRadius = options?.smoothingRadius ?? 4;\n\n if (typeof threshold !== 'number' || Number.isNaN(threshold) || threshold < 0 || threshold > 1) {\n throw new RangeError(`Invalid option 'threshold': expected a number between 0 and 1, got ${threshold}.`);\n }\n\n if (\n typeof downsampleWidth !== 'number' ||\n Number.isNaN(downsampleWidth) ||\n downsampleWidth < 16 ||\n !Number.isInteger(downsampleWidth)\n ) {\n throw new RangeError(\n `Invalid option 'downsampleWidth': expected an integer >= 16, got ${downsampleWidth}.`\n );\n }\n\n if (\n typeof minIlluminationDelta !== 'number' ||\n Number.isNaN(minIlluminationDelta) ||\n minIlluminationDelta < 0 ||\n minIlluminationDelta > 255\n ) {\n throw new RangeError(\n `Invalid option 'minIlluminationDelta': expected a number between 0 and 255, got ${minIlluminationDelta}.`\n );\n }\n\n if (\n typeof smoothingRadius !== 'number' ||\n Number.isNaN(smoothingRadius) ||\n smoothingRadius < 1 ||\n smoothingRadius > 32 ||\n !Number.isInteger(smoothingRadius)\n ) {\n throw new RangeError(\n `Invalid option 'smoothingRadius': expected an integer between 1 and 32, got ${smoothingRadius}.`\n );\n }\n\n return {\n threshold,\n downsampleWidth,\n minIlluminationDelta,\n smoothingRadius,\n };\n}\n","import sharp from 'sharp';\nimport type { ShadowOptions, ShadowResult, ShadowDetails } from './types';\nimport { normalizeOptions, validateInput } from './utils';\n\n/**\n * Applies a separable 2D box filter to eliminate high-frequency details (like fine text)\n * and extract the underlying illumination field.\n */\nfunction extractIlluminationMap(\n src: Uint8Array,\n width: number,\n height: number,\n radius: number\n): Uint8Array {\n const temp = new Uint8Array(width * height);\n const dst = new Uint8Array(width * height);\n const div = 2 * radius + 1;\n\n // Horizontal pass\n for (let y = 0; y < height; y++) {\n const row = y * width;\n let sum = 0;\n for (let i = -radius; i <= radius; i++) {\n const c = i < 0 ? 0 : (i >= width ? width - 1 : i);\n sum += src[row + c]!;\n }\n for (let x = 0; x < width; x++) {\n temp[row + x] = Math.round(sum / div);\n const removeX = x - radius;\n const addX = x + radius + 1;\n const removeVal = src[row + (removeX < 0 ? 0 : removeX)]!;\n const addVal = src[row + (addX >= width ? width - 1 : addX)]!;\n sum += addVal - removeVal;\n }\n }\n\n // Vertical pass\n for (let x = 0; x < width; x++) {\n let sum = 0;\n for (let i = -radius; i <= radius; i++) {\n const r = i < 0 ? 0 : (i >= height ? height - 1 : i);\n sum += temp[r * width + x]!;\n }\n for (let y = 0; y < height; y++) {\n dst[y * width + x] = Math.round(sum / div);\n const removeY = y - radius;\n const addY = y + radius + 1;\n const removeVal = temp[(removeY < 0 ? 0 : removeY) * width + x]!;\n const addVal = temp[(addY >= height ? height - 1 : addY) * width + x]!;\n sum += addVal - removeVal;\n }\n }\n\n return dst;\n}\n\n/**\n * Measures the ratio of the largest connected shadow cluster to total shadow pixels.\n */\nfunction computeContiguity(\n isShadow: Uint8Array,\n width: number,\n height: number,\n totalShadowPixels: number\n): number {\n if (totalShadowPixels === 0) return 0;\n\n const totalPixels = width * height;\n const visited = new Uint8Array(totalPixels);\n const queue = new Int32Array(totalPixels);\n let maxClusterSize = 0;\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const startIdx = y * width + x;\n if (visited[startIdx] === 1 || isShadow[startIdx] === 0) continue;\n\n let head = 0;\n let tail = 0;\n queue[tail++] = startIdx;\n visited[startIdx] = 1;\n let clusterSize = 0;\n\n while (head < tail) {\n const curr = queue[head++]!;\n clusterSize++;\n const cy = Math.floor(curr / width);\n const cx = curr % width;\n\n if (cx > 0) {\n const n = curr - 1;\n if (visited[n] === 0 && isShadow[n] === 1) {\n visited[n] = 1;\n queue[tail++] = n;\n }\n }\n if (cx < width - 1) {\n const n = curr + 1;\n if (visited[n] === 0 && isShadow[n] === 1) {\n visited[n] = 1;\n queue[tail++] = n;\n }\n }\n if (cy > 0) {\n const n = curr - width;\n if (visited[n] === 0 && isShadow[n] === 1) {\n visited[n] = 1;\n queue[tail++] = n;\n }\n }\n if (cy < height - 1) {\n const n = curr + width;\n if (visited[n] === 0 && isShadow[n] === 1) {\n visited[n] = 1;\n queue[tail++] = n;\n }\n }\n }\n\n if (clusterSize > maxClusterSize) {\n maxClusterSize = clusterSize;\n }\n }\n }\n\n return maxClusterSize / totalShadowPixels;\n}\n\n/**\n * Analyzes an image for directional shadows using Otsu bimodal luminance segmentation\n * and spatial illumination gradient analysis.\n *\n * @param input - Image file path, Buffer, or Uint8Array.\n * @param options - Configuration options for shadow analysis.\n * @returns Detailed ShadowResult object.\n */\nexport async function analyzeShadow(\n input: string | Buffer | Uint8Array,\n options?: ShadowOptions\n): Promise<ShadowResult> {\n validateInput(input);\n const opts = normalizeOptions(options);\n\n // 1. Downsample and convert to raw grayscale buffer\n const imagePipeline = sharp(input);\n const { data, info } = await imagePipeline\n .resize({ width: opts.downsampleWidth, withoutEnlargement: true })\n .grayscale()\n .raw()\n .toBuffer({ resolveWithObject: true });\n\n const width = info.width;\n const height = info.height;\n const totalPixels = width * height;\n\n if (totalPixels === 0) {\n throw new Error('Image contains no pixel data.');\n }\n\n // 2. Extract smooth illumination field (removing high-frequency text & fine textures)\n const illum = extractIlluminationMap(data, width, height, opts.smoothingRadius);\n\n // 3. Compute histogram, global mean, and total variance\n const hist = new Int32Array(256);\n let totalSum = 0;\n for (let i = 0; i < totalPixels; i++) {\n const val = illum[i]!;\n hist[val]++;\n totalSum += val;\n }\n\n const meanT = totalSum / totalPixels;\n let varT = 0;\n for (let i = 0; i < 256; i++) {\n const count = hist[i]!;\n if (count > 0) {\n const diff = i - meanT;\n varT += count * diff * diff;\n }\n }\n varT /= totalPixels;\n\n // If variance is negligible, image lighting is completely uniform\n if (varT < 1.0) {\n const roundedMean = Number(meanT.toFixed(2));\n return {\n score: 0,\n hasShadow: false,\n shadowAreaPercentage: 0,\n illuminationDelta: 0,\n quality: 'excellent',\n details: {\n litLuminance: roundedMean,\n shadowLuminance: roundedMean,\n otsuThreshold: Math.round(meanT),\n separability: 0,\n boundaryGradient: 0,\n },\n };\n }\n\n // 4. Otsu's bimodal threshold search to maximize between-class variance\n let bestT = 128;\n let maxBetweenVar = 0;\n let weight0 = 0;\n let sum0 = 0;\n\n for (let t = 0; t < 255; t++) {\n const count = hist[t]!;\n weight0 += count;\n if (weight0 === 0) continue;\n const weight1 = totalPixels - weight0;\n if (weight1 === 0) break;\n\n sum0 += t * count;\n const mean0 = sum0 / weight0;\n const mean1 = (totalSum - sum0) / weight1;\n const meanDiff = mean1 - mean0;\n const betweenVar = (weight0 * weight1 * meanDiff * meanDiff) / (totalPixels * totalPixels);\n\n if (betweenVar > maxBetweenVar) {\n maxBetweenVar = betweenVar;\n bestT = t;\n }\n }\n\n // Calculate class metrics at best threshold\n let shadowCount = 0;\n let shadowSum = 0;\n let litCount = 0;\n let litSum = 0;\n\n const isShadow = new Uint8Array(totalPixels);\n for (let i = 0; i < totalPixels; i++) {\n const val = illum[i]!;\n if (val <= bestT) {\n isShadow[i] = 1;\n shadowCount++;\n shadowSum += val;\n } else {\n litCount++;\n litSum += val;\n }\n }\n\n const shadowLuminance = shadowCount > 0 ? shadowSum / shadowCount : 0;\n const litLuminance = litCount > 0 ? litSum / litCount : meanT;\n const illuminationDelta = Math.max(0, litLuminance - shadowLuminance);\n const shadowAreaPercentage = (shadowCount / totalPixels) * 100;\n const separability = Math.min(1.0, maxBetweenVar / varT);\n\n // 5. Spatial illumination gradient at the shadow boundary\n let boundaryGradientSum = 0;\n let boundaryPixelCount = 0;\n\n for (let y = 1; y < height - 1; y++) {\n const rowOffset = y * width;\n for (let x = 1; x < width - 1; x++) {\n const idx = rowOffset + x;\n if (isShadow[idx] === 1) {\n if (\n isShadow[idx - 1] === 0 ||\n isShadow[idx + 1] === 0 ||\n isShadow[idx - width] === 0 ||\n isShadow[idx + width] === 0\n ) {\n const gx = (illum[idx + 1]! - illum[idx - 1]!) * 0.5;\n const gy = (illum[idx + width]! - illum[idx - width]!) * 0.5;\n boundaryGradientSum += Math.sqrt(gx * gx + gy * gy);\n boundaryPixelCount++;\n }\n }\n }\n }\n\n const avgBoundaryGradient =\n boundaryPixelCount > 0 ? boundaryGradientSum / boundaryPixelCount : 0;\n\n // 6. Spatial contiguity of the shadow\n const contiguity = computeContiguity(isShadow, width, height, shadowCount);\n\n // 7. Calculate normalized shadow severity score (0.0 to 1.0)\n let score = 0;\n\n if (illuminationDelta >= opts.minIlluminationDelta && shadowAreaPercentage > 0) {\n // Delta factor: ramp from minIlluminationDelta up to delta ~ 120\n const deltaRange = Math.max(1, 120 - opts.minIlluminationDelta);\n const fDelta = Math.min(1.0, (illuminationDelta - opts.minIlluminationDelta) / deltaRange);\n\n // Separability factor (how clearly bimodal the distribution is)\n const fSep = Math.min(1.0, Math.max(0.0, (separability - 0.25) / 0.55));\n\n // Contrast ratio factor: (lit - shadow) / (lit + shadow)\n const contrastRatio = illuminationDelta / (litLuminance + shadowLuminance + 1e-5);\n const fContrast = Math.min(1.0, contrastRatio / 0.45);\n\n // Boundary gradient factor (harsh directional shadows have sharper boundaries than gentle ambient gradients)\n const fGrad = Math.min(1.0, avgBoundaryGradient / 8.0);\n const gradientTerm = 0.65 + 0.35 * fGrad;\n\n // Area factor: shadows covering 5% to 75% are typical directional shadows.\n // If area < 5%, linearly ramp up. If area > 75%, ramp down towards 95% (underexposed photo, not a directional shadow).\n let fArea = 1.0;\n if (shadowAreaPercentage < 5) {\n fArea = Math.max(0.0, shadowAreaPercentage / 5.0);\n } else if (shadowAreaPercentage > 75) {\n fArea = Math.max(0.0, (95 - shadowAreaPercentage) / 20.0);\n }\n\n // Lit brightness factor: if the lit region itself is dark (< 60), the entire image is underexposed\n const fLit = Math.min(1.0, litLuminance / 60.0);\n\n // Contiguity factor: directional shadows are clustered, not random speckles\n const fContig = 0.4 + 0.6 * Math.min(1.0, Math.max(0.0, (contiguity - 0.2) / 0.5));\n\n const contrastTerm = 0.5 * fSep + 0.5 * fContrast;\n const rawScore = fDelta * contrastTerm * gradientTerm * fArea * fLit * fContig;\n score = Math.max(0.0, Math.min(1.0, rawScore));\n }\n\n score = Number(score.toFixed(4));\n const hasShadowResult = score >= opts.threshold;\n\n // Qualitative rating\n let quality: 'excellent' | 'good' | 'fair' | 'poor';\n if (score < 0.1) {\n quality = 'excellent';\n } else if (score < 0.2) {\n quality = 'good';\n } else if (score < 0.45) {\n quality = 'fair';\n } else {\n quality = 'poor';\n }\n\n const details: ShadowDetails = {\n litLuminance: Number(litLuminance.toFixed(2)),\n shadowLuminance: Number(shadowLuminance.toFixed(2)),\n otsuThreshold: bestT,\n separability: Number(separability.toFixed(4)),\n boundaryGradient: Number(avgBoundaryGradient.toFixed(2)),\n };\n\n return {\n score,\n hasShadow: hasShadowResult,\n shadowAreaPercentage: Number(shadowAreaPercentage.toFixed(2)),\n illuminationDelta: Number(illuminationDelta.toFixed(2)),\n quality,\n details,\n };\n}\n\n/**\n * Returns the normalized shadow severity score (0.0 to 1.0) for an image.\n *\n * @param input - Image file path, Buffer, or Uint8Array.\n * @param options - Optional configuration options.\n * @returns Promise resolving to shadow score (0.0 to 1.0).\n */\nexport async function getShadowScore(\n input: string | Buffer | Uint8Array,\n options?: ShadowOptions\n): Promise<number> {\n const result = await analyzeShadow(input, options);\n return result.score;\n}\n\n/**\n * Checks whether an image has harsh shadows exceeding the threshold.\n *\n * @param input - Image file path, Buffer, or Uint8Array.\n * @param thresholdOrOptions - Number threshold (e.g. 0.2) or ShadowOptions configuration.\n * @returns Promise resolving to boolean.\n */\nexport async function hasShadow(\n input: string | Buffer | Uint8Array,\n thresholdOrOptions?: number | ShadowOptions\n): Promise<boolean> {\n let opts: ShadowOptions | undefined;\n if (typeof thresholdOrOptions === 'number') {\n opts = { threshold: thresholdOrOptions };\n } else if (thresholdOrOptions !== undefined) {\n opts = thresholdOrOptions;\n }\n\n const result = await analyzeShadow(input, opts);\n return result.hasShadow;\n}\n"]}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Low-level illumination and segmentation diagnostics.
3
+ */
4
+ interface ShadowDetails {
5
+ /**
6
+ * Average luminance of the lit region (0.0 to 255.0).
7
+ */
8
+ litLuminance: number;
9
+ /**
10
+ * Average luminance of the shadowed region (0.0 to 255.0).
11
+ */
12
+ shadowLuminance: number;
13
+ /**
14
+ * Optimal Otsu threshold (0-255) computed across the illumination field.
15
+ */
16
+ otsuThreshold: number;
17
+ /**
18
+ * Otsu bimodal separability ratio (eta = between-class variance / total variance, 0.0 to 1.0).
19
+ */
20
+ separability: number;
21
+ /**
22
+ * Average directional spatial gradient magnitude along the shadow transition boundary.
23
+ */
24
+ boundaryGradient: number;
25
+ }
26
+ /**
27
+ * Configuration options for shadow detection and quantification.
28
+ */
29
+ interface ShadowOptions {
30
+ /**
31
+ * Shadow score cutoff to determine whether an image is considered shadowed.
32
+ * If `score >= threshold`, `hasShadow` is set to `true`.
33
+ * @default 0.2
34
+ */
35
+ threshold?: number;
36
+ /**
37
+ * Target maximum dimension (width) to downsample the image before analysis.
38
+ * Enables blazing-fast execution speeds (typically < 10ms, sub-millisecond core pass).
39
+ * @default 256
40
+ */
41
+ downsampleWidth?: number;
42
+ /**
43
+ * Minimum luminance difference (0-255) required between lit and shadow regions
44
+ * for a valid directional shadow detection. Filters out flat lighting or sensor noise.
45
+ * @default 20
46
+ */
47
+ minIlluminationDelta?: number;
48
+ /**
49
+ * Radius of the separable box filter used to remove high-frequency text and texture details
50
+ * and isolate the macro illumination field.
51
+ * @default 4
52
+ */
53
+ smoothingRadius?: number;
54
+ }
55
+ /**
56
+ * Result of the shadow analysis.
57
+ */
58
+ interface ShadowResult {
59
+ /**
60
+ * Normalized shadow severity score from 0.0 (uniform lighting / shadow-free) to 1.0 (heavy dark shadow).
61
+ */
62
+ score: number;
63
+ /**
64
+ * Whether the image exceeds the shadow threshold (`score >= threshold`).
65
+ */
66
+ hasShadow: boolean;
67
+ /**
68
+ * Percentage of total image area cast under shadow (0.0 to 100.0).
69
+ */
70
+ shadowAreaPercentage: number;
71
+ /**
72
+ * Difference in average luminance between lit and shadowed regions (0.0 to 255.0).
73
+ */
74
+ illuminationDelta: number;
75
+ /**
76
+ * Qualitative lighting assessment:
77
+ * - `'excellent'`: Uniform, pristine illumination (< 0.10).
78
+ * - `'good'`: Minor lighting variation or faint shadow, fully legible (< 0.20).
79
+ * - `'fair'`: Noticeable directional shadow, may degrade OCR accuracy (< 0.45).
80
+ * - `'poor'`: Severe, harsh shadow obscuring document content (>= 0.45).
81
+ */
82
+ quality: 'excellent' | 'good' | 'fair' | 'poor';
83
+ /**
84
+ * Low-level illumination and segmentation diagnostics.
85
+ */
86
+ details: ShadowDetails;
87
+ }
88
+
89
+ /**
90
+ * Analyzes an image for directional shadows using Otsu bimodal luminance segmentation
91
+ * and spatial illumination gradient analysis.
92
+ *
93
+ * @param input - Image file path, Buffer, or Uint8Array.
94
+ * @param options - Configuration options for shadow analysis.
95
+ * @returns Detailed ShadowResult object.
96
+ */
97
+ declare function analyzeShadow(input: string | Buffer | Uint8Array, options?: ShadowOptions): Promise<ShadowResult>;
98
+ /**
99
+ * Returns the normalized shadow severity score (0.0 to 1.0) for an image.
100
+ *
101
+ * @param input - Image file path, Buffer, or Uint8Array.
102
+ * @param options - Optional configuration options.
103
+ * @returns Promise resolving to shadow score (0.0 to 1.0).
104
+ */
105
+ declare function getShadowScore(input: string | Buffer | Uint8Array, options?: ShadowOptions): Promise<number>;
106
+ /**
107
+ * Checks whether an image has harsh shadows exceeding the threshold.
108
+ *
109
+ * @param input - Image file path, Buffer, or Uint8Array.
110
+ * @param thresholdOrOptions - Number threshold (e.g. 0.2) or ShadowOptions configuration.
111
+ * @returns Promise resolving to boolean.
112
+ */
113
+ declare function hasShadow(input: string | Buffer | Uint8Array, thresholdOrOptions?: number | ShadowOptions): Promise<boolean>;
114
+
115
+ /**
116
+ * Validates the image input parameter.
117
+ */
118
+ declare function validateInput(input: unknown): void;
119
+ /**
120
+ * Validates and normalizes user-provided shadow options with safe defaults.
121
+ */
122
+ declare function normalizeOptions(options?: ShadowOptions): Required<ShadowOptions>;
123
+
124
+ export { type ShadowDetails, type ShadowOptions, type ShadowResult, analyzeShadow, getShadowScore, hasShadow, normalizeOptions, validateInput };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Low-level illumination and segmentation diagnostics.
3
+ */
4
+ interface ShadowDetails {
5
+ /**
6
+ * Average luminance of the lit region (0.0 to 255.0).
7
+ */
8
+ litLuminance: number;
9
+ /**
10
+ * Average luminance of the shadowed region (0.0 to 255.0).
11
+ */
12
+ shadowLuminance: number;
13
+ /**
14
+ * Optimal Otsu threshold (0-255) computed across the illumination field.
15
+ */
16
+ otsuThreshold: number;
17
+ /**
18
+ * Otsu bimodal separability ratio (eta = between-class variance / total variance, 0.0 to 1.0).
19
+ */
20
+ separability: number;
21
+ /**
22
+ * Average directional spatial gradient magnitude along the shadow transition boundary.
23
+ */
24
+ boundaryGradient: number;
25
+ }
26
+ /**
27
+ * Configuration options for shadow detection and quantification.
28
+ */
29
+ interface ShadowOptions {
30
+ /**
31
+ * Shadow score cutoff to determine whether an image is considered shadowed.
32
+ * If `score >= threshold`, `hasShadow` is set to `true`.
33
+ * @default 0.2
34
+ */
35
+ threshold?: number;
36
+ /**
37
+ * Target maximum dimension (width) to downsample the image before analysis.
38
+ * Enables blazing-fast execution speeds (typically < 10ms, sub-millisecond core pass).
39
+ * @default 256
40
+ */
41
+ downsampleWidth?: number;
42
+ /**
43
+ * Minimum luminance difference (0-255) required between lit and shadow regions
44
+ * for a valid directional shadow detection. Filters out flat lighting or sensor noise.
45
+ * @default 20
46
+ */
47
+ minIlluminationDelta?: number;
48
+ /**
49
+ * Radius of the separable box filter used to remove high-frequency text and texture details
50
+ * and isolate the macro illumination field.
51
+ * @default 4
52
+ */
53
+ smoothingRadius?: number;
54
+ }
55
+ /**
56
+ * Result of the shadow analysis.
57
+ */
58
+ interface ShadowResult {
59
+ /**
60
+ * Normalized shadow severity score from 0.0 (uniform lighting / shadow-free) to 1.0 (heavy dark shadow).
61
+ */
62
+ score: number;
63
+ /**
64
+ * Whether the image exceeds the shadow threshold (`score >= threshold`).
65
+ */
66
+ hasShadow: boolean;
67
+ /**
68
+ * Percentage of total image area cast under shadow (0.0 to 100.0).
69
+ */
70
+ shadowAreaPercentage: number;
71
+ /**
72
+ * Difference in average luminance between lit and shadowed regions (0.0 to 255.0).
73
+ */
74
+ illuminationDelta: number;
75
+ /**
76
+ * Qualitative lighting assessment:
77
+ * - `'excellent'`: Uniform, pristine illumination (< 0.10).
78
+ * - `'good'`: Minor lighting variation or faint shadow, fully legible (< 0.20).
79
+ * - `'fair'`: Noticeable directional shadow, may degrade OCR accuracy (< 0.45).
80
+ * - `'poor'`: Severe, harsh shadow obscuring document content (>= 0.45).
81
+ */
82
+ quality: 'excellent' | 'good' | 'fair' | 'poor';
83
+ /**
84
+ * Low-level illumination and segmentation diagnostics.
85
+ */
86
+ details: ShadowDetails;
87
+ }
88
+
89
+ /**
90
+ * Analyzes an image for directional shadows using Otsu bimodal luminance segmentation
91
+ * and spatial illumination gradient analysis.
92
+ *
93
+ * @param input - Image file path, Buffer, or Uint8Array.
94
+ * @param options - Configuration options for shadow analysis.
95
+ * @returns Detailed ShadowResult object.
96
+ */
97
+ declare function analyzeShadow(input: string | Buffer | Uint8Array, options?: ShadowOptions): Promise<ShadowResult>;
98
+ /**
99
+ * Returns the normalized shadow severity score (0.0 to 1.0) for an image.
100
+ *
101
+ * @param input - Image file path, Buffer, or Uint8Array.
102
+ * @param options - Optional configuration options.
103
+ * @returns Promise resolving to shadow score (0.0 to 1.0).
104
+ */
105
+ declare function getShadowScore(input: string | Buffer | Uint8Array, options?: ShadowOptions): Promise<number>;
106
+ /**
107
+ * Checks whether an image has harsh shadows exceeding the threshold.
108
+ *
109
+ * @param input - Image file path, Buffer, or Uint8Array.
110
+ * @param thresholdOrOptions - Number threshold (e.g. 0.2) or ShadowOptions configuration.
111
+ * @returns Promise resolving to boolean.
112
+ */
113
+ declare function hasShadow(input: string | Buffer | Uint8Array, thresholdOrOptions?: number | ShadowOptions): Promise<boolean>;
114
+
115
+ /**
116
+ * Validates the image input parameter.
117
+ */
118
+ declare function validateInput(input: unknown): void;
119
+ /**
120
+ * Validates and normalizes user-provided shadow options with safe defaults.
121
+ */
122
+ declare function normalizeOptions(options?: ShadowOptions): Required<ShadowOptions>;
123
+
124
+ export { type ShadowDetails, type ShadowOptions, type ShadowResult, analyzeShadow, getShadowScore, hasShadow, normalizeOptions, validateInput };
package/dist/index.js ADDED
@@ -0,0 +1,320 @@
1
+ import sharp from 'sharp';
2
+
3
+ // src/detector.ts
4
+
5
+ // src/utils.ts
6
+ function validateInput(input) {
7
+ if (input === null || input === void 0) {
8
+ throw new TypeError("Invalid image input: input must be a file path string, Buffer, or Uint8Array.");
9
+ }
10
+ if (typeof input === "string") {
11
+ if (input.trim().length === 0) {
12
+ throw new Error("Invalid image input: file path string cannot be empty.");
13
+ }
14
+ return;
15
+ }
16
+ if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
17
+ if (input.length === 0) {
18
+ throw new Error("Invalid image input: buffer cannot be empty.");
19
+ }
20
+ return;
21
+ }
22
+ throw new TypeError("Invalid image input: expected a file path string, Buffer, or Uint8Array.");
23
+ }
24
+ function normalizeOptions(options) {
25
+ const threshold = options?.threshold ?? 0.2;
26
+ const downsampleWidth = options?.downsampleWidth ?? 256;
27
+ const minIlluminationDelta = options?.minIlluminationDelta ?? 20;
28
+ const smoothingRadius = options?.smoothingRadius ?? 4;
29
+ if (typeof threshold !== "number" || Number.isNaN(threshold) || threshold < 0 || threshold > 1) {
30
+ throw new RangeError(`Invalid option 'threshold': expected a number between 0 and 1, got ${threshold}.`);
31
+ }
32
+ if (typeof downsampleWidth !== "number" || Number.isNaN(downsampleWidth) || downsampleWidth < 16 || !Number.isInteger(downsampleWidth)) {
33
+ throw new RangeError(
34
+ `Invalid option 'downsampleWidth': expected an integer >= 16, got ${downsampleWidth}.`
35
+ );
36
+ }
37
+ if (typeof minIlluminationDelta !== "number" || Number.isNaN(minIlluminationDelta) || minIlluminationDelta < 0 || minIlluminationDelta > 255) {
38
+ throw new RangeError(
39
+ `Invalid option 'minIlluminationDelta': expected a number between 0 and 255, got ${minIlluminationDelta}.`
40
+ );
41
+ }
42
+ if (typeof smoothingRadius !== "number" || Number.isNaN(smoothingRadius) || smoothingRadius < 1 || smoothingRadius > 32 || !Number.isInteger(smoothingRadius)) {
43
+ throw new RangeError(
44
+ `Invalid option 'smoothingRadius': expected an integer between 1 and 32, got ${smoothingRadius}.`
45
+ );
46
+ }
47
+ return {
48
+ threshold,
49
+ downsampleWidth,
50
+ minIlluminationDelta,
51
+ smoothingRadius
52
+ };
53
+ }
54
+
55
+ // src/detector.ts
56
+ function extractIlluminationMap(src, width, height, radius) {
57
+ const temp = new Uint8Array(width * height);
58
+ const dst = new Uint8Array(width * height);
59
+ const div = 2 * radius + 1;
60
+ for (let y = 0; y < height; y++) {
61
+ const row = y * width;
62
+ let sum = 0;
63
+ for (let i = -radius; i <= radius; i++) {
64
+ const c = i < 0 ? 0 : i >= width ? width - 1 : i;
65
+ sum += src[row + c];
66
+ }
67
+ for (let x = 0; x < width; x++) {
68
+ temp[row + x] = Math.round(sum / div);
69
+ const removeX = x - radius;
70
+ const addX = x + radius + 1;
71
+ const removeVal = src[row + (removeX < 0 ? 0 : removeX)];
72
+ const addVal = src[row + (addX >= width ? width - 1 : addX)];
73
+ sum += addVal - removeVal;
74
+ }
75
+ }
76
+ for (let x = 0; x < width; x++) {
77
+ let sum = 0;
78
+ for (let i = -radius; i <= radius; i++) {
79
+ const r = i < 0 ? 0 : i >= height ? height - 1 : i;
80
+ sum += temp[r * width + x];
81
+ }
82
+ for (let y = 0; y < height; y++) {
83
+ dst[y * width + x] = Math.round(sum / div);
84
+ const removeY = y - radius;
85
+ const addY = y + radius + 1;
86
+ const removeVal = temp[(removeY < 0 ? 0 : removeY) * width + x];
87
+ const addVal = temp[(addY >= height ? height - 1 : addY) * width + x];
88
+ sum += addVal - removeVal;
89
+ }
90
+ }
91
+ return dst;
92
+ }
93
+ function computeContiguity(isShadow, width, height, totalShadowPixels) {
94
+ if (totalShadowPixels === 0) return 0;
95
+ const totalPixels = width * height;
96
+ const visited = new Uint8Array(totalPixels);
97
+ const queue = new Int32Array(totalPixels);
98
+ let maxClusterSize = 0;
99
+ for (let y = 0; y < height; y++) {
100
+ for (let x = 0; x < width; x++) {
101
+ const startIdx = y * width + x;
102
+ if (visited[startIdx] === 1 || isShadow[startIdx] === 0) continue;
103
+ let head = 0;
104
+ let tail = 0;
105
+ queue[tail++] = startIdx;
106
+ visited[startIdx] = 1;
107
+ let clusterSize = 0;
108
+ while (head < tail) {
109
+ const curr = queue[head++];
110
+ clusterSize++;
111
+ const cy = Math.floor(curr / width);
112
+ const cx = curr % width;
113
+ if (cx > 0) {
114
+ const n = curr - 1;
115
+ if (visited[n] === 0 && isShadow[n] === 1) {
116
+ visited[n] = 1;
117
+ queue[tail++] = n;
118
+ }
119
+ }
120
+ if (cx < width - 1) {
121
+ const n = curr + 1;
122
+ if (visited[n] === 0 && isShadow[n] === 1) {
123
+ visited[n] = 1;
124
+ queue[tail++] = n;
125
+ }
126
+ }
127
+ if (cy > 0) {
128
+ const n = curr - width;
129
+ if (visited[n] === 0 && isShadow[n] === 1) {
130
+ visited[n] = 1;
131
+ queue[tail++] = n;
132
+ }
133
+ }
134
+ if (cy < height - 1) {
135
+ const n = curr + width;
136
+ if (visited[n] === 0 && isShadow[n] === 1) {
137
+ visited[n] = 1;
138
+ queue[tail++] = n;
139
+ }
140
+ }
141
+ }
142
+ if (clusterSize > maxClusterSize) {
143
+ maxClusterSize = clusterSize;
144
+ }
145
+ }
146
+ }
147
+ return maxClusterSize / totalShadowPixels;
148
+ }
149
+ async function analyzeShadow(input, options) {
150
+ validateInput(input);
151
+ const opts = normalizeOptions(options);
152
+ const imagePipeline = sharp(input);
153
+ const { data, info } = await imagePipeline.resize({ width: opts.downsampleWidth, withoutEnlargement: true }).grayscale().raw().toBuffer({ resolveWithObject: true });
154
+ const width = info.width;
155
+ const height = info.height;
156
+ const totalPixels = width * height;
157
+ if (totalPixels === 0) {
158
+ throw new Error("Image contains no pixel data.");
159
+ }
160
+ const illum = extractIlluminationMap(data, width, height, opts.smoothingRadius);
161
+ const hist = new Int32Array(256);
162
+ let totalSum = 0;
163
+ for (let i = 0; i < totalPixels; i++) {
164
+ const val = illum[i];
165
+ hist[val]++;
166
+ totalSum += val;
167
+ }
168
+ const meanT = totalSum / totalPixels;
169
+ let varT = 0;
170
+ for (let i = 0; i < 256; i++) {
171
+ const count = hist[i];
172
+ if (count > 0) {
173
+ const diff = i - meanT;
174
+ varT += count * diff * diff;
175
+ }
176
+ }
177
+ varT /= totalPixels;
178
+ if (varT < 1) {
179
+ const roundedMean = Number(meanT.toFixed(2));
180
+ return {
181
+ score: 0,
182
+ hasShadow: false,
183
+ shadowAreaPercentage: 0,
184
+ illuminationDelta: 0,
185
+ quality: "excellent",
186
+ details: {
187
+ litLuminance: roundedMean,
188
+ shadowLuminance: roundedMean,
189
+ otsuThreshold: Math.round(meanT),
190
+ separability: 0,
191
+ boundaryGradient: 0
192
+ }
193
+ };
194
+ }
195
+ let bestT = 128;
196
+ let maxBetweenVar = 0;
197
+ let weight0 = 0;
198
+ let sum0 = 0;
199
+ for (let t = 0; t < 255; t++) {
200
+ const count = hist[t];
201
+ weight0 += count;
202
+ if (weight0 === 0) continue;
203
+ const weight1 = totalPixels - weight0;
204
+ if (weight1 === 0) break;
205
+ sum0 += t * count;
206
+ const mean0 = sum0 / weight0;
207
+ const mean1 = (totalSum - sum0) / weight1;
208
+ const meanDiff = mean1 - mean0;
209
+ const betweenVar = weight0 * weight1 * meanDiff * meanDiff / (totalPixels * totalPixels);
210
+ if (betweenVar > maxBetweenVar) {
211
+ maxBetweenVar = betweenVar;
212
+ bestT = t;
213
+ }
214
+ }
215
+ let shadowCount = 0;
216
+ let shadowSum = 0;
217
+ let litCount = 0;
218
+ let litSum = 0;
219
+ const isShadow = new Uint8Array(totalPixels);
220
+ for (let i = 0; i < totalPixels; i++) {
221
+ const val = illum[i];
222
+ if (val <= bestT) {
223
+ isShadow[i] = 1;
224
+ shadowCount++;
225
+ shadowSum += val;
226
+ } else {
227
+ litCount++;
228
+ litSum += val;
229
+ }
230
+ }
231
+ const shadowLuminance = shadowCount > 0 ? shadowSum / shadowCount : 0;
232
+ const litLuminance = litCount > 0 ? litSum / litCount : meanT;
233
+ const illuminationDelta = Math.max(0, litLuminance - shadowLuminance);
234
+ const shadowAreaPercentage = shadowCount / totalPixels * 100;
235
+ const separability = Math.min(1, maxBetweenVar / varT);
236
+ let boundaryGradientSum = 0;
237
+ let boundaryPixelCount = 0;
238
+ for (let y = 1; y < height - 1; y++) {
239
+ const rowOffset = y * width;
240
+ for (let x = 1; x < width - 1; x++) {
241
+ const idx = rowOffset + x;
242
+ if (isShadow[idx] === 1) {
243
+ if (isShadow[idx - 1] === 0 || isShadow[idx + 1] === 0 || isShadow[idx - width] === 0 || isShadow[idx + width] === 0) {
244
+ const gx = (illum[idx + 1] - illum[idx - 1]) * 0.5;
245
+ const gy = (illum[idx + width] - illum[idx - width]) * 0.5;
246
+ boundaryGradientSum += Math.sqrt(gx * gx + gy * gy);
247
+ boundaryPixelCount++;
248
+ }
249
+ }
250
+ }
251
+ }
252
+ const avgBoundaryGradient = boundaryPixelCount > 0 ? boundaryGradientSum / boundaryPixelCount : 0;
253
+ const contiguity = computeContiguity(isShadow, width, height, shadowCount);
254
+ let score = 0;
255
+ if (illuminationDelta >= opts.minIlluminationDelta && shadowAreaPercentage > 0) {
256
+ const deltaRange = Math.max(1, 120 - opts.minIlluminationDelta);
257
+ const fDelta = Math.min(1, (illuminationDelta - opts.minIlluminationDelta) / deltaRange);
258
+ const fSep = Math.min(1, Math.max(0, (separability - 0.25) / 0.55));
259
+ const contrastRatio = illuminationDelta / (litLuminance + shadowLuminance + 1e-5);
260
+ const fContrast = Math.min(1, contrastRatio / 0.45);
261
+ const fGrad = Math.min(1, avgBoundaryGradient / 8);
262
+ const gradientTerm = 0.65 + 0.35 * fGrad;
263
+ let fArea = 1;
264
+ if (shadowAreaPercentage < 5) {
265
+ fArea = Math.max(0, shadowAreaPercentage / 5);
266
+ } else if (shadowAreaPercentage > 75) {
267
+ fArea = Math.max(0, (95 - shadowAreaPercentage) / 20);
268
+ }
269
+ const fLit = Math.min(1, litLuminance / 60);
270
+ const fContig = 0.4 + 0.6 * Math.min(1, Math.max(0, (contiguity - 0.2) / 0.5));
271
+ const contrastTerm = 0.5 * fSep + 0.5 * fContrast;
272
+ const rawScore = fDelta * contrastTerm * gradientTerm * fArea * fLit * fContig;
273
+ score = Math.max(0, Math.min(1, rawScore));
274
+ }
275
+ score = Number(score.toFixed(4));
276
+ const hasShadowResult = score >= opts.threshold;
277
+ let quality;
278
+ if (score < 0.1) {
279
+ quality = "excellent";
280
+ } else if (score < 0.2) {
281
+ quality = "good";
282
+ } else if (score < 0.45) {
283
+ quality = "fair";
284
+ } else {
285
+ quality = "poor";
286
+ }
287
+ const details = {
288
+ litLuminance: Number(litLuminance.toFixed(2)),
289
+ shadowLuminance: Number(shadowLuminance.toFixed(2)),
290
+ otsuThreshold: bestT,
291
+ separability: Number(separability.toFixed(4)),
292
+ boundaryGradient: Number(avgBoundaryGradient.toFixed(2))
293
+ };
294
+ return {
295
+ score,
296
+ hasShadow: hasShadowResult,
297
+ shadowAreaPercentage: Number(shadowAreaPercentage.toFixed(2)),
298
+ illuminationDelta: Number(illuminationDelta.toFixed(2)),
299
+ quality,
300
+ details
301
+ };
302
+ }
303
+ async function getShadowScore(input, options) {
304
+ const result = await analyzeShadow(input, options);
305
+ return result.score;
306
+ }
307
+ async function hasShadow(input, thresholdOrOptions) {
308
+ let opts;
309
+ if (typeof thresholdOrOptions === "number") {
310
+ opts = { threshold: thresholdOrOptions };
311
+ } else if (thresholdOrOptions !== void 0) {
312
+ opts = thresholdOrOptions;
313
+ }
314
+ const result = await analyzeShadow(input, opts);
315
+ return result.hasShadow;
316
+ }
317
+
318
+ export { analyzeShadow, getShadowScore, hasShadow, normalizeOptions, validateInput };
319
+ //# sourceMappingURL=index.js.map
320
+ //# sourceMappingURL=index.js.map