@trustchex/react-native-sdk 1.362.6 → 1.381.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.
Files changed (58) hide show
  1. package/TrustchexSDK.podspec +3 -3
  2. package/android/build.gradle +3 -3
  3. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +64 -19
  4. package/android/src/main/java/com/trustchex/reactnativesdk/opencv/OpenCVModule.kt +636 -301
  5. package/ios/Camera/TrustchexCameraView.swift +166 -119
  6. package/ios/OpenCV/OpenCVHelper.h +0 -7
  7. package/ios/OpenCV/OpenCVHelper.mm +0 -60
  8. package/ios/OpenCV/OpenCVModule.h +0 -4
  9. package/ios/OpenCV/OpenCVModule.mm +440 -358
  10. package/lib/module/Shared/Components/DebugOverlay.js +541 -0
  11. package/lib/module/Shared/Components/FaceCamera.js +1 -0
  12. package/lib/module/Shared/Components/IdentityDocumentCamera.constants.js +44 -0
  13. package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +270 -0
  14. package/lib/module/Shared/Components/IdentityDocumentCamera.js +708 -1593
  15. package/lib/module/Shared/Components/IdentityDocumentCamera.types.js +3 -0
  16. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +273 -0
  17. package/lib/module/Shared/Components/QrCodeScannerCamera.js +1 -8
  18. package/lib/module/Shared/Libs/mrz.utils.js +202 -9
  19. package/lib/module/Translation/Resources/en.js +0 -4
  20. package/lib/module/Translation/Resources/tr.js +0 -4
  21. package/lib/module/version.js +1 -1
  22. package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts +30 -0
  23. package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -0
  24. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  25. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts +35 -0
  26. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts.map +1 -0
  27. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts +3 -56
  28. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  29. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +88 -0
  30. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -0
  31. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +116 -0
  32. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -0
  33. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +93 -0
  34. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -0
  35. package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
  36. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +1 -0
  37. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
  38. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +8 -0
  39. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  40. package/lib/typescript/src/Translation/Resources/en.d.ts +0 -4
  41. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  42. package/lib/typescript/src/Translation/Resources/tr.d.ts +0 -4
  43. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  44. package/lib/typescript/src/version.d.ts +1 -1
  45. package/package.json +1 -1
  46. package/src/Shared/Components/DebugOverlay.tsx +656 -0
  47. package/src/Shared/Components/FaceCamera.tsx +1 -0
  48. package/src/Shared/Components/IdentityDocumentCamera.constants.ts +44 -0
  49. package/src/Shared/Components/IdentityDocumentCamera.flows.ts +342 -0
  50. package/src/Shared/Components/IdentityDocumentCamera.tsx +1105 -2324
  51. package/src/Shared/Components/IdentityDocumentCamera.types.ts +136 -0
  52. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +364 -0
  53. package/src/Shared/Components/QrCodeScannerCamera.tsx +1 -9
  54. package/src/Shared/Components/TrustchexCamera.tsx +1 -0
  55. package/src/Shared/Libs/mrz.utils.ts +238 -26
  56. package/src/Translation/Resources/en.ts +0 -4
  57. package/src/Translation/Resources/tr.ts +0 -4
  58. package/src/version.ts +1 -1
@@ -30,6 +30,14 @@ class TrustchexCameraView: UIView {
30
30
  }
31
31
  }
32
32
  }
33
+ @objc var resolution: String = "fullhd" {
34
+ didSet {
35
+ if resolution != oldValue {
36
+ // "hd" or "fullhd" - reinitialize camera with new resolution
37
+ setupCamera()
38
+ }
39
+ }
40
+ }
33
41
  @objc var torchEnabled: Bool = false {
34
42
  didSet {
35
43
  if torchEnabled != oldValue {
@@ -114,27 +122,23 @@ class TrustchexCameraView: UIView {
114
122
  // Add video input
115
123
  let cameraPosition: AVCaptureDevice.Position = (_cameraType == "front") ? .front : .back
116
124
 
117
- // Set quality based on camera type
118
- // Front camera (liveness): Full HD (1920x1080) for high-quality face detection
119
- // Back camera (documents): Full HD (1920x1080) for sharp document capture
120
- if cameraPosition == .front {
121
- // Front camera: Use Full HD for high-quality liveness detection
122
- if session.canSetSessionPreset(.hd1920x1080) {
123
- session.sessionPreset = .hd1920x1080
124
- } else if session.canSetSessionPreset(.hd1280x720) {
125
- session.sessionPreset = .hd1280x720
126
- } else {
127
- session.sessionPreset = .high
128
- }
125
+ // Set quality based on resolution setting
126
+ // "hd": 720x1280 (HD) - lower bandwidth, faster processing
127
+ // "fullhd": 1920x1080 (Full HD, default) - sharp text/document capture
128
+ let sessionPreset: AVCaptureSession.Preset
129
+ if resolution.lowercased() == "hd" {
130
+ sessionPreset = .hd1280x720
129
131
  } else {
130
- // Back camera: Use Full HD for document scanning
131
- if session.canSetSessionPreset(.hd1920x1080) {
132
- session.sessionPreset = .hd1920x1080
133
- } else if session.canSetSessionPreset(.hd1280x720) {
134
- session.sessionPreset = .hd1280x720
135
- } else {
136
- session.sessionPreset = .high
137
- }
132
+ sessionPreset = .hd1920x1080 // Full HD (default)
133
+ }
134
+
135
+ if session.canSetSessionPreset(sessionPreset) {
136
+ session.sessionPreset = sessionPreset
137
+ } else if sessionPreset == .hd1920x1080 && session.canSetSessionPreset(.hd1280x720) {
138
+ // Fallback from Full HD to HD
139
+ session.sessionPreset = .hd1280x720
140
+ } else if session.canSetSessionPreset(.high) {
141
+ session.sessionPreset = .high
138
142
  }
139
143
  let camera = selectBestCamera(for: cameraPosition)
140
144
  guard let camera = camera,
@@ -418,6 +422,12 @@ class TrustchexCameraView: UIView {
418
422
  targetFps = fps
419
423
  }
420
424
 
425
+ @objc(changeResolution:)
426
+ func changeResolution(_ res: String) {
427
+ // "hd" (720x1280) or "fullhd" (1920x1080, default)
428
+ resolution = res.lowercased() == "hd" ? "hd" : "fullhd"
429
+ }
430
+
421
431
  @objc func setFocusPoint(_ x: NSNumber, _ y: NSNumber) {
422
432
  sessionQueue.async { [weak self] in
423
433
  guard let camera = self?.currentCamera else { return }
@@ -695,6 +705,12 @@ extension TrustchexCameraView: AVCaptureVideoDataOutputSampleBufferDelegate {
695
705
  }
696
706
  lastFrameTime = currentTime
697
707
 
708
+ // ML Kit Performance Optimization Tips Applied (iOS):
709
+ // 1. alwaysDiscardsLateVideoFrames = true throttles detector calls
710
+ // 2. Drop frames if detector is still busy (prevents queue buildup)
711
+ // 3. Process at 1080x1920 (Full HD) for optimal real-time detection
712
+ // 4. Use synchronous results(in:) API for video frames (Google recommended)
713
+ // 5. Dispatch to background queue to unblock videoQueue immediately
698
714
  // Mark as processing on videoQueue
699
715
  isProcessing = true
700
716
 
@@ -733,16 +749,30 @@ extension TrustchexCameraView: AVCaptureVideoDataOutputSampleBufferDelegate {
733
749
  let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
734
750
  let orientedImage = isBufferLandscape ? ciImage.oriented(.right) : ciImage
735
751
 
736
- // Create VisionImage from the oriented CIImage for better text recognition
752
+ // Compute brightness early for OCR-B enhancement decision
753
+ let brightness = computeBrightness(from: pixelBuffer, width: portraitWidth, height: portraitHeight)
754
+
755
+ // For text recognition, use original image directly
756
+ // ML Kit works best with native camera frames
757
+ let textEnhancedImage = orientedImage
758
+
759
+ // Create VisionImage from the image (enhanced or original) for better text recognition
737
760
  // This ensures MLKit processes the image in the correct orientation
738
- guard let cgImage = self.ciContext.createCGImage(orientedImage, from: orientedImage.extent) else {
761
+ guard let cgImage = self.ciContext.createCGImage(textEnhancedImage, from: textEnhancedImage.extent) else {
739
762
  resetProcessingState()
740
763
  return
741
764
  }
742
765
  let visionImage = VisionImage(image: UIImage(cgImage: cgImage))
743
766
  visionImage.orientation = .up // Already oriented correctly
744
767
 
745
- // Use original image for text recognition
768
+ // Use image for text recognition
769
+ // ML Kit text recognition best practices (iOS):
770
+ // - Requires minimum 16x16 pixels per character (ideal 16-24px per character)
771
+ // - Input image: 1080x1920 (portrait Full HD) provides excellent accuracy at real-time speed
772
+ // - Each character at ~30px = 36 characters per line @ 1080px width
773
+ // - Use synchronous results(in:) API from captureOutput(_:didOutput:from:)
774
+ // - Set AVCaptureVideoDataOutput.alwaysDiscardsLateVideoFrames = true (throttle)
775
+ // - Get results then render overlay in single step for optimal performance
746
776
  let textVisionImage = visionImage
747
777
 
748
778
  // Generate JPEG base64 only when JS side explicitly needs the image
@@ -755,125 +785,114 @@ extension TrustchexCameraView: AVCaptureVideoDataOutputSampleBufferDelegate {
755
785
  }
756
786
  }
757
787
 
758
- // Synchronous processing using a semaphore to keep the buffer locked
759
- // This blocks processingQueue, which is fine (we are off videoQueue)
760
- let semaphore = DispatchSemaphore(value: 0)
788
+ // ML Kit Performance Best Practice (iOS):
789
+ // Use synchronous results(in:) API for video processing instead of async process()
790
+ // This is Google's recommended approach for real-time video frame processing
791
+ // Source: https://developers.google.com/ml-kit/vision/barcode-scanning/ios#performance-tips
761
792
  var facesArray: [[String: Any]] = []
762
793
  var textBlocksArray: [[String: Any]] = []
763
794
  var barcodesArray: [[String: Any]] = []
764
795
 
796
+ // Face detection using synchronous API
765
797
  if enableFaceDetection {
766
- faceDetector.process(visionImage) { faces, error in
767
- if let faces = faces {
768
- for face in faces {
769
- var faceMap: [String: Any] = [:]
770
- let isFront = self._cameraType == "front"
771
- let faceX = isFront ? CGFloat(portraitWidth) - face.frame.origin.x - face.frame.width : face.frame.origin.x
772
-
773
- faceMap["bounds"] = [
774
- "x": Int(faceX),
775
- "y": Int(face.frame.origin.y),
776
- "width": Int(face.frame.width),
777
- "height": Int(face.frame.height)
778
- ]
779
- faceMap["yawAngle"] = face.hasHeadEulerAngleY ? Double(face.headEulerAngleY) : 0.0
780
- faceMap["pitchAngle"] = face.hasHeadEulerAngleX ? Double(face.headEulerAngleX) : 0.0
781
- faceMap["rollAngle"] = face.hasHeadEulerAngleZ ? Double(face.headEulerAngleZ) : 0.0
782
- if face.hasTrackingID {
783
- faceMap["trackingId"] = face.trackingID
784
- }
785
- // Only include probability fields when available (matching Android behavior)
786
- if face.hasSmilingProbability {
787
- faceMap["smilingProbability"] = Double(face.smilingProbability)
788
- }
789
- if face.hasLeftEyeOpenProbability {
790
- faceMap["leftEyeOpenProbability"] = Double(face.leftEyeOpenProbability)
791
- }
792
- if face.hasRightEyeOpenProbability {
793
- faceMap["rightEyeOpenProbability"] = Double(face.rightEyeOpenProbability)
794
- }
795
- facesArray.append(faceMap)
798
+ do {
799
+ let faces = try faceDetector.results(in: visionImage)
800
+ for face in faces {
801
+ var faceMap: [String: Any] = [:]
802
+ let isFront = self._cameraType == "front"
803
+ let faceX = isFront ? CGFloat(portraitWidth) - face.frame.origin.x - face.frame.width : face.frame.origin.x
804
+
805
+ faceMap["bounds"] = [
806
+ "x": Int(faceX),
807
+ "y": Int(face.frame.origin.y),
808
+ "width": Int(face.frame.width),
809
+ "height": Int(face.frame.height)
810
+ ]
811
+ faceMap["yawAngle"] = face.hasHeadEulerAngleY ? Double(face.headEulerAngleY) : 0.0
812
+ faceMap["pitchAngle"] = face.hasHeadEulerAngleX ? Double(face.headEulerAngleX) : 0.0
813
+ faceMap["rollAngle"] = face.hasHeadEulerAngleZ ? Double(face.headEulerAngleZ) : 0.0
814
+ if face.hasTrackingID {
815
+ faceMap["trackingId"] = face.trackingID
816
+ }
817
+ // Only include probability fields when available (matching Android behavior)
818
+ if face.hasSmilingProbability {
819
+ faceMap["smilingProbability"] = Double(face.smilingProbability)
820
+ }
821
+ if face.hasLeftEyeOpenProbability {
822
+ faceMap["leftEyeOpenProbability"] = Double(face.leftEyeOpenProbability)
796
823
  }
824
+ if face.hasRightEyeOpenProbability {
825
+ faceMap["rightEyeOpenProbability"] = Double(face.rightEyeOpenProbability)
826
+ }
827
+ facesArray.append(faceMap)
797
828
  }
798
- semaphore.signal()
829
+ } catch {
830
+ // Face detection failed - continue with empty array
799
831
  }
800
- } else {
801
- semaphore.signal()
802
832
  }
803
833
 
804
- // Text recognition
805
- let textSemaphore = DispatchSemaphore(value: 0)
834
+ // Text recognition using synchronous API
835
+ // On iOS, this completes quickly at 1080x1920 resolution (~100-300ms per frame)
806
836
  var resultText = ""
807
837
  if enableTextRecognition {
808
- textRecognizer.process(textVisionImage) { text, error in
809
- if let text = text {
810
- resultText = text.text
811
- for block in text.blocks {
812
- var blockMap: [String: Any] = ["text": block.text]
813
- let bb = block.frame
814
-
815
- // When buffer is landscape (1920x1080) but we set orientation to .right,
816
- // ML Kit might still return coordinates in landscape space.
817
- // We need to rotate them to portrait space (1080x1920) to match face detection.
818
- let blockX: Int
819
- let blockY: Int
820
- let blockWidth: Int
821
- let blockHeight: Int
822
-
823
- if isBufferLandscape {
824
- // Rotate from landscape (1920x1080) to portrait (1080x1920)
825
- // When rotating 90° clockwise (.right):
826
- // new_x = old_y
827
- // new_y = landscape_width - old_x - width
828
- // new_width = old_height
829
- // new_height = old_width
830
- blockX = Int(bb.origin.y)
831
- blockY = pixelWidth - Int(bb.origin.x) - Int(bb.width)
832
- blockWidth = Int(bb.height)
833
- blockHeight = Int(bb.width)
834
- } else {
835
- // Already portrait, use directly
836
- blockX = Int(bb.origin.x)
837
- blockY = Int(bb.origin.y)
838
- blockWidth = Int(bb.width)
839
- blockHeight = Int(bb.height)
840
- }
841
-
842
- blockMap["blockFrame"] = [
843
- "x": blockX,
844
- "y": blockY,
845
- "width": blockWidth,
846
- "height": blockHeight,
847
- "boundingCenterX": blockX + blockWidth / 2,
848
- "boundingCenterY": blockY + blockHeight / 2
849
- ]
850
- textBlocksArray.append(blockMap)
838
+ do {
839
+ let text = try textRecognizer.results(in: textVisionImage)
840
+ resultText = text.text
841
+ for block in text.blocks {
842
+ var blockMap: [String: Any] = ["text": block.text]
843
+ let bb = block.frame
844
+
845
+ // When buffer is landscape (1920x1080) but we set orientation to .right,
846
+ // ML Kit might still return coordinates in landscape space.
847
+ // We need to rotate them to portrait space (1080x1920) to match face detection.
848
+ let blockX: Int
849
+ let blockY: Int
850
+ let blockWidth: Int
851
+ let blockHeight: Int
852
+
853
+ if isBufferLandscape {
854
+ // Rotate from landscape (1920x1080) to portrait (1080x1920)
855
+ // When rotating 90° clockwise (.right):
856
+ // new_x = old_y
857
+ // new_y = landscape_width - old_x - width
858
+ // new_width = old_height
859
+ // new_height = old_width
860
+ blockX = Int(bb.origin.y)
861
+ blockY = pixelWidth - Int(bb.origin.x) - Int(bb.width)
862
+ blockWidth = Int(bb.height)
863
+ blockHeight = Int(bb.width)
864
+ } else {
865
+ // Already portrait, use directly
866
+ blockX = Int(bb.origin.x)
867
+ blockY = Int(bb.origin.y)
868
+ blockWidth = Int(bb.width)
869
+ blockHeight = Int(bb.height)
851
870
  }
871
+
872
+ blockMap["blockFrame"] = [
873
+ "x": blockX,
874
+ "y": blockY,
875
+ "width": blockWidth,
876
+ "height": blockHeight,
877
+ "boundingCenterX": blockX + blockWidth / 2,
878
+ "boundingCenterY": blockY + blockHeight / 2
879
+ ]
880
+ textBlocksArray.append(blockMap)
852
881
  }
853
- textSemaphore.signal()
882
+ } catch {
883
+ // Text recognition failed - continue with empty result
854
884
  }
855
- } else {
856
- textSemaphore.signal()
857
885
  }
858
886
 
859
887
  // Barcode scanning - use native AVFoundation results (captured via metadata delegate)
860
888
  // This is much faster than MLKit barcode scanning
861
- let barcodeSemaphore = DispatchSemaphore(value: 0)
862
889
  if enableBarcodeScanning {
863
890
  // Use the barcodes detected by the native AVCaptureMetadataOutput
864
891
  barcodesArray = lastDetectedBarcodes
865
- barcodeSemaphore.signal()
866
- } else {
867
- barcodeSemaphore.signal()
868
892
  }
869
893
 
870
- // Wait for vision tasks (with timeout to prevent hang)
871
- _ = semaphore.wait(timeout: .now() + 2.0)
872
- _ = textSemaphore.wait(timeout: .now() + 2.0)
873
-
874
- // Only compute brightness if we haven't timed out or crashed
875
- // Brightness calculation restricted to scanning frame area (between 36% from top and 36% from bottom, 5% margins on sides)
876
- let brightness = computeBrightness(from: pixelBuffer, width: portraitWidth, height: portraitHeight)
894
+ // Brightness was already computed earlier for OCR-B enhancement
895
+ // No need to recompute here
877
896
 
878
897
  let currentTime = CACurrentMediaTime() * 1000 // Convert to milliseconds to match Android
879
898
 
@@ -962,6 +981,35 @@ extension TrustchexCameraView: AVCaptureVideoDataOutputSampleBufferDelegate {
962
981
 
963
982
  return Double(sum) / Double(sampleCount)
964
983
  }
984
+
985
+ /**
986
+ * Validates if the image dimensions are suitable for ML Kit text recognition (iOS).
987
+ *
988
+ * ML Kit text recognition requirements (iOS):
989
+ * - Minimum 16x16 pixels per character for acceptable accuracy
990
+ * - Ideal: 16-24 pixels per character for optimal performance
991
+ * - At 1080x1920 (portrait Full HD), assuming ~30px average character width:
992
+ * - Can fit ~36 characters per line (1080/30)
993
+ * - Can fit ~64 lines (1920/30)
994
+ * - Performance: ~100-300ms per frame at 1080x1920 with asynchronous API
995
+ *
996
+ * @return quality score (0.0-1.0) where 1.0 is optimal
997
+ */
998
+ private func calculateTextRecognitionQuality(width: Int, height: Int) -> Double {
999
+ // iOS uses 1080x1920 as standard, so we prefer that
1000
+ let pixelScore: Double
1001
+ switch (width, height) {
1002
+ case (1080..., 1920...):
1003
+ pixelScore = 1.0 // Optimal (Full HD)
1004
+ case (720..., 1280...):
1005
+ pixelScore = 0.85 // Good (HD)
1006
+ case (640..., 960...):
1007
+ pixelScore = 0.65 // Acceptable
1008
+ default:
1009
+ pixelScore = 0.4 // Poor
1010
+ }
1011
+ return pixelScore
1012
+ }
965
1013
  }
966
1014
 
967
1015
  // MARK: - AVCaptureFileOutputRecordingDelegate
@@ -1131,7 +1179,6 @@ extension TrustchexCameraView: AVCaptureMetadataOutputObjectsDelegate {
1131
1179
  lastDetectedBarcodes = barcodes
1132
1180
  }
1133
1181
  }
1134
-
1135
1182
  // MARK: - Helper Extensions
1136
1183
  extension Comparable {
1137
1184
  func clamped(to limits: ClosedRange<Self>) -> Self {
@@ -5,13 +5,6 @@ NS_ASSUME_NONNULL_BEGIN
5
5
 
6
6
  @interface OpenCVHelper : NSObject
7
7
 
8
- /// Preprocesses an image for better OCR text recognition
9
- /// Applies bilateral filtering, CLAHE, and sharpening to enhance text clarity
10
- /// @param image The input UIImage to preprocess
11
- /// @param applyThresholding Whether to apply adaptive thresholding (for binary output)
12
- /// @return A preprocessed UIImage optimized for text recognition, or nil if preprocessing fails
13
- + (UIImage * _Nullable)preprocessImageForOCR:(UIImage *)image applyThresholding:(BOOL)applyThresholding;
14
-
15
8
  @end
16
9
 
17
10
  NS_ASSUME_NONNULL_END
@@ -65,64 +65,4 @@
65
65
  return image;
66
66
  }
67
67
 
68
- + (UIImage *)preprocessImageForOCR:(UIImage *)image applyThresholding:(BOOL)applyThresholding {
69
- @try {
70
- if (!image) return nil;
71
-
72
- cv::Mat mat = [self imageToMat:image];
73
- if (mat.empty()) return nil;
74
-
75
- // Step 1: Convert to grayscale
76
- cv::Mat gray;
77
- cv::cvtColor(mat, gray, cv::COLOR_RGB2GRAY);
78
- mat.release();
79
-
80
- // Step 2: Suppress background using blackhat morphology
81
- cv::Mat blackhat;
82
- cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(15, 5));
83
- cv::morphologyEx(gray, blackhat, cv::MORPH_BLACKHAT, kernel);
84
- gray.release();
85
-
86
- // Step 3: Advanced denoising - removes artifacts while keeping character details
87
- cv::Mat denoised;
88
- cv::fastNlMeansDenoising(blackhat, denoised, 6.0, 7, 21);
89
- blackhat.release();
90
-
91
- // Step 4: CLAHE for local contrast without over-amplifying noise
92
- cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(2.0, cv::Size(8, 8));
93
- cv::Mat enhanced;
94
- clahe->apply(denoised, enhanced);
95
- denoised.release();
96
-
97
- // Step 5: Unsharp masking for clearer edges without halos
98
- cv::Mat blurred;
99
- cv::GaussianBlur(enhanced, blurred, cv::Size(0, 0), 1.2);
100
- cv::Mat sharpened;
101
- cv::addWeighted(enhanced, 1.8, blurred, -0.8, 0, sharpened);
102
- blurred.release();
103
- enhanced.release();
104
-
105
- // Step 6: Normalize to full 0-255 range
106
- // Ensures maximum contrast for ML Kit
107
- cv::Mat result;
108
- cv::normalize(sharpened, result, 0, 255, cv::NORM_MINMAX);
109
- sharpened.release();
110
-
111
- if (applyThresholding) {
112
- cv::Mat thresholded;
113
- cv::adaptiveThreshold(result, thresholded, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, 31, 10);
114
- result.release();
115
- result = thresholded;
116
- }
117
-
118
- UIImage *resultImage = [self matToImage:result];
119
- result.release();
120
-
121
- return resultImage;
122
- } @catch (NSException *exception) {
123
- NSLog(@"OpenCV preprocessing error: %@", exception.reason);
124
- return nil;
125
- }
126
- }
127
-
128
68
  @end
@@ -3,8 +3,4 @@
3
3
 
4
4
  @interface OpenCVModule : NSObject <RCTBridgeModule>
5
5
 
6
- // Synchronous method for preprocessingimage for OCR
7
- // This is called directly from Swift camera code for better performance
8
- - (UIImage * _Nullable)preprocessImageForOCRSync:(UIImage * _Nonnull)image applyThresholding:(BOOL)applyThresholding;
9
-
10
6
  @end