id-scanner-lib 1.6.5 → 1.6.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "id-scanner-lib",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "description": "Browser-based ID card, QR code, and face recognition scanner with liveness detection",
5
5
  "main": "dist/id-scanner-lib.js",
6
6
  "module": "dist/id-scanner-lib.esm.js",
@@ -52,6 +52,15 @@ export class EventEmitter {
52
52
  this.eventHandlers.delete(eventName);
53
53
  }
54
54
  }
55
+
56
+ /**
57
+ * 取消订阅事件 (off的别名)
58
+ * @param eventName 事件名称
59
+ * @param handler 事件处理器
60
+ */
61
+ removeListener(eventName: string, handler: EventHandler): void {
62
+ this.off(eventName, handler);
63
+ }
55
64
 
56
65
  /**
57
66
  * 订阅事件,但只触发一次
@@ -9,19 +9,24 @@ import { ConfigManager } from './config';
9
9
  /**
10
10
  * 日志级别枚举
11
11
  */
12
- export enum LogLevel {
12
+ export enum LoggerLevel {
13
13
  DEBUG = 'debug',
14
14
  INFO = 'info',
15
15
  WARN = 'warn',
16
16
  ERROR = 'error'
17
17
  }
18
18
 
19
+ /**
20
+ * @deprecated 使用 LoggerLevel 代替
21
+ */
22
+ export const LogLevel = LoggerLevel;
23
+
19
24
  /**
20
25
  * 日志条目接口
21
26
  */
22
27
  export interface LogEntry {
23
28
  /** 日志级别 */
24
- level: LogLevel;
29
+ level: LoggerLevel;
25
30
  /** 日志标签 */
26
31
  tag: string;
27
32
  /** 日志消息 */
@@ -59,16 +64,16 @@ export class ConsoleLogHandler implements LogHandler {
59
64
  const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
60
65
 
61
66
  switch (entry.level) {
62
- case LogLevel.DEBUG:
67
+ case LoggerLevel.DEBUG:
63
68
  console.debug(prefix, entry.message, entry.error || '');
64
69
  break;
65
- case LogLevel.INFO:
70
+ case LoggerLevel.INFO:
66
71
  console.info(prefix, entry.message, entry.error || '');
67
72
  break;
68
- case LogLevel.WARN:
73
+ case LoggerLevel.WARN:
69
74
  console.warn(prefix, entry.message, entry.error || '');
70
75
  break;
71
- case LogLevel.ERROR:
76
+ case LoggerLevel.ERROR:
72
77
  console.error(prefix, entry.message, entry.error || '');
73
78
  break;
74
79
  default:
@@ -119,7 +124,7 @@ export class MemoryLogHandler implements LogHandler {
119
124
  * 根据级别过滤日志条目
120
125
  * @param level 日志级别
121
126
  */
122
- getEntriesByLevel(level: LogLevel): LogEntry[] {
127
+ getEntriesByLevel(level: LoggerLevel): LogEntry[] {
123
128
  return this.entries.filter(entry => entry.level === level);
124
129
  }
125
130
 
@@ -154,6 +159,8 @@ export class RemoteLogHandler implements LogHandler {
154
159
  private flushInterval: number;
155
160
  /** 定时发送的计时器ID */
156
161
  private timerId: number | null = null;
162
+ /** 是否在浏览器环境 */
163
+ private readonly isBrowser: boolean;
157
164
 
158
165
  /**
159
166
  * 构造函数
@@ -165,14 +172,17 @@ export class RemoteLogHandler implements LogHandler {
165
172
  this.endpoint = endpoint;
166
173
  this.maxQueueSize = maxQueueSize;
167
174
  this.flushInterval = flushInterval;
175
+ this.isBrowser = typeof window !== 'undefined' && typeof window.addEventListener === 'function';
168
176
 
169
177
  // 设置定时发送
170
178
  this.startTimer();
171
179
 
172
180
  // 页面卸载前尝试发送剩余日志
173
- window.addEventListener('beforeunload', () => {
174
- this.flush();
175
- });
181
+ if (this.isBrowser) {
182
+ window.addEventListener('beforeunload', () => {
183
+ this.flush();
184
+ });
185
+ }
176
186
  }
177
187
 
178
188
  /**
@@ -181,7 +191,7 @@ export class RemoteLogHandler implements LogHandler {
181
191
  */
182
192
  handle(entry: LogEntry): void {
183
193
  // 只处理INFO以上级别的日志
184
- if (entry.level >= LogLevel.INFO) {
194
+ if (entry.level >= LoggerLevel.INFO) {
185
195
  this.queue.push(entry);
186
196
 
187
197
  // 如果队列满了,立即发送
@@ -249,6 +259,7 @@ export class RemoteLogHandler implements LogHandler {
249
259
  * 开始定时发送
250
260
  */
251
261
  startTimer(): void {
262
+ if (!this.isBrowser) return;
252
263
  if (this.timerId !== null) return;
253
264
 
254
265
  this.timerId = window.setInterval(() => {
@@ -281,7 +292,7 @@ export class Logger {
281
292
  /** 默认标签 */
282
293
  private defaultTag: string = 'IDScanner';
283
294
  /** 日志级别 */
284
- private logLevel: LogLevel = LogLevel.INFO;
295
+ private logLevel: LoggerLevel = LoggerLevel.INFO;
285
296
 
286
297
  /**
287
298
  * 私有构造函数,防止直接实例化
@@ -293,7 +304,7 @@ export class Logger {
293
304
  this.addHandler(new ConsoleLogHandler());
294
305
 
295
306
  // 监听配置变化
296
- this.config.onConfigChange('logLevel', (level: LogLevel) => {
307
+ this.config.onConfigChange('logLevel', (level: LoggerLevel) => {
297
308
  this.debug('Logger', `Log level changed to ${level}`);
298
309
  });
299
310
  }
@@ -307,6 +318,13 @@ export class Logger {
307
318
  }
308
319
  return Logger.instance;
309
320
  }
321
+
322
+ /**
323
+ * 重置单例实例(主要用于测试)
324
+ */
325
+ public static resetInstance(): void {
326
+ Logger.instance = undefined as any;
327
+ }
310
328
 
311
329
  /**
312
330
  * 添加日志处理器
@@ -349,7 +367,7 @@ export class Logger {
349
367
  * @param error 错误
350
368
  */
351
369
  debug(tag: string, message: string, error?: Error): void {
352
- this.log(LogLevel.DEBUG, tag, message, error);
370
+ this.log(LoggerLevel.DEBUG, tag, message, error);
353
371
  }
354
372
 
355
373
  /**
@@ -359,7 +377,7 @@ export class Logger {
359
377
  * @param error 错误
360
378
  */
361
379
  info(tag: string, message: string, error?: Error): void {
362
- this.log(LogLevel.INFO, tag, message, error);
380
+ this.log(LoggerLevel.INFO, tag, message, error);
363
381
  }
364
382
 
365
383
  /**
@@ -369,7 +387,7 @@ export class Logger {
369
387
  * @param error 错误
370
388
  */
371
389
  warn(tag: string, message: string, error?: Error): void {
372
- this.log(LogLevel.WARN, tag, message, error);
390
+ this.log(LoggerLevel.WARN, tag, message, error);
373
391
  }
374
392
 
375
393
  /**
@@ -379,7 +397,7 @@ export class Logger {
379
397
  * @param error 错误
380
398
  */
381
399
  error(tag: string, message: string, error?: Error): void {
382
- this.log(LogLevel.ERROR, tag, message, error);
400
+ this.log(LoggerLevel.ERROR, tag, message, error);
383
401
  }
384
402
 
385
403
  /**
@@ -397,7 +415,7 @@ export class Logger {
397
415
  * @param message 消息
398
416
  * @param error 错误
399
417
  */
400
- private log(level: LogLevel, tag: string, message: string, error?: Error): void {
418
+ private log(level: LoggerLevel, tag: string, message: string, error?: Error): void {
401
419
  // 检查日志级别
402
420
  const levelValue = this.getLevelValue(level);
403
421
  const currentLevelValue = this.getLevelValue(this.logLevel);
@@ -439,16 +457,16 @@ export class Logger {
439
457
  const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
440
458
 
441
459
  switch (entry.level) {
442
- case LogLevel.DEBUG:
460
+ case LoggerLevel.DEBUG:
443
461
  console.debug(`${prefix} ${entry.message}`, entry.error || '');
444
462
  break;
445
- case LogLevel.INFO:
463
+ case LoggerLevel.INFO:
446
464
  console.info(`${prefix} ${entry.message}`, entry.error || '');
447
465
  break;
448
- case LogLevel.WARN:
466
+ case LoggerLevel.WARN:
449
467
  console.warn(`${prefix} ${entry.message}`, entry.error || '');
450
468
  break;
451
- case LogLevel.ERROR:
469
+ case LoggerLevel.ERROR:
452
470
  console.error(`${prefix} ${entry.message}`, entry.error || '');
453
471
  break;
454
472
  }
@@ -458,15 +476,15 @@ export class Logger {
458
476
  * 获取日志级别值
459
477
  * @param level 日志级别
460
478
  */
461
- private getLevelValue(level: LogLevel): number {
479
+ private getLevelValue(level: LoggerLevel): number {
462
480
  switch (level) {
463
- case LogLevel.DEBUG:
481
+ case LoggerLevel.DEBUG:
464
482
  return 0;
465
- case LogLevel.INFO:
483
+ case LoggerLevel.INFO:
466
484
  return 1;
467
- case LogLevel.WARN:
485
+ case LoggerLevel.WARN:
468
486
  return 2;
469
- case LogLevel.ERROR:
487
+ case LoggerLevel.ERROR:
470
488
  return 3;
471
489
  default:
472
490
  return 1; // 默认INFO级别
@@ -477,23 +495,23 @@ export class Logger {
477
495
  * 设置日志级别
478
496
  * @param level 日志级别
479
497
  */
480
- public setLevel(level: LogLevel | string): void {
498
+ public setLevel(level: LoggerLevel | string): void {
481
499
  if (typeof level === 'string') {
482
500
  switch (level) {
483
501
  case 'debug':
484
- this.logLevel = LogLevel.DEBUG;
502
+ this.logLevel = LoggerLevel.DEBUG;
485
503
  break;
486
504
  case 'info':
487
- this.logLevel = LogLevel.INFO;
505
+ this.logLevel = LoggerLevel.INFO;
488
506
  break;
489
507
  case 'warn':
490
- this.logLevel = LogLevel.WARN;
508
+ this.logLevel = LoggerLevel.WARN;
491
509
  break;
492
510
  case 'error':
493
- this.logLevel = LogLevel.ERROR;
511
+ this.logLevel = LoggerLevel.ERROR;
494
512
  break;
495
513
  default:
496
- this.logLevel = LogLevel.INFO;
514
+ this.logLevel = LoggerLevel.INFO;
497
515
  }
498
516
  } else {
499
517
  this.logLevel = level;
@@ -506,7 +524,7 @@ export class Logger {
506
524
  * 获取当前日志级别
507
525
  * @returns 当前日志级别
508
526
  */
509
- public getLevel(): LogLevel {
527
+ public getLevel(): LoggerLevel {
510
528
  return this.logLevel;
511
529
  }
512
530
  }
@@ -118,6 +118,8 @@ export class ResourceManager extends EventEmitter {
118
118
  /** 加载中的资源请求 */
119
119
  private pendingRequests: Map<string, Promise<any>> = new Map();
120
120
  private initialized: boolean = false;
121
+ /** 是否在浏览器环境 */
122
+ private readonly isBrowser: boolean;
121
123
 
122
124
  /**
123
125
  * 私有构造函数
@@ -126,16 +128,19 @@ export class ResourceManager extends EventEmitter {
126
128
  super();
127
129
  this.config = ConfigManager.getInstance();
128
130
  this.logger = Logger.getInstance();
131
+ this.isBrowser = typeof window !== 'undefined' && typeof window.addEventListener === 'function';
129
132
 
130
133
  // 初始化资源清理定时器
131
134
  this.setupCleanupTimer();
132
135
 
133
136
  // 页面卸载时尝试释放资源
134
- window.addEventListener('beforeunload', () => {
135
- if (this.config.get('autoReleaseResources', true)) {
136
- this.releaseAll();
137
- }
138
- });
137
+ if (this.isBrowser) {
138
+ window.addEventListener('beforeunload', () => {
139
+ if (this.config.get('autoReleaseResources', true)) {
140
+ this.releaseAll();
141
+ }
142
+ });
143
+ }
139
144
  }
140
145
 
141
146
  /**
@@ -147,6 +152,13 @@ export class ResourceManager extends EventEmitter {
147
152
  }
148
153
  return ResourceManager.instance;
149
154
  }
155
+
156
+ /**
157
+ * 重置单例实例(主要用于测试)
158
+ */
159
+ public static resetInstance(): void {
160
+ ResourceManager.instance = undefined as any;
161
+ }
150
162
 
151
163
  /**
152
164
  * 设置基础路径
@@ -402,10 +414,10 @@ export class ResourceManager extends EventEmitter {
402
414
  const promises = resources.map(async ({ id, url, options }) => {
403
415
  const result = await this.load(id, url, options);
404
416
 
405
- if (result.isSuccess() && result.data !== undefined) {
406
- results[id] = result.data;
407
- } else if (result.isFailure() && result.error) {
408
- errors.push({ id, error: result.error });
417
+ if (result.isSuccess() && result.getData() !== undefined) {
418
+ results[id] = result.getData();
419
+ } else if (result.isFailure() && result.getError()) {
420
+ errors.push({ id, error: result.getError()! });
409
421
  }
410
422
  });
411
423
 
@@ -668,6 +680,8 @@ export class ResourceManager extends EventEmitter {
668
680
  * 设置资源清理定时器
669
681
  */
670
682
  private setupCleanupTimer(): void {
683
+ if (!this.isBrowser) return;
684
+
671
685
  const interval = 60000; // 每分钟检查一次
672
686
 
673
687
  this.cleanupTimerId = window.setInterval(() => {
@@ -72,21 +72,21 @@ export class Result<T = any> {
72
72
  /**
73
73
  * 获取结果数据
74
74
  */
75
- get data(): T | undefined {
75
+ getData(): T | undefined {
76
76
  return this._data;
77
77
  }
78
78
 
79
79
  /**
80
80
  * 获取错误对象
81
81
  */
82
- get error(): Error | undefined {
82
+ getError(): Error | undefined {
83
83
  return this._error;
84
84
  }
85
85
 
86
86
  /**
87
87
  * 获取元数据
88
88
  */
89
- get meta(): Record<string, any> | undefined {
89
+ getMeta(): Record<string, any> | undefined {
90
90
  return this._meta;
91
91
  }
92
92
 
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ import { IDCardModule } from './modules/id-card';
10
10
  import { QRCodeModule } from './modules/qrcode';
11
11
  import { FaceModule } from './modules/face';
12
12
  import { VERSION, BUILD_DATE } from './version';
13
- import { Logger, LogLevel } from './core/logger';
13
+ import { Logger, LoggerLevel } from './core/logger';
14
14
  import { IDCardModuleOptions } from './modules/id-card/types';
15
15
  import { QRCodeModuleOptions } from './modules/qrcode/types';
16
16
  import { FaceModuleOptions } from './modules/face/types';
@@ -20,7 +20,7 @@ import { FaceModuleOptions } from './modules/face/types';
20
20
  */
21
21
  export interface IDScannerOptions {
22
22
  /** 日志级别 */
23
- logLevel?: LogLevel;
23
+ logLevel?: LoggerLevel;
24
24
  /** 是否启用身份证识别模块 */
25
25
  enableIDCard?: boolean;
26
26
  /** 是否启用二维码识别模块 */
@@ -66,15 +66,15 @@ export class IDScanner {
66
66
  this.moduleManager = ModuleManager.getInstance();
67
67
 
68
68
  // 注册模块
69
- if (options.enableIDCard !== false) {
69
+ if (options.enableIDCard === true) {
70
70
  this.moduleManager.register(new IDCardModule(options.idCard));
71
71
  }
72
72
 
73
- if (options.enableQRCode !== false) {
73
+ if (options.enableQRCode === true) {
74
74
  this.moduleManager.register(new QRCodeModule(options.qrCode));
75
75
  }
76
76
 
77
- if (options.enableFace !== false) {
77
+ if (options.enableFace === true) {
78
78
  this.moduleManager.register(new FaceModule(options.face));
79
79
  }
80
80
  }
@@ -898,13 +898,14 @@ export class FaceDetector extends BaseScannerModule {
898
898
  if (typeof source === 'string' || source instanceof HTMLImageElement) {
899
899
  // 处理图片源
900
900
  const result = await this.processImage(source, { withEmbedding: true });
901
- if (!result.isSuccess() || !result.data || result.data.length === 0) {
901
+ const resultData = result.getData();
902
+ if (!result.isSuccess() || !resultData || resultData.length === 0) {
902
903
  throw new FaceComparisonError('无法从源图像检测人脸');
903
904
  }
904
- if (!result.data[0].embedding) {
905
+ if (!resultData[0].embedding) {
905
906
  throw new FaceComparisonError('源图像未提取特征向量');
906
907
  }
907
- sourceEmbedding = result.data[0].embedding.vector;
908
+ sourceEmbedding = resultData[0].embedding.vector;
908
909
  } else {
909
910
  // 使用现有检测结果
910
911
  if (!source.embedding || !source.embedding.vector) {
@@ -919,13 +920,14 @@ export class FaceDetector extends BaseScannerModule {
919
920
  if (typeof target === 'string' || target instanceof HTMLImageElement) {
920
921
  // 处理图片源
921
922
  const result = await this.processImage(target, { withEmbedding: true });
922
- if (!result.isSuccess() || !result.data || result.data.length === 0) {
923
+ const resultData = result.getData();
924
+ if (!result.isSuccess() || !resultData || resultData.length === 0) {
923
925
  throw new FaceComparisonError('无法从目标图像检测人脸');
924
926
  }
925
- if (!result.data[0].embedding) {
927
+ if (!resultData[0].embedding) {
926
928
  throw new FaceComparisonError('目标图像未提取特征向量');
927
929
  }
928
- targetEmbedding = result.data[0].embedding.vector;
930
+ targetEmbedding = resultData[0].embedding.vector;
929
931
  } else {
930
932
  // 使用现有检测结果
931
933
  if (!target.embedding || !target.embedding.vector) {
@@ -227,13 +227,14 @@ export class LivenessDetector extends BaseScannerModule {
227
227
  withLandmarks: true,
228
228
  withAttributes: true
229
229
  });
230
+ const faceData = faceResult.getData();
230
231
 
231
- if (!faceResult.isSuccess() || !faceResult.data || faceResult.data.length === 0) {
232
+ if (!faceResult.isSuccess() || !faceData || faceData.length === 0) {
232
233
  throw new LivenessDetectionError('未在图像中检测到人脸');
233
234
  }
234
235
 
235
236
  // 获取检测到的第一个人脸
236
- const face = faceResult.data[0];
237
+ const face = faceData[0];
237
238
 
238
239
  // 执行被动式活体检测
239
240
  const livenessResult = this.performPassiveLivenessDetection(face);
@@ -129,21 +129,22 @@ export class IDCardModule extends BaseModule {
129
129
  try {
130
130
  // 检测身份证
131
131
  const detectionResult = await this.detector.processImage(image);
132
+ const detectionData = detectionResult.getData();
132
133
 
133
- if (!detectionResult.isSuccess() || !detectionResult.data) {
134
+ if (!detectionResult.isSuccess() || !detectionData) {
134
135
  throw new Error('未检测到身份证');
135
136
  }
136
137
 
137
138
  // 创建结果对象
138
139
  const idCardInfo: IDCardInfo = {
139
- type: detectionResult.data.type || IDCardType.FRONT,
140
- confidence: detectionResult.data.confidence
140
+ type: detectionData.type || IDCardType.FRONT,
141
+ confidence: detectionData.confidence
141
142
  };
142
143
 
143
144
  // 如果启用OCR且OCR处理器已初始化
144
145
  if (this.options.detector?.enableOCR && this.ocrProcessor) {
145
146
  // 裁剪并处理图像
146
- const processedImage = detectionResult.data.image || this.convertToImageData(image);
147
+ const processedImage = detectionData.image || this.convertToImageData(image);
147
148
 
148
149
  // 识别文本信息
149
150
  const ocrResult = await this.ocrProcessor.processIDCard(processedImage);
@@ -364,15 +365,16 @@ export class IDCardModule extends BaseModule {
364
365
  // 调用检测器处理图像
365
366
  const result = await this.detector.processImage(image);
366
367
 
367
- if (!result.isSuccess() || !result.data) {
368
+ if (!result.isSuccess() || !result.getData()) {
368
369
  return { success: false, confidence: 0 };
369
370
  }
370
371
 
372
+ const data = result.getData()!;
371
373
  return {
372
374
  success: true,
373
- type: result.data.type,
374
- confidence: result.data.confidence || 0,
375
- croppedImage: result.data.image
375
+ type: data.type,
376
+ confidence: data.confidence || 0,
377
+ croppedImage: data.image
376
378
  };
377
379
  } catch (error) {
378
380
  this.logger.error(this.name, '身份证检测失败', error instanceof Error ? error : new Error(String(error)));
@@ -174,17 +174,20 @@ export class ImageProcessor {
174
174
  }
175
175
  }
176
176
 
177
- // 处理边缘像素
178
- for (let y = 0; y < height; y++) {
179
- for (let x = 0; x < width; x++) {
180
- if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
181
- const pos = (y * width + x) * 4
182
- outputData[pos] = data[pos]
183
- outputData[pos + 1] = data[pos + 1]
184
- outputData[pos + 2] = data[pos + 2]
185
- outputData[pos + 3] = data[pos + 3]
186
- }
187
- }
177
+ // 处理边缘像素(仅遍历四条边,而非全图 O(width×height) → O(width+height))
178
+ // 上边 + 下边
179
+ for (let x = 0; x < width; x++) {
180
+ const topPos = x * 4
181
+ const bottomPos = ((height - 1) * width + x) * 4
182
+ outputData[topPos] = data[topPos]; outputData[topPos + 1] = data[topPos + 1]; outputData[topPos + 2] = data[topPos + 2]; outputData[topPos + 3] = data[topPos + 3]
183
+ outputData[bottomPos] = data[bottomPos]; outputData[bottomPos + 1] = data[bottomPos + 1]; outputData[bottomPos + 2] = data[bottomPos + 2]; outputData[bottomPos + 3] = data[bottomPos + 3]
184
+ }
185
+ // 左边 + 右边(排除四角,它们已在上下一行处理)
186
+ for (let y = 1; y < height - 1; y++) {
187
+ const leftPos = y * width * 4
188
+ const rightPos = (y * width + width - 1) * 4
189
+ outputData[leftPos] = data[leftPos]; outputData[leftPos + 1] = data[leftPos + 1]; outputData[leftPos + 2] = data[leftPos + 2]; outputData[leftPos + 3] = data[leftPos + 3]
190
+ outputData[rightPos] = data[rightPos]; outputData[rightPos + 1] = data[rightPos + 1]; outputData[rightPos + 2] = data[rightPos + 2]; outputData[rightPos + 3] = data[rightPos + 3]
188
191
  }
189
192
 
190
193
  // 创建新的ImageData对象
@@ -199,19 +202,12 @@ export class ImageProcessor {
199
202
  * @returns 处理后的图像数据
200
203
  */
201
204
  static threshold(imageData: ImageData, threshold: number = 128): ImageData {
202
- // 先转换为灰度图
203
- const grayscaleImage = this.toGrayscale(
204
- new ImageData(
205
- new Uint8ClampedArray(imageData.data),
206
- imageData.width,
207
- imageData.height
208
- )
209
- )
205
+ // 先转换为灰度图(toGrayscale 内部已创建新 ImageData,无需外部拷贝)
206
+ const grayscaleImage = this.toGrayscale(imageData)
210
207
 
211
208
  const data = grayscaleImage.data
212
- const length = data.length
213
209
 
214
- for (let i = 0; i < length; i += 4) {
210
+ for (let i = 0; i < data.length; i += 4) {
215
211
  // 二值化处理
216
212
  const value = data[i] < threshold ? 0 : 255
217
213
  data[i] = data[i + 1] = data[i + 2] = value
@@ -227,14 +223,8 @@ export class ImageProcessor {
227
223
  * @returns 二值化后的图像数据
228
224
  */
229
225
  static toBinaryImage(imageData: ImageData): ImageData {
230
- // 先转换为灰度图
231
- const grayscaleImage = this.toGrayscale(
232
- new ImageData(
233
- new Uint8ClampedArray(imageData.data),
234
- imageData.width,
235
- imageData.height
236
- )
237
- )
226
+ // 先转换为灰度图(toGrayscale 内部已创建新 ImageData,无需外部拷贝)
227
+ const grayscaleImage = this.toGrayscale(imageData)
238
228
 
239
229
  // 使用OTSU算法自动确定阈值
240
230
  const threshold = this.getOtsuThreshold(grayscaleImage)
@@ -250,9 +240,10 @@ export class ImageProcessor {
250
240
  */
251
241
  private static getOtsuThreshold(imageData: ImageData): number {
252
242
  const data = imageData.data
253
- const histogram = new Array(256).fill(0)
243
+ // 使用 Uint8Array 替代 Array<number>,避免 boxing 开销,提升直方图统计性能
244
+ const histogram = new Uint32Array(256)
254
245
 
255
- // 统计灰度直方图
246
+ // 统计灰度直方图(每4字节取R通道,即灰度值)
256
247
  for (let i = 0; i < data.length; i += 4) {
257
248
  histogram[data[i]]++
258
249
  }