์ด์ „์— ์–ด๋А ์„œ๋น„์Šค์—์„œ๋„ ๊ทธ๋žฌ์ง€๋งŒ ๊ฐ€์ž…์‹œ ๋ฐ›์•„์•ผ ํ•˜๋Š” ์„œ๋ฅ˜ ์ค‘ ์ˆ˜๊ธฐ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋‹ค. ์ด๋ฒˆ ์„œ๋น„์Šค ๋˜ํ•œ ์ตœ๋Œ€ 5๊ฐœ์˜ ์„œ๋ฅ˜๋ฅผ ๋ฐ›์•„์•ผ ํ•˜๊ณ , ๊ทธ ์„œ๋ฅ˜๊ฐ€ ์œ ํšจํ•œ์ง€ ์ˆ˜๊ธฐ๋กœ ๊ฒ€์ฆ ํ›„์— ๊ฐ€์ž…์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค. 

  ์ฒจ๋ถ€ ํ›„ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณ„๋„๋กœ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ ์ฒจ๋ถ€๋จ- ์ƒํƒœ ํ‘œ์‹œ ํ›„ ๋์ด๊ธฐ ๋•Œ๋ฌธ์—, ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ํ„ฐ์น˜ ํ•œ๋ฒˆ ์ž˜ ๋ชป ํ•ด์„œ ์ด์ƒํ•œ ์‚ฌ์ง„์ด ๋“ค์–ด๊ฐ€๋ฉด ์•Œ ๋ฐฉ๋ฒ•์ด ์—†๋‹ค. ์ด ํ›„ CS์—์„œ๋Š” ์‚ฌ์šฉ์ž์™€ ์—ฐ๋ฝํ•˜๊ณ  ์ˆ˜์ •ํ•˜๋Š” ๊ณผ์ •์„ ๊ฑฐ์ณ์•ผ ํ•œ๋‹ค. 

  ์™„์ „ํžˆ VisionKit์—๊ฒŒ ๋งก๊ธฐ๋Š” ๊ฒƒ์€ ๋ฌด๋ฆฌ์ด๊ฒ ์ง€๋งŒ, ๊ธ€์ž๋ฅผ ์ถ”์ถœํ•ด์„œ ์˜์‹ฌ์Šค๋Ÿฌ์šด ์„œ๋ฅ˜๋Š” ์–ผ๋Ÿฟ ์ฐฝ ํ•œ๋ฒˆ ๋„์›Œ์„œ ์ธ์‹ ์‹œ์ผœ์ฃผ๋Š” ๊ฒƒ๋„ ๋‚˜์˜์ง€ ์•Š๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค. CS ํŒ€ ์—…๋ฌด๋ฅผ ์กฐ๊ธˆ ์ค„์—ฌ๋“ค์ผ ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ?! 

 

์• ํ”Œ์—์„œ ์ง€์›ํ•˜๋Š” VisionKit์œผ๋กœ ํ…์ŠคํŠธ๋ฅผ ์ธ์‹ํ•  ๋•Œ ์• ํ”Œ ์„œ๋ฒ„๋ฅผ ํƒ€๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ๊ธฐ๊ธฐ ๋‚ด๋ถ€์—์„œ ์ง„ํ–‰ํ•˜๊ฒŒ ๋œ๋‹ค. ๋ณด์•ˆ์ ์œผ๋กœ๋Š” ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์“ฐ๋Š” ๊ฒƒ ๋ณด๋‹ค ์•ˆ์ „ํ•˜์ง€ ์•Š์„๊นŒ ์‹ถ๋‹ค. 

 

1. ๋ฌธ์„œ ์Šค์บ๋„ˆ ๋งŒ๋“ค๊ธฐ

2. ์ด๋ฏธ์ง€ ํ…์ŠคํŠธ ์ธ์‹ํ•˜๊ธฐ

 


1. ๋ฌธ์„œ ์Šค์บ๋„ˆ ๋งŒ๋“ค๊ธฐ

VNDocumentCameraViewController

VNDocumentCameraViewController๋Š” VisionKit์—์„œ ์ œ๊ณตํ•œ๋‹ค. ๋ฌธ์„œ ์Šค์บ”์„ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์นด๋ฉ”๋ผ UI๋ฅผ ์ œ๊ณตํ•ด ์ค€๋‹ค. 

struct DocumentScannerView: UIViewControllerRepresentable {
}

 

SwiftUI์—์„œ ์‚ฌ์šฉํ•˜๋ ค๋ฉด UIViewControllerRepresentable๋กœ ๊ตฌํ˜„ํ•ด ์ฃผ์–ด์•ผ ํ•œ๋‹ค. 

    @Binding var recognizedText: String

    func makeUIViewController(context: Context) -> VNDocumentCameraViewController {
        let viewController = VNDocumentCameraViewController()
        viewController.delegate = context.coordinator
        return viewController
    }

    func updateUIViewController(_ uiViewController: VNDocumentCameraViewController, context: Context) { }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

recognizedText๋Š” VisionKit์ด ์ธ์‹ํ•œ ํ…์ŠคํŠธ๊ฐ€ ๋‹ด๊ธฐ๊ฒŒ ๋˜๊ณ , ๋ฐ”์ธ๋”ฉ์œผ๋กœ ์—ฐ๊ฒฐ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด ๊ฐ’์ด ์—…๋ฐ์ดํŠธ ๋˜๋ฉด recognizedText State๋ฅผ ์‚ฌ์šฉํ•˜๋Š” View ๋˜ํ•œ ์—…๋ฐ์ดํŠธ ๋˜์–ด ํ™”๋ฉด์— ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค

 

class Coordinator: NSObject, VNDocumentCameraViewControllerDelegate { }

 

Coordinator ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค. VNDocumentCameraViewController์˜ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•ด ์ค€๋‹ค.

        var parent: DocumentScannerView

        init(_ parent: DocumentScannerView) {
            self.parent = parent
        }

Parent๋Š” DocumentScannerView

 

        func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
            for pageIndex in 0..<scan.pageCount {
                let image = scan.imageOfPage(at: pageIndex)
                recognizeText(in: image)
            }
            controller.dismiss(animated: true)
        }

document camera๋กœ ๋ถ€ํ„ฐ ์Šค์บ”๋œ ๋ฌธ์„œ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋˜๋ฉด ์ด ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. ์—ฌ๋Ÿฌ์žฅ์„ ์Šค์บ”ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— scan๋œ ์ด๋ฏธ์ง€๋“ค์ด scan์ด๋ผ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋‹ด๊ฒจ ์˜จ๋‹ค. scan.imageOfPage(at: index)์€ UIImage๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋œ๋‹ค.

 

        private func recognizeText(in image: UIImage) {
            guard let cgImage = image.cgImage else { return }

            let request = VNRecognizeTextRequest { (request, error) in
                guard let observations = request.results as? [VNRecognizedTextObservation],
                      error == nil else { return }

                let recognizedStrings = observations.compactMap { observation in
                    observation.topCandidates(1).first?.string
                }

                DispatchQueue.main.async {
                    self.parent.recognizedText += recognizedStrings.joined(separator: "\n")
                }
            }
            
            request.recognitionLanguages = ["ko"]
            request.usesLanguageCorrection = true

            let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])
            do {
                try requestHandler.perform([request])
            } catch {
                print("\(error).")
            }
        }

 

VNRecognizeTextRequest๋Š” Vision ํ”„๋ ˆ์ž„์›Œํฌ์— ํฌํ•จ๋˜์–ด ์žˆ๊ณ  ์ด๋ฅผ ์ด์šฉํ•˜๋ฉด ์ด๋ฏธ์ง€์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ๋‹ค. 

 

VNRecognizeTextRequest ๋‹จ๊ณ„

1. request ์ƒ์„ฑ

ํ…์ŠคํŠธ๋ฅผ ์ฝ์€ ๋’ค ๋ฌด์—‡์„ ํ• ์ง€ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๋‹จ๊ณ„์ด๋‹ค. 

            let request = VNRecognizeTextRequest { (request, error) in
                guard let observations = request.results as? [VNRecognizedTextObservation],
                      error == nil else { return }

                let recognizedStrings = observations.compactMap { observation in
                    observation.topCandidates(1).first?.string
                }

                DispatchQueue.main.async {
                    self.parent.recognizedText += recognizedStrings.joined(separator: "\n")
                }
            }

์ด๋ฏธ์ง€๋กœ ๋ถ€ํ„ฐ ํ…์ŠคํŠธ๋ฅผ ์ธ์‹ํ•œ ๋’ค recognizedText ๋ฐ”์ธ๋”ฉ ๊ฐ’์„ ์—…๋ฐ์ดํŠธ ํ•ด์ฃผ๋Š” ๋กœ์ง์ด๋‹ค. 

 

2. ์†์„ฑ ์ง€์ •ํ•˜๊ธฐ

            request.recognitionLanguages = ["ko"]
            request.usesLanguageCorrection = true

๊ผญ ํ•œ๊ตญ์–ด๊ฐ€ ํฌํ•จ๋  ์ˆ˜ ์žˆ๋„๋ก recognitionLanguages์— ์ง€์ •ํ•ด ์ฃผ์–ด์•ผ ํ•œ๋‹ค! ๊ฐ™์€ ์ด๋ฏธ์ง€๋ผ ํ• ์ง€๋ผ๋„ ํ•œ๊ตญ์–ด๋ฅผ ์˜์–ด๋กœ ์ธ์‹ํ–ˆ์„ ๋•Œ๋ž‘ ํ•œ๊ตญ์–ด๋กœ ์ธ์‹ํ–ˆ์„ ๋•Œ๋ž‘ ๊ฒฐ๊ณผ๊ฐ€ ์™„์ „ํžˆ ๋‹ค๋ฅด๋‹ค!

 

3. requestHandler ์ƒ์„ฑํ•˜๊ธฐ

let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])

 

์ด๋ฏธ์ง€์™€ ์˜ต์…˜์„ ์ง€์ •

 

4. ์ƒ์„ฑ ํ•œ request๋ฅผ handler ํ†ตํ•ด ์‹คํ–‰ํ•˜๊ธฐ

            do {
                try requestHandler.perform([request])
            } catch {
                print("\(error).")
            }

perform ์— request๋“ค์„ ๋‹ด์•„ ์ฃผ๋ฉด ๋œ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด request์—์„œ ์ง€์ •ํ•œ ํƒœ์Šคํฌ๊ฐ€ ์‹คํ–‰๋œ๋‹ค.

 

ContentView

struct ContentView: View {
    @State private var recognizedText = ""
    @State private var isShowDocumentScanner = false

    var body: some View {
        VStack {
            ScrollView {
                Text(recognizedText)
            }

            Button("Scan Documents") {
                isShowDocumentScanner = true
                recognizedText = ""
            }
            .sheet(isPresented: $isShowDocumentScanner) {
                DocumentScannerView(recognizedText: $recognizedText)
            }
        }
        .padding()
    }
}

 

 

๋„ค๋ชจ๋‚œ ์˜์—ญ ์ธ์‹ํ•˜๋ฉด ์Šค์บ๋„ˆ๊ฐ€ ๋ƒ…๋‹ค ์ฐ์–ด๋ฒ„๋ฆฐ๋‹ค. ์Šค์บ๋„ˆ ์นด๋ฉ”๋ผ ๋„ˆ๋ฌด ์ž˜ ๋งŒ๋“ค์–ด ๋†“์Œ;;; ์• ํ”Œ ๊ฐ์‚ฌํ•ด์šฉ


 

์ด๋ฏธ์ง€์—์„œ ํ…์ŠคํŠธ ์ถ”์ถœํ•˜๊ธฐ

์•ž์—์„œ VNRecognizeTextRequest ํ…์ŠคํŠธ ์ถ”์ถœํ•˜๋Š” ์ฝ”๋“œ๋งŒ ๋ถ„๋ฆฌํ•˜๋ฉด

    private func recognizeText(in image: UIImage?) {
        guard let cgImage = image?.cgImage else { return }

        let request = VNRecognizeTextRequest { (request, error) in
            guard let observations = request.results as? [VNRecognizedTextObservation],
                  error == nil else { return }

            let text = observations.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n")

            DispatchQueue.main.async {
                print(text)
                self.recognizedText = text
            }
        }

        request.recognitionLanguages = ["ko"]
        request.usesLanguageCorrection = true

        let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])
        do {
            try requestHandler.perform([request])
        } catch {
            print("\(error)")
        }
    }

 

SwiftUI ์—์„œ ๋ฐ”๋กœ ์จ์ฃผ๋ฉด ๋œ๋‹ค

    @State private var selectedImage: UIImage? = UIImage()
    @State private var isShowingImagePicker = false
    @State private var recognizedText: String = ""

    var body: some View {
        VStack {
            if let selectedImage = selectedImage {
                Image(uiImage: selectedImage)
                    .resizable()
                    .scaledToFit()
                    .onChange(of: selectedImage) { image in
                        recognizedText = ""
                        recognizeText(in: image)
                    }
            }

            Button("Select Image") {
                isShowingImagePicker = true
            }

            if !recognizedText.isEmpty {
                Text(recognizedText)
                    .padding()
            }
        }
        .sheet(isPresented: $isShowingImagePicker) {
            ImagePicker(selectedImage: $selectedImage)
        }
    }

ImagePicker์— UIImage ํƒ€์ž…์˜ State๋ฅผ ์ „๋‹ฌํ•˜๋ฉด ์„ ํƒํ•œ ์‚ฌ์ง„์„ ์‰ฝ๊ฒŒ observing ํ•  ์ˆ˜ ์žˆ๋‹ค ๐Ÿฅน

 

 

๋ช‡ ๊ฐœ ๊ธ€์ž ๋นผ๊ณ  ์•„์ฃผ ์ž˜ ์ถ”์ถœ ๋ฌ๋‹ค. 

์•„ ์• ํ”Œ ๋„ˆ๋ฌด ์ข‹์•„....

references

https://developer.apple.com/documentation/visionkit

 

VisionKit | Apple Developer Documentation

Identify and extract information in the environment using the deviceโ€™s camera, or in images that your app displays.

developer.apple.com

https://gwonran.tistory.com/54

 

Swift] privacy-sensitive data ํ•ด๊ฒฐํ•˜๋Š” ๋ฒ•

This app has usage desctiption. The app's Info.plist must contain an NSCameraUsageDescription key with a string value explaning to the user how the app uses this data. ์ด๋Ÿฐ ์—๋Ÿฌ๊ฐ€ ๋–ด๋‹ค. ๋ฐ”๋กœ ๊ตฌ๊ธ€๋ง์— ์ฐฉ์ˆ˜ํ–ˆ๋‹ค. [iOS tip] Privacy-sensitive

gwonran.tistory.com

https://ohwhatisthis.tistory.com/17

 

[iOS/Swift] VisionKit OCR Api ์˜ˆ์ œ ํ•œ๊ธ€

Swift ์–ธ์–ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Apple Developer์—์„œ ์ œ๊ณตํ•˜๋Š” VisionKit Framework๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ OCR ๊ฐœ๋ฐœ ์„œ๋ก  VisionKit์€ Apple์—์„œ ๊ฐœ๋ฐœํ•œ OCR Api๋กœ VisionKit์€ ์ด๋ฏธ์ง€์™€ iOS ์นด๋ฉ”๋ผ์˜ Live Video ์™€ Text ๋ฐ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ

ohwhatisthis.tistory.com

 

 

 

ํ˜น์‹œ ๊ธ€์— ๋ฌธ์ œ ์žˆ๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•ด์•ผ ํ•  ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด

์•Œ๋ ค์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค :)

'iOS ๐ŸŽ > Library' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[SwiftUI/TipKit] ์‚ฌ์šฉ์ž์—๊ฒŒ Tip ์ œ๊ณตํ•˜๊ธฐ  (1) 2023.12.02
[SwiftData] Query and Filtering Data  (0) 2023.10.24