@jaak.ai/stamps 2.0.0-dev.42 → 2.0.0-dev.44
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.
- package/README.md +216 -218
- package/dist/cjs/jaak-stamps-webcomponent.cjs.js +1 -1
- package/dist/cjs/jaak-stamps.cjs.entry.js +14 -652
- package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
- package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/collection/components/my-component/my-component.css +40 -0
- package/dist/collection/components/my-component/my-component.js +35 -343
- package/dist/collection/components/my-component/my-component.js.map +1 -1
- package/dist/collection/services/DetectionService.js +1 -164
- package/dist/collection/services/DetectionService.js.map +1 -1
- package/dist/collection/services/ServiceContainer.js +1 -6
- package/dist/collection/services/ServiceContainer.js.map +1 -1
- package/dist/collection/services/interfaces/IDetectionService.js.map +1 -1
- package/dist/collection/types/component-types.js +2 -0
- package/dist/collection/types/component-types.js.map +1 -0
- package/dist/components/jaak-stamps.js +15 -663
- package/dist/components/jaak-stamps.js.map +1 -1
- package/dist/esm/jaak-stamps-webcomponent.js +1 -1
- package/dist/esm/jaak-stamps.entry.js +14 -652
- package/dist/esm/jaak-stamps.entry.js.map +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/p-3074405d.entry.js +2 -0
- package/dist/jaak-stamps-webcomponent/p-3074405d.entry.js.map +1 -0
- package/dist/types/components/my-component/my-component.d.ts +4 -45
- package/dist/types/components.d.ts +11 -62
- package/dist/types/services/DetectionService.d.ts +2 -22
- package/dist/types/services/ServiceContainer.d.ts +0 -6
- package/dist/types/services/interfaces/IDetectionService.d.ts +0 -13
- package/dist/types/types/component-types.d.ts +36 -0
- package/package.json +1 -1
- package/dist/collection/services/ImageQualityService.js +0 -329
- package/dist/collection/services/ImageQualityService.js.map +0 -1
- package/dist/collection/services/interfaces/IImageQualityService.js +0 -2
- package/dist/collection/services/interfaces/IImageQualityService.js.map +0 -1
- package/dist/jaak-stamps-webcomponent/p-47f37982.entry.js +0 -2
- package/dist/jaak-stamps-webcomponent/p-47f37982.entry.js.map +0 -1
- package/dist/types/services/ImageQualityService.d.ts +0 -31
- package/dist/types/services/interfaces/IImageQualityService.d.ts +0 -58
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* It contains typing information for all components that exist in this project.
|
|
6
6
|
*/
|
|
7
7
|
import { HTMLStencilElement, JSXBase } from "./stencil-public-runtime";
|
|
8
|
-
import {
|
|
9
|
-
export {
|
|
8
|
+
import { CameraInfoResponse, CapturedImagesResponse, StatusResponse } from "./types/component-types";
|
|
9
|
+
export { CameraInfoResponse, CapturedImagesResponse, StatusResponse } from "./types/component-types";
|
|
10
10
|
export namespace Components {
|
|
11
11
|
interface JaakStamps {
|
|
12
12
|
/**
|
|
13
|
-
* @default
|
|
13
|
+
* @default 20
|
|
14
14
|
*/
|
|
15
15
|
"alignmentTolerance": number;
|
|
16
16
|
/**
|
|
@@ -18,58 +18,31 @@ export namespace Components {
|
|
|
18
18
|
*/
|
|
19
19
|
"captureDelay": number;
|
|
20
20
|
/**
|
|
21
|
-
* @default
|
|
21
|
+
* @default 20
|
|
22
22
|
*/
|
|
23
23
|
"cropMargin": number;
|
|
24
24
|
/**
|
|
25
25
|
* @default false
|
|
26
26
|
*/
|
|
27
27
|
"debug": boolean;
|
|
28
|
-
/**
|
|
29
|
-
* @default true
|
|
30
|
-
*/
|
|
31
|
-
"enableQualityValidation": boolean;
|
|
32
28
|
"focusAtPoint": (x: number, y: number) => Promise<{ success: boolean; coordinates: { x: number; y: number; }; }>;
|
|
33
|
-
"getCameraInfo": () => Promise<
|
|
29
|
+
"getCameraInfo": () => Promise<CameraInfoResponse>;
|
|
34
30
|
"getCaptureDelay": () => Promise<number>;
|
|
35
|
-
"getCapturedImages": () => Promise<
|
|
36
|
-
"
|
|
37
|
-
"getQualityThresholds": () => Promise<{ minQualityScore: number; minFocusScore: number; minBlurScore: number; maxReflectionScore: number; }>;
|
|
38
|
-
"getStatus": () => Promise<{ isVideoActive: boolean; captureStep: "front" | "back" | "completed"; hasImages: boolean; isProcessCompleted: boolean; isModelPreloaded: boolean; }>;
|
|
31
|
+
"getCapturedImages": () => Promise<CapturedImagesResponse>;
|
|
32
|
+
"getStatus": () => Promise<StatusResponse>;
|
|
39
33
|
"isProcessCompleted": () => Promise<boolean>;
|
|
40
34
|
/**
|
|
41
|
-
* @default
|
|
35
|
+
* @default 80
|
|
42
36
|
*/
|
|
43
37
|
"maskSize": number;
|
|
44
|
-
/**
|
|
45
|
-
* @default 15
|
|
46
|
-
*/
|
|
47
|
-
"maxReflectionScore": number;
|
|
48
|
-
/**
|
|
49
|
-
* @default 22
|
|
50
|
-
*/
|
|
51
|
-
"minBlurScore": number;
|
|
52
|
-
/**
|
|
53
|
-
* @default 16
|
|
54
|
-
*/
|
|
55
|
-
"minFocusScore": number;
|
|
56
|
-
/**
|
|
57
|
-
* @default 45
|
|
58
|
-
*/
|
|
59
|
-
"minQualityScore": number;
|
|
60
38
|
/**
|
|
61
39
|
* @default 'auto'
|
|
62
40
|
*/
|
|
63
41
|
"preferredCamera": 'auto' | 'front' | 'back';
|
|
64
42
|
"preloadModel": () => Promise<{ success: boolean; message: string; error?: undefined; } | { success: boolean; error: any; message?: undefined; }>;
|
|
65
|
-
/**
|
|
66
|
-
* @default 60
|
|
67
|
-
*/
|
|
68
|
-
"qualityThreshold": number;
|
|
69
43
|
"resetCapture": () => Promise<void>;
|
|
70
44
|
"setCaptureDelay": (delay: number) => Promise<{ success: boolean; captureDelay: number; }>;
|
|
71
45
|
"setPreferredCamera": (camera: "auto" | "front" | "back") => Promise<{ success: boolean; selectedCamera: string; availableCameras: number; }>;
|
|
72
|
-
"setQualityThresholds": (thresholds: QualityThresholds) => Promise<{ success: boolean; thresholds: QualityThresholds; }>;
|
|
73
46
|
"setTorchEnabled": (enabled: boolean) => Promise<{ success: boolean; enabled: boolean; }>;
|
|
74
47
|
"skipBackCapture": () => Promise<void>;
|
|
75
48
|
"startCapture": () => Promise<void>;
|
|
@@ -110,7 +83,7 @@ declare global {
|
|
|
110
83
|
declare namespace LocalJSX {
|
|
111
84
|
interface JaakStamps {
|
|
112
85
|
/**
|
|
113
|
-
* @default
|
|
86
|
+
* @default 20
|
|
114
87
|
*/
|
|
115
88
|
"alignmentTolerance"?: number;
|
|
116
89
|
/**
|
|
@@ -118,7 +91,7 @@ declare namespace LocalJSX {
|
|
|
118
91
|
*/
|
|
119
92
|
"captureDelay"?: number;
|
|
120
93
|
/**
|
|
121
|
-
* @default
|
|
94
|
+
* @default 20
|
|
122
95
|
*/
|
|
123
96
|
"cropMargin"?: number;
|
|
124
97
|
/**
|
|
@@ -126,39 +99,15 @@ declare namespace LocalJSX {
|
|
|
126
99
|
*/
|
|
127
100
|
"debug"?: boolean;
|
|
128
101
|
/**
|
|
129
|
-
* @default
|
|
130
|
-
*/
|
|
131
|
-
"enableQualityValidation"?: boolean;
|
|
132
|
-
/**
|
|
133
|
-
* @default 90
|
|
102
|
+
* @default 80
|
|
134
103
|
*/
|
|
135
104
|
"maskSize"?: number;
|
|
136
|
-
/**
|
|
137
|
-
* @default 15
|
|
138
|
-
*/
|
|
139
|
-
"maxReflectionScore"?: number;
|
|
140
|
-
/**
|
|
141
|
-
* @default 22
|
|
142
|
-
*/
|
|
143
|
-
"minBlurScore"?: number;
|
|
144
|
-
/**
|
|
145
|
-
* @default 16
|
|
146
|
-
*/
|
|
147
|
-
"minFocusScore"?: number;
|
|
148
|
-
/**
|
|
149
|
-
* @default 45
|
|
150
|
-
*/
|
|
151
|
-
"minQualityScore"?: number;
|
|
152
105
|
"onCaptureCompleted"?: (event: JaakStampsCustomEvent<any>) => void;
|
|
153
106
|
"onIsReady"?: (event: JaakStampsCustomEvent<boolean>) => void;
|
|
154
107
|
/**
|
|
155
108
|
* @default 'auto'
|
|
156
109
|
*/
|
|
157
110
|
"preferredCamera"?: 'auto' | 'front' | 'back';
|
|
158
|
-
/**
|
|
159
|
-
* @default 60
|
|
160
|
-
*/
|
|
161
|
-
"qualityThreshold"?: number;
|
|
162
111
|
/**
|
|
163
112
|
* @default false
|
|
164
113
|
*/
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import { IDetectionService, DetectionBox, SideAlignment, ClassificationResult
|
|
1
|
+
import { IDetectionService, DetectionBox, SideAlignment, ClassificationResult } from './interfaces/IDetectionService';
|
|
2
2
|
import { ILogger } from './interfaces/ILogger';
|
|
3
|
-
import { ImageQualityResult } from './interfaces/IImageQualityService';
|
|
4
3
|
export declare class DetectionService implements IDetectionService {
|
|
5
4
|
private logger;
|
|
6
5
|
private debug;
|
|
7
6
|
private useDocumentClassification;
|
|
8
|
-
private qualityThresholds;
|
|
9
7
|
private session?;
|
|
10
8
|
private mobileNetSession?;
|
|
11
9
|
private mobileNetClassMap?;
|
|
12
10
|
private modelLoaded;
|
|
13
11
|
private deviceStrategy;
|
|
14
|
-
private imageQualityService;
|
|
15
12
|
private readonly MODEL_PATH;
|
|
16
13
|
private readonly MOBILENET_MODEL_PATH;
|
|
17
14
|
private readonly MOBILENET_CLASSES_PATH;
|
|
@@ -20,7 +17,7 @@ export declare class DetectionService implements IDetectionService {
|
|
|
20
17
|
private preprocessCanvas?;
|
|
21
18
|
private preprocessCtx?;
|
|
22
19
|
private captureCanvas?;
|
|
23
|
-
constructor(logger: ILogger, debug?: boolean, useDocumentClassification?: boolean
|
|
20
|
+
constructor(logger: ILogger, debug?: boolean, useDocumentClassification?: boolean);
|
|
24
21
|
loadModel(): Promise<void>;
|
|
25
22
|
loadClassificationModel(): Promise<void>;
|
|
26
23
|
isModelLoaded(): boolean;
|
|
@@ -35,21 +32,4 @@ export declare class DetectionService implements IDetectionService {
|
|
|
35
32
|
private cleanupCanvasPool;
|
|
36
33
|
private float32ToFloat16;
|
|
37
34
|
private preprocessMobileNet;
|
|
38
|
-
validateImageQuality(video: HTMLVideoElement, detectedBox?: DetectionBox): ImageQualityResult;
|
|
39
|
-
isImageQualityAcceptable(qualityResult: ImageQualityResult): boolean;
|
|
40
|
-
updateQualityThresholds(thresholds: QualityThresholds): void;
|
|
41
|
-
getQualityThresholds(): QualityThresholds;
|
|
42
|
-
getQualityFeedback(qualityResult: ImageQualityResult): string;
|
|
43
|
-
runInferenceWithQuality(inputTensor: any, video: HTMLVideoElement): Promise<{
|
|
44
|
-
detections: DetectionBox[];
|
|
45
|
-
qualityResult: ImageQualityResult;
|
|
46
|
-
canCapture: boolean;
|
|
47
|
-
feedback: string;
|
|
48
|
-
}>;
|
|
49
|
-
getQuickQualityFeedback(video: HTMLVideoElement): {
|
|
50
|
-
hasBlur: boolean;
|
|
51
|
-
hasReflections: boolean;
|
|
52
|
-
isFocused: boolean;
|
|
53
|
-
feedback: string;
|
|
54
|
-
};
|
|
55
35
|
}
|
|
@@ -11,12 +11,6 @@ export interface ComponentConfig {
|
|
|
11
11
|
useDocumentClassification: boolean;
|
|
12
12
|
preferredCamera: 'auto' | 'front' | 'back';
|
|
13
13
|
captureDelay: number;
|
|
14
|
-
enableQualityValidation?: boolean;
|
|
15
|
-
qualityThreshold?: number;
|
|
16
|
-
minQualityScore?: number;
|
|
17
|
-
minFocusScore?: number;
|
|
18
|
-
minBlurScore?: number;
|
|
19
|
-
maxReflectionScore?: number;
|
|
20
14
|
}
|
|
21
15
|
export declare class ServiceContainer {
|
|
22
16
|
private services;
|
|
@@ -17,12 +17,6 @@ export interface ClassificationResult {
|
|
|
17
17
|
confidence: number;
|
|
18
18
|
classIndex: number;
|
|
19
19
|
}
|
|
20
|
-
export interface QualityThresholds {
|
|
21
|
-
minQualityScore?: number;
|
|
22
|
-
minFocusScore?: number;
|
|
23
|
-
minBlurScore?: number;
|
|
24
|
-
maxReflectionScore?: number;
|
|
25
|
-
}
|
|
26
20
|
export interface IDetectionService {
|
|
27
21
|
loadModel(): Promise<void>;
|
|
28
22
|
loadClassificationModel(): Promise<void>;
|
|
@@ -33,12 +27,5 @@ export interface IDetectionService {
|
|
|
33
27
|
checkSideAlignment(box: DetectionBox, maskConfig: any): SideAlignment;
|
|
34
28
|
isCardInFrame(box: DetectionBox): boolean;
|
|
35
29
|
areAllSidesAligned(alignment: SideAlignment): boolean;
|
|
36
|
-
validateImageQuality(video: HTMLVideoElement, detectedBox?: DetectionBox): any;
|
|
37
|
-
isImageQualityAcceptable(qualityResult: any): boolean;
|
|
38
|
-
getQualityFeedback(qualityResult: any): string;
|
|
39
|
-
runInferenceWithQuality(inputTensor: any, video: HTMLVideoElement): Promise<any>;
|
|
40
|
-
getQuickQualityFeedback(video: HTMLVideoElement): any;
|
|
41
|
-
updateQualityThresholds(thresholds: QualityThresholds): void;
|
|
42
|
-
getQualityThresholds(): QualityThresholds;
|
|
43
30
|
cleanup(): void;
|
|
44
31
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for component methods to avoid absolute paths in components.d.ts
|
|
3
|
+
*/
|
|
4
|
+
export interface CameraInfoResponse {
|
|
5
|
+
availableCameras: Array<{
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
selected: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
selectedCameraId: string | null;
|
|
11
|
+
deviceType: string;
|
|
12
|
+
isMultipleCamerasAvailable: boolean;
|
|
13
|
+
preferredFacing: 'environment' | 'user' | null;
|
|
14
|
+
}
|
|
15
|
+
export interface CapturedImagesResponse {
|
|
16
|
+
front: {
|
|
17
|
+
fullFrame: string | null;
|
|
18
|
+
cropped: string | null;
|
|
19
|
+
};
|
|
20
|
+
back: {
|
|
21
|
+
fullFrame: string | null;
|
|
22
|
+
cropped: string | null;
|
|
23
|
+
};
|
|
24
|
+
metadata: {
|
|
25
|
+
totalImages: number;
|
|
26
|
+
processCompleted: boolean;
|
|
27
|
+
backCaptureSkipped: boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface StatusResponse {
|
|
31
|
+
isVideoActive: boolean;
|
|
32
|
+
captureStep: 'front' | 'back' | 'completed';
|
|
33
|
+
hasImages: boolean;
|
|
34
|
+
isProcessCompleted: boolean;
|
|
35
|
+
isModelPreloaded: boolean;
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
export class ImageQualityService {
|
|
2
|
-
logger;
|
|
3
|
-
// Thresholds for quality assessment
|
|
4
|
-
BLUR_THRESHOLD = 80; // Reduced from 100 to 80 for better acceptance
|
|
5
|
-
FOCUS_THRESHOLD = 40; // Reduced from 50 to 40 for better acceptance
|
|
6
|
-
REFLECTION_THRESHOLD = 240; // Pixel brightness threshold for reflections
|
|
7
|
-
MIN_BRIGHTNESS = 50; // Minimum acceptable brightness
|
|
8
|
-
MAX_BRIGHTNESS = 200; // Maximum acceptable brightness
|
|
9
|
-
MIN_CONTRAST = 30; // Minimum acceptable contrast
|
|
10
|
-
constructor(logger) {
|
|
11
|
-
this.logger = logger;
|
|
12
|
-
}
|
|
13
|
-
analyzeImageQuality(canvas, documentBounds) {
|
|
14
|
-
const ctx = canvas.getContext('2d');
|
|
15
|
-
if (!ctx) {
|
|
16
|
-
throw new Error('Unable to get canvas context');
|
|
17
|
-
}
|
|
18
|
-
// Get image data for the document area or full canvas
|
|
19
|
-
let imageData;
|
|
20
|
-
if (documentBounds) {
|
|
21
|
-
imageData = ctx.getImageData(documentBounds.x, documentBounds.y, documentBounds.w, documentBounds.h);
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
25
|
-
}
|
|
26
|
-
// Perform all quality assessments
|
|
27
|
-
const blur = this.detectBlur(imageData);
|
|
28
|
-
const reflection = this.detectReflections(imageData);
|
|
29
|
-
const focus = this.measureFocus(imageData);
|
|
30
|
-
const brightness = this.measureBrightness(imageData);
|
|
31
|
-
const contrast = this.measureContrast(imageData);
|
|
32
|
-
// Calculate overall quality score (0-100)
|
|
33
|
-
const qualityScore = this.calculateOverallQuality(blur, reflection, focus, brightness, contrast);
|
|
34
|
-
const overallQuality = this.getQualityLevel(qualityScore);
|
|
35
|
-
// Identify issues and recommendations
|
|
36
|
-
const issues = this.identifyIssues(blur, reflection, focus, brightness, contrast);
|
|
37
|
-
const recommendations = this.getQualityRecommendations({
|
|
38
|
-
blur, reflection, focus, brightness, contrast,
|
|
39
|
-
overallQuality, qualityScore, issues, recommendations: []
|
|
40
|
-
});
|
|
41
|
-
const result = {
|
|
42
|
-
blur,
|
|
43
|
-
reflection,
|
|
44
|
-
focus,
|
|
45
|
-
brightness,
|
|
46
|
-
contrast,
|
|
47
|
-
overallQuality,
|
|
48
|
-
qualityScore,
|
|
49
|
-
issues,
|
|
50
|
-
recommendations
|
|
51
|
-
};
|
|
52
|
-
this.logger.state('CALIDAD_IMAGEN_ANALIZADA', {
|
|
53
|
-
qualityScore,
|
|
54
|
-
overallQuality,
|
|
55
|
-
issues: issues.length,
|
|
56
|
-
hasBlur: !blur.isAcceptable,
|
|
57
|
-
hasReflection: reflection.hasReflection,
|
|
58
|
-
isFocused: focus.isFocused
|
|
59
|
-
});
|
|
60
|
-
return result;
|
|
61
|
-
}
|
|
62
|
-
detectBlur(imageData) {
|
|
63
|
-
const { data, width, height } = imageData;
|
|
64
|
-
let totalVariance = 0;
|
|
65
|
-
let pixelCount = 0;
|
|
66
|
-
// Calculate Laplacian variance for blur detection
|
|
67
|
-
for (let y = 1; y < height - 1; y++) {
|
|
68
|
-
for (let x = 1; x < width - 1; x++) {
|
|
69
|
-
const idx = (y * width + x) * 4;
|
|
70
|
-
// Convert to grayscale
|
|
71
|
-
const gray = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2];
|
|
72
|
-
// Calculate Laplacian (edge detection)
|
|
73
|
-
const neighbors = [
|
|
74
|
-
this.getGrayscale(data, x - 1, y - 1, width),
|
|
75
|
-
this.getGrayscale(data, x, y - 1, width),
|
|
76
|
-
this.getGrayscale(data, x + 1, y - 1, width),
|
|
77
|
-
this.getGrayscale(data, x - 1, y, width),
|
|
78
|
-
this.getGrayscale(data, x + 1, y, width),
|
|
79
|
-
this.getGrayscale(data, x - 1, y + 1, width),
|
|
80
|
-
this.getGrayscale(data, x, y + 1, width),
|
|
81
|
-
this.getGrayscale(data, x + 1, y + 1, width)
|
|
82
|
-
];
|
|
83
|
-
const laplacian = 8 * gray - neighbors.reduce((sum, val) => sum + val, 0);
|
|
84
|
-
totalVariance += laplacian * laplacian;
|
|
85
|
-
pixelCount++;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const blurScore = Math.sqrt(totalVariance / pixelCount);
|
|
89
|
-
const isAcceptable = blurScore > this.BLUR_THRESHOLD;
|
|
90
|
-
return {
|
|
91
|
-
blurScore,
|
|
92
|
-
isAcceptable,
|
|
93
|
-
threshold: this.BLUR_THRESHOLD
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
detectReflections(imageData) {
|
|
97
|
-
const { data, width, height } = imageData;
|
|
98
|
-
let reflectionPixels = 0;
|
|
99
|
-
const totalPixels = width * height;
|
|
100
|
-
const reflectionAreas = [];
|
|
101
|
-
// Track bright regions that could be reflections
|
|
102
|
-
const brightRegions = Array(height).fill(null).map(() => Array(width).fill(false));
|
|
103
|
-
for (let y = 0; y < height; y++) {
|
|
104
|
-
for (let x = 0; x < width; x++) {
|
|
105
|
-
const idx = (y * width + x) * 4;
|
|
106
|
-
const brightness = (data[idx] + data[idx + 1] + data[idx + 2]) / 3;
|
|
107
|
-
if (brightness > this.REFLECTION_THRESHOLD) {
|
|
108
|
-
brightRegions[y][x] = true;
|
|
109
|
-
reflectionPixels++;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// Find connected bright regions (potential reflections)
|
|
114
|
-
const visited = Array(height).fill(null).map(() => Array(width).fill(false));
|
|
115
|
-
for (let y = 0; y < height; y++) {
|
|
116
|
-
for (let x = 0; x < width; x++) {
|
|
117
|
-
if (brightRegions[y][x] && !visited[y][x]) {
|
|
118
|
-
const region = this.findConnectedRegion(brightRegions, visited, x, y, width, height);
|
|
119
|
-
if (region.size > 100) { // Minimum size for a reflection
|
|
120
|
-
reflectionAreas.push({
|
|
121
|
-
x: region.minX,
|
|
122
|
-
y: region.minY,
|
|
123
|
-
width: region.maxX - region.minX,
|
|
124
|
-
height: region.maxY - region.minY
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const reflectionScore = (reflectionPixels / totalPixels) * 100;
|
|
131
|
-
const hasReflection = reflectionScore > 5 || reflectionAreas.length > 0; // 5% threshold
|
|
132
|
-
return {
|
|
133
|
-
reflectionScore,
|
|
134
|
-
hasReflection,
|
|
135
|
-
threshold: 5,
|
|
136
|
-
reflectionAreas
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
measureFocus(imageData) {
|
|
140
|
-
const { data, width, height } = imageData;
|
|
141
|
-
let edgeStrength = 0;
|
|
142
|
-
let edgeCount = 0;
|
|
143
|
-
// Sobel edge detection for focus measurement
|
|
144
|
-
for (let y = 1; y < height - 1; y++) {
|
|
145
|
-
for (let x = 1; x < width - 1; x++) {
|
|
146
|
-
const gx = this.sobelX(data, x, y, width);
|
|
147
|
-
const gy = this.sobelY(data, x, y, width);
|
|
148
|
-
const magnitude = Math.sqrt(gx * gx + gy * gy);
|
|
149
|
-
edgeStrength += magnitude;
|
|
150
|
-
edgeCount++;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
const focusScore = edgeCount > 0 ? edgeStrength / edgeCount : 0;
|
|
154
|
-
const isFocused = focusScore > this.FOCUS_THRESHOLD;
|
|
155
|
-
return {
|
|
156
|
-
focusScore,
|
|
157
|
-
isFocused,
|
|
158
|
-
threshold: this.FOCUS_THRESHOLD,
|
|
159
|
-
edgeStrength
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
measureBrightness(imageData) {
|
|
163
|
-
const { data } = imageData;
|
|
164
|
-
let totalBrightness = 0;
|
|
165
|
-
const pixelCount = data.length / 4;
|
|
166
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
167
|
-
const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3;
|
|
168
|
-
totalBrightness += brightness;
|
|
169
|
-
}
|
|
170
|
-
const brightness = totalBrightness / pixelCount;
|
|
171
|
-
const isAcceptable = brightness >= this.MIN_BRIGHTNESS && brightness <= this.MAX_BRIGHTNESS;
|
|
172
|
-
return {
|
|
173
|
-
brightness,
|
|
174
|
-
isAcceptable,
|
|
175
|
-
minThreshold: this.MIN_BRIGHTNESS,
|
|
176
|
-
maxThreshold: this.MAX_BRIGHTNESS
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
measureContrast(imageData) {
|
|
180
|
-
const { data } = imageData;
|
|
181
|
-
let min = 255;
|
|
182
|
-
let max = 0;
|
|
183
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
184
|
-
const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3;
|
|
185
|
-
min = Math.min(min, brightness);
|
|
186
|
-
max = Math.max(max, brightness);
|
|
187
|
-
}
|
|
188
|
-
const contrast = max - min;
|
|
189
|
-
const isAcceptable = contrast > this.MIN_CONTRAST;
|
|
190
|
-
return {
|
|
191
|
-
contrast,
|
|
192
|
-
isAcceptable,
|
|
193
|
-
threshold: this.MIN_CONTRAST
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
getQualityRecommendations(result) {
|
|
197
|
-
const recommendations = [];
|
|
198
|
-
if (!result.blur.isAcceptable) {
|
|
199
|
-
recommendations.push('Mantenga el dispositivo estable para reducir el desenfoque');
|
|
200
|
-
recommendations.push('Acérquese al documento para mejorar la nitidez');
|
|
201
|
-
}
|
|
202
|
-
if (result.reflection.hasReflection) {
|
|
203
|
-
recommendations.push('Cambie el ángulo del dispositivo para evitar reflejos');
|
|
204
|
-
recommendations.push('Mejore la iluminación indirecta del área');
|
|
205
|
-
}
|
|
206
|
-
if (!result.focus.isFocused) {
|
|
207
|
-
recommendations.push('Permita que la cámara enfoque automáticamente');
|
|
208
|
-
recommendations.push('Toque la pantalla sobre el documento para enfocar');
|
|
209
|
-
}
|
|
210
|
-
if (!result.brightness.isAcceptable) {
|
|
211
|
-
if (result.brightness.brightness < result.brightness.minThreshold) {
|
|
212
|
-
recommendations.push('Aumente la iluminación del área');
|
|
213
|
-
recommendations.push('Muévase a un lugar con mejor luz');
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
recommendations.push('Reduzca la iluminación directa sobre el documento');
|
|
217
|
-
recommendations.push('Evite la luz solar directa');
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (!result.contrast.isAcceptable) {
|
|
221
|
-
recommendations.push('Mejore la iluminación para aumentar el contraste');
|
|
222
|
-
recommendations.push('Asegúrese de que el fondo contraste con el documento');
|
|
223
|
-
}
|
|
224
|
-
if (recommendations.length === 0) {
|
|
225
|
-
recommendations.push('La calidad de la imagen es óptima para la captura');
|
|
226
|
-
}
|
|
227
|
-
return recommendations;
|
|
228
|
-
}
|
|
229
|
-
calculateOverallQuality(blur, reflection, focus, brightness, contrast) {
|
|
230
|
-
let score = 0;
|
|
231
|
-
// Blur contribution (25%)
|
|
232
|
-
score += blur.isAcceptable ? 25 : Math.max(0, (blur.blurScore / this.BLUR_THRESHOLD) * 25);
|
|
233
|
-
// Reflection contribution (20%)
|
|
234
|
-
score += reflection.hasReflection ? Math.max(0, 20 - reflection.reflectionScore * 2) : 20;
|
|
235
|
-
// Focus contribution (25%)
|
|
236
|
-
score += focus.isFocused ? 25 : Math.max(0, (focus.focusScore / this.FOCUS_THRESHOLD) * 25);
|
|
237
|
-
// Brightness contribution (15%)
|
|
238
|
-
if (brightness.isAcceptable) {
|
|
239
|
-
score += 15;
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
const brightnessPenalty = Math.abs(brightness.brightness - (brightness.minThreshold + brightness.maxThreshold) / 2);
|
|
243
|
-
score += Math.max(0, 15 - brightnessPenalty / 10);
|
|
244
|
-
}
|
|
245
|
-
// Contrast contribution (15%)
|
|
246
|
-
score += contrast.isAcceptable ? 15 : Math.max(0, (contrast.contrast / this.MIN_CONTRAST) * 15);
|
|
247
|
-
return Math.round(Math.max(0, Math.min(100, score)));
|
|
248
|
-
}
|
|
249
|
-
getQualityLevel(score) {
|
|
250
|
-
if (score >= 85)
|
|
251
|
-
return 'excellent';
|
|
252
|
-
if (score >= 70)
|
|
253
|
-
return 'good';
|
|
254
|
-
if (score >= 50)
|
|
255
|
-
return 'acceptable';
|
|
256
|
-
return 'poor';
|
|
257
|
-
}
|
|
258
|
-
identifyIssues(blur, reflection, focus, brightness, contrast) {
|
|
259
|
-
const issues = [];
|
|
260
|
-
if (!blur.isAcceptable) {
|
|
261
|
-
issues.push('Imagen desenfocada por movimiento');
|
|
262
|
-
}
|
|
263
|
-
if (reflection.hasReflection) {
|
|
264
|
-
issues.push('Reflejos detectados en el documento');
|
|
265
|
-
}
|
|
266
|
-
if (!focus.isFocused) {
|
|
267
|
-
issues.push('Documento fuera de foco');
|
|
268
|
-
}
|
|
269
|
-
if (!brightness.isAcceptable) {
|
|
270
|
-
if (brightness.brightness < brightness.minThreshold) {
|
|
271
|
-
issues.push('Iluminación insuficiente');
|
|
272
|
-
}
|
|
273
|
-
else {
|
|
274
|
-
issues.push('Imagen sobreexpuesta');
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
if (!contrast.isAcceptable) {
|
|
278
|
-
issues.push('Contraste insuficiente');
|
|
279
|
-
}
|
|
280
|
-
return issues;
|
|
281
|
-
}
|
|
282
|
-
getGrayscale(data, x, y, width) {
|
|
283
|
-
const idx = (y * width + x) * 4;
|
|
284
|
-
return 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2];
|
|
285
|
-
}
|
|
286
|
-
findConnectedRegion(brightRegions, visited, startX, startY, width, height) {
|
|
287
|
-
const stack = [{ x: startX, y: startY }];
|
|
288
|
-
let size = 0;
|
|
289
|
-
let minX = startX, maxX = startX, minY = startY, maxY = startY;
|
|
290
|
-
while (stack.length > 0) {
|
|
291
|
-
const { x, y } = stack.pop();
|
|
292
|
-
if (x < 0 || x >= width || y < 0 || y >= height || visited[y][x] || !brightRegions[y][x]) {
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
visited[y][x] = true;
|
|
296
|
-
size++;
|
|
297
|
-
minX = Math.min(minX, x);
|
|
298
|
-
maxX = Math.max(maxX, x);
|
|
299
|
-
minY = Math.min(minY, y);
|
|
300
|
-
maxY = Math.max(maxY, y);
|
|
301
|
-
// Add adjacent pixels
|
|
302
|
-
stack.push({ x: x + 1, y }, { x: x - 1, y }, { x, y: y + 1 }, { x, y: y - 1 });
|
|
303
|
-
}
|
|
304
|
-
return { size, minX, maxX, minY, maxY };
|
|
305
|
-
}
|
|
306
|
-
sobelX(data, x, y, width) {
|
|
307
|
-
const kernel = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
|
|
308
|
-
let sum = 0;
|
|
309
|
-
for (let ky = -1; ky <= 1; ky++) {
|
|
310
|
-
for (let kx = -1; kx <= 1; kx++) {
|
|
311
|
-
const gray = this.getGrayscale(data, x + kx, y + ky, width);
|
|
312
|
-
sum += gray * kernel[ky + 1][kx + 1];
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return sum;
|
|
316
|
-
}
|
|
317
|
-
sobelY(data, x, y, width) {
|
|
318
|
-
const kernel = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
|
|
319
|
-
let sum = 0;
|
|
320
|
-
for (let ky = -1; ky <= 1; ky++) {
|
|
321
|
-
for (let kx = -1; kx <= 1; kx++) {
|
|
322
|
-
const gray = this.getGrayscale(data, x + kx, y + ky, width);
|
|
323
|
-
sum += gray * kernel[ky + 1][kx + 1];
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return sum;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
//# sourceMappingURL=ImageQualityService.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ImageQualityService.js","sourceRoot":"","sources":["../../src/services/ImageQualityService.ts"],"names":[],"mappings":"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","sourcesContent":["import { \n IImageQualityService, \n ImageQualityResult, \n BlurMetrics, \n ReflectionMetrics, \n FocusMetrics, \n BrightnessMetrics, \n ContrastMetrics \n} from './interfaces/IImageQualityService';\nimport { ILogger } from './interfaces/ILogger';\n\nexport class ImageQualityService implements IImageQualityService {\n // Thresholds for quality assessment\n private readonly BLUR_THRESHOLD = 80; // Reduced from 100 to 80 for better acceptance\n private readonly FOCUS_THRESHOLD = 40; // Reduced from 50 to 40 for better acceptance\n private readonly REFLECTION_THRESHOLD = 240; // Pixel brightness threshold for reflections\n private readonly MIN_BRIGHTNESS = 50; // Minimum acceptable brightness\n private readonly MAX_BRIGHTNESS = 200; // Maximum acceptable brightness\n private readonly MIN_CONTRAST = 30; // Minimum acceptable contrast\n\n constructor(private logger: ILogger) {}\n\n analyzeImageQuality(canvas: HTMLCanvasElement, documentBounds?: { x: number; y: number; w: number; h: number }): ImageQualityResult {\n const ctx = canvas.getContext('2d');\n if (!ctx) {\n throw new Error('Unable to get canvas context');\n }\n\n // Get image data for the document area or full canvas\n let imageData: ImageData;\n if (documentBounds) {\n imageData = ctx.getImageData(documentBounds.x, documentBounds.y, documentBounds.w, documentBounds.h);\n } else {\n imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n }\n\n // Perform all quality assessments\n const blur = this.detectBlur(imageData);\n const reflection = this.detectReflections(imageData);\n const focus = this.measureFocus(imageData);\n const brightness = this.measureBrightness(imageData);\n const contrast = this.measureContrast(imageData);\n\n // Calculate overall quality score (0-100)\n const qualityScore = this.calculateOverallQuality(blur, reflection, focus, brightness, contrast);\n const overallQuality = this.getQualityLevel(qualityScore);\n \n // Identify issues and recommendations\n const issues = this.identifyIssues(blur, reflection, focus, brightness, contrast);\n const recommendations = this.getQualityRecommendations({ \n blur, reflection, focus, brightness, contrast, \n overallQuality, qualityScore, issues, recommendations: [] \n });\n\n const result: ImageQualityResult = {\n blur,\n reflection,\n focus,\n brightness,\n contrast,\n overallQuality,\n qualityScore,\n issues,\n recommendations\n };\n\n this.logger.state('CALIDAD_IMAGEN_ANALIZADA', {\n qualityScore,\n overallQuality,\n issues: issues.length,\n hasBlur: !blur.isAcceptable,\n hasReflection: reflection.hasReflection,\n isFocused: focus.isFocused\n });\n\n return result;\n }\n\n detectBlur(imageData: ImageData): BlurMetrics {\n const { data, width, height } = imageData;\n let totalVariance = 0;\n let pixelCount = 0;\n\n // Calculate Laplacian variance for blur detection\n for (let y = 1; y < height - 1; y++) {\n for (let x = 1; x < width - 1; x++) {\n const idx = (y * width + x) * 4;\n \n // Convert to grayscale\n const gray = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2];\n \n // Calculate Laplacian (edge detection)\n const neighbors = [\n this.getGrayscale(data, x - 1, y - 1, width),\n this.getGrayscale(data, x, y - 1, width),\n this.getGrayscale(data, x + 1, y - 1, width),\n this.getGrayscale(data, x - 1, y, width),\n this.getGrayscale(data, x + 1, y, width),\n this.getGrayscale(data, x - 1, y + 1, width),\n this.getGrayscale(data, x, y + 1, width),\n this.getGrayscale(data, x + 1, y + 1, width)\n ];\n \n const laplacian = 8 * gray - neighbors.reduce((sum, val) => sum + val, 0);\n totalVariance += laplacian * laplacian;\n pixelCount++;\n }\n }\n\n const blurScore = Math.sqrt(totalVariance / pixelCount);\n const isAcceptable = blurScore > this.BLUR_THRESHOLD;\n\n return {\n blurScore,\n isAcceptable,\n threshold: this.BLUR_THRESHOLD\n };\n }\n\n detectReflections(imageData: ImageData): ReflectionMetrics {\n const { data, width, height } = imageData;\n let reflectionPixels = 0;\n const totalPixels = width * height;\n const reflectionAreas: { x: number; y: number; width: number; height: number }[] = [];\n \n // Track bright regions that could be reflections\n const brightRegions: boolean[][] = Array(height).fill(null).map(() => Array(width).fill(false));\n \n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = (y * width + x) * 4;\n const brightness = (data[idx] + data[idx + 1] + data[idx + 2]) / 3;\n \n if (brightness > this.REFLECTION_THRESHOLD) {\n brightRegions[y][x] = true;\n reflectionPixels++;\n }\n }\n }\n\n // Find connected bright regions (potential reflections)\n const visited: boolean[][] = Array(height).fill(null).map(() => Array(width).fill(false));\n \n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n if (brightRegions[y][x] && !visited[y][x]) {\n const region = this.findConnectedRegion(brightRegions, visited, x, y, width, height);\n if (region.size > 100) { // Minimum size for a reflection\n reflectionAreas.push({\n x: region.minX,\n y: region.minY,\n width: region.maxX - region.minX,\n height: region.maxY - region.minY\n });\n }\n }\n }\n }\n\n const reflectionScore = (reflectionPixels / totalPixels) * 100;\n const hasReflection = reflectionScore > 5 || reflectionAreas.length > 0; // 5% threshold\n\n return {\n reflectionScore,\n hasReflection,\n threshold: 5,\n reflectionAreas\n };\n }\n\n measureFocus(imageData: ImageData): FocusMetrics {\n const { data, width, height } = imageData;\n let edgeStrength = 0;\n let edgeCount = 0;\n\n // Sobel edge detection for focus measurement\n for (let y = 1; y < height - 1; y++) {\n for (let x = 1; x < width - 1; x++) {\n const gx = this.sobelX(data, x, y, width);\n const gy = this.sobelY(data, x, y, width);\n const magnitude = Math.sqrt(gx * gx + gy * gy);\n \n edgeStrength += magnitude;\n edgeCount++;\n }\n }\n\n const focusScore = edgeCount > 0 ? edgeStrength / edgeCount : 0;\n const isFocused = focusScore > this.FOCUS_THRESHOLD;\n\n return {\n focusScore,\n isFocused,\n threshold: this.FOCUS_THRESHOLD,\n edgeStrength\n };\n }\n\n measureBrightness(imageData: ImageData): BrightnessMetrics {\n const { data } = imageData;\n let totalBrightness = 0;\n const pixelCount = data.length / 4;\n\n for (let i = 0; i < data.length; i += 4) {\n const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3;\n totalBrightness += brightness;\n }\n\n const brightness = totalBrightness / pixelCount;\n const isAcceptable = brightness >= this.MIN_BRIGHTNESS && brightness <= this.MAX_BRIGHTNESS;\n\n return {\n brightness,\n isAcceptable,\n minThreshold: this.MIN_BRIGHTNESS,\n maxThreshold: this.MAX_BRIGHTNESS\n };\n }\n\n measureContrast(imageData: ImageData): ContrastMetrics {\n const { data } = imageData;\n let min = 255;\n let max = 0;\n\n for (let i = 0; i < data.length; i += 4) {\n const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3;\n min = Math.min(min, brightness);\n max = Math.max(max, brightness);\n }\n\n const contrast = max - min;\n const isAcceptable = contrast > this.MIN_CONTRAST;\n\n return {\n contrast,\n isAcceptable,\n threshold: this.MIN_CONTRAST\n };\n }\n\n getQualityRecommendations(result: ImageQualityResult): string[] {\n const recommendations: string[] = [];\n\n if (!result.blur.isAcceptable) {\n recommendations.push('Mantenga el dispositivo estable para reducir el desenfoque');\n recommendations.push('Acérquese al documento para mejorar la nitidez');\n }\n\n if (result.reflection.hasReflection) {\n recommendations.push('Cambie el ángulo del dispositivo para evitar reflejos');\n recommendations.push('Mejore la iluminación indirecta del área');\n }\n\n if (!result.focus.isFocused) {\n recommendations.push('Permita que la cámara enfoque automáticamente');\n recommendations.push('Toque la pantalla sobre el documento para enfocar');\n }\n\n if (!result.brightness.isAcceptable) {\n if (result.brightness.brightness < result.brightness.minThreshold) {\n recommendations.push('Aumente la iluminación del área');\n recommendations.push('Muévase a un lugar con mejor luz');\n } else {\n recommendations.push('Reduzca la iluminación directa sobre el documento');\n recommendations.push('Evite la luz solar directa');\n }\n }\n\n if (!result.contrast.isAcceptable) {\n recommendations.push('Mejore la iluminación para aumentar el contraste');\n recommendations.push('Asegúrese de que el fondo contraste con el documento');\n }\n\n if (recommendations.length === 0) {\n recommendations.push('La calidad de la imagen es óptima para la captura');\n }\n\n return recommendations;\n }\n\n private calculateOverallQuality(\n blur: BlurMetrics, \n reflection: ReflectionMetrics, \n focus: FocusMetrics, \n brightness: BrightnessMetrics, \n contrast: ContrastMetrics\n ): number {\n let score = 0;\n\n // Blur contribution (25%)\n score += blur.isAcceptable ? 25 : Math.max(0, (blur.blurScore / this.BLUR_THRESHOLD) * 25);\n\n // Reflection contribution (20%)\n score += reflection.hasReflection ? Math.max(0, 20 - reflection.reflectionScore * 2) : 20;\n\n // Focus contribution (25%)\n score += focus.isFocused ? 25 : Math.max(0, (focus.focusScore / this.FOCUS_THRESHOLD) * 25);\n\n // Brightness contribution (15%)\n if (brightness.isAcceptable) {\n score += 15;\n } else {\n const brightnessPenalty = Math.abs(brightness.brightness - (brightness.minThreshold + brightness.maxThreshold) / 2);\n score += Math.max(0, 15 - brightnessPenalty / 10);\n }\n\n // Contrast contribution (15%)\n score += contrast.isAcceptable ? 15 : Math.max(0, (contrast.contrast / this.MIN_CONTRAST) * 15);\n\n return Math.round(Math.max(0, Math.min(100, score)));\n }\n\n private getQualityLevel(score: number): 'excellent' | 'good' | 'acceptable' | 'poor' {\n if (score >= 85) return 'excellent';\n if (score >= 70) return 'good';\n if (score >= 50) return 'acceptable';\n return 'poor';\n }\n\n private identifyIssues(\n blur: BlurMetrics, \n reflection: ReflectionMetrics, \n focus: FocusMetrics, \n brightness: BrightnessMetrics, \n contrast: ContrastMetrics\n ): string[] {\n const issues: string[] = [];\n\n if (!blur.isAcceptable) {\n issues.push('Imagen desenfocada por movimiento');\n }\n\n if (reflection.hasReflection) {\n issues.push('Reflejos detectados en el documento');\n }\n\n if (!focus.isFocused) {\n issues.push('Documento fuera de foco');\n }\n\n if (!brightness.isAcceptable) {\n if (brightness.brightness < brightness.minThreshold) {\n issues.push('Iluminación insuficiente');\n } else {\n issues.push('Imagen sobreexpuesta');\n }\n }\n\n if (!contrast.isAcceptable) {\n issues.push('Contraste insuficiente');\n }\n\n return issues;\n }\n\n private getGrayscale(data: Uint8ClampedArray, x: number, y: number, width: number): number {\n const idx = (y * width + x) * 4;\n return 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2];\n }\n\n private findConnectedRegion(\n brightRegions: boolean[][], \n visited: boolean[][], \n startX: number, \n startY: number, \n width: number, \n height: number\n ) {\n const stack = [{ x: startX, y: startY }];\n let size = 0;\n let minX = startX, maxX = startX, minY = startY, maxY = startY;\n\n while (stack.length > 0) {\n const { x, y } = stack.pop()!;\n \n if (x < 0 || x >= width || y < 0 || y >= height || visited[y][x] || !brightRegions[y][x]) {\n continue;\n }\n\n visited[y][x] = true;\n size++;\n \n minX = Math.min(minX, x);\n maxX = Math.max(maxX, x);\n minY = Math.min(minY, y);\n maxY = Math.max(maxY, y);\n\n // Add adjacent pixels\n stack.push({ x: x + 1, y }, { x: x - 1, y }, { x, y: y + 1 }, { x, y: y - 1 });\n }\n\n return { size, minX, maxX, minY, maxY };\n }\n\n private sobelX(data: Uint8ClampedArray, x: number, y: number, width: number): number {\n const kernel = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];\n let sum = 0;\n\n for (let ky = -1; ky <= 1; ky++) {\n for (let kx = -1; kx <= 1; kx++) {\n const gray = this.getGrayscale(data, x + kx, y + ky, width);\n sum += gray * kernel[ky + 1][kx + 1];\n }\n }\n\n return sum;\n }\n\n private sobelY(data: Uint8ClampedArray, x: number, y: number, width: number): number {\n const kernel = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];\n let sum = 0;\n\n for (let ky = -1; ky <= 1; ky++) {\n for (let kx = -1; kx <= 1; kx++) {\n const gray = this.getGrayscale(data, x + kx, y + ky, width);\n sum += gray * kernel[ky + 1][kx + 1];\n }\n }\n\n return sum;\n }\n}"]}
|