diff --git a/LegacyComponents.xcodeproj/project.pbxproj b/LegacyComponents.xcodeproj/project.pbxproj index c7616a3487..365a259e90 100644 --- a/LegacyComponents.xcodeproj/project.pbxproj +++ b/LegacyComponents.xcodeproj/project.pbxproj @@ -7,8 +7,20 @@ objects = { /* Begin PBXBuildFile section */ + 09053D7920A5CCF10029652D /* genann.c in Sources */ = {isa = PBXBuildFile; fileRef = 09053D7120A5CCEF0029652D /* genann.c */; }; + 09053D7A20A5CCF10029652D /* fast-edge.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 09053D7220A5CCEF0029652D /* fast-edge.cpp */; }; + 09053D7B20A5CCF10029652D /* TGPassportOCR.h in Headers */ = {isa = PBXBuildFile; fileRef = 09053D7320A5CCF00029652D /* TGPassportOCR.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 09053D7C20A5CCF10029652D /* ocr.h in Headers */ = {isa = PBXBuildFile; fileRef = 09053D7420A5CCF00029652D /* ocr.h */; }; + 09053D7D20A5CCF10029652D /* TGPassportOCR.mm in Sources */ = {isa = PBXBuildFile; fileRef = 09053D7520A5CCF00029652D /* TGPassportOCR.mm */; }; + 09053D7E20A5CCF10029652D /* genann.h in Headers */ = {isa = PBXBuildFile; fileRef = 09053D7620A5CCF00029652D /* genann.h */; }; + 09053D7F20A5CCF10029652D /* fast-edge.h in Headers */ = {isa = PBXBuildFile; fileRef = 09053D7720A5CCF00029652D /* fast-edge.h */; }; + 09053D8020A5CCF10029652D /* ocr.mm in Sources */ = {isa = PBXBuildFile; fileRef = 09053D7820A5CCF00029652D /* ocr.mm */; }; 090671C41F67F71700CCF2F5 /* TGLocationOptionsView.h in Headers */ = {isa = PBXBuildFile; fileRef = 090671C21F67F71700CCF2F5 /* TGLocationOptionsView.h */; }; 090671C51F67F71700CCF2F5 /* TGLocationOptionsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 090671C31F67F71700CCF2F5 /* TGLocationOptionsView.m */; }; + 0916FEA720A1EA7B0084A755 /* TGPassportScanView.h in Headers */ = {isa = PBXBuildFile; fileRef = 0916FEA520A1EA7B0084A755 /* TGPassportScanView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0916FEA820A1EA7B0084A755 /* TGPassportScanView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0916FEA620A1EA7B0084A755 /* TGPassportScanView.m */; }; + 0916FEAB20A1EBFA0084A755 /* TGPassportMRZ.m in Sources */ = {isa = PBXBuildFile; fileRef = 0916FEA920A1EBF90084A755 /* TGPassportMRZ.m */; }; + 0916FEAC20A1EBFA0084A755 /* TGPassportMRZ.h in Headers */ = {isa = PBXBuildFile; fileRef = 0916FEAA20A1EBFA0084A755 /* TGPassportMRZ.h */; settings = {ATTRIBUTES = (Public, ); }; }; 09750F761F2FA816001B9886 /* SSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09750F741F2FA5E8001B9886 /* SSignalKit.framework */; }; 09750FB71F30DB0E001B9886 /* TGClipboardGalleryModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 09750FAF1F30DB0E001B9886 /* TGClipboardGalleryModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; 09750FB81F30DB0E001B9886 /* TGClipboardGalleryModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09750FB01F30DB0E001B9886 /* TGClipboardGalleryModel.m */; }; @@ -1179,8 +1191,20 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 09053D7120A5CCEF0029652D /* genann.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = genann.c; sourceTree = ""; }; + 09053D7220A5CCEF0029652D /* fast-edge.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "fast-edge.cpp"; sourceTree = ""; }; + 09053D7320A5CCF00029652D /* TGPassportOCR.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPassportOCR.h; sourceTree = ""; }; + 09053D7420A5CCF00029652D /* ocr.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ocr.h; sourceTree = ""; }; + 09053D7520A5CCF00029652D /* TGPassportOCR.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TGPassportOCR.mm; sourceTree = ""; }; + 09053D7620A5CCF00029652D /* genann.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = genann.h; sourceTree = ""; }; + 09053D7720A5CCF00029652D /* fast-edge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "fast-edge.h"; sourceTree = ""; }; + 09053D7820A5CCF00029652D /* ocr.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ocr.mm; sourceTree = ""; }; 090671C21F67F71700CCF2F5 /* TGLocationOptionsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLocationOptionsView.h; sourceTree = ""; }; 090671C31F67F71700CCF2F5 /* TGLocationOptionsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLocationOptionsView.m; sourceTree = ""; }; + 0916FEA520A1EA7B0084A755 /* TGPassportScanView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TGPassportScanView.h; sourceTree = ""; }; + 0916FEA620A1EA7B0084A755 /* TGPassportScanView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TGPassportScanView.m; sourceTree = ""; }; + 0916FEA920A1EBF90084A755 /* TGPassportMRZ.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPassportMRZ.m; sourceTree = ""; }; + 0916FEAA20A1EBFA0084A755 /* TGPassportMRZ.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPassportMRZ.h; sourceTree = ""; }; 09750F741F2FA5E8001B9886 /* SSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SSignalKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegraph-grvwvmixbmcefwboxkzfazvpcrxb/Build/Products/Release Hockeyapp-iphonesimulator/SSignalKit.framework"; sourceTree = ""; }; 09750FAF1F30DB0E001B9886 /* TGClipboardGalleryModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGClipboardGalleryModel.h; sourceTree = ""; }; 09750FB01F30DB0E001B9886 /* TGClipboardGalleryModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGClipboardGalleryModel.m; sourceTree = ""; }; @@ -2365,6 +2389,25 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 093134BC20A1EA3F003045EE /* Passport */ = { + isa = PBXGroup; + children = ( + 09053D7220A5CCEF0029652D /* fast-edge.cpp */, + 09053D7720A5CCF00029652D /* fast-edge.h */, + 09053D7120A5CCEF0029652D /* genann.c */, + 09053D7620A5CCF00029652D /* genann.h */, + 09053D7420A5CCF00029652D /* ocr.h */, + 09053D7820A5CCF00029652D /* ocr.mm */, + 09053D7320A5CCF00029652D /* TGPassportOCR.h */, + 09053D7520A5CCF00029652D /* TGPassportOCR.mm */, + 0916FEA520A1EA7B0084A755 /* TGPassportScanView.h */, + 0916FEA620A1EA7B0084A755 /* TGPassportScanView.m */, + 0916FEAA20A1EBFA0084A755 /* TGPassportMRZ.h */, + 0916FEA920A1EBF90084A755 /* TGPassportMRZ.m */, + ); + name = Passport; + sourceTree = ""; + }; 09750FAE1F30DAE1001B9886 /* Clipboard Menu */ = { isa = PBXGroup; children = ( @@ -2406,6 +2449,7 @@ D01777291F1F8F100044446D /* LegacyComponents */ = { isa = PBXGroup; children = ( + 093134BC20A1EA3F003045EE /* Passport */, D0EB409E1F2FC0AA00838FE6 /* Resources */, D017776F1F1F91B00044446D /* Utils */, D01779B01F2139720044446D /* POP */, @@ -3908,6 +3952,7 @@ D01778191F1F961D0044446D /* TGMessageEntityHashtag.h in Headers */, D07BCA341F2A9B0400ED97AA /* TGModernGalleryEditableItemView.h in Headers */, D0177A911F221BB10044446D /* TGModernGalleryTransitionView.h in Headers */, + 0916FEAC20A1EBFA0084A755 /* TGPassportMRZ.h in Headers */, D01778E51F20CAE60044446D /* TGNavigationController.h in Headers */, D01777721F1F92420044446D /* TGPhoneUtils.h in Headers */, D07BC7721F2A2B3700ED97AA /* TGPhotoEditorCurvesHistogramView.h in Headers */, @@ -3949,11 +3994,13 @@ D01779261F20FE480044446D /* TGObserverProxy.h in Headers */, D07BCADF1F2B4F5E00ED97AA /* TGLegacyCameraController.h in Headers */, D017782F1F1F961D0044446D /* TGReplyMarkupAttachment.h in Headers */, + 0916FEA720A1EA7B0084A755 /* TGPassportScanView.h in Headers */, D07BCB211F2B646A00ED97AA /* TGPasscodeBackground.h in Headers */, D01778371F1F961D0044446D /* TGAudioMediaAttachment.h in Headers */, D07BC6F51F2A19A700ED97AA /* TGCameraSegmentsView.h in Headers */, D04269051F586A070037ECE8 /* TGVideoMessageScrubber.h in Headers */, D07BC87F1F2A365000ED97AA /* TGProgressWindow.h in Headers */, + 09053D7F20A5CCF10029652D /* fast-edge.h in Headers */, D07BC8001F2A2C0B00ED97AA /* PGGrainTool.h in Headers */, D01779EA1F2139980044446D /* POPAnimationPrivate.h in Headers */, D017775C1F1F8FE60044446D /* PSKeyValueStore.h in Headers */, @@ -4011,6 +4058,7 @@ 090671C41F67F71700CCF2F5 /* TGLocationOptionsView.h in Headers */, D07BCAAE1F2B45DA00ED97AA /* TGFileUtils.h in Headers */, D0177B1E1F2641B10044446D /* PGCameraMovieWriter.h in Headers */, + 09053D7B20A5CCF10029652D /* TGPassportOCR.h in Headers */, D01779641F2103910044446D /* TGPaintUtils.h in Headers */, D07BCB741F2B6A5600ED97AA /* TGEmbedYoutubePlayerView.h in Headers */, D07BC8C51F2A37EC00ED97AA /* TGPhotoPaintEntityView.h in Headers */, @@ -4050,6 +4098,7 @@ D0177A071F2139980044446D /* POPSpringAnimation.h in Headers */, D07BC9951F2A481C00ED97AA /* TGStickerKeyboardTabSettingsCell.h in Headers */, D0177A341F21F1980044446D /* UIImage+TGMediaEditableItem.h in Headers */, + 09053D7E20A5CCF10029652D /* genann.h in Headers */, D07BC94E1F2A3EA900ED97AA /* TGHashtagPanelCell.h in Headers */, D07BCBAF1F2B6F6300ED97AA /* CBCoubAudioSource.h in Headers */, D07BC7841F2A2B3700ED97AA /* TGPhotoEditorSliderView.h in Headers */, @@ -4117,6 +4166,7 @@ D01779FE1F2139980044446D /* POPGeometry.h in Headers */, D07BCA981F2B443700ED97AA /* TGMediaAssetsPickerController.h in Headers */, D07BCBF51F2B72DC00ED97AA /* STKLocalFileDataSource.h in Headers */, + 09053D7C20A5CCF10029652D /* ocr.h in Headers */, D07BC9F11F2A9A2B00ED97AA /* TGMediaPickerCell.h in Headers */, D07BC8371F2A2D0C00ED97AA /* TGSecretTimerPickerItemView.h in Headers */, D07BC8D31F2A37EC00ED97AA /* TGPhotoPaintTextEntity.h in Headers */, @@ -4690,6 +4740,7 @@ D07BC9BB1F2A705D00ED97AA /* TGPhotoFilterCell.m in Sources */, D0177A571F21F7F40044446D /* TGDoubleTapGestureRecognizer.m in Sources */, D07BC7FF1F2A2C0B00ED97AA /* PGFadeTool.m in Sources */, + 0916FEAB20A1EBFA0084A755 /* TGPassportMRZ.m in Sources */, D01778C31F200AF70044446D /* TGAnimationBlockDelegate.m in Sources */, D07BC85F1F2A2DBD00ED97AA /* TGMenuSheetTitleItemView.m in Sources */, D07BC6EC1F2A19A700ED97AA /* TGCameraFlashActiveView.m in Sources */, @@ -4821,6 +4872,7 @@ D07BC85D1F2A2DBD00ED97AA /* TGMenuSheetItemView.m in Sources */, D07BC90E1F2A380D00ED97AA /* TGPainting.m in Sources */, D0177A441F21F62A0044446D /* TGMediaVideoConverter.m in Sources */, + 09053D8020A5CCF10029652D /* ocr.mm in Sources */, D01779A51F210A120044446D /* TGMediaAssetLegacyImageSignals.m in Sources */, D017777B1F1F927A0044446D /* NSObject+TGLock.m in Sources */, D01778281F1F961D0044446D /* TGMessageEntitiesAttachment.m in Sources */, @@ -4836,6 +4888,7 @@ D07BC7181F2A29B700ED97AA /* TGPhotoEditorController.m in Sources */, D07BC8251F2A2C0B00ED97AA /* PGSharpenTool.m in Sources */, D0177AF41F23DF6D0044446D /* TGImageManagerTask.m in Sources */, + 09053D7920A5CCF10029652D /* genann.c in Sources */, D017783F1F1F961D0044446D /* TGDocumentAttributeImageSize.m in Sources */, D01778E61F20CAE60044446D /* TGNavigationController.m in Sources */, D07BC8C61F2A37EC00ED97AA /* TGPhotoPaintEntityView.m in Sources */, @@ -4922,6 +4975,7 @@ D07BCB361F2B65F100ED97AA /* TGBuiltinWallpaperInfo.m in Sources */, D07BC90A1F2A380D00ED97AA /* TGPaintFaceDebugView.m in Sources */, D017785A1F1F961D0044446D /* TGImageMediaAttachment.m in Sources */, + 09053D7D20A5CCF10029652D /* TGPassportOCR.mm in Sources */, D017784C1F1F961D0044446D /* TGUnsupportedMediaAttachment.m in Sources */, D01779151F20F4500044446D /* TGStaticBackdropAreaData.m in Sources */, D07BC91C1F2A380D00ED97AA /* TGPaintRender.m in Sources */, @@ -5031,6 +5085,7 @@ D01778AA1F1FD0900044446D /* TGImageUtils.mm in Sources */, D07BCADA1F2B4F2800ED97AA /* TGOverlayFormsheetController.m in Sources */, D07BC8571F2A2DBD00ED97AA /* TGMenuSheetButtonItemView.m in Sources */, + 0916FEA820A1EA7B0084A755 /* TGPassportScanView.m in Sources */, D07BCA161F2A9A2B00ED97AA /* TGMediaPickerPhotoStripCell.m in Sources */, D07BCBAD1F2B6F6300ED97AA /* CBConstance.m in Sources */, D017780A1F1F961D0044446D /* TGGameMediaAttachment.m in Sources */, @@ -5077,6 +5132,7 @@ D07BCBD01F2B6F6300ED97AA /* NSDictionary+CBExtensions.m in Sources */, D07BC7731F2A2B3700ED97AA /* TGPhotoEditorCurvesHistogramView.m in Sources */, D07BCB3E1F2B65F100ED97AA /* TGWallpaperInfo.m in Sources */, + 09053D7A20A5CCF10029652D /* fast-edge.cpp in Sources */, D07BCA1C1F2A9A2B00ED97AA /* TGMediaPickerSelectionGestureRecognizer.m in Sources */, D07BC9141F2A380D00ED97AA /* TGPaintNeonBrush.m in Sources */, D01778161F1F961D0044446D /* TGMessageEntityCode.m in Sources */, diff --git a/LegacyComponents/LegacyComponents.h b/LegacyComponents/LegacyComponents.h index 5e6f85df1c..4b4d9175a4 100644 --- a/LegacyComponents/LegacyComponents.h +++ b/LegacyComponents/LegacyComponents.h @@ -278,6 +278,9 @@ FOUNDATION_EXPORT const unsigned char LegacyComponentsVersionString[]; #import #import #import +#import +#import +#import #import #import #import diff --git a/LegacyComponents/Resources/LegacyComponentsResources.bundle/ocr_nn.bin b/LegacyComponents/Resources/LegacyComponentsResources.bundle/ocr_nn.bin new file mode 100755 index 0000000000..ccab89e554 Binary files /dev/null and b/LegacyComponents/Resources/LegacyComponentsResources.bundle/ocr_nn.bin differ diff --git a/LegacyComponents/TGCameraController.m b/LegacyComponents/TGCameraController.m index b794c0abf6..dc2d586a82 100644 --- a/LegacyComponents/TGCameraController.m +++ b/LegacyComponents/TGCameraController.m @@ -30,6 +30,7 @@ #import #import +#import "TGMediaVideoConverter.h" #import #import #import @@ -2404,6 +2405,10 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus SSignal *thumbnailSignal = adjustments.trimStartValue > FLT_EPSILON ? trimmedVideoThumbnailSignal : videoThumbnailSignal; + TGMediaVideoConversionPreset preset = [TGMediaVideoConverter presetFromAdjustments:adjustments]; + CGSize dimensions = [TGMediaVideoConverter dimensionsFor:asset.originalSize adjustments:adjustments preset:preset]; + NSTimeInterval duration = adjustments.trimApplied ? (adjustments.trimEndValue - adjustments.trimStartValue) : video.videoDuration; + [signals addObject:[thumbnailSignal map:^id(UIImage *image) { NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; @@ -2411,6 +2416,8 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus dict[@"url"] = video.avAsset.URL; dict[@"previewImage"] = image; dict[@"adjustments"] = adjustments; + dict[@"dimensions"] = [NSValue valueWithCGSize:dimensions]; + dict[@"duration"] = @(duration); if (adjustments.paintingData.stickers.count > 0) dict[@"stickers"] = adjustments.paintingData.stickers; diff --git a/LegacyComponents/TGMediaAssetsController.m b/LegacyComponents/TGMediaAssetsController.m index bd4d18d640..d9648d8ad5 100644 --- a/LegacyComponents/TGMediaAssetsController.m +++ b/LegacyComponents/TGMediaAssetsController.m @@ -749,6 +749,9 @@ NSArray *entities = [editingContext entitiesForItem:asset]; id adjustments = [editingContext adjustmentsForItem:asset]; + CGSize dimensions = asset.originalSize; + NSTimeInterval duration = asset.videoDuration; + [signals addObject:[inlineThumbnailSignal(asset) map:^id(UIImage *image) { NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; @@ -757,6 +760,8 @@ dict[@"asset"] = asset; dict[@"previewImage"] = image; dict[@"fileName"] = asset.fileName; + dict[@"dimensions"] = [NSValue valueWithCGSize:dimensions]; + dict[@"duration"] = @(duration); if (adjustments.paintingData.stickers.count > 0) dict[@"stickers"] = adjustments.paintingData.stickers; @@ -799,6 +804,10 @@ SSignal *thumbnailSignal = adjustments.trimStartValue > FLT_EPSILON ? trimmedVideoThumbnailSignal : videoThumbnailSignal; + TGMediaVideoConversionPreset preset = [TGMediaVideoConverter presetFromAdjustments:adjustments]; + CGSize dimensions = [TGMediaVideoConverter dimensionsFor:asset.originalSize adjustments:adjustments preset:preset]; + NSTimeInterval duration = adjustments.trimApplied ? (adjustments.trimEndValue - adjustments.trimStartValue) : asset.videoDuration; + [signals addObject:[thumbnailSignal map:^id(UIImage *image) { NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; @@ -807,6 +816,8 @@ dict[@"asset"] = asset; dict[@"previewImage"] = image; dict[@"adjustments"] = adjustments; + dict[@"dimensions"] = [NSValue valueWithCGSize:dimensions]; + dict[@"duration"] = @(duration); if (adjustments.paintingData.stickers.count > 0) dict[@"stickers"] = adjustments.paintingData.stickers; diff --git a/LegacyComponents/TGMediaVideoConverter.h b/LegacyComponents/TGMediaVideoConverter.h index 0bea891602..a79246c6cc 100644 --- a/LegacyComponents/TGMediaVideoConverter.h +++ b/LegacyComponents/TGMediaVideoConverter.h @@ -23,6 +23,7 @@ + (TGMediaVideoConversionPreset)bestAvailablePresetForDimensions:(CGSize)dimensions; + (CGSize)_renderSizeWithCropSize:(CGSize)cropSize; ++ (TGMediaVideoConversionPreset)presetFromAdjustments:(TGMediaVideoEditAdjustments *)adjustments; + (CGSize)dimensionsFor:(CGSize)dimensions adjustments:(TGMediaVideoEditAdjustments *)adjustments preset:(TGMediaVideoConversionPreset)preset; @end diff --git a/LegacyComponents/TGMediaVideoConverter.m b/LegacyComponents/TGMediaVideoConverter.m index 8e6fdc0125..bce62cfcb7 100644 --- a/LegacyComponents/TGMediaVideoConverter.m +++ b/LegacyComponents/TGMediaVideoConverter.m @@ -116,7 +116,7 @@ return; CGSize dimensions = [avAsset tracksWithMediaType:AVMediaTypeVideo].firstObject.naturalSize; - TGMediaVideoConversionPreset preset = adjustments.sendAsGif ? TGMediaVideoConversionPresetAnimation : [self _presetFromAdjustments:adjustments]; + TGMediaVideoConversionPreset preset = adjustments.sendAsGif ? TGMediaVideoConversionPresetAnimation : [self presetFromAdjustments:adjustments]; if (!CGSizeEqualToSize(dimensions, CGSizeZero) && preset != TGMediaVideoConversionPresetAnimation && preset != TGMediaVideoConversionPresetVideoMessage) { TGMediaVideoConversionPreset bestPreset = [self bestAvailablePresetForDimensions:dimensions]; @@ -524,7 +524,7 @@ NSError *error; NSData *fileData = [NSData dataWithContentsOfURL:fileUrl options:NSDataReadingMappedIfSafe error:&error]; if (error == nil) - return [SSignal single:[self _hashForVideoWithFileData:fileData timingData:timingData preset:[self _presetFromAdjustments:adjustments]]]; + return [SSignal single:[self _hashForVideoWithFileData:fileData timingData:timingData preset:[self presetFromAdjustments:adjustments]]]; else return [SSignal fail:error]; } @@ -572,7 +572,7 @@ return hash; } -+ (TGMediaVideoConversionPreset)_presetFromAdjustments:(TGMediaVideoEditAdjustments *)adjustments ++ (TGMediaVideoConversionPreset)presetFromAdjustments:(TGMediaVideoEditAdjustments *)adjustments { TGMediaVideoConversionPreset preset = adjustments.preset; if (preset == TGMediaVideoConversionPresetCompressedDefault) diff --git a/LegacyComponents/TGPassportAttachMenu.h b/LegacyComponents/TGPassportAttachMenu.h index 5e31dc1fce..f46efc5afd 100644 --- a/LegacyComponents/TGPassportAttachMenu.h +++ b/LegacyComponents/TGPassportAttachMenu.h @@ -4,8 +4,16 @@ @class TGViewController; @class TGMenuSheetController; +typedef enum +{ + TGPassportAttachIntentDefault, + TGPassportAttachIntentIdentityCard, + TGPassportAttachIntentSelfie, + TGPassportAttachIntentMultiple +} TGPassportAttachIntent; + @interface TGPassportAttachMenu : NSObject -+ (TGMenuSheetController *)presentWithContext:(id)context parentController:(TGViewController *)parentController menuController:(TGMenuSheetController *)menuController title:(NSString *)title identity:(bool)identity selfie:(bool)selfie uploadAction:(void (^)(SSignal *, void (^)(void)))uploadAction sourceView:(UIView *)sourceView sourceRect:(CGRect (^)(void))sourceRect barButtonItem:(UIBarButtonItem *)barButtonItem; ++ (TGMenuSheetController *)presentWithContext:(id)context parentController:(TGViewController *)parentController menuController:(TGMenuSheetController *)menuController title:(NSString *)title intent:(TGPassportAttachIntent)intent uploadAction:(void (^)(SSignal *, void (^)(void)))uploadAction sourceView:(UIView *)sourceView sourceRect:(CGRect (^)(void))sourceRect barButtonItem:(UIBarButtonItem *)barButtonItem; @end diff --git a/LegacyComponents/TGPassportAttachMenu.m b/LegacyComponents/TGPassportAttachMenu.m index f07ebf15e3..e55887a2d5 100644 --- a/LegacyComponents/TGPassportAttachMenu.m +++ b/LegacyComponents/TGPassportAttachMenu.m @@ -27,7 +27,7 @@ @implementation TGPassportAttachMenu -+ (TGMenuSheetController *)presentWithContext:(id)context parentController:(TGViewController *)parentController menuController:(TGMenuSheetController *)menuController title:(NSString *)title identity:(bool)identity selfie:(bool)selfie uploadAction:(void (^)(SSignal *, void (^)(void)))uploadAction sourceView:(UIView *)sourceView sourceRect:(CGRect (^)(void))sourceRect barButtonItem:(UIBarButtonItem *)barButtonItem ++ (TGMenuSheetController *)presentWithContext:(id)context parentController:(TGViewController *)parentController menuController:(TGMenuSheetController *)menuController title:(NSString *)title intent:(TGPassportAttachIntent)intent uploadAction:(void (^)(SSignal *, void (^)(void)))uploadAction sourceView:(UIView *)sourceView sourceRect:(CGRect (^)(void))sourceRect barButtonItem:(UIBarButtonItem *)barButtonItem { if (uploadAction == nil) return nil; @@ -51,7 +51,7 @@ __weak TGMenuSheetController *weakController = controller; __weak TGViewController *weakParentController = parentController; - TGAttachmentCarouselItemView *carouselItem = [[TGAttachmentCarouselItemView alloc] initWithContext:context camera:true selfPortrait:selfie forProfilePhoto:false assetType:TGMediaAssetPhotoType saveEditedPhotos:false allowGrouping:false document:true]; + TGAttachmentCarouselItemView *carouselItem = [[TGAttachmentCarouselItemView alloc] initWithContext:context camera:true selfPortrait:intent == TGPassportAttachIntentSelfie forProfilePhoto:false assetType:TGMediaAssetPhotoType saveEditedPhotos:false allowGrouping:false document:true]; __weak TGAttachmentCarouselItemView *weakCarouselItem = carouselItem; carouselItem.onlyCrop = true; carouselItem.parentController = parentController; @@ -65,7 +65,7 @@ if (strongParentController == nil) return; - [TGPassportAttachMenu _displayCameraWithView:cameraView menuController:strongController parentController:strongParentController context:context identity:identity uploadAction:uploadAction]; + [TGPassportAttachMenu _displayCameraWithView:cameraView menuController:strongController parentController:strongParentController context:context intent:intent uploadAction:uploadAction]; }; carouselItem.sendPressed = ^(TGMediaAsset *currentItem, __unused bool asFiles) { @@ -99,7 +99,7 @@ }]; [itemViews addObject:galleryItem]; - if (iosMajorVersion() >= 8 && !selfie) + if (iosMajorVersion() >= 8 && intent != TGPassportAttachIntentSelfie) { TGMenuSheetButtonItemView *icloudItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.FileICloudDrive") type:TGMenuSheetButtonTypeDefault action:^ { @@ -270,7 +270,7 @@ [parentController presentViewController:legacyCameraController animated:true completion:nil]; } -+ (void)_displayCameraWithView:(TGAttachmentCameraView *)cameraView menuController:(TGMenuSheetController *)menuController parentController:(TGViewController *)parentController context:(id)context identity:(bool)identity uploadAction:(void (^)(SSignal *, void (^)(void)))uploadAction ++ (void)_displayCameraWithView:(TGAttachmentCameraView *)cameraView menuController:(TGMenuSheetController *)menuController parentController:(TGViewController *)parentController context:(id)context intent:(bool)intent uploadAction:(void (^)(SSignal *, void (^)(void)))uploadAction { if (![[[LegacyComponentsGlobals provider] accessChecker] checkCameraAuthorizationStatusForIntent:TGCameraAccessIntentDefault alertDismissCompletion:nil]) return; @@ -291,9 +291,9 @@ id windowManager = [context makeOverlayWindowManager]; if (cameraView.previewView != nil) - controller = [[TGCameraController alloc] initWithContext:[windowManager context] saveEditedPhotos:false saveCapturedMedia:false camera:cameraView.previewView.camera previewView:cameraView.previewView intent:identity ? TGCameraControllerPassportIdIntent : TGCameraControllerPassportIntent]; + controller = [[TGCameraController alloc] initWithContext:[windowManager context] saveEditedPhotos:false saveCapturedMedia:false camera:cameraView.previewView.camera previewView:cameraView.previewView intent:intent == TGPassportAttachIntentIdentityCard ? TGCameraControllerPassportIdIntent : TGCameraControllerPassportIntent]; else - controller = [[TGCameraController alloc] initWithContext:[windowManager context] saveEditedPhotos:false saveCapturedMedia:false intent:identity ? TGCameraControllerPassportIdIntent : TGCameraControllerPassportIntent]; + controller = [[TGCameraController alloc] initWithContext:[windowManager context] saveEditedPhotos:false saveCapturedMedia:false intent:intent == TGPassportAttachIntentIdentityCard ? TGCameraControllerPassportIdIntent : TGCameraControllerPassportIntent]; controller.shouldStoreCapturedAssets = false; diff --git a/LegacyComponents/TGPassportMRZ.h b/LegacyComponents/TGPassportMRZ.h new file mode 100644 index 0000000000..ee41fc30fe --- /dev/null +++ b/LegacyComponents/TGPassportMRZ.h @@ -0,0 +1,25 @@ +#import + +@interface TGPassportMRZ : NSObject + +@property (nonatomic, readonly) NSString *documentType; +@property (nonatomic, readonly) NSString *documentSubtype; +@property (nonatomic, readonly) NSString *issuingCountry; +@property (nonatomic, readonly) NSString *lastName; +@property (nonatomic, readonly) NSString *firstName; +@property (nonatomic, readonly) NSString *documentNumber; +@property (nonatomic, readonly) NSString *nationality; +@property (nonatomic, readonly) NSDate *birthDate; +@property (nonatomic, readonly) NSString *gender; +@property (nonatomic, readonly) NSDate *expiryDate; +@property (nonatomic, readonly) NSString *optional1; +@property (nonatomic, readonly) NSString *optional2; + +@property (nonatomic, readonly) NSString *mrz; + ++ (instancetype)parseLines:(NSArray *)lines; + +@end + +extern const NSUInteger TGPassportTD1Length; +extern const NSUInteger TGPassportTD23Length; diff --git a/LegacyComponents/TGPassportMRZ.m b/LegacyComponents/TGPassportMRZ.m new file mode 100644 index 0000000000..fa971d795d --- /dev/null +++ b/LegacyComponents/TGPassportMRZ.m @@ -0,0 +1,316 @@ +#import "TGPassportMRZ.h" + +const NSUInteger TGPassportTD1Length = 30; +const NSUInteger TGPassportTD23Length = 44; +NSString *const TGPassportEmptyCharacter = @"<"; + +@implementation TGPassportMRZ + ++ (instancetype)parseLines:(NSArray *)lines +{ + if (lines.count == 2) + { + if (lines[0].length != TGPassportTD23Length || lines[1].length != TGPassportTD23Length) + return nil; + + TGPassportMRZ *result = [[TGPassportMRZ alloc] init]; + result->_documentType = [lines[0] substringToIndex:1]; + result->_documentSubtype = [self cleanString:[lines[0] substringWithRange:NSMakeRange(1, 1)]]; + result->_issuingCountry = [self cleanString:[lines[0] substringWithRange:NSMakeRange(2, 3)]]; + + NSCharacterSet *emptyCharacterSet = [NSCharacterSet characterSetWithCharactersInString:TGPassportEmptyCharacter]; + NSString *fullName = [[lines[0] substringWithRange:NSMakeRange(5, 39)] stringByTrimmingCharactersInSet:emptyCharacterSet]; + NSArray *names = [fullName componentsSeparatedByString:@"<<"]; + result->_lastName = [self nameString:names.firstObject]; + result->_firstName = [self nameString:names.lastObject]; + + NSString *documentNumber = [self ensureNumberString:[lines[1] substringToIndex:9]]; + NSInteger documentNumberCheck = [[self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(9, 1)]] integerValue]; + if ([self isDataValid:documentNumber check:documentNumberCheck]) + result->_documentNumber = documentNumber; + + result->_nationality = [lines[1] substringWithRange:NSMakeRange(10, 3)]; + NSString *birthDate = [self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(13, 6)]]; + NSInteger birthDateCheck = [[self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(19, 1)]] integerValue]; + if ([self isDataValid:birthDate check:birthDateCheck]) + result->_birthDate = [self dateFromString:birthDate]; + + NSString *gender = [lines[1] substringWithRange:NSMakeRange(20, 1)]; + if ([gender isEqualToString:TGPassportEmptyCharacter]) + gender = nil; + result->_gender = gender; + + NSString *expiryDate = [self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(21, 6)]]; + NSInteger expiryDateCheck = [[self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(27, 1)]] integerValue]; + if ([self isDataValid:expiryDate check:expiryDateCheck]) + result->_expiryDate = [self dateFromString:expiryDate]; + + NSString *optional1 = [lines[1] substringWithRange:NSMakeRange(28, 14)]; + NSString *optional1CheckString = [self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(42, 1)]]; + NSInteger optional1CheckValue = [optional1CheckString isEqualToString:TGPassportEmptyCharacter] ? 0 : [optional1CheckString integerValue]; + if ([self isDataValid:optional1 check:optional1CheckValue]) + result->_optional1 = [self cleanString:optional1]; + + NSString *data = [NSString stringWithFormat:@"%@%d%@%d%@%d%@%@", documentNumber, (int)documentNumberCheck, birthDate, (int)birthDateCheck, expiryDate, (int)expiryDateCheck, optional1, optional1CheckString]; + NSInteger dataCheck = [[self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(43, 1)]] integerValue]; + if ([self isDataValid:data check:dataCheck]) + { + if ([result->_documentType isEqualToString:@"P"] && [result->_documentSubtype isEqualToString:@"N"] && [result->_issuingCountry isEqualToString:@"RUS"]) + { + NSString *lastName = [self transliterateRussianMRZString:result->_lastName]; + result->_lastName = [self transliterateRussianName:lastName]; + NSString *firstName = [self transliterateRussianMRZString:result->_firstName]; + result->_firstName = [self transliterateRussianName:firstName]; + + NSString *lastSeriesDigit = [optional1 substringToIndex:1]; + NSMutableString *fullDocumentNo = [[NSMutableString alloc] init]; + [fullDocumentNo insertString:[result->_documentNumber substringToIndex:3] atIndex:0]; + [fullDocumentNo appendString:lastSeriesDigit]; + [fullDocumentNo appendString:[result->_documentNumber substringFromIndex:3]]; + result->_documentNumber = fullDocumentNo; + } + + + result->_mrz = [lines componentsJoinedByString:@"\n"]; + return result; + } + } + else if (lines.count == 3) + { + if (lines[0].length != TGPassportTD1Length || lines[1].length != TGPassportTD1Length || lines[2].length != TGPassportTD1Length) + return nil; + + TGPassportMRZ *result = [[TGPassportMRZ alloc] init]; + result->_documentType = [lines[0] substringToIndex:1]; + result->_documentSubtype = [self cleanString:[lines[0] substringWithRange:NSMakeRange(1, 1)]]; + result->_issuingCountry = [self cleanString:[lines[0] substringWithRange:NSMakeRange(2, 3)]]; + + NSString *documentNumber = [self ensureNumberString:[lines[0] substringWithRange:NSMakeRange(5, 9)]]; + NSInteger documentNumberCheck = [[self ensureNumberString:[lines[0] substringWithRange:NSMakeRange(14, 1)]] integerValue]; + if ([self isDataValid:documentNumber check:documentNumberCheck]) + result->_documentNumber = documentNumber; + + NSString *optional1 = [lines[0] substringWithRange:NSMakeRange(15, 15)]; + result->_optional1 = [self cleanString:optional1]; + + NSString *birthDate = [self ensureNumberString:[lines[1] substringToIndex:6]]; + NSInteger birthDateCheck = [[lines[1] substringWithRange:NSMakeRange(6, 1)] integerValue]; + if ([self isDataValid:birthDate check:birthDateCheck]) + result->_birthDate = [self dateFromString:birthDate]; + + NSString *gender = [lines[1] substringWithRange:NSMakeRange(7, 1)]; + if ([gender isEqualToString:TGPassportEmptyCharacter]) + gender = nil; + result->_gender = gender; + + NSString *expiryDate = [self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(8, 6)]]; + NSInteger expiryDateCheck = [[self ensureNumberString:[lines[1] substringWithRange:NSMakeRange(14, 1)]] integerValue]; + if ([self isDataValid:expiryDate check:expiryDateCheck]) + result->_expiryDate = [self dateFromString:expiryDate]; + + result->_nationality = [lines[1] substringWithRange:NSMakeRange(15, 3)]; + + NSString *optional2 = [lines[1] substringWithRange:NSMakeRange(18, 11)]; + result->_optional2 = optional2; + + NSCharacterSet *emptyCharacterSet = [NSCharacterSet characterSetWithCharactersInString:TGPassportEmptyCharacter]; + NSString *fullName = [self ensureAlphaString:lines[2]]; + fullName = [fullName stringByTrimmingCharactersInSet:emptyCharacterSet]; + NSArray *names = [fullName componentsSeparatedByString:@"<<"]; + result->_lastName = [self nameString:names.firstObject]; + result->_firstName = [self nameString:names.lastObject]; + result->_mrz = [lines componentsJoinedByString:@"\n"]; + + return result; + } + return nil; +} + ++ (NSDateFormatter *)dateFormatter +{ + static dispatch_once_t onceToken; + static NSDateFormatter *dateFormatter; + dispatch_once(&onceToken, ^ + { + dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateFormat = @"YYMMdd"; + dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + }); + return dateFormatter; +} + + ++ (NSDate *)dateFromString:(NSString *)string +{ + return [[self dateFormatter] dateFromString:string]; +} + ++ (NSString *)cleanString:(NSString *)string +{ + return [string stringByReplacingOccurrencesOfString:TGPassportEmptyCharacter withString:@""]; +} + ++ (NSString *)nameString:(NSString *)string +{ + return [string stringByReplacingOccurrencesOfString:TGPassportEmptyCharacter withString:@" "]; +} + ++ (NSString *)ensureNumberString:(NSString *)string +{ + return [[[[string stringByReplacingOccurrencesOfString:@"O" withString:@"0"] stringByReplacingOccurrencesOfString:@"U" withString:@"0"] stringByReplacingOccurrencesOfString:@"Q" withString:@"0"] stringByReplacingOccurrencesOfString:@"J" withString:@"0"]; +} + ++ (NSString *)ensureAlphaString:(NSString *)string +{ + NSCharacterSet *validChars = [NSCharacterSet characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZ<"]; + NSCharacterSet *invalidChars = [validChars invertedSet]; + + string = [string stringByReplacingOccurrencesOfString:@"0" withString:@"O"]; + return [self string:string byReplacingCharactersInSet:invalidChars withString:TGPassportEmptyCharacter]; +} + ++ (NSString *)string:(NSString *)string byReplacingCharactersInSet:(NSCharacterSet *)charSet withString:(NSString *)aString { + NSMutableString *s = [NSMutableString stringWithCapacity:string.length]; + for (NSUInteger i = 0; i < string.length; ++i) { + unichar c = [string characterAtIndex:i]; + if (![charSet characterIsMember:c]) { + [s appendFormat:@"%C", c]; + } else { + [s appendString:aString]; + } + } + return s; +} + ++ (NSString *)transliterateRussianMRZString:(NSString *)string +{ + NSDictionary *map = @ + { + @"A": @"А", + @"B": @"Б", + @"V": @"В", + @"G": @"Г", + @"D": @"Д", + @"E": @"Е", + @"2": @"Ё", + @"J": @"Ж", + @"Z": @"З", + @"I": @"И", + @"Q": @"Й", + @"K": @"К", + @"L": @"Л", + @"M": @"М", + @"N": @"Н", + @"O": @"О", + @"P": @"П", + @"R": @"Р", + @"S": @"С", + @"T": @"Т", + @"U": @"У", + @"F": @"Ф", + @"H": @"Х", + @"C": @"Ц", + @"3": @"Ч", + @"4": @"Ш", + @"W": @"Щ", + @"X": @"Ъ", + @"Y": @"Ы", + @"9": @"Ь", + @"6": @"Э", + @"7": @"Ю", + @"8": @"Я", + @" ": @" " + }; + + NSMutableString *result = [[NSMutableString alloc] init]; + [string enumerateSubstringsInRange:NSMakeRange(0, string.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, __unused BOOL *stop) + { + if (substring == nil) + return; + + NSString *letter = map[substring]; + if (letter != nil) + [result appendString:letter]; + }]; + return result; +} + ++ (NSString *)transliterateRussianName:(NSString *)string +{ + NSDictionary *map = @ + { + @"А": @"A", + @"Б": @"B", + @"В": @"V", + @"Г": @"G", + @"Д": @"D", + @"Е": @"E", + @"Ё": @"E", + @"Ж": @"ZH", + @"З": @"Z", + @"И": @"I", + @"Й": @"I", + @"К": @"K", + @"Л": @"L", + @"М": @"M", + @"Н": @"N", + @"О": @"O", + @"П": @"P", + @"Р": @"R", + @"С": @"S", + @"Т": @"T", + @"У": @"U", + @"Ф": @"F", + @"Х": @"KH", + @"Ц": @"TS", + @"Ч": @"CH", + @"Ш": @"SH", + @"Щ": @"SHCH", + @"Ъ": @"IE", + @"Ы": @"Y", + @"Ь": @"", + @"Э": @"E", + @"Ю": @"IU", + @"Я": @"IA", + @" ": @" " + }; + + NSMutableString *result = [[NSMutableString alloc] init]; + [string enumerateSubstringsInRange:NSMakeRange(0, string.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, __unused BOOL *stop) + { + if (substring == nil) + return; + + NSString *letter = map[substring]; + if (letter != nil) + [result appendString:letter]; + }]; + return result; +} + ++ (bool)isDataValid:(NSString *)data check:(NSInteger)check +{ + int32_t sum = 0; + uint8_t w[3] = { 7, 3, 1 }; + + for (NSUInteger i = 0; i < data.length; i++) + { + unichar c = [data characterAtIndex:i]; + NSInteger d = 0; + if (c >= '0' && c <= '9') + d = c - '0'; + else if (c >= 'A' && c <= 'Z') + d = (10 + c) - 'A'; + else if (c != '<') + return false; + + sum += d * w[i % 3]; + } + + if (sum % 10 != check) + return false; + + return true; +} + +@end diff --git a/LegacyComponents/TGPassportOCR.h b/LegacyComponents/TGPassportOCR.h new file mode 100644 index 0000000000..c6909660c3 --- /dev/null +++ b/LegacyComponents/TGPassportOCR.h @@ -0,0 +1,8 @@ +#import +#import + +@interface TGPassportOCR : NSObject + ++ (SSignal *)recognizeMRZInImage:(UIImage *)image; + +@end diff --git a/LegacyComponents/TGPassportOCR.mm b/LegacyComponents/TGPassportOCR.mm new file mode 100644 index 0000000000..df158eb9b9 --- /dev/null +++ b/LegacyComponents/TGPassportOCR.mm @@ -0,0 +1,21 @@ +#import "TGPassportOCR.h" +#import "TGPassportMRZ.h" +#import "ocr.h" + +@implementation TGPassportOCR + ++ (SSignal *)recognizeMRZInImage:(UIImage *)image +{ + return [[SSignal defer:^SSignal * + { + CGRect boundingRect; + NSString *string = recognizeMRZ(image, &boundingRect); + + NSArray *lines = [string componentsSeparatedByString:@"\n"]; + TGPassportMRZ *mrz = [TGPassportMRZ parseLines:lines]; + + return [SSignal single:mrz]; + }] startOn:[SQueue concurrentDefaultQueue]]; +} + +@end diff --git a/LegacyComponents/TGPassportScanView.h b/LegacyComponents/TGPassportScanView.h new file mode 100644 index 0000000000..406137adf9 --- /dev/null +++ b/LegacyComponents/TGPassportScanView.h @@ -0,0 +1,13 @@ +#import + +@class TGPassportMRZ; + +@interface TGPassportScanView : UIView + +@property (nonatomic, copy) void (^finishedWithMRZ)(TGPassportMRZ *); + +- (void)start; +- (void)stop; +- (void)pause; + +@end diff --git a/LegacyComponents/TGPassportScanView.m b/LegacyComponents/TGPassportScanView.m new file mode 100644 index 0000000000..81b70e14a4 --- /dev/null +++ b/LegacyComponents/TGPassportScanView.m @@ -0,0 +1,98 @@ +#import "TGPassportScanView.h" +#import "PGCamera.h" +#import "TGCameraPreviewView.h" + +#import "TGPassportOCR.h" + +#import "LegacyComponentsInternal.h" + +#import "TGTimerTarget.h" + +@interface TGPassportScanView () +{ + PGCamera *_camera; + TGCameraPreviewView *_previewView; + + NSTimer *_timer; + SMetaDisposable *_ocrDisposable; +} +@end + +@implementation TGPassportScanView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + _camera = [[PGCamera alloc] initWithMode:PGCameraModePhoto position:PGCameraPositionRear]; + _previewView = [[TGCameraPreviewView alloc] initWithFrame:self.bounds]; + [self addSubview:_previewView]; + + [_camera attachPreviewView:_previewView]; + + _ocrDisposable = [[SMetaDisposable alloc] init]; + } + return self; +} + +- (void)dealloc +{ + [_ocrDisposable dispose]; +} + +- (void)start +{ + [_camera startCaptureForResume:false completion:nil]; + + _timer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(handleNextFrame) interval:1.0 repeat:false]; +} + +- (void)stop +{ + [_camera stopCaptureForPause:false completion:nil]; + _camera = nil; + + [_timer invalidate]; + _timer = nil; +} + +- (void)pause +{ + [_camera stopCaptureForPause:true completion:nil]; +} + +- (void)handleNextFrame +{ + __weak TGPassportScanView *weakSelf = self; + [_camera captureNextFrameCompletion:^(UIImage *image) + { + [_ocrDisposable setDisposable:[[[TGPassportOCR recognizeMRZInImage:image] deliverOn:[SQueue mainQueue]] startWithNext:^(TGPassportMRZ *next) + { + __strong TGPassportScanView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (next != nil) + { + [strongSelf->_camera stopCaptureForPause:true completion:nil]; + + if (strongSelf.finishedWithMRZ != nil) + strongSelf.finishedWithMRZ(next); + } + else + { + strongSelf->_timer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(handleNextFrame) interval:0.45 repeat:false]; + } + }]]; + }]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + _previewView.frame = self.bounds; +} + +@end diff --git a/LegacyComponents/fast-edge.cpp b/LegacyComponents/fast-edge.cpp new file mode 100755 index 0000000000..0e67626501 --- /dev/null +++ b/LegacyComponents/fast-edge.cpp @@ -0,0 +1,550 @@ +/* + FAST-EDGE + Copyright (c) 2009 Benjamin C. Haynor + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include "fast-edge.h" + +#define LOW_THRESHOLD_PERCENTAGE 0.8 // percentage of the high threshold value that the low threshold shall be set at +#define PI 3.14159265 +#define HIGH_THRESHOLD_PERCENTAGE 0.10 // percentage of pixels that meet the high threshold - for example 0.15 will ensure that at least 15% of edge pixels are considered to meet the high threshold + +#define min(X,Y) ((X) < (Y) ? (X) : (Y)) +#define max(X,Y) ((X) < (Y) ? (Y) : (X)) + +namespace ocr{ +/* + CANNY EDGE DETECT + DOES NOT PERFORM NOISE REDUCTION - PERFORM NOISE REDUCTION PRIOR TO USE + Noise reduction omitted, as some applications benefit from morphological operations such as opening or closing as opposed to Gaussian noise reduction + If your application always takes the same size input image, uncomment the definitions of WIDTH and HEIGHT in the header file and define them to the size of your input image, + otherwise the required intermediate arrays will be dynamically allocated. + If WIDTH and HEIGHT are defined, the arrays will be allocated in the compiler directive that follows: +*/ +#ifdef WIDTH +int g[WIDTH * HEIGHT], dir[WIDTH * HEIGHT] = {0}; +unsigned char img_scratch_data[WIDTH * HEIGHT] = {0}; +#endif +void canny_edge_detect(struct image * img_in, struct image * img_out) { + struct image img_scratch; + int high, low; + #ifndef WIDTH + int * g = (int*)calloc(static_cast(img_in->width*img_in->height), sizeof(int)); + int * dir = (int*)calloc(static_cast(img_in->width*img_in->height), sizeof(int)); + unsigned char * img_scratch_data = (unsigned char*)calloc(static_cast(img_in->width*img_in->height), sizeof(unsigned char)); + #endif + img_scratch.width = img_in->width; + img_scratch.height = img_in->height; + img_scratch.pixel_data = img_scratch_data; + calc_gradient_sobel(img_in, g, dir); + //printf("*** performing non-maximum suppression ***\n"); + non_max_suppression(&img_scratch, g, dir); + estimate_threshold(&img_scratch, &high, &low); + hysteresis(high, low, &img_scratch, img_out); + #ifndef WIDTH + free(g); + free(dir); + free(img_scratch_data); + #endif +} + +/* + GAUSSIAN_NOISE_ REDUCE + apply 5x5 Gaussian convolution filter, shrinks the image by 4 pixels in each direction, using Gaussian filter found here: + http://en.wikipedia.org/wiki/Canny_edge_detector +*/ +void gaussian_noise_reduce(struct image * img_in, struct image * img_out) +{ + #ifdef CLOCK + clock_t start = clock(); + #endif + int w, h, x, y, max_x, max_y; + w = img_in->width; + h = img_in->height; + img_out->width = w; + img_out->height = h; + max_x = w - 2; + max_y = w * (h - 2); + for (y = w * 2; y < max_y; y += w) { + for (x = 2; x < max_x; x++) { + img_out->pixel_data[x + y] = (2 * img_in->pixel_data[x + y - 2 - w - w] + + 4 * img_in->pixel_data[x + y - 1 - w - w] + + 5 * img_in->pixel_data[x + y - w - w] + + 4 * img_in->pixel_data[x + y + 1 - w - w] + + 2 * img_in->pixel_data[x + y + 2 - w - w] + + 4 * img_in->pixel_data[x + y - 2 - w] + + 9 * img_in->pixel_data[x + y - 1 - w] + + 12 * img_in->pixel_data[x + y - w] + + 9 * img_in->pixel_data[x + y + 1 - w] + + 4 * img_in->pixel_data[x + y + 2 - w] + + 5 * img_in->pixel_data[x + y - 2] + + 12 * img_in->pixel_data[x + y - 1] + + 15 * img_in->pixel_data[x + y] + + 12 * img_in->pixel_data[x + y + 1] + + 5 * img_in->pixel_data[x + y + 2] + + 4 * img_in->pixel_data[x + y - 2 + w] + + 9 * img_in->pixel_data[x + y - 1 + w] + + 12 * img_in->pixel_data[x + y + w] + + 9 * img_in->pixel_data[x + y + 1 + w] + + 4 * img_in->pixel_data[x + y + 2 + w] + + 2 * img_in->pixel_data[x + y - 2 + w + w] + + 4 * img_in->pixel_data[x + y - 1 + w + w] + + 5 * img_in->pixel_data[x + y + w + w] + + 4 * img_in->pixel_data[x + y + 1 + w + w] + + 2 * img_in->pixel_data[x + y + 2 + w + w]) / 159; + } + } + #ifdef CLOCK + printf("Gaussian noise reduction - time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC); + #endif +} + +/* + CALC_GRADIENT_SOBEL + calculates the result of the Sobel operator - http://en.wikipedia.org/wiki/Sobel_operator - and estimates edge direction angle +*/ +/*void calc_gradient_sobel(struct image * img_in, int g_x[], int g_y[], int g[], int dir[]) {//float theta[]) {*/ +void calc_gradient_sobel(struct image * img_in, int g[], int dir[]) { + #ifdef CLOCK + clock_t start = clock(); + #endif + int w, h, x, y, max_x, max_y, g_x, g_y; + float g_div; + w = img_in->width; + h = img_in->height; + max_x = w - 3; + max_y = w * (h - 3); + for (y = w * 3; y < max_y; y += w) { + for (x = 3; x < max_x; x++) { + g_x = (2 * img_in->pixel_data[x + y + 1] + + img_in->pixel_data[x + y - w + 1] + + img_in->pixel_data[x + y + w + 1] + - 2 * img_in->pixel_data[x + y - 1] + - img_in->pixel_data[x + y - w - 1] + - img_in->pixel_data[x + y + w - 1]); + g_y = 2 * img_in->pixel_data[x + y - w] + + img_in->pixel_data[x + y - w + 1] + + img_in->pixel_data[x + y - w - 1] + - 2 * img_in->pixel_data[x + y + w] + - img_in->pixel_data[x + y + w + 1] + - img_in->pixel_data[x + y + w - 1]; + #ifndef ABS_APPROX + g[x + y] = sqrt(g_x * g_x + g_y * g_y); + #endif + #ifdef ABS_APPROX + g[x + y] = abs(g_x[x + y]) + abs(g_y[x + y]); + #endif + if (g_x == 0) { + dir[x + y] = 2; + } else { + g_div = g_y / (float) g_x; + /* the following commented-out code is slightly faster than the code that follows, but is a slightly worse approximation for determining the edge direction angle + if (g_div < 0) { + if (g_div < -1) { + dir[n] = 0; + } else { + dir[n] = 1; + } + } else { + if (g_div > 1) { + dir[n] = 0; + } else { + dir[n] = 3; + } + } + */ + if (g_div < 0) { + if (g_div < -2.41421356237) { + dir[x + y] = 0; + } else { + if (g_div < -0.414213562373) { + dir[x + y] = 1; + } else { + dir[x + y] = 2; + } + } + } else { + if (g_div > 2.41421356237) { + dir[x + y] = 0; + } else { + if (g_div > 0.414213562373) { + dir[x + y] = 3; + } else { + dir[x + y] = 2; + } + } + } + } + } + + } + #ifdef CLOCK + printf("Calculate gradient Sobel - time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC); + #endif +} + +/* + CALC_GRADIENT_SCHARR + calculates the result of the Scharr version of the Sobel operator - http://en.wikipedia.org/wiki/Sobel_operator - and estimates edge direction angle + may have better rotational symmetry +*/ +void calc_gradient_scharr(struct image * img_in, int g_x[], int g_y[], int g[], int dir[]) {//float theta[]) { + #ifdef CLOCK + clock_t start = clock(); + #endif + int w, h, x, y, max_x, max_y, n; + float g_div; + w = img_in->width; + h = img_in->height; + max_x = w - 1; + max_y = w * (h - 1); + n = 0; + for (y = w; y < max_y; y += w) { + for (x = 1; x < max_x; x++) { + g_x[n] = (10 * img_in->pixel_data[x + y + 1] + + 3 * img_in->pixel_data[x + y - w + 1] + + 3 * img_in->pixel_data[x + y + w + 1] + - 10 * img_in->pixel_data[x + y - 1] + - 3 * img_in->pixel_data[x + y - w - 1] + - 3 * img_in->pixel_data[x + y + w - 1]); + g_y[n] = 10 * img_in->pixel_data[x + y - w] + + 3 * img_in->pixel_data[x + y - w + 1] + + 3 * img_in->pixel_data[x + y - w - 1] + - 10 * img_in->pixel_data[x + y + w] + - 3 * img_in->pixel_data[x + y + w + 1] + - 3 * img_in->pixel_data[x + y + w - 1]; + #ifndef ABS_APPROX + g[n] = sqrt(g_x[n] * g_x[n] + g_y[n] * g_y[n]); + #endif + #ifdef ABS_APPROX + g[n] = abs(g_x[n]) + abs(g_y[n]); + #endif + if (g_x[n] == 0) { + dir[n] = 2; + } else { + g_div = g_y[n] / (float) g_x[n]; + if (g_div < 0) { + if (g_div < -2.41421356237) { + dir[n] = 0; + } else { + if (g_div < -0.414213562373) { + dir[n] = 1; + } else { + dir[n] = 2; + } + } + } else { + if (g_div > 2.41421356237) { + dir[n] = 0; + } else { + if (g_div > 0.414213562373) { + dir[n] = 3; + } else { + dir[n] = 2; + } + } + } + } + n++; + } + } + #ifdef CLOCK + printf("Calculate gradient Scharr - time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC); + #endif +} +/* + NON_MAX_SUPPRESSION + using the estimates of the Gx and Gy image gradients and the edge direction angle determines whether the magnitude of the gradient assumes a local maximum in the gradient direction + if the rounded edge direction angle is 0 degrees, checks the north and south directions + if the rounded edge direction angle is 45 degrees, checks the northwest and southeast directions + if the rounded edge direction angle is 90 degrees, checks the east and west directions + if the rounded edge direction angle is 135 degrees, checks the northeast and southwest directions +*/ +void non_max_suppression(struct image * img, int g[], int dir[]) {//float theta[]) { + #ifdef CLOCK + clock_t start = clock(); + #endif + int w, h, x, y, max_x, max_y; + w = img->width; + h = img->height; + max_x = w; + max_y = w * h; + for (y = 0; y < max_y; y += w) { + for (x = 0; x < max_x; x++) { + switch (dir[x + y]) { + case 0: + if(x+y-w-1<0){ + continue; + } + if (g[x + y] > g[x + y - w] && g[x + y] > g[x + y + w]) { + if (g[x + y] > 255) { + img->pixel_data[x + y] = 0xFF; + } else { + img->pixel_data[x + y] = g[x + y]; + } + } else { + img->pixel_data[x + y] = 0x00; + } + break; + case 1: + if(x+y-w-1<0){ + continue; + } + if (g[x + y] > g[x + y - w - 1] && g[x + y] > g[x + y + w + 1]) { + if (g[x + y] > 255) { + img->pixel_data[x + y] = 0xFF; + } else { + img->pixel_data[x + y] = g[x + y]; + } + } else { + img->pixel_data[x + y] = 0x00; + } + break; + case 2: + if (g[x + y] > g[x + y - 1] && g[x + y] > g[x + y + 1]) { + if (g[x + y] > 255) { + img->pixel_data[x + y] = 0xFF; + } else { + img->pixel_data[x + y] = g[x + y]; + } + } else { + img->pixel_data[x + y] = 0x00; + } + break; + case 3: + if(x+y-w-1<0){ + continue; + } + if (g[x + y] > g[x + y - w + 1] && g[x + y] > g[x + y + w - 1]) { + if (g[x + y] > 255) { + img->pixel_data[x + y] = 0xFF; + } else { + img->pixel_data[x + y] = g[x + y]; + } + } else { + img->pixel_data[x + y] = 0x00; + } + break; + default: + printf("ERROR - direction outside range 0 to 3"); + break; + } + } + } + #ifdef CLOCK + printf("Non-maximum suppression - time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC); + #endif +} +/* + ESTIMATE_THRESHOLD + estimates hysteresis threshold, assuming that the top X% (as defined by the HIGH_THRESHOLD_PERCENTAGE) of edge pixels with the greatest intesity are true edges + and that the low threshold is equal to the quantity of the high threshold plus the total number of 0s at the low end of the histogram divided by 2 +*/ +void estimate_threshold(struct image * img, int * high, int * low) { + #ifdef CLOCK + clock_t start = clock(); + #endif + int i, max, pixels, high_cutoff; + int histogram[256]; + max = img->width * img->height; + for (i = 0; i < 256; i++) { + histogram[i] = 0; + } + for (i = 0; i < max; i++) { + histogram[img->pixel_data[i]]++; + } + pixels = (max - histogram[0]) * HIGH_THRESHOLD_PERCENTAGE; + high_cutoff = 0; + i = 255; + while (high_cutoff < pixels) { + high_cutoff += histogram[i]; + i--; + } + *high = i; + i = 1; + while (histogram[i] == 0) { + i++; + } + *low = (*high + i) * LOW_THRESHOLD_PERCENTAGE; + #ifdef PRINT_HISTOGRAM + for (i = 0; i < 256; i++) { + printf("i %d count %d\n", i, histogram[i]); + } + #endif + + #ifdef CLOCK + printf("Estimate threshold - time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC); + #endif +} + +void hysteresis (int high, int low, struct image * img_in, struct image * img_out) +{ + #ifdef CLOCK + clock_t start = clock(); + #endif + int x, y, n, max; + max = img_in->width * img_in->height; + for (n = 0; n < max; n++) { + img_out->pixel_data[n] = 0x00; + } + for (y=0; y < img_out->height; y++) { + for (x=0; x < img_out->width; x++) { + if (img_in->pixel_data[y * img_out->width + x] >= high) { + trace (x, y, low, img_in, img_out); + } + } + } + #ifdef CLOCK + printf("Hysteresis - time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC); + #endif +} + +int trace(int x, int y, int low, struct image * img_in, struct image * img_out) +{ + int y_off, x_off;//, flag; + if (img_out->pixel_data[y * img_out->width + x] == 0) + { + img_out->pixel_data[y * img_out->width + x] = 0xFF; + for (y_off = -1; y_off <=1; y_off++) + { + for(x_off = -1; x_off <= 1; x_off++) + { + if (!(y == 0 && x_off == 0) && range(img_in, x + x_off, y + y_off) && img_in->pixel_data[(y + y_off) * img_out->width + x + x_off] >= low) { + if (trace(x + x_off, y + y_off, low, img_in, img_out)) + { + return(1); + } + } + } + } + return(1); + } + return(0); +} + +int range(struct image * img, int x, int y) +{ + if ((x < 0) || (x >= img->width)) { + return(0); + } + if ((y < 0) || (y >= img->height)) { + return(0); + } + return(1); +} + +void dilate_1d_h(struct image * img, struct image * img_out) { + int x, y, offset, y_max; + y_max = img->height * (img->width - 2); + for (y = 2 * img->width; y < y_max; y += img->width) { + for (x = 2; x < img->width - 2; x++) { + offset = x + y; + img_out->pixel_data[offset] = max(max(max(max(img->pixel_data[offset-2], img->pixel_data[offset-1]), img->pixel_data[offset]), img->pixel_data[offset+1]), img->pixel_data[offset+2]); + } + } +} + +void dilate_1d_v(struct image * img, struct image * img_out) { + int x, y, offset, y_max; + y_max = img->height * (img->width - 2); + for (y = 2 * img->width; y < y_max; y += img->width) { + for (x = 2; x < img->width - 2; x++) { + offset = x + y; + img_out->pixel_data[offset] = max(max(max(max(img->pixel_data[offset-2 * img->width], img->pixel_data[offset-img->width]), img->pixel_data[offset]), img->pixel_data[offset+img->width]), img->pixel_data[offset+2*img->width]); + } + } +} + +void erode_1d_h(struct image * img, struct image * img_out) { + int x, y, offset, y_max; + y_max = img->height * (img->width - 2); + for (y = 2 * img->width; y < y_max; y += img->width) { + for (x = 2; x < img->width - 2; x++) { + offset = x + y; + img_out->pixel_data[offset] = min(min(min(min(img->pixel_data[offset-2], img->pixel_data[offset-1]), img->pixel_data[offset]), img->pixel_data[offset+1]), img->pixel_data[offset+2]); + } + } +} + +void erode_1d_v(struct image * img, struct image * img_out) { + int x, y, offset, y_max; + y_max = img->height * (img->width - 2); + for (y = 2 * img->width; y < y_max; y += img->width) { + for (x = 2; x < img->width - 2; x++) { + offset = x + y; + img_out->pixel_data[offset] = min(min(min(min(img->pixel_data[offset-2 * img->width], img->pixel_data[offset-img->width]), img->pixel_data[offset]), img->pixel_data[offset+img->width]), img->pixel_data[offset+2*img->width]); + } + } +} + +void erode(struct image * img_in, struct image * img_scratch, struct image * img_out) { + #ifdef CLOCK + clock_t start = clock(); + #endif + erode_1d_h(img_in, img_scratch); + erode_1d_v(img_scratch, img_out); + #ifdef CLOCK + printf("Erosion - time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC); + #endif +} + +void dilate(struct image * img_in, struct image * img_scratch, struct image * img_out) { + #ifdef CLOCK + clock_t start = clock(); + #endif + dilate_1d_h(img_in, img_scratch); + dilate_1d_v(img_scratch, img_out); + #ifdef CLOCK + printf("Dilation - time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC); + #endif +} + +void morph_open(struct image * img_in, struct image * img_scratch, struct image * img_scratch2, struct image * img_out) { + #ifdef CLOCK + clock_t start = clock(); + #endif + erode(img_in, img_scratch, img_scratch2); + dilate(img_scratch2, img_scratch, img_out); + #ifdef CLOCK + printf("Morphological opening - time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC); + #endif +} + +void morph_close(struct image * img_in, struct image * img_scratch, struct image * img_scratch2, struct image * img_out) { + #ifdef CLOCK + clock_t start = clock(); + #endif + dilate(img_in, img_scratch, img_scratch2); + erode(img_scratch2, img_scratch, img_out); + #ifdef CLOCK + printf("Morphological closing - time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC); + #endif +} + +} diff --git a/LegacyComponents/fast-edge.h b/LegacyComponents/fast-edge.h new file mode 100755 index 0000000000..4ebee6c545 --- /dev/null +++ b/LegacyComponents/fast-edge.h @@ -0,0 +1,61 @@ +/* + FAST-EDGE + Copyright (c) 2009 Benjamin C. Haynor + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef _FASTEDGE +#define _FASTEDGE + +namespace ocr{ +//#define WIDTH 640 // uncomment to define width for situations where width is always known +//#define HEIGHT 480 // uncomment to define heigh for situations where height is always known + +//#define CLOCK // uncomment to show running times of image processing functions (in seconds) +//#define ABS_APPROX // uncomment to use the absolute value approximation of sqrt(Gx ^ 2 + Gy ^2) +//#define PRINT_HISTOGRAM // uncomment to print the histogram used to estimate the threshold + struct image { + int width; + int height; + unsigned char * pixel_data; + }; + +void canny_edge_detect(struct image * img_in, struct image * img_out); +void gaussian_noise_reduce(struct image * img_in, struct image * img_out); +void calc_gradient_sobel(struct image * img_in, int g[], int dir[]); +void calc_gradient_scharr(struct image * img_in, int g_x[], int g_y[], int g[], int dir[]); +void non_max_suppression(struct image * img, int g[], int dir[]); +void estimate_threshold(struct image * img, int * high, int * low); +void hysteresis (int high, int low, struct image * img_in, struct image * img_out); +int trace (int x, int y, int low, struct image * img_in, struct image * img_out); +int range (struct image * img, int x, int y); +void dilate_1d_h(struct image * img, struct image * img_out); +void dilate_1d_v(struct image * img, struct image * img_out); +void erode_1d_h(struct image * img, struct image * img_out); +void erode_1d_v(struct image * img, struct image * img_out); +void erode(struct image * img_in, struct image * img_scratch, struct image * img_out); +void dilate(struct image * img_in, struct image * img_scratch, struct image * img_out); +void morph_open(struct image * img_in, struct image * img_scratch, struct image * img_scratch2, struct image * img_out); +void morph_close(struct image * img_in, struct image * img_scratch, struct image * img_scratch2, struct image * img_out); +} +#endif diff --git a/LegacyComponents/genann.c b/LegacyComponents/genann.c new file mode 100755 index 0000000000..11c80bbaf9 --- /dev/null +++ b/LegacyComponents/genann.c @@ -0,0 +1,357 @@ +/* + * GENANN - Minimal C Artificial Neural Network + * + * Copyright (c) 2015, 2016 Lewis Van Winkle + * + * http://CodePlea.com + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgement in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + */ + +#include "genann.h" + +#include +#include +#include +#include +#include + +#define LOOKUP_SIZE 4096 + +double genann_act_sigmoid(double a) { + if (a < -45.0) return 0; + if (a > 45.0) return 1; + return 1.0 / (1 + exp(-a)); +} + + +double genann_act_sigmoid_cached(double a) { + /* If you're optimizing for memory usage, just + * delete this entire function and replace references + * of genann_act_sigmoid_cached to genann_act_sigmoid + */ + const double min = -15.0; + const double max = 15.0; + static double interval; + static int initialized = 0; + static double lookup[LOOKUP_SIZE]; + + /* Calculate entire lookup table on first run. */ + if (!initialized) { + interval = (max - min) / LOOKUP_SIZE; + int i; + for (i = 0; i < LOOKUP_SIZE; ++i) { + lookup[i] = genann_act_sigmoid(min + interval * i); + } + /* This is down here to make this thread safe. */ + initialized = 1; + } + + int i; + i = (int)((a-min)/interval+0.5); + if (i <= 0) return lookup[0]; + if (i >= LOOKUP_SIZE) return lookup[LOOKUP_SIZE-1]; + return lookup[i]; +} + + +double genann_act_threshold(double a) { + return a > 0; +} + + +double genann_act_linear(double a) { + return a; +} + + +genann *genann_init(int inputs, int hidden_layers, int hidden, int outputs) { + if (hidden_layers < 0) return 0; + if (inputs < 1) return 0; + if (outputs < 1) return 0; + if (hidden_layers > 0 && hidden < 1) return 0; + + + const int hidden_weights = hidden_layers ? (inputs+1) * hidden + (hidden_layers-1) * (hidden+1) * hidden : 0; + const int output_weights = (hidden_layers ? (hidden+1) : (inputs+1)) * outputs; + const int total_weights = (hidden_weights + output_weights); + + const int total_neurons = (inputs + hidden * hidden_layers + outputs); + + /* Allocate extra size for weights, outputs, and deltas. */ + const int size = sizeof(genann) + sizeof(double) * (total_weights + total_neurons + (total_neurons - inputs)); + genann *ret = malloc(size); + if (!ret) return 0; + + ret->inputs = inputs; + ret->hidden_layers = hidden_layers; + ret->hidden = hidden; + ret->outputs = outputs; + + ret->total_weights = total_weights; + ret->total_neurons = total_neurons; + + /* Set pointers. */ + ret->weight = (double*)((char*)ret + sizeof(genann)); + ret->output = ret->weight + ret->total_weights; + ret->delta = ret->output + ret->total_neurons; + + genann_randomize(ret); + + ret->activation_hidden = genann_act_sigmoid_cached; + ret->activation_output = genann_act_sigmoid_cached; + + return ret; +} + + +genann *genann_read(FILE *in) { + int inputs, hidden_layers, hidden, outputs; + fscanf(in, "%d %d %d %d", &inputs, &hidden_layers, &hidden, &outputs); + + genann *ann = genann_init(inputs, hidden_layers, hidden, outputs); + + int i; + for (i = 0; i < ann->total_weights; ++i) { + fscanf(in, " %le", ann->weight + i); + } + + return ann; +} + + +genann *genann_copy(genann const *ann) { + const int size = sizeof(genann) + sizeof(double) * (ann->total_weights + ann->total_neurons + (ann->total_neurons - ann->inputs)); + genann *ret = malloc(size); + if (!ret) return 0; + + memcpy(ret, ann, size); + + /* Set pointers. */ + ret->weight = (double*)((char*)ret + sizeof(genann)); + ret->output = ret->weight + ret->total_weights; + ret->delta = ret->output + ret->total_neurons; + + return ret; +} + + +void genann_randomize(genann *ann) { + int i; + for (i = 0; i < ann->total_weights; ++i) { + double r = GENANN_RANDOM(); + /* Sets weights from -0.5 to 0.5. */ + ann->weight[i] = r - 0.5; + } +} + + +void genann_free(genann *ann) { + /* The weight, output, and delta pointers go to the same buffer. */ + free(ann); +} + + +double const *genann_run(genann const *ann, double const *inputs) { + double const *w = ann->weight; + double *o = ann->output + ann->inputs; + double const *i = ann->output; + + /* Copy the inputs to the scratch area, where we also store each neuron's + * output, for consistency. This way the first layer isn't a special case. */ + memcpy(ann->output, inputs, sizeof(double) * ann->inputs); + + int h, j, k; + + const genann_actfun act = ann->activation_hidden; + const genann_actfun acto = ann->activation_output; + + /* Figure hidden layers, if any. */ + for (h = 0; h < ann->hidden_layers; ++h) { + for (j = 0; j < ann->hidden; ++j) { + double sum = 0; + for (k = 0; k < (h == 0 ? ann->inputs : ann->hidden) + 1; ++k) { + if (k == 0) { + sum += *w++ * -1.0; + } else { + sum += *w++ * i[k-1]; + } + } + *o++ = act(sum); + } + + + i += (h == 0 ? ann->inputs : ann->hidden); + } + + double const *ret = o; + + /* Figure output layer. */ + for (j = 0; j < ann->outputs; ++j) { + double sum = 0; + for (k = 0; k < (ann->hidden_layers ? ann->hidden : ann->inputs) + 1; ++k) { + if (k == 0) { + sum += *w++ * -1.0; + } else { + sum += *w++ * i[k-1]; + } + } + *o++ = acto(sum); + } + + /* Sanity check that we used all weights and wrote all outputs. */ + assert(w - ann->weight == ann->total_weights); + assert(o - ann->output == ann->total_neurons); + + return ret; +} + + +void genann_train(genann const *ann, double const *inputs, double const *desired_outputs, double learning_rate) { + /* To begin with, we must run the network forward. */ + genann_run(ann, inputs); + + int h, j, k; + + /* First set the output layer deltas. */ + { + double const *o = ann->output + ann->inputs + ann->hidden * ann->hidden_layers; /* First output. */ + double *d = ann->delta + ann->hidden * ann->hidden_layers; /* First delta. */ + double const *t = desired_outputs; /* First desired output. */ + + + /* Set output layer deltas. */ + if (ann->activation_output == genann_act_linear) { + for (j = 0; j < ann->outputs; ++j) { + *d++ = *t++ - *o++; + } + } else { + for (j = 0; j < ann->outputs; ++j) { + *d++ = (*t - *o) * *o * (1.0 - *o); + ++o; ++t; + } + } + } + + + /* Set hidden layer deltas, start on last layer and work backwards. */ + /* Note that loop is skipped in the case of hidden_layers == 0. */ + for (h = ann->hidden_layers - 1; h >= 0; --h) { + + /* Find first output and delta in this layer. */ + double const *o = ann->output + ann->inputs + (h * ann->hidden); + double *d = ann->delta + (h * ann->hidden); + + /* Find first delta in following layer (which may be hidden or output). */ + double const * const dd = ann->delta + ((h+1) * ann->hidden); + + /* Find first weight in following layer (which may be hidden or output). */ + double const * const ww = ann->weight + ((ann->inputs+1) * ann->hidden) + ((ann->hidden+1) * ann->hidden * (h)); + + for (j = 0; j < ann->hidden; ++j) { + + double delta = 0; + + for (k = 0; k < (h == ann->hidden_layers-1 ? ann->outputs : ann->hidden); ++k) { + const double forward_delta = dd[k]; + const int windex = k * (ann->hidden + 1) + (j + 1); + const double forward_weight = ww[windex]; + delta += forward_delta * forward_weight; + } + + *d = *o * (1.0-*o) * delta; + ++d; ++o; + } + } + + + /* Train the outputs. */ + { + /* Find first output delta. */ + double const *d = ann->delta + ann->hidden * ann->hidden_layers; /* First output delta. */ + + /* Find first weight to first output delta. */ + double *w = ann->weight + (ann->hidden_layers + ? ((ann->inputs+1) * ann->hidden + (ann->hidden+1) * ann->hidden * (ann->hidden_layers-1)) + : (0)); + + /* Find first output in previous layer. */ + double const * const i = ann->output + (ann->hidden_layers + ? (ann->inputs + (ann->hidden) * (ann->hidden_layers-1)) + : 0); + + /* Set output layer weights. */ + for (j = 0; j < ann->outputs; ++j) { + for (k = 0; k < (ann->hidden_layers ? ann->hidden : ann->inputs) + 1; ++k) { + if (k == 0) { + *w++ += *d * learning_rate * -1.0; + } else { + *w++ += *d * learning_rate * i[k-1]; + } + } + + ++d; + } + + assert(w - ann->weight == ann->total_weights); + } + + + /* Train the hidden layers. */ + for (h = ann->hidden_layers - 1; h >= 0; --h) { + + /* Find first delta in this layer. */ + double const *d = ann->delta + (h * ann->hidden); + + /* Find first input to this layer. */ + double const *i = ann->output + (h + ? (ann->inputs + ann->hidden * (h-1)) + : 0); + + /* Find first weight to this layer. */ + double *w = ann->weight + (h + ? ((ann->inputs+1) * ann->hidden + (ann->hidden+1) * (ann->hidden) * (h-1)) + : 0); + + + for (j = 0; j < ann->hidden; ++j) { + for (k = 0; k < (h == 0 ? ann->inputs : ann->hidden) + 1; ++k) { + if (k == 0) { + *w++ += *d * learning_rate * -1.0; + } else { + *w++ += *d * learning_rate * i[k-1]; + } + } + ++d; + } + + } + +} + + +void genann_write(genann const *ann, FILE *out) { + fprintf(out, "%d %d %d %d", ann->inputs, ann->hidden_layers, ann->hidden, ann->outputs); + + int i; + for (i = 0; i < ann->total_weights; ++i) { + fprintf(out, " %.20e", ann->weight[i]); + } +} + + diff --git a/LegacyComponents/genann.h b/LegacyComponents/genann.h new file mode 100755 index 0000000000..3678ab60b4 --- /dev/null +++ b/LegacyComponents/genann.h @@ -0,0 +1,110 @@ +/* + * GENANN - Minimal C Artificial Neural Network + * + * Copyright (c) 2015, 2016 Lewis Van Winkle + * + * http://CodePlea.com + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgement in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + */ + + +#ifndef __GENANN_H__ +#define __GENANN_H__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef GENANN_RANDOM +/* We use the following for uniform random numbers between 0 and 1. + * If you have a better function, redefine this macro. */ +#define GENANN_RANDOM() (((double)rand())/RAND_MAX) +#endif + + +typedef double (*genann_actfun)(double a); + + +typedef struct genann { + /* How many inputs, outputs, and hidden neurons. */ + int inputs, hidden_layers, hidden, outputs; + + /* Which activation function to use for hidden neurons. Default: gennann_act_sigmoid_cached*/ + genann_actfun activation_hidden; + + /* Which activation function to use for output. Default: gennann_act_sigmoid_cached*/ + genann_actfun activation_output; + + /* Total number of weights, and size of weights buffer. */ + int total_weights; + + /* Total number of neurons + inputs and size of output buffer. */ + int total_neurons; + + /* All weights (total_weights long). */ + double *weight; + + /* Stores input array and output of each neuron (total_neurons long). */ + double *output; + + /* Stores delta of each hidden and output neuron (total_neurons - inputs long). */ + double *delta; + +} genann; + + + +/* Creates and returns a new ann. */ +genann *genann_init(int inputs, int hidden_layers, int hidden, int outputs); + +/* Creates ANN from file saved with genann_write. */ +genann *genann_read(FILE *in); + +/* Sets weights randomly. Called by init. */ +void genann_randomize(genann *ann); + +/* Returns a new copy of ann. */ +genann *genann_copy(genann const *ann); + +/* Frees the memory used by an ann. */ +void genann_free(genann *ann); + +/* Runs the feedforward algorithm to calculate the ann's output. */ +double const *genann_run(genann const *ann, double const *inputs); + +/* Does a single backprop update. */ +void genann_train(genann const *ann, double const *inputs, double const *desired_outputs, double learning_rate); + +/* Saves the ann. */ +void genann_write(genann const *ann, FILE *out); + + +double genann_act_sigmoid(double a); +double genann_act_sigmoid_cached(double a); +double genann_act_threshold(double a); +double genann_act_linear(double a); + + +#ifdef __cplusplus +} +#endif + +#endif /*__GENANN_H__*/ diff --git a/LegacyComponents/ocr.h b/LegacyComponents/ocr.h new file mode 100644 index 0000000000..5808d04eee --- /dev/null +++ b/LegacyComponents/ocr.h @@ -0,0 +1,11 @@ +#import + +#ifdef __cplusplus +extern "C" { +#endif + +NSString *recognizeMRZ(UIImage *input, CGRect *boundingRect); + +#ifdef __cplusplus +} +#endif diff --git a/LegacyComponents/ocr.mm b/LegacyComponents/ocr.mm new file mode 100755 index 0000000000..4469b5f8c7 --- /dev/null +++ b/LegacyComponents/ocr.mm @@ -0,0 +1,645 @@ +#import "ocr.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "fast-edge.h" +#include "genann.h" + +#import "LegacyComponentsInternal.h" + +#ifndef max +#define max(a, b) (a>b ? a : b) +#define min(a, b) (a detectLines(struct image* img, int threshold){ + // The size of the neighbourhood in which to search for other local maxima + const int neighbourhoodSize = 4; + + // How many discrete values of theta shall we check? + const int maxTheta = 180; + + // Using maxTheta, work out the step + const double thetaStep = M_PI / maxTheta; + + int width=img->width; + int height=img->height; + // Calculate the maximum height the hough array needs to have + int houghHeight = (int) (sqrt(2.0) * max(height, width)) / 2; + + // Double the height of the hough array to cope with negative r values + int doubleHeight = 2 * houghHeight; + + // Create the hough array + int* houghArray = new int[maxTheta*doubleHeight]; + memset(houghArray, 0, sizeof(int)*maxTheta*doubleHeight); + + // Find edge points and vote in array + int centerX = width / 2; + int centerY = height / 2; + + // Count how many points there are + int numPoints = 0; + + // cache the values of sin and cos for faster processing + double* sinCache = new double[maxTheta]; + double* cosCache = new double[maxTheta]; + for (int t = 0; t < maxTheta; t++) { + double realTheta = t * thetaStep; + sinCache[t] = sin(realTheta); + cosCache[t] = cos(realTheta); + } + + // Now find edge points and update the hough array + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + // Find non-black pixels + if ((img->pixel_data[y*width+x] & 0x000000ff) != 0) { + // Go through each value of theta + for (int t = 0; t < maxTheta; t++) { + + //Work out the r values for each theta step + int r = (int) (((x - centerX) * cosCache[t]) + ((y - centerY) * sinCache[t])); + + // this copes with negative values of r + r += houghHeight; + + if (r < 0 || r >= doubleHeight) continue; + + // Increment the hough array + houghArray[t*doubleHeight+r]++; + + } + + numPoints++; + } + } + } + + // Initialise the vector of lines that we'll return + std::vector lines; + + // Only proceed if the hough array is not empty + if (numPoints == 0){ + delete[] houghArray; + delete[] sinCache; + delete[] cosCache; + return lines; + } + + // Search for local peaks above threshold to draw + for (int t = 0; t < maxTheta; t++) { + //loop: + for (int r = neighbourhoodSize; r < doubleHeight - neighbourhoodSize; r++) { + + // Only consider points above threshold + if (houghArray[t*doubleHeight+r] > threshold) { + + int peak = houghArray[t*doubleHeight+r]; + + // Check that this peak is indeed the local maxima + for (int dx = -neighbourhoodSize; dx <= neighbourhoodSize; dx++) { + for (int dy = -neighbourhoodSize; dy <= neighbourhoodSize; dy++) { + int dt = t + dx; + int dr = r + dy; + if (dt < 0) dt = dt + maxTheta; + else if (dt >= maxTheta) dt = dt - maxTheta; + if (houghArray[dt*doubleHeight+dr] > peak) { + // found a bigger point nearby, skip + goto loop; + } + } + } + + // calculate the true value of theta + double theta = t * thetaStep; + + // add the line to the vector + line l={theta, (double)r-houghHeight}; + lines.push_back(l); + + } + loop: + continue; + } + } + + delete[] houghArray; + delete[] sinCache; + delete[] cosCache; + return lines; + } +} + +NSDictionary *findCornerPoints(UIImage *bitmap) { + CGImageRef imageRef = bitmap.CGImage; + uint32_t width = (uint32_t)CGImageGetWidth(imageRef); + uint32_t height = (uint32_t)CGImageGetHeight(imageRef); + + struct ocr::image imgIn, imgOut; + imgIn.width = imgOut.width = width; + imgIn.height = imgOut.height = height; + imgIn.pixel_data = (uint8_t *)malloc(width * height); + imgOut.pixel_data = (uint8_t *)calloc(width * height, 1); + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + uint8_t *bitmapPixels = (uint8_t *)calloc(height * width * 4, sizeof(unsigned char)); + NSUInteger bytesPerPixel = 4; + NSUInteger bytesPerRow = bytesPerPixel * width; + NSUInteger bitsPerComponent = 8; + CGContextRef context = CGBitmapContextCreate(bitmapPixels, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); + CGColorSpaceRelease(colorSpace); + + CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); + + CGContextRelease(context); + + for(unsigned int y=0;y> 8) + ((px & 0xFF0000) >> 16))/3); + } + } + + ocr::canny_edge_detect(&imgIn, &imgOut); + + std::vector lines=ocr::detectLines(&imgOut, 100); + for(NSUInteger i = 0; i < width * height; i++) { + imgOut.pixel_data[i]/=2; + } + std::vector> parallelGroups; + for(int i = 0; i < 36; i++) { + parallelGroups.emplace_back(); + } + ocr::line *left = NULL; + ocr::line *right = NULL; + ocr::line *top = NULL; + ocr::line *bottom = NULL; + for(std::vector::iterator l = lines.begin(); l!= lines.end();) { + // remove lines at irrelevant angles + if(!(l->theta>M_PI*0.4 && l->thetathetatheta>M_PI*0.9)){ + l=lines.erase(l); + continue; + } + // remove vertical lines close to the middle of the image + if((l->thetatheta>M_PI*0.9) && (uint32_t)abs((int)l->r) < height / 4){ + l=lines.erase(l); + continue; + } + // find the leftmost and rightmost lines + if(l->thetatheta>M_PI*0.9){ + double rk=l->theta<0.5 ? 1.0 : -1.0; + if(!left || left->r>l->r*rk){ + left=&*l; + } + if(!right || right->rr*rk){ + right=&*l; + } + } + // group parallel-ish lines with 5-degree increments + parallelGroups[(uint32_t)floor(l->theta / M_PI * 36)].push_back(*l); + ++l; + } + + // the text on the page tends to produce a lot of parallel lines - so we assume the top & bottom edges of the page + // are topmost & bottommost lines in the largest group of horizontal lines + std::vector& largestParallelGroup=parallelGroups[0]; + for(std::vector>::iterator group=parallelGroups.begin();group!=parallelGroups.end();++group){ + if(largestParallelGroup.size()size()) + largestParallelGroup=*group; + } + + for(std::vector::iterator l=largestParallelGroup.begin();l!=largestParallelGroup.end();++l){ + // If the image is horizontal, we assume it's just the data page or an ID card so we're going for the topmost line. + // If it's vertical, it likely contains both the data page and the page adjacent to it so we're going for the line that is closest to the center of the image. + // Nobody in their right mind is going to be taking vertical pictures of ID cards, right? + if(width>height){ + if(!top || top->r>l->r){ + top=&*l; + } + }else{ + if(!top || fabs(l->r)r)){ + top=&*l; + } + } + if(!bottom || bottom->rr){ + bottom=&*l; + } + } + + bool foundTopLeft=false, foundTopRight=false, foundBottomLeft=false, foundBottomRight=false; + NSMutableDictionary *points = [[NSMutableDictionary alloc] init]; + + if(top && bottom && left && right){ + //LOGI("bottom theta %f", bottom->theta); + if(bottom->theta>1.65 || bottom->theta<1.55){ + //LOGD("left: %f, right: %f\n", left->r, right->r); + double centerX=width/2.0; + double centerY=height/2.0; + double ltsin=sin(left->theta); + double ltcos=cos(left->theta); + double rtsin=sin(right->theta); + double rtcos=cos(right->theta); + double ttsin=sin(top->theta); + double ttcos=cos(top->theta); + double btsin=sin(bottom->theta); + double btcos=cos(bottom->theta); + for (int y = -((int)height)/4; y < (int)height; y++) { + int lx = (int) (((left->r - ((y - centerY) * ltsin)) / ltcos) + centerX); + int ty = (int) (((top->r - ((lx - centerX) * ttcos)) / ttsin) + centerY); + if(ty==y){ + points[@0]=@(lx); + points[@1]=@(y); + foundTopLeft=true; + if(foundTopRight) + break; + } + int rx = (int) (((right->r - ((y - centerY) * rtsin)) / rtcos) + centerX); + ty = (int) (((top->r - ((rx - centerX) * ttcos)) / ttsin) + centerY); + if(ty==y){ + points[@2]=@(rx); + points[@3]=@(y); + foundTopRight=true; + if(foundTopLeft) + break; + } + } + for (int y = height+height/3; y>=0; y--) { + int lx = (int) (((left->r - ((y - centerY) * ltsin)) / ltcos) + centerX); + int by = (int) (((bottom->r - ((lx - centerX) * btcos)) / btsin) + centerY); + if(by==y){ + points[@4]=@(lx); + points[@5]=@(y); + foundBottomLeft=true; + if(foundBottomRight) + break; + } + int rx = (int) (((right->r - ((y - centerY) * rtsin)) / rtcos) + centerX); + by = (int) (((bottom->r - ((rx - centerX) * btcos)) / btsin) + centerY); + if(by==y){ + points[@6]=@(rx); + points[@7]=@(y); + foundBottomRight=true; + if(foundBottomLeft) + break; + } + } + }else{ + //LOGD("No perspective correction needed"); + } + } + + free(imgIn.pixel_data); + free(imgOut.pixel_data); + + if(foundTopLeft && foundTopRight && foundBottomLeft && foundBottomRight) { + return points; + } + return nil; +} + +NSArray *binarizeAndFindCharacters(UIImage *inBmp, UIImage **outBinaryImage) { + CGImageRef imageRef = inBmp.CGImage; + uint32_t width = (uint32_t)CGImageGetWidth(imageRef); + uint32_t height = (uint32_t)CGImageGetHeight(imageRef); + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + uint8_t *bitmapPixels = (uint8_t *)calloc(height * width * 4, sizeof(unsigned char)); + NSUInteger bytesPerPixel = 4; + NSUInteger bytesPerRow = bytesPerPixel * width; + NSUInteger bitsPerComponent = 8; + CGContextRef context = CGBitmapContextCreate(bitmapPixels, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaNoneSkipFirst); + CGColorSpaceRelease(colorSpace); + CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); + + CGContextRelease(context); + + uint8_t *outPixels = (uint8_t *)malloc(width * height * 1); + + uint32_t histogram[256]={0}; + uint32_t intensitySum=0; + for(unsigned int y=0;y best_sigma) { + best_sigma = sigma; + threshold = thresh; + } + } + + for(unsigned int y=0;y0 + && outPixels[width * y + x +1]==0 + && outPixels[width * y + x -1]==0 + && outPixels[width * (y + 1) + x]==0 + && outPixels[width * (y - 1) + x]==0){ + outPixels[width * y + x]=0; + } + } + } + + if (outBinaryImage != nil) + { + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); + CGContextRef context = CGBitmapContextCreate(outPixels, width, height, 8, width, colorSpace, kCGImageAlphaNone); + CGColorSpaceRelease(colorSpace); + + CGImageRef imgRef = CGBitmapContextCreateImage(context); + UIImage *img = [UIImage imageWithCGImage:imgRef]; + CGImageRelease(imgRef); + CGContextRelease(context); + + *outBinaryImage = img; + } + // search from the bottom up for continuous areas of mostly empty pixels + unsigned int consecutiveEmptyRows=0; + std::vector> emptyAreaYs; + for(unsigned int y=height-1;y>=height/2;y--){ + unsigned int consecutiveEmptyPixels=0; + unsigned int maxEmptyPixels=0; + for(unsigned int x=0;xwidth/10*8){ + consecutiveEmptyRows++; + }else if(consecutiveEmptyRows>0){ + emptyAreaYs.emplace_back(y, y+consecutiveEmptyRows); + consecutiveEmptyRows=0; + } + } + + NSMutableArray *result = [[NSMutableArray alloc] init]; + // using the areas found above, do the same thing but horizontally and between them in an attempt to ultimately find the bounds of the MRZ characters + for(std::vector>::iterator p=emptyAreaYs.begin();p!=emptyAreaYs.end();++p){ + std::vector>::iterator next=std::next(p); + if(next!=emptyAreaYs.end()){ + unsigned int lineHeight=p->first-next->second; + // An MRZ line can't really be this thin so this probably isn't one + if(lineHeight<10) + continue; + unsigned int consecutiveEmptyCols=0; + std::vector> emptyAreaXs; + for(unsigned int x=0;xsecond;yfirst;y++){ + if(outPixels[width * y + x]==0){ + consecutiveEmptyPixels++; + }else{ + maxEmptyPixels=max(maxEmptyPixels, consecutiveEmptyPixels); + consecutiveEmptyPixels=0; + if(y>p->first-3) + bottomFilledPixels++; + } + } + maxEmptyPixels=max(maxEmptyPixels, consecutiveEmptyPixels); + if(lineHeight-maxEmptyPixels0){ + emptyAreaXs.emplace_back(x-consecutiveEmptyCols, x); + consecutiveEmptyCols=0; + } + } + if(consecutiveEmptyCols>0){ + emptyAreaXs.emplace_back(width-consecutiveEmptyCols, width); + } + if(emptyAreaXs.size()>30){ + bool foundLeftPadding=false; + NSMutableArray *rects = [[NSMutableArray alloc] init]; + for(std::vector>::iterator h=emptyAreaXs.begin();h!=emptyAreaXs.end();++h){ + std::vector>::iterator nextH=std::next(h); + if(!foundLeftPadding && h->second-h->first>width/35){ + foundLeftPadding=true; + }else if(foundLeftPadding && h->second-h->first>width/30){ + if(rects.count>=30){ + break; + }else{ + // restart the search because now we've (hopefully) found the real padding + [rects removeAllObjects]; + } + } + if(nextH!=emptyAreaXs.end() && foundLeftPadding){ + unsigned int top=next->second; + unsigned int bottom=p->first; + // move the top and bottom edges towards each other as part of normalization + for(unsigned int y=top;ysecond; xfirst; x++){ + if(outPixels[width * y + x]!=0){ + top=y; + found=true; + break; + } + } + if(found) + break; + } + for(unsigned int y=bottom;y>top;y--){ + bool found=false; + for(unsigned int x=h->second; xfirst; x++){ + if(outPixels[width * y + x]!=0){ + bottom=y; + found=true; + break; + } + } + if(found) + break; + } + if(bottom-topsecond, top, nextH->first - h->second, bottom - top); + [rects addObject:[NSValue valueWithCGRect:rect]]; + } + } + } + [result addObject:rects]; + if((rects.count>=44 && result.count == 2) || (rects.count>=30 && result.count==3)){ + break; + } + } + } + } + + free(outPixels); + + if(result.count == 0) + return NULL; + + return result; +} + +NSString *performRecognition(UIImage *bitmap, int numRows, int numCols) +{ + NSString *filePath = TGComponentsPathForResource(@"ocr_nn", @"bin"); + NSData *nnData = [NSData dataWithContentsOfFile:filePath]; + + struct genann* ann=genann_init(150, 1, 90, 37); + memcpy(ann->weight, nnData.bytes, sizeof(double)*ann->total_weights); + + NSMutableString *res = [[NSMutableString alloc] init]; + const char* alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890<"; + + CGImageRef imageRef = bitmap.CGImage; + uint32_t width = (uint32_t)CGImageGetWidth(imageRef); + uint32_t height = (uint32_t)CGImageGetHeight(imageRef); + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); + uint8_t *bitmapPixels = (uint8_t *)calloc(height * width * 1, sizeof(unsigned char)); + NSUInteger bytesPerPixel = 1; + NSUInteger bytesPerRow = bytesPerPixel * width; + NSUInteger bitsPerComponent = 8; + CGContextRef context = CGBitmapContextCreate(bitmapPixels, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaNone); + CGColorSpaceRelease(colorSpace); + + CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); + CGContextRelease(context); + + double nnInput[150]; + for(int row=0;row(col*10); + unsigned int offY=static_cast(row*15); + for(unsigned int y=0;y<15;y++){ + for(unsigned int x=0;x<10;x++){ + nnInput[y*10+x]=(double)bitmapPixels[bytesPerRow * (offY+y) + offX + x]/255.0; + } + } + const double* nnOut=genann_run(ann, nnInput); + unsigned int bestIndex=0; + for(unsigned int i=0;i<37;i++){ + if(nnOut[i]>nnOut[bestIndex]) + bestIndex=i; + } + + [res appendString:[NSString stringWithFormat:@"%c", alphabet[bestIndex]]]; + } + if(row!=numRows-1) + [res appendString:@"\n"]; + } + genann_free(ann); + return res; +} + +UIImage *normalizeImage(UIImage *image) +{ + if (image.imageOrientation == UIImageOrientationUp) return image; + + UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); + [image drawInRect:(CGRect){0, 0, image.size}]; + UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return normalizedImage; +} + +NSString *recognizeMRZ(UIImage *input, CGRect *outBoundingRect) +{ + input = normalizeImage(input); + + UIImage *binaryImage; + NSArray *charRects = binarizeAndFindCharacters(input, &binaryImage); + if (charRects.count == 0) + return nil; + + uint32_t width = 10 * (int)[charRects.firstObject count]; + uint32_t height = 15 * (int)charRects.count; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); + CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, width, colorSpace, kCGImageAlphaNone); + CGColorSpaceRelease(colorSpace); + + int x, y = 0; + for (NSArray *line in charRects) + { + x = 0; + for (NSValue *v in line) + { + CGRect rect = v.CGRectValue; + CGRect dest = CGRectMake(x * 10, y * 15, 10, 15); + + CGImageRef charImage = CGImageCreateWithImageInRect(binaryImage.CGImage, rect); + CGContextDrawImage(context, dest, charImage); + CGImageRelease(charImage); + + x++; + } + y++; + } + + CGImageRef charsImageRef = CGBitmapContextCreateImage(context); + CGContextRelease(context); + + UIImage *charsImage = [UIImage imageWithCGImage:charsImageRef]; + CGImageRelease(charsImageRef); + + NSString *result = performRecognition(charsImage, (int)charRects.count, (int)[charRects.firstObject count]); + if (result != nil && outBoundingRect != NULL) + { + CGRect firstRect = [[charRects.firstObject firstObject] CGRectValue]; + firstRect.origin.y = input.size.height - firstRect.origin.y; + CGRect lastRect = [[charRects.lastObject lastObject] CGRectValue]; + lastRect.origin.y = input.size.height - lastRect.origin.y; + CGRect boundingRect = CGRectMake(CGRectGetMinX(firstRect), CGRectGetMinY(firstRect), CGRectGetMaxX(lastRect) - CGRectGetMinX(firstRect), CGRectGetMaxY(lastRect) - CGRectGetMinY(firstRect)); + *outBoundingRect = boundingRect; + } + return result; +}