소개
이 게시물은 오랫동안 내 초안 폴더에 있었습니다. 올해 초 언젠가 누군가가 Apple의 비전 프레임워크를 사용하여 텍스트 인식을 수행하려는 채용 공고를 보았습니다. 저는 그 직업에 관심이 없었지만, Vision 프레임워크를 한 번도 사용해 본 적이 없었기 때문에 궁
금했습니다.
이 기사에서는 PyObjC를 사용하여 Apple Vision Framework와 인터페이스하고 이미지에서 텍스트를 감지하는 스크립트를 만드는 방법을 살펴보겠습니다. 이 과정에서 PyObjC가 어떻게 작동하고 Objective C에서 Python으로 함수와 메서드를 매핑하는 방법을 배우게 됩니다.
이 기사가 끝나면 이러한 이미지를 입력으로 사용하는 스크립트를 작성할 수 있을 만큼 충분히 알게 될 것입니다.
그리고 Vision Framework를 사용하여 OCR을 실행하고 오버레이된 경계 상자가 있는 출력 이미지를 생성합니다.
또한 터미널에서 감지 된 텍스트를 인쇄합니다.
• Hi everyone!
I am Yasoob! You might know me from Practical Python Projects or the Intermediate Python book.
Welcome to my personal blog which is going to be the new home for all of my old and new articles. You
can turn on "dark mode" by clicking on the moon (right before the RSS icon) in the side-bar. I hope you
enjoy your stay g
Source
더 이상 고민하지 않고 시작하겠습니다!
면책 조항: 이 프로젝트의 절반 정도를 진행하는 동안 GitHub에서 Vision Framework와 인터페이스하는 멋진 osxphotos 프로젝트를 발견했습니다. 필자는 이 기사에 있는 대부분의 코드를 독자적으로 개발했지만, 이 프로젝트에 대해 알게 된 후 이 프로젝트에서 많은 영감을 얻었다.osxphotos
Apple Vision Framework란 무엇입니까?
iOS 및 macOS에서 사용하도록 특별히 설계된 Apple의 독점 프레임워크입니다. 이미지 분석 및 컴퓨터 비전 기능을 앱에 빠르고 쉽게 추가하는 데 사용할 수 있는 다양한 사전 학습된 모델 및 API를 제공합니다.
저는 Objective C 또는 Swift 프로그래머가 아니기 때문에이 프레임 워크에 대해 들었을 때 바로 다음 질문은 Python에서 사용할 수 있는지 여부였습니다. 결과적으로 프레임워크는 전용 Python 라이브러리를 통해 노출되지 않습니다. 그러나 PyObjC를 통해 사용할 수 있습니다. PyObjC를 사용하거나 들어본 적이 없다면 Python과 Objective C 사이에 다리를 제공하고 macOS에서 대부분의 Apple 프레임워크에 바인딩을 제공합니다.
Python에서 Apple Vision Framework를 사용하는 방법
이 자습서에서는 다음 라이브러리에 의존할 것입니다.
- PyObjC
- PIL/Pillow (OCR 결과 시각화)
다음 PIP 명령을 사용하여 이 두 가지를 모두 설치할 수 있습니다.
$ pip install Pillow pyobjc
이미 언급했듯이 PyObjC는 Python과 Objective-C 사이의 다리입니다. 이를 통해 모든 기능을 갖춘 Cocoa 애플리케이션을 순수 Python으로 작성할 수 있습니다. Apple 머신에서 대부분의 Objective C 클래스 및 프레임워크에 대한 래퍼를 제공합니다. 우리는 Python을 통해 기본 Apple 프레임워크와 클래스를 사용하기 위해 그것에 의존할 것입니다.
새 Python 파일을 만들고 다음 가져오기를 추가합니다.
import Quartz
from Foundation import NSURL, NSRange
import Vision
from PIL import Image, ImageDraw
Quartz, Vision 및 Foundation 패키지는 모두 라이브러리에서 가져옵니다. 다음 기능에 대한 액세스를 제공합니다.pyobjc
- Quartz(Mac에서 그래픽 작업을 위한 도구 제공)
- Foundation(다양한 핵심 데이터 유형에 대한 액세스 제공)
- Vision(비전 프레임워크에 대한 액세스 제공)
Vision Framework를 사용하여 이미지의 텍스트를 인식하기 위한 공식 Apple 튜토리얼을 중심으로 이 전체 튜토리얼을 기반으로 할 것입니다. 유일한 차이점은 공식 문서가 Swift를 사용하는 반면 Python을 사용한다는 것입니다.
텍스트 인식 프로세스에는 광범위하게 VNRecognizeTextRequest 및 VNImageRequestHandler를 사용하는 것이 포함됩니다. 이미지 기반 입력을 받아 해당 이미지에서 텍스트를 찾아 추출하고 이 요청을 수행합니다.VNRecognizeTextRequestVNImageRequestHandler
Apple 문서에 따르면 텍스트 인식을 수행하는 코드는 다음과 같습니다.
// Get the CGImage on which to perform requests.
guard let cgImage = UIImage(named: "snapshot")?.cgImage else { return }
// Create a new image-request handler.
let requestHandler = VNImageRequestHandler(cgImage: cgImage)
// Create a new request to recognize text.
let request = VNRecognizeTextRequest(completionHandler: recognizeTextHandler)
do {
// Perform the text-recognition request.
try requestHandler.perform([request])
} catch {
print("Unable to perform the requests: \(error).")
}
그리고 (응답 처리를 위해) 다음과 같이 보입니다.recognizeTextHandler
func recognizeTextHandler(request: VNRequest, error: Error?) {
guard let observations =
request.results as? [VNRecognizedTextObservation] else {
return
}
let recognizedStrings = observations.compactMap { observation in
// Return the string of the top VNRecognizedText instance.
return observation.topCandidates(1).first?.string
}
// Process the recognized strings.
processResults(recognizedStrings)
}
우리의 목표는 PyObjC를 사용하여 이러한 모든 단계를 복제하는 것입니다. 첫 번째 문제는 Apple 문서가 의 일부인 것을 사용한다는 것입니다. 그러나 pyobjc는 현재 이 프레임워크에 대한 래퍼를 제공하지 않습니다. 이 주장에 대한 신뢰할 수 있는 출처를 찾기 위해 약간의 파헤쳐야 했습니다. 공식 pyobjc 문서에 언급되어 있지만이 페이지를 찾는 데 시간이 걸렸습니다.UIImageUIKit
즉, 디스크의 파일에서 CGImage 인스턴스를 가져 오는 다른 방법을 찾아야합니다.
몇 가지 연구를 한 후, 나는 a를 사용하여 초기화 한 다음 디스크의 파일에서 a를 만들 수 있다는 것을 알았습니다. 코드는 다음과 같습니다.VNImageRequestHandlerCIImageCIImage
# Get the CIImage on which to perform requests.
input_url = NSURL.fileURLWithPath_(img_path)
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
# Create a new image-request handler.
request_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
input_image, None
)
이 코드는 Swift 예제에 비해 상당히 장황해 보일 수 있지만 pyobjc의 작동 방식을 이해하려고 하면 이해가 되기 시작할 것입니다. pyobjc 문서의 이 페이지를 읽은 다음 다시 돌아오십시오. 이제 위의 코드를 단계별로 살펴보겠습니다.
OCR을 수행하려는 이미지 파일의 경로를 전달하여 먼저 만듭니다. PyObjC는 인자를 취하는 모든 메서드 호출(함수 호출이 아닌)에 후행을 추가합니다. 다음으로, 의 메소드에 전달합니다. 마지막으로, 이것을 의 메소드에 전달합니다.NSURL_NSURLimageWithContentsOfURL_CIImageCIImageinitWithCIImage_options_VNImageRequestHandler
Objective C는 두 단계로 개체 초기화를 수행합니다. 첫 번째 단계에서는 약간의 공간을 할당한 다음 개체를 초기화합니다. 스위프트는 이 모든 과정을 감추고 있습니다. PyObjC를 사용하면 필요에 따라 수정된 형식을 먼저 호출한 다음 호출하여 동일한 2단계 절차를 따를 수 있습니다.alloc()init
공식 문서 에 따르면 VNImageRequestHandler 에는 꽤 많은 이니셜 라이저가 있습니다.
PyObjC를 사용하면 인수 이름에 추가한 다음 그 뒤에 추가하고 각 인수 이름에 a를 추가하여 이러한 이니셜라이저 중 하나를 사용할 수 있습니다. 예를 들어, 및 인수로 호출하려면 를 사용할 수 있으며 및 인수로 호출하려면 를 사용할 수 있습니다. 이 형식을 내면화하면 거의 또는 전혀 노력하지 않고 모든 Objective C 메서드/함수 호출을 pyobjc 형식으로 변환할 수 있습니다.Withinit_initciImageoptionsinitWithCIImage_options_initciImageorientationoptionsinitWithCIImage_orientation_options_
다음으로 Python 기반 함수를 만들어야 합니다. 이것이 내가 끝낸 것입니다.recognizeTextHandler
def recognize_text_handler(request, error):
observations = request.results()
results = []
for observation in observations:
recognized_text = observation.topCandidates_(1)[0]
results.append([recognized_text.string(), recognized_text.confidence()])
print(results)
이제 새로운 Vision 요청을 생성하고 전달해야 합니다.recognizeTextHandler
# Create a new request to recognize text.
request = Vision.VNRecognizeTextRequest.alloc().initWithCompletionHandler_(recognize_text_handler)
VNRequest 수퍼 클래스에서 상속되고 해당 수퍼 클래스에는 인수를 사용하는 init 메소드가 있기 때문에 사용할 수 있다는 것을 알고 있습니다.initWithCompletionHandlerVNRecognizeTextRequestcompletionHandler
이제 요청을 시작하는 일만 남았습니다. perform 메소드에 해당하는 Python을 호출하여이를 수행 할 수 있습니다.
# Perform the text-recognition request.
error = request_handler.performRequests_error_([vision_request], None)
지금까지의 전체 Python 코드는 다음과 같습니다.
import Quartz
from Foundation import NSURL
import Vision
def recognize_text_handler(request, error):
observations = request.results()
results = []
for observation in observations:
# Return the string of the top VNRecognizedText instance.
recognized_text = observation.topCandidates_(1)[0]
results.append([recognized_text.string(), recognized_text.confidence()])
for result in results:
print(result)
# TODO Process the recognized strings.
img_path = "./screenshot.png"
# Get the CIImage on which to perform requests.
input_url = NSURL.fileURLWithPath_(img_path)
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
# Create a new image-request handler.
request_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
input_image, None
)
# Create a new request to recognize text.
request = Vision.VNRecognizeTextRequest.alloc().initWithCompletionHandler_(recognize_text_handler)
# Perform the text-recognition request.
error = request_handler.performRequests_error_([request], None)
이 코드를 실행하면 터미널에서 감지된 문자열을 볼 수 있습니다. 테스트를 위해 다음 이미지를 사용했습니다 (내 홈페이지에서 가져옴).
그리고 이것은 터미널에서 본 출력입니다.
['• Hi everyone!', 0.5]
['I am Yasoob! You might know me from Practical Python Projects or the Intermediate Python book.', 1.0]
['Welcome to my personal blog which is going to be the new home for all of my old and new articles. You', 1.0]
['can turn on "dark mode" by clicking on the moon (right before the RSS icon) in the side-bar. I hope you', 0.5]
['enjoy your stay g', 1.0]
['Source', 1.0]
Pillow를 사용하여 감지된 바운딩 박스 표시
Vision Framework를 사용하여 이미지의 텍스트를 성공적으로 감지할 수 있었지만 Vision이 이미지에서 해당 텍스트를 감지한 위치를 시각화하는 것은 매우 어렵습니다. 감지된 경계 상자를 원본 이미지에 오버레이하여 문제를 해결할 수 있습니다. Pillow](https://pillow.readthedocs.io/en/stable/index.html)는 이 오버레이 작업을 매우 편리하게 만듭니다.
observation.topCandidates_ 메서드 호출은 VNRecognizedText 개체를 반환하고 이러한 개체에는 관찰의 문자 범위 주위의 경계 상자를 반환하는 boundingBoxForRange 메서드가 있습니다. 이것이 바로 내가 필요로하는 것입니다.
이것은 내가 bounding box를 얻는 데 사용한 것입니다.
from Foundation import NSRange
# ...
box_range = NSRange(0, len(recognized_text.string()))
boxObservation = recognized_text.boundingBoxForRange_error_(box_range, None)
is는 CGRect 유형입니다. 그러나 이러한 좌표는 정규화 된 좌표 공간에서와 같이 직접 사용할 수 없습니다. 즉, 범위는 -1에서 1 사이입니다. 먼저 이미지 좌표로 변환해야합니다. 이는 VNImageRectForNormalizedRect 함수를 통해 가능합니다. 이 함수는 정규화된 좌표와 이미지의 차원을 가져와서 이미지 좌표 공간에서 정규화된 좌표를 매핑합니다.boxObservation
# Convert the rectangle from normalized coordinates to image coordinates.
image_width, image_height = input_image.extent().size.width, input_image.extent().size.height
rect = Vision.VNImageRectForNormalizedRect(boundingBox, image_width, image_height)
이 rect를 배열에 넣고 모든 결과를 처리하고 입력 이미지에 오버레이하는 새 함수를 만들어 보겠습니다.results
def recognize_text_handler(request, error):
# --snip--
for observation in observations:
# --snip--
results.append([recognized_text.string(), recognized_text.confidence(), rect])
visualize_results(results)
def visualize_results(results):
image = Image.open(img_path)
draw=ImageDraw.Draw(image)
for result in results:
# TODO: Draw the result bounding box
image.show()
조금 파고 들면 다음 함수를 통해 x와 y 좌표를 얻을 수있는 것으로 보입니다.rect
- CGRectGetMinX
- CGRectGetMinY
- CGRectGetMaxX
- CGRectGetMaxY
시각화 코드를 업데이트하여 적절한 x 및 y 좌표를 얻고 이미지 위에 그릴 수 있습니다. 주석을 다음 코드로 바꿨습니다.TODO
rect = result[-1]
min_x = Quartz.CGRectGetMinX(rect)
min_y = Quartz.CGRectGetMinY(rect)
max_x = Quartz.CGRectGetMaxX(rect)
max_y = Quartz.CGRectGetMaxY(rect)
draw.rectangle([(min_x, min_y),(max_x, max_y)],outline="black", width=3)
코드를 저장하고 실행 한 후 다음과 같은 결과가 출력되었습니다.
이것은 옳지 않은 것 같습니다! 자세히 살펴보니 y 좌표가 거꾸로 되어 있는 것을 볼 수 있었습니다. 이미지 높이에서 y 좌표를 빼서 쉽게 고칠 수 있다는 것을 알아 내기 위해 시간이 걸렸습니다.
min_y = input_image.extent().size.height - Quartz.CGRectGetMinY(rect)
max_y = input_image.extent().size.height - Quartz.CGRectGetMaxY(rect)
좌표가 뒤집힌 이유를 완전히 확신 할 수 없습니다. 나는 약간의 연구를 했고 Core Graphics 라이브러리는 단순히 뒤집힌 Y 좌표를 반환하고 우리가 직접 보상해야 하는 것으로 보입니다.
이것은 지금까지의 전체 코드입니다 (약간의 수정 포함).
import Quartz
from Foundation import NSURL, NSRange
import Vision
from PIL import Image, ImageDraw
def recognize_text_handler(request, error):
observations = request.results()
results = []
for observation in observations:
# Return the string of the top VNRecognizedText instance.
recognized_text = observation.topCandidates_(1)[0]
# Find the bounding-box observation for the string range.
box_range = NSRange(0, len(recognized_text.string()))
boxObservation = recognized_text.boundingBoxForRange_error_(box_range, None)
# Get the normalized CGRect value.
boundingBox = boxObservation[0].boundingBox()
# Convert the rectangle from normalized coordinates to image coordinates.
image_width, image_height = input_image.extent().size.width, input_image.extent().size.height
rect = Vision.VNImageRectForNormalizedRect(boundingBox, image_width, image_height)
results.append([recognized_text.string(), recognized_text.confidence(), rect])
# Print out the results in the terminal for inspection
for result in results:
print(result[0])
visualize_results(results)
def visualize_results(results):
image = Image.open(img_path)
draw=ImageDraw.Draw(image)
for result in results:
rect = result[-1]
min_x = Quartz.CGRectGetMinX(rect)
max_x = Quartz.CGRectGetMaxX(rect)
# The y coordinates need to be flipped
min_y = input_image.extent().size.height - Quartz.CGRectGetMinY(rect)
max_y = input_image.extent().size.height - Quartz.CGRectGetMaxY(rect)
# Draw the observation rect on the image
draw.rectangle([(min_x, min_y),(max_x, max_y)],outline="black", width=3)
# Display the final image with all the observations overlayed
image.show()
img_path = "./screenshot.png"
# Get the CIImage on which to perform requests.
input_url = NSURL.fileURLWithPath_(img_path)
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
# Create a new image-request handler.
request_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
input_image, None
)
# Create a new request to recognize text.
request = Vision.VNRecognizeTextRequest.alloc().initWithCompletionHandler_(recognize_text_handler)
# Perform the text-recognition request.
error = request_handler.performRequests_error_([request], None)
# Deallocate memory
request_handler.dealloc()
request.dealloc()
코드를 저장하고 실행하면 다음과 유사한 내용이 표시됩니다.
결론
PyObjC를 사용하는 방법과 기본 Objective C 클래스, 함수 및 프레임워크와 상호 작용하는 방법에 대해 조금 배웠기를 바랍니다. 이전에 Quartz, Vision 및 Core Graphics 라이브러리로 작업한 적이 없었기 때문에 매우 흥미로운 여정이었습니다. PyObjC가 어떻게 작동하는지에 대한 단서도 없었습니다. 나는 텍스트 감지를 위해 Vision 프레임 워크를 사용할 수 있다는 비전 (말장난 의도 없음)을 가지고 있었고 다른 모든 것을 즉석에서 배웠습니다.
Objective C 프레임워크에 대한 프로그래밍을 위해 PyObjC를 다시 사용하시겠습니까? 그것은 사정 나름이에요. Objective C 프레임 워크를 기반으로하는 매우 복잡한 응용 프로그램을 개발해야한다면 도구가 훨씬 낫기 때문에 Swift를 사용할 수 있습니다. 그러나 Objective C 프레임 워크가 응용 프로그램의 작은 부분 일뿐이라면 PyObjC를 사용할 수 있습니다. 전용 도구와 별도로 이것에 대한 가장 큰 이유는 때때로 PyObjC 기반 코드에서 오류를 디버그하기 어려울 수 있기 때문입니다. 이 샘플 응용 프로그램을 개발하는 동안 머리를 쥐어뜯어야 했는데 훨씬 더 복잡한 PyObjC 코드가 있는 더 큰 프로젝트에서 동일한 경험을 하고 싶지 않았습니다.
'개발 언어 > Python' 카테고리의 다른 글
6강: 병행성 vs 병렬성 - 파이썬 고급편 (5) | 2025.05.28 |
---|---|
파이썬 고급 5강: async/await으로 시작하는 비동기 파이썬: asyncio 기초 (3) | 2025.05.28 |
4강: 파이썬 객체 생성의 비밀: 메타클래스 들여다보기 (0) | 2025.05.27 |
3강: 파이썬 고급 - with 구문 마스터하기: 리소스 관리의 효율화 (0) | 2025.05.27 |
파이썬 고급 2강: 게으른 평가 (Lazy Evaluation): 제너레이터와 이터레이터 활용 (8) | 2025.05.26 |