Skip to content

Commit

Permalink
[Example] Fix the bounding rects sizing issue
Browse files Browse the repository at this point in the history
Also update comments and slightly refactor the scanner package
  • Loading branch information
romanmazeev committed Feb 8, 2025
1 parent 4de797a commit ff489ef
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 131 deletions.
3 changes: 2 additions & 1 deletion Example/MRZScannerExample/CameraView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ final class CameraViewController: UIViewController {
}

override func viewDidLoad() {
super.viewDidLoad()

previewLayer.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.connection?.videoRotationAngle = 90
view.layer.addSublayer(previewLayer)
}
Expand Down
38 changes: 22 additions & 16 deletions Example/MRZScannerExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,24 @@ struct ContentView: View {
}()

@StateObject private var viewModel = ViewModel()
@State private var cameraRect: CGRect?
@State private var mrzRect: CGRect?

var body: some View {
GeometryReader { proxy in
Group {
if let cameraRect {
CameraView(captureSession: viewModel.captureSession)
.frame(width: cameraRect.width, height: cameraRect.height)
}
CameraView(captureSession: viewModel.captureSession)

ZStack {
Color.black.opacity(0.5)

if let mrzRect {
Rectangle()
.blendMode(.destinationOut)
.frame(width: mrzRect.size.width, height: mrzRect.size.height)
.position(mrzRect.origin)
.frame(width: mrzRect.width, height: mrzRect.height)
.position(x: mrzRect.origin.x + mrzRect.width / 2,
y: mrzRect.origin.y + mrzRect.height / 2)
.task {
guard let cameraRect else { return }

await viewModel.startMRZScanning(cameraRect: cameraRect, mrzRect: mrzRect)
await viewModel.startMRZScanning(mrzRect: mrzRect)
}
}
}
Expand All @@ -56,9 +51,19 @@ struct ContentView: View {
}
}
.onAppear {
cameraRect = proxy.frame(in: .global)
mrzRect = .init(origin: .init(x: proxy.size.width / 2, y: proxy.size.height / 2),
size: .init(width: proxy.size.width - 40, height: proxy.size.width / 5))
let cameraRect = proxy.frame(in: .global)

let mrzRectWidth = cameraRect.width - 40
let mrzRectHeight: CGFloat = 65
let mrzRect = CGRect(
x: (cameraRect.width - mrzRectWidth) / 2, // Center horizontally
y: (cameraRect.height - mrzRectHeight) / 2, // Center vertically
width: mrzRectWidth,
height: mrzRectHeight
)
self.mrzRect = mrzRect

viewModel.setContentRects(cameraRect: cameraRect, mrzRect: mrzRect)
}
}
.alert(isPresented: .init(get: { viewModel.result != nil }, set: { _ in viewModel.result = nil })) {
Expand All @@ -67,9 +72,9 @@ struct ContentView: View {
message: Text(createAlertMessage(result: viewModel.result!)),
dismissButton: .default(Text("Restart scanning")) {
Task {
guard let cameraRect, let mrzRect else { return }
guard let mrzRect else { return }

await viewModel.startMRZScanning(cameraRect: cameraRect, mrzRect: mrzRect)
await viewModel.startMRZScanning(mrzRect: mrzRect)
}
}
)
Expand All @@ -85,7 +90,8 @@ struct ContentView: View {
Rectangle()
.stroke(color)
.frame(width: rect.width, height: rect.height)
.position(rect.origin)
.position(x: rect.origin.x + rect.width / 2,
y: rect.origin.y + rect.height / 2)
}

private func createAlertTitle(result: Result<ParserResult, Error>) -> String {
Expand Down
86 changes: 60 additions & 26 deletions Example/MRZScannerExample/ViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ final class ViewModel: ObservableObject {
@Published var boundingRects: ScannedBoundingRects?
@Published var result: Result<ParserResult, Error>?

private var cameraRect: CGRect?

func startCamera() async {
do {
try await camera.startCamera()
Expand All @@ -28,28 +30,44 @@ final class ViewModel: ObservableObject {
}
}

func startMRZScanning(cameraRect: CGRect, mrzRect: CGRect) async {
func setContentRects(cameraRect: CGRect, mrzRect: CGRect) {
self.cameraRect = cameraRect
}

func startMRZScanning(mrzRect: CGRect) async {
do {
try await scanImageStream(camera.imageStream, cameraRect: cameraRect, mrzRect: mrzRect)
try await scanImageStream(camera.imageStream, mrzRect: mrzRect)
} catch {
result = .failure(error)
}
}

private func scanImageStream(_ imageStream: AsyncStream<CIImage>, cameraRect: CGRect, mrzRect: CGRect) async throws {
private func scanImageStream(_ imageStream: AsyncStream<CIImage>, mrzRect: CGRect) async throws {
guard let cameraRect else {
throw ScanningError.cameraRectNotSet
}

// Convert from view coordinates to normalized coordinates.
let normalisedMRZRect = VNNormalizedRectForImageRect(
convertRect(mrzRect, to: .bottom, containerHeight: cameraRect.height),
Int(cameraRect.width),
Int(cameraRect.height)
)

for try await scanningResult in imageStream.scanForMRZCode(
configuration: .init(
orientation: .up,
regionOfInterest: VNNormalizedRectForImageRect(
correctCoordinates(to: .leftTop, rect: mrzRect),
Int(cameraRect.width),
Int(cameraRect.height)
),
regionOfInterest: normalisedMRZRect,
minimumTextHeight: 0.1,
recognitionLevel: .fast
)
) {
boundingRects = correctBoundingRects(to: .center, rects: scanningResult.boundingRects, mrzRect: mrzRect)
boundingRects = correctBoundingRects(
rects: scanningResult.boundingRects,
normalisedMRZRect: normalisedMRZRect,
cameraRect: cameraRect
)

if let bestResult = scanningResult.best(repetitions: 5) {
result = .success(bestResult)
boundingRects = nil
Expand All @@ -58,29 +76,45 @@ final class ViewModel: ObservableObject {
}
}

// MARK: - Correct CGRect origin from top left to center

enum CorrectionType {
case center
case leftTop
enum CoordinateSystem {
case bottom
case top
}

private func correctBoundingRects(to type: CorrectionType, rects: ScannedBoundingRects, mrzRect: CGRect) -> ScannedBoundingRects {
let convertedCoordinates = rects.convertedToImageRects(imageWidth: Int(mrzRect.width), imageHeight: Int(mrzRect.height))
let correctedMRZRect = correctCoordinates(to: .leftTop, rect: mrzRect)
private func correctBoundingRects(
rects: ScannedBoundingRects,
normalisedMRZRect: CGRect,
cameraRect: CGRect
) -> ScannedBoundingRects {
func translateToRootView(normalizedRect: CGRect) -> CGRect {
// Convert from normalized coordinates inside the MRZ to view coordinates inside the cameraRect.
let imageRect = VNImageRectForNormalizedRectUsingRegionOfInterest(
normalizedRect,
Int(cameraRect.width),
Int(cameraRect.height),
normalisedMRZRect
)

func correctRects(_ rects: [CGRect]) -> [CGRect] {
rects
.map { correctCoordinates(to: type, rect: $0) }
.map { .init(origin: .init(x: $0.origin.x + correctedMRZRect.minX, y: $0.origin.y + correctedMRZRect.minY), size: $0.size) }
return convertRect(imageRect, to: .top, containerHeight: cameraRect.height)
}

return .init(valid: correctRects(convertedCoordinates.valid), invalid: correctRects(convertedCoordinates.invalid))
return ScannedBoundingRects(
valid: rects.valid.map { translateToRootView(normalizedRect: $0) },
invalid: rects.invalid.map { translateToRootView(normalizedRect: $0) }
)
}

private func correctCoordinates(to type: CorrectionType, rect: CGRect) -> CGRect {
let x = type == .center ? rect.minX + rect.width / 2 : rect.minX - rect.width / 2
let y = type == .center ? rect.minY + rect.height / 2 : rect.minY - rect.height / 2
return CGRect(origin: .init(x: x, y: y), size: rect.size)
/// Converts a rectangle's Y-coordinate between top-based and bottom-based coordinate systems.
private func convertRect(_ rect: CGRect, to coordinateSystem: CoordinateSystem, containerHeight: CGFloat) -> CGRect {
.init(
x: rect.origin.x,
y: coordinateSystem == .top ? containerHeight - rect.origin.y - rect.height : containerHeight - rect.maxY,
width: rect.width,
height: rect.height
)
}
}

enum ScanningError: Error {
case cameraRectNotSet
}
20 changes: 0 additions & 20 deletions Sources/MRZScanner/Public/ScannedBoundingRectsHelper.swift

This file was deleted.

6 changes: 3 additions & 3 deletions Sources/MRZScanner/Public/Scanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import CoreImage
import Dependencies
import Vision

/// Configuration for scanning
/// Configuration for scanning.
public struct ScanningConfiguration: Sendable {
let orientation: CGImagePropertyOrientation
let regionOfInterest: CGRect
Expand All @@ -24,7 +24,7 @@ public struct ScanningConfiguration: Sendable {
}
}

// MARK: Image stream scanning
// MARK: - Image stream scanning

public extension AsyncStream<CIImage> {
func scanForMRZCode(configuration: ScanningConfiguration) -> AsyncThrowingStream<ScanningResult<TrackerResult>, Error> {
Expand Down Expand Up @@ -52,7 +52,7 @@ public extension AsyncStream<CIImage> {
}
}

// MARK: Single image scanning
// MARK: - Single image scanning

public extension CIImage {
func scanForMRZCode(configuration: ScanningConfiguration) async throws -> ScanningResult<ParserResult>? {
Expand Down
15 changes: 8 additions & 7 deletions Sources/MRZScanner/Public/ScanningResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import CoreImage
import MRZParser

/// Bounding rectangles of the scanned text
/// Bounding rectangles of the scanned text.
/// - Note: Rects are normalized and use a bottom-right coordinate system.
public struct ScannedBoundingRects: Sendable {
public let valid: [CGRect], invalid: [CGRect]

Expand All @@ -18,18 +19,18 @@ public struct ScannedBoundingRects: Sendable {
}
}

/// Represents the result of the scanning process
/// - Note: In case of MRZ scanning, the result is sent as each frame is scanned
/// Represents the result of the scanning process.
/// - Note: In case of MRZ scanning, the result is sent as each frame is scanned.
public struct ScanningResult<T: Sendable>: Sendable {
/// Results of scanning
/// Results of scanning.
public let results: T
public let boundingRects: ScannedBoundingRects
}

/// Sets the text style
/// - Parameter repetitions: Minimum number of repetitions that the result should be found
/// - Returns: The best result
public extension ScanningResult where T == TrackerResult {
/// Returns the best result that has been seen at least `repetitions` times.
/// - Parameter repetitions: Minimum number of repetitions that the result should be found.
/// - Returns: The best result.
func best(repetitions: Int) -> ParserResult? {
guard let maxElement = results.max(by: { $0.value < $1.value }) else {
return nil
Expand Down
58 changes: 0 additions & 58 deletions Tests/MRZScannerTests/Private/BoundingRectConverterTests.swift

This file was deleted.

7 changes: 7 additions & 0 deletions Tests/MRZScannerTests/Public/ScannerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,10 @@ extension ScanningConfiguration: Equatable {
lhs.recognitionLevel == rhs.recognitionLevel
}
}

extension ScannedBoundingRects: Equatable {
public static func == (lhs: ScannedBoundingRects, rhs: ScannedBoundingRects) -> Bool {
lhs.valid == rhs.valid &&
lhs.invalid == rhs.invalid
}
}

0 comments on commit ff489ef

Please sign in to comment.