푸터 콘텐츠로 바로가기
동영상

Azure 컴퓨터 비전 OCR에서 마이그레이션하기

이 가이드는 .NET 개발자가 Azure 컴퓨터 비전 OCR을 클라우드 인프라 없이 로컬에서 문서를 처리하는 온프레미스 OCR 라이브러리인 IronOCR 로 교체하는 과정을 안내합니다. 이 문서에서는 NuGet 패키지 및 네임스페이스 교체, Azure 비동기 폴링 모델을 동기 로컬 호출로 변환하는 작업, 그리고 마이그레이션 중에 가장 주의가 필요한 특정 패턴(종속성 주입 연결, 폼 인식기 폴링 루프, 다중 페이지 TIFF 처리 및 배치 처리량) 처리와 같은 기계적인 단계를 다룹니다.

Azure 컴퓨터 비전 OCR에서 마이그레이션해야 하는 이유는 무엇일까요?

이민의 필요성은 추상적인 것이 아닙니다. 일반적으로 팀은 프로덕션 환경에서 Azure Computer Vision을 사용하면서 하나 이상의 구체적인 운영 문제를 겪은 후에 이러한 결정을 내립니다.

엔드포인트 및 API 키 관리는 끝이 없습니다. 개발, 스테이징, 프로덕션, 재해 복구 등 모든 배포 환경에는 프로비저닝된 Azure Cognitive Services 리소스, 엔드포인트 URL 및 하나 이상의 API 키가 필요합니다. 키를 돌려야 합니다. 리소스가 지역을 이동하면 엔드포인트가 변경됩니다. 모든 환경은 cognitiveservices.azure.com에 도달하기 위한 아웃바운드 방화벽 규칙이 필요합니다. 운영 영역은 팀 내 환경 및 개발자가 늘어날수록 확장됩니다.IronOCR이 모든 것을 애플리케이션 시작 시 한 번만 설정하는 단일 문자열 라이선스 키로 대체하며, 키 교체 일정이나 외부 네트워크 요구 사항이 없습니다.

페이지별 요금 청구 방식은 여러 페이지로 구성된 문서에 불리합니다. Azure Computer Vision은 모든 PDF 페이지를 별도의 거래로 계산합니다. 20페이지짜리 계약서는 20건의 유료 통화에 해당합니다. 1,000건의 거래당 1달러의 비용이 발생한다고 가정할 때, 평균 4페이지 분량의 여러 페이지 문서를 매달 50,000건 처리하는 팀은 총 200,000건의 거래를 처리하게 되며, 무료 이용 기간 이후에는 월 195달러, 연간 2,340달러의 비용이 발생합니다. 이것이IronOCR Lite ($999)에 대한 손익 분기점인데, 4개월 미만 후에는 추가되는 모든 페이지가 비용이 들지 않습니다.

비동기 전파는 전체 호출 스택에 걸쳐 확산됩니다. Azure Computer Vision은 동기적으로 결과를 반환할 수 없습니다. 클라우드 I/O에는 네트워크 지연 시간이 발생하기 때문입니다. AnalyzeAsyncawait 요구 사항은 모든 호출 메서드를 비동기로 강제하여 서비스 레이어에서 컨트롤러, 백그라운드 작업자 및 이를 수용하기 위해 리팩터링이 필요한 동기 코드까지 패턴을 전파합니다. Form Recognizer의 폴링 기반 작업은 이를 복합적으로 만듭니다: WaitUntil.Completed는 스레드를 차단하고, 진정한 비차단 동작은 UpdateStatusAsync 폴링 루프를 수동으로 관리해야 합니다.

모든 통화마다 문서가 네트워크를 통해 전송됩니다. HIPAA(미국 의료정보보호법)의 적용을 받는 의료 정보, ITAR(국제 무기 거래 규정)의 통제를 받는 국방 관련 문서, 변호사와 의뢰인 간의 기밀 ​​통신 또는 데이터 상주 규칙이 적용되는 모든 문서 범주를 처리하는 팀의 경우, 필수적인 클라우드 전송은 타협이 아니라 아키텍처적 비호환성입니다. 문서 콘텐츠를 Microsoft 데이터 센터로 전송하지 않는 Azure 컴퓨터 비전 모드는 없습니다.

속도 제한은 처리량에 상한선을 만듭니다. Azure Computer Vision의 S1 계층은 초당 10건의 트랜잭션으로 제한됩니다. 시간당 3,600개의 이미지를 처리하는 배치 작업이 정확히 최대치에 도달했습니다. 이를 초과하면 HTTP 429 응답이 반환되므로 모든 호출 경로에 지수 백오프를 사용하는 재시도 로직이 필요합니다. IronOCR의 처리량 상한선은 호스팅 하드웨어에 있으며, 서비스에서 부과하는 제한이나 재시도 인프라가 필요하지 않습니다.

이미지 OCR과 PDF OCR은 두 개의 별도 서비스가 필요합니다. 표준 이미지 OCR은 ImageAnalysisClientAzure.AI.Vision.ImageAnalysis에서 사용합니다. 전체 PDF 처리는 DocumentAnalysisClientAzure.AI.FormRecognizer.DocumentAnalysis에서 필요로 하며 — 서로 다른 NuGet 패키지, 다른 Azure 리소스, 다른 엔드포인트, 다른 결과 스키마가 필요합니다. 이미지와 PDF를 모두 처리하는 모든 애플리케이션은 이러한 이중 구성 오버헤드를 수반합니다. IronOCR는 IronTesseract.Read()와 단일 OcrInput 로더로 둘 다 처리합니다.

근본적인 문제

// Azure: endpoint URL + API key + async + nested block traversal — before a single character
var client = new ImageAnalysisClient(new Uri(endpoint), new AzureKeyCredential(apiKey));
using var stream = File.OpenRead(imagePath);
var result = await client.AnalyzeAsync(BinaryData.FromStream(stream), VisualFeatures.Read);
var text = string.Join("\n", result.Value.Read.Blocks.SelectMany(b => b.Lines).Select(l => l.Text));

// IronOCR: no endpoint, no key rotation, no async, no traversal
var text = new IronTesseract().Read(imagePath).Text;
// Azure: endpoint URL + API key + async + nested block traversal — before a single character
var client = new ImageAnalysisClient(new Uri(endpoint), new AzureKeyCredential(apiKey));
using var stream = File.OpenRead(imagePath);
var result = await client.AnalyzeAsync(BinaryData.FromStream(stream), VisualFeatures.Read);
var text = string.Join("\n", result.Value.Read.Blocks.SelectMany(b => b.Lines).Select(l => l.Text));

// IronOCR: no endpoint, no key rotation, no async, no traversal
var text = new IronTesseract().Read(imagePath).Text;
Imports System
Imports System.IO
Imports Azure
Imports Azure.AI.Vision
Imports IronOcr

' Azure: endpoint URL + API key + async + nested block traversal — before a single character
Dim client As New ImageAnalysisClient(New Uri(endpoint), New AzureKeyCredential(apiKey))
Using stream As FileStream = File.OpenRead(imagePath)
    Dim result = Await client.AnalyzeAsync(BinaryData.FromStream(stream), VisualFeatures.Read)
    Dim text As String = String.Join(vbLf, result.Value.Read.Blocks.SelectMany(Function(b) b.Lines).Select(Function(l) l.Text))
End Using

' IronOCR: no endpoint, no key rotation, no async, no traversal
Dim text As String = New IronTesseract().Read(imagePath).Text
$vbLabelText   $csharpLabel

IronOCR과 Azure 컴퓨터 비전 OCR: 기능 비교

아래 표는 이번 마이그레이션을 평가하는 팀에게 가장 중요한 기능들을 다룹니다.

기능 Azure 컴퓨터 비전 OCR IronOCR
처리 위치 마이크로소프트 Azure 클라우드 현지, 현장
인터넷 필요 네, 모든 요청 아니요
Azure 구독이 필요합니다. 아니요
가격 모델 거래당 (1,000건당 1달러) 영구 라이선스 (From $999)
여러 페이지로 구성된 PDF 파일에 대한 페이지별 요금 청구 네, 페이지당 하나의 거래가 성립됩니다. 페이지당 비용 없음
무료 티어 월 5,000건의 거래 체험 모드(워터마크 포함)
이미지 OCR API AnalyzeAsync (비동기 전용) Read() (동기)
PDF OCR 별도 양식 인식 서비스 내장된, 동일한 Read() 호출
비밀번호로 보호된 PDF 형태 인식기를 통해 input.LoadPdf(path, Password: "x")
검색 가능한 PDF 출력 수동 제작 result.SaveAsSearchablePdf()
여러 페이지로 구성된 TIFF 파일 지원되지 않음 input.LoadImageFrames()
자동 이미지 전처리 서버 측은 불투명하며 구성할 수 없습니다. 기울기 보정, 노이즈 제거, 대비 조정, 이진화, 선명도 향상, 크기 조정
심층 노이즈 제거 아니요 input.DeepCleanBackgroundNoise()
OCR 중 바코드 판독 별도의 이미지 분석 기능 ocr.Configuration.ReadBarCodes = true
영역 기반 OCR 직접 자르지 않았습니다 (업로드 전에 수동으로 자르기). CropRectangle on OcrInput
요금 제한 S1 티어에서 10 TPS 하드웨어 제약 전용
재시도 로직 필요 예 (HTTP 429, 5xx) 아니요
에어갭 배포 불가능 완전히 지원
지원되는 언어 164+ (서버 관리형) 125개 이상 (NuGet 언어 팩)
다국어 동시 예 (OcrLanguage.French + OcrLanguage.German)
단어 경계 상자 다각형(가변 정점 개수) 직사각형 (x, y, 너비, 높이)
자신감 점수 단어당 부동 소수점 값(0.0~1.0) 단어별 및 전체 점수 (0~100점 척도)
hOCR 내보내기 아니요 result.SaveAsHocrFile()
구조화된 출력 계층 구조 블록 / 선 / 단어 페이지 / 단락 / 줄 / 단어 / 문자
.NET 호환성 .NET Standard 2.0 이상 .NET Framework 4.6.2 이상, .NET Core, .NET 5~9
크로스 플랫폼 Windows, Linux, macOS (클라우드 이용) 윈도우, 리눅스, macOS, Docker, ARM64
상업적 지원 Azure 지원 플랜 라이선스에IronOCR지원이 포함되어 있습니다.

빠른 시작: Azure 컴퓨터 비전 OCR에서IronOCR로 마이그레이션

1단계: NuGet 패키지 교체

Azure Computer Vision 패키지를 제거합니다.

dotnet remove package Azure.AI.Vision.ImageAnalysis
dotnet remove package Azure.AI.Vision.ImageAnalysis
SHELL

프로젝트에서 PDF 처리를 위해 Form Recognizer를 사용하는 경우 해당 패키지도 제거하십시오.

dotnet remove package Azure.AI.FormRecognizer
dotnet remove package Azure.AI.FormRecognizer
SHELL

NuGet 에서IronOCR설치하세요.

dotnet add package IronOcr

단계 2: 네임스페이스 업데이트

// Before (Azure Computer Vision)
using Azure;
using Azure.AI.Vision.ImageAnalysis;
// For PDF processing:
// using Azure.AI.FormRecognizer.DocumentAnalysis;

// After (IronOCR)
using IronOcr;
// Before (Azure Computer Vision)
using Azure;
using Azure.AI.Vision.ImageAnalysis;
// For PDF processing:
// using Azure.AI.FormRecognizer.DocumentAnalysis;

// After (IronOCR)
using IronOcr;
Imports IronOcr
' Before (Azure Computer Vision)
' Imports Azure
' Imports Azure.AI.Vision.ImageAnalysis
' For PDF processing:
' Imports Azure.AI.FormRecognizer.DocumentAnalysis

' After (IronOCR)
$vbLabelText   $csharpLabel

단계 3: 라이선스 초기화

애플리케이션 시작 시, OCR 호출 전에 라이선스 키를 한 번만 추가하십시오.

IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"
$vbLabelText   $csharpLabel

운영 환경에서 환경 변수에 키를 저장하세요.

IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
Imports System

IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")
$vbLabelText   $csharpLabel

코드 마이그레이션 예제

종속성 주입 방식의 Azure 클라이언트 구성 교체

권장 Azure SDK 패턴을 따르는 팀은 ImageAnalysisClient를 DI 컨테이너에 IOptions<AzureComputerVisionOptions> 또는 직접 IConfiguration 바인딩을 사용하여 등록합니다. 이 와이어링은 appsettings.json에서 엔드포인트 URL과 API 키를 가져오고 모든 배포 환경에서 아웃바운드 네트워크 구성을 필요로 합니다.

Azure 컴퓨터 비전 접근 방식:

// appsettings.json binds to this class
public class AzureComputerVisionOptions
{
    public string Endpoint { get; set; }   // "https://your-resource.cognitiveservices.azure.com/"
    public string ApiKey   { get; set; }   // rotated periodically
}

// Program.cs / Startup.cs
services.Configure<AzureComputerVisionOptions>(
    configuration.GetSection("AzureComputerVision"));

services.AddSingleton<ImageAnalysisClient>(sp =>
{
    var opts = sp.GetRequiredService<IOptions<AzureComputerVisionOptions>>().Value;
    return new ImageAnalysisClient(
        new Uri(opts.Endpoint),
        new AzureKeyCredential(opts.ApiKey));
});

services.AddScoped<IOcrService, AzureOcrService>();
// appsettings.json binds to this class
public class AzureComputerVisionOptions
{
    public string Endpoint { get; set; }   // "https://your-resource.cognitiveservices.azure.com/"
    public string ApiKey   { get; set; }   // rotated periodically
}

// Program.cs / Startup.cs
services.Configure<AzureComputerVisionOptions>(
    configuration.GetSection("AzureComputerVision"));

services.AddSingleton<ImageAnalysisClient>(sp =>
{
    var opts = sp.GetRequiredService<IOptions<AzureComputerVisionOptions>>().Value;
    return new ImageAnalysisClient(
        new Uri(opts.Endpoint),
        new AzureKeyCredential(opts.ApiKey));
});

services.AddScoped<IOcrService, AzureOcrService>();
' appsettings.json binds to this class
Public Class AzureComputerVisionOptions
    Public Property Endpoint As String   ' "https://your-resource.cognitiveservices.azure.com/"
    Public Property ApiKey As String     ' rotated periodically
End Class

' Program.vb / Startup.vb
services.Configure(Of AzureComputerVisionOptions)(
    configuration.GetSection("AzureComputerVision"))

services.AddSingleton(Of ImageAnalysisClient)(Function(sp)
    Dim opts = sp.GetRequiredService(Of IOptions(Of AzureComputerVisionOptions))().Value
    Return New ImageAnalysisClient(
        New Uri(opts.Endpoint),
        New AzureKeyCredential(opts.ApiKey))
End Function)

services.AddScoped(Of IOcrService, AzureOcrService)()
$vbLabelText   $csharpLabel
// AzureOcrService.cs
public class AzureOcrService : IOcrService
{
    private readonly ImageAnalysisClient _client;

    public AzureOcrService(ImageAnalysisClient client)
    {
        _client = client;
    }

    public async Task<string> ReadAsync(string imagePath)
    {
        using var stream = File.OpenRead(imagePath);
        var data = BinaryData.FromStream(stream);
        var result = await _client.AnalyzeAsync(data, VisualFeatures.Read);

        return string.Join("\n",
            result.Value.Read.Blocks
                .SelectMany(b => b.Lines)
                .Select(l => l.Text));
    }
}
// AzureOcrService.cs
public class AzureOcrService : IOcrService
{
    private readonly ImageAnalysisClient _client;

    public AzureOcrService(ImageAnalysisClient client)
    {
        _client = client;
    }

    public async Task<string> ReadAsync(string imagePath)
    {
        using var stream = File.OpenRead(imagePath);
        var data = BinaryData.FromStream(stream);
        var result = await _client.AnalyzeAsync(data, VisualFeatures.Read);

        return string.Join("\n",
            result.Value.Read.Blocks
                .SelectMany(b => b.Lines)
                .Select(l => l.Text));
    }
}
Imports System.IO
Imports System.Threading.Tasks

Public Class AzureOcrService
    Implements IOcrService

    Private ReadOnly _client As ImageAnalysisClient

    Public Sub New(client As ImageAnalysisClient)
        _client = client
    End Sub

    Public Async Function ReadAsync(imagePath As String) As Task(Of String) Implements IOcrService.ReadAsync
        Using stream = File.OpenRead(imagePath)
            Dim data = BinaryData.FromStream(stream)
            Dim result = Await _client.AnalyzeAsync(data, VisualFeatures.Read)

            Return String.Join(vbLf, result.Value.Read.Blocks _
                .SelectMany(Function(b) b.Lines) _
                .Select(Function(l) l.Text))
        End Using
    End Function
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

// Program.cs / Startup.cs
// One-time license key — no endpoint, no credential class, no options binding
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");

// Register IronTesseract as a singleton — it is thread-safe
services.AddSingleton<IronTesseract>();
services.AddScoped<IOcrService, IronOcrService>();
// Program.cs / Startup.cs
// One-time license key — no endpoint, no credential class, no options binding
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");

// Register IronTesseract as a singleton — it is thread-safe
services.AddSingleton<IronTesseract>();
services.AddScoped<IOcrService, IronOcrService>();
' Program.vb / Startup.vb
' One-time license key — no endpoint, no credential class, no options binding
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")

' Register IronTesseract as a singleton — it is thread-safe
services.AddSingleton(Of IronTesseract)()
services.AddScoped(Of IOcrService, IronOcrService)()
$vbLabelText   $csharpLabel
// IronOcrService.cs
public class IronOcrService : IOcrService
{
    private readonly IronTesseract _ocr;

    public IronOcrService(IronTesseract ocr)
    {
        _ocr = ocr;
    }

    public string Read(string imagePath)
    {
        return _ocr.Read(imagePath).Text;
    }
}
// IronOcrService.cs
public class IronOcrService : IOcrService
{
    private readonly IronTesseract _ocr;

    public IronOcrService(IronTesseract ocr)
    {
        _ocr = ocr;
    }

    public string Read(string imagePath)
    {
        return _ocr.Read(imagePath).Text;
    }
}
' IronOcrService.vb
Public Class IronOcrService
    Implements IOcrService

    Private ReadOnly _ocr As IronTesseract

    Public Sub New(ocr As IronTesseract)
        _ocr = ocr
    End Sub

    Public Function Read(imagePath As String) As String Implements IOcrService.Read
        Return _ocr.Read(imagePath).Text
    End Function
End Class
$vbLabelText   $csharpLabel

DI 와이어링은 두 개의 구성 클래스(옵션 + 클라이언트 팩토리)에서 단일 AddSingleton<IronTesseract>() 호출로 줄어듭니다. appsettings.json Azure 섹션, 키 볼트 참조, cognitiveservices.azure.com에 대한 아웃바운드 방화벽 규칙이 모두 제거됩니다. 싱글턴 인스턴스에서 사용 가능한 구성 옵션은 IronTesseract 설정 가이드를 참조하십시오.

폼 인식기 폴링 루프 제거

Form Recognizer의 AnalyzeDocumentAsyncLongRunningOperation를 반환합니다. WaitUntil.Completed는 클라우드 작업이 완료될 때까지 호출 스레드를 차단합니다 — 일반적으로 문서당 2-10초가 걸립니다. 비차단 동작을 위해, 팀은 폴 간의 딜레이와 함께 UpdateStatusAsync 폴링 루프를 작성하며, OCR 로직이 없는 30-50줄의 인프라 코드를 추가합니다.

Azure 컴퓨터 비전 접근 방식:

// DocumentAnalysisClient — separate from ImageAnalysisClient, separate resource
public class FormRecognizerPdfService
{
    private readonly DocumentAnalysisClient _client;

    public FormRecognizerPdfService(string endpoint, string apiKey)
    {
        _client = new DocumentAnalysisClient(
            new Uri(endpoint),
            new AzureKeyCredential(apiKey));
    }

    // Blocking wait — thread is held for the duration of cloud processing
    public async Task<string> ExtractPdfTextBlocking(string pdfPath)
    {
        using var stream = File.OpenRead(pdfPath);

        var operation = await _client.AnalyzeDocumentAsync(
            WaitUntil.Completed,  // blocks until Azure finishes
            "prebuilt-read",
            stream);

        var docResult = operation.Value;
        var sb = new StringBuilder();
        foreach (var page in docResult.Pages)
        {
            foreach (var line in page.Lines)
            {
                sb.AppendLine(line.Content);  // .Content, not .Text
            }
        }
        return sb.ToString();
    }

    // True async — manual polling loop required
    public async Task<string> ExtractPdfTextNonBlocking(string pdfPath)
    {
        using var stream = File.OpenRead(pdfPath);

        var operation = await _client.AnalyzeDocumentAsync(
            WaitUntil.Started,  // returns immediately, not complete yet
            "prebuilt-read",
            stream);

        // Poll every 500ms until the operation finishes
        while (!operation.HasCompleted)
        {
            await Task.Delay(500);
            await operation.UpdateStatusAsync();
        }

        var docResult = operation.Value;
        var sb = new StringBuilder();
        foreach (var page in docResult.Pages)
        {
            foreach (var line in page.Lines)
            {
                sb.AppendLine(line.Content);
            }
        }
        return sb.ToString();
    }
}
// DocumentAnalysisClient — separate from ImageAnalysisClient, separate resource
public class FormRecognizerPdfService
{
    private readonly DocumentAnalysisClient _client;

    public FormRecognizerPdfService(string endpoint, string apiKey)
    {
        _client = new DocumentAnalysisClient(
            new Uri(endpoint),
            new AzureKeyCredential(apiKey));
    }

    // Blocking wait — thread is held for the duration of cloud processing
    public async Task<string> ExtractPdfTextBlocking(string pdfPath)
    {
        using var stream = File.OpenRead(pdfPath);

        var operation = await _client.AnalyzeDocumentAsync(
            WaitUntil.Completed,  // blocks until Azure finishes
            "prebuilt-read",
            stream);

        var docResult = operation.Value;
        var sb = new StringBuilder();
        foreach (var page in docResult.Pages)
        {
            foreach (var line in page.Lines)
            {
                sb.AppendLine(line.Content);  // .Content, not .Text
            }
        }
        return sb.ToString();
    }

    // True async — manual polling loop required
    public async Task<string> ExtractPdfTextNonBlocking(string pdfPath)
    {
        using var stream = File.OpenRead(pdfPath);

        var operation = await _client.AnalyzeDocumentAsync(
            WaitUntil.Started,  // returns immediately, not complete yet
            "prebuilt-read",
            stream);

        // Poll every 500ms until the operation finishes
        while (!operation.HasCompleted)
        {
            await Task.Delay(500);
            await operation.UpdateStatusAsync();
        }

        var docResult = operation.Value;
        var sb = new StringBuilder();
        foreach (var page in docResult.Pages)
        {
            foreach (var line in page.Lines)
            {
                sb.AppendLine(line.Content);
            }
        }
        return sb.ToString();
    }
}
Imports System
Imports System.IO
Imports System.Text
Imports System.Threading.Tasks
Imports Azure
Imports Azure.AI.FormRecognizer.DocumentAnalysis

' DocumentAnalysisClient — separate from ImageAnalysisClient, separate resource
Public Class FormRecognizerPdfService
    Private ReadOnly _client As DocumentAnalysisClient

    Public Sub New(endpoint As String, apiKey As String)
        _client = New DocumentAnalysisClient(
            New Uri(endpoint),
            New AzureKeyCredential(apiKey))
    End Sub

    ' Blocking wait — thread is held for the duration of cloud processing
    Public Async Function ExtractPdfTextBlocking(pdfPath As String) As Task(Of String)
        Using stream = File.OpenRead(pdfPath)
            Dim operation = Await _client.AnalyzeDocumentAsync(
                WaitUntil.Completed,  ' blocks until Azure finishes
                "prebuilt-read",
                stream)

            Dim docResult = operation.Value
            Dim sb = New StringBuilder()
            For Each page In docResult.Pages
                For Each line In page.Lines
                    sb.AppendLine(line.Content)  ' .Content, not .Text
                Next
            Next
            Return sb.ToString()
        End Using
    End Function

    ' True async — manual polling loop required
    Public Async Function ExtractPdfTextNonBlocking(pdfPath As String) As Task(Of String)
        Using stream = File.OpenRead(pdfPath)
            Dim operation = Await _client.AnalyzeDocumentAsync(
                WaitUntil.Started,  ' returns immediately, not complete yet
                "prebuilt-read",
                stream)

            ' Poll every 500ms until the operation finishes
            While Not operation.HasCompleted
                Await Task.Delay(500)
                Await operation.UpdateStatusAsync()
            End While

            Dim docResult = operation.Value
            Dim sb = New StringBuilder()
            For Each page In docResult.Pages
                For Each line In page.Lines
                    sb.AppendLine(line.Content)
                Next
            Next
            Return sb.ToString()
        End Using
    End Function
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

// One class handles both images and PDFs — no second client or second resource
public class IronOcrDocumentService
{
    private readonly IronTesseract _ocr;

    public IronOcrDocumentService(IronTesseract ocr)
    {
        _ocr = ocr;
    }

    // Synchronous — returns immediately when local processing completes
    public string ExtractPdfText(string pdfPath)
    {
        using var input = new OcrInput();
        input.LoadPdf(pdfPath);
        return _ocr.Read(input).Text;
    }

    // Specific page range — no per-page billing penalty
    public string ExtractPageRange(string pdfPath, int startPage, int endPage)
    {
        using var input = new OcrInput();
        input.LoadPdfPages(pdfPath, startPage, endPage);
        return _ocr.Read(input).Text;
    }

    // If an async signature is required by an interface or controller
    public Task<string> ExtractPdfTextAsync(string pdfPath)
    {
        return Task.Run(() => ExtractPdfText(pdfPath));
    }
}
// One class handles both images and PDFs — no second client or second resource
public class IronOcrDocumentService
{
    private readonly IronTesseract _ocr;

    public IronOcrDocumentService(IronTesseract ocr)
    {
        _ocr = ocr;
    }

    // Synchronous — returns immediately when local processing completes
    public string ExtractPdfText(string pdfPath)
    {
        using var input = new OcrInput();
        input.LoadPdf(pdfPath);
        return _ocr.Read(input).Text;
    }

    // Specific page range — no per-page billing penalty
    public string ExtractPageRange(string pdfPath, int startPage, int endPage)
    {
        using var input = new OcrInput();
        input.LoadPdfPages(pdfPath, startPage, endPage);
        return _ocr.Read(input).Text;
    }

    // If an async signature is required by an interface or controller
    public Task<string> ExtractPdfTextAsync(string pdfPath)
    {
        return Task.Run(() => ExtractPdfText(pdfPath));
    }
}
Imports System.Threading.Tasks

' One class handles both images and PDFs — no second client or second resource
Public Class IronOcrDocumentService
    Private ReadOnly _ocr As IronTesseract

    Public Sub New(ocr As IronTesseract)
        _ocr = ocr
    End Sub

    ' Synchronous — returns immediately when local processing completes
    Public Function ExtractPdfText(pdfPath As String) As String
        Using input As New OcrInput()
            input.LoadPdf(pdfPath)
            Return _ocr.Read(input).Text
        End Using
    End Function

    ' Specific page range — no per-page billing penalty
    Public Function ExtractPageRange(pdfPath As String, startPage As Integer, endPage As Integer) As String
        Using input As New OcrInput()
            input.LoadPdfPages(pdfPath, startPage, endPage)
            Return _ocr.Read(input).Text
        End Using
    End Function

    ' If an async signature is required by an interface or controller
    Public Function ExtractPdfTextAsync(pdfPath As String) As Task(Of String)
        Return Task.Run(Function() ExtractPdfText(pdfPath))
    End Function
End Class
$vbLabelText   $csharpLabel

폴링 루프와 지연 로직이 완전히 사라집니다. LoadPdfPages 는 페이지 범위 선택을 처리합니다 — 별도의 페이지별 호출, 거래 수량 계산 필요 없음. PDF 입력 가이드는 스트림 입력, 바이트 배열 로딩 및 페이지 범위 매개변수에 대해 자세히 설명합니다.

Azure 단어 수준 결과를IronOCR구조화된 출력으로 매핑

Azure Computer Vision은 단어 경계 상자를 가변적인 수의 꼭짓점을 가진 다각형으로 반환합니다. 일반적으로 꼭짓점 수는 4개이지만, 항상 그런 것은 아닙니다. 결과 계층 구조는 Blocks → Lines → Words이며, 신뢰도는 0.0-1.0 스케일의 float입니다. 단어 위치를 읽는 코드는 다각형 꼭짓점 배열을 처리하고 임계값 비교를 위해 신뢰도 척도를 정규화해야 합니다.

Azure 컴퓨터 비전 접근 방식:

public async Task<List<WordResult>> ExtractWordPositionsAsync(string imagePath)
{
    using var stream = File.OpenRead(imagePath);
    var imageData = BinaryData.FromStream(stream);

    var response = await _client.AnalyzeAsync(imageData, VisualFeatures.Read);
    var words = new List<WordResult>();

    foreach (var block in response.Value.Read.Blocks)
    {
        foreach (var line in block.Lines)
        {
            foreach (var word in line.Words)
            {
                // BoundingPolygon is a list of ImagePoint — variable vertex count
                var polygon = word.BoundingPolygon;
                int minX = polygon.Min(p => p.X);
                int minY = polygon.Min(p => p.Y);
                int maxX = polygon.Max(p => p.X);
                int maxY = polygon.Max(p => p.Y);

                words.Add(new WordResult
                {
                    Text        = word.Text,
                    // Azure confidence: 0.0 to 1.0 — multiply by 100 for comparison
                    Confidence  = (double)word.Confidence * 100.0,
                    X           = minX,
                    Y           = minY,
                    Width       = maxX - minX,
                    Height      = maxY - minY
                });
            }
        }
    }
    return words;
}

public record WordResult(string Text, double Confidence, int X, int Y, int Width, int Height);
public async Task<List<WordResult>> ExtractWordPositionsAsync(string imagePath)
{
    using var stream = File.OpenRead(imagePath);
    var imageData = BinaryData.FromStream(stream);

    var response = await _client.AnalyzeAsync(imageData, VisualFeatures.Read);
    var words = new List<WordResult>();

    foreach (var block in response.Value.Read.Blocks)
    {
        foreach (var line in block.Lines)
        {
            foreach (var word in line.Words)
            {
                // BoundingPolygon is a list of ImagePoint — variable vertex count
                var polygon = word.BoundingPolygon;
                int minX = polygon.Min(p => p.X);
                int minY = polygon.Min(p => p.Y);
                int maxX = polygon.Max(p => p.X);
                int maxY = polygon.Max(p => p.Y);

                words.Add(new WordResult
                {
                    Text        = word.Text,
                    // Azure confidence: 0.0 to 1.0 — multiply by 100 for comparison
                    Confidence  = (double)word.Confidence * 100.0,
                    X           = minX,
                    Y           = minY,
                    Width       = maxX - minX,
                    Height      = maxY - minY
                });
            }
        }
    }
    return words;
}

public record WordResult(string Text, double Confidence, int X, int Y, int Width, int Height);
Imports System.IO
Imports System.Threading.Tasks
Imports System.Linq

Public Class ImageAnalyzer
    Private _client As SomeClientType ' Replace with the actual type of _client

    Public Async Function ExtractWordPositionsAsync(imagePath As String) As Task(Of List(Of WordResult))
        Using stream = File.OpenRead(imagePath)
            Dim imageData = BinaryData.FromStream(stream)

            Dim response = Await _client.AnalyzeAsync(imageData, VisualFeatures.Read)
            Dim words = New List(Of WordResult)()

            For Each block In response.Value.Read.Blocks
                For Each line In block.Lines
                    For Each word In line.Words
                        ' BoundingPolygon is a list of ImagePoint — variable vertex count
                        Dim polygon = word.BoundingPolygon
                        Dim minX = polygon.Min(Function(p) p.X)
                        Dim minY = polygon.Min(Function(p) p.Y)
                        Dim maxX = polygon.Max(Function(p) p.X)
                        Dim maxY = polygon.Max(Function(p) p.Y)

                        words.Add(New WordResult With {
                            .Text = word.Text,
                            ' Azure confidence: 0.0 to 1.0 — multiply by 100 for comparison
                            .Confidence = CDbl(word.Confidence) * 100.0,
                            .X = minX,
                            .Y = minY,
                            .Width = maxX - minX,
                            .Height = maxY - minY
                        })
                    Next
                Next
            Next
            Return words
        End Using
    End Function
End Class

Public Class WordResult
    Public Property Text As String
    Public Property Confidence As Double
    Public Property X As Integer
    Public Property Y As Integer
    Public Property Width As Integer
    Public Property Height As Integer
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

public List<WordResult> ExtractWordPositions(string imagePath)
{
    var result = new IronTesseract().Read(imagePath);
    var words = new List<WordResult>();

    foreach (var page in result.Pages)
    {
        foreach (var line in page.Lines)
        {
            foreach (var word in line.Words)
            {
                // Rectangle-based bounding box — no polygon math required
                // Confidence is already 0–100, matching the converted Azure scale
                words.Add(new WordResult
                {
                    Text       = word.Text,
                    Confidence = word.Confidence,   // 0–100, no conversion needed
                    X          = word.X,
                    Y          = word.Y,
                    Width      = word.Width,
                    Height     = word.Height
                });
            }
        }
    }
    return words;
}

// Filter to only high-confidence words — common post-processing pattern
public IEnumerable<string> ExtractHighConfidenceWords(string imagePath, double threshold = 80.0)
{
    var result = new IronTesseract().Read(imagePath);
    return result.Words
        .Where(w => w.Confidence >= threshold)
        .Select(w => w.Text);
}

public record WordResult(string Text, double Confidence, int X, int Y, int Width, int Height);
public List<WordResult> ExtractWordPositions(string imagePath)
{
    var result = new IronTesseract().Read(imagePath);
    var words = new List<WordResult>();

    foreach (var page in result.Pages)
    {
        foreach (var line in page.Lines)
        {
            foreach (var word in line.Words)
            {
                // Rectangle-based bounding box — no polygon math required
                // Confidence is already 0–100, matching the converted Azure scale
                words.Add(new WordResult
                {
                    Text       = word.Text,
                    Confidence = word.Confidence,   // 0–100, no conversion needed
                    X          = word.X,
                    Y          = word.Y,
                    Width      = word.Width,
                    Height     = word.Height
                });
            }
        }
    }
    return words;
}

// Filter to only high-confidence words — common post-processing pattern
public IEnumerable<string> ExtractHighConfidenceWords(string imagePath, double threshold = 80.0)
{
    var result = new IronTesseract().Read(imagePath);
    return result.Words
        .Where(w => w.Confidence >= threshold)
        .Select(w => w.Text);
}

public record WordResult(string Text, double Confidence, int X, int Y, int Width, int Height);
Imports System.Collections.Generic
Imports System.Linq

Public Class WordExtractor

    Public Function ExtractWordPositions(imagePath As String) As List(Of WordResult)
        Dim result = New IronTesseract().Read(imagePath)
        Dim words = New List(Of WordResult)()

        For Each page In result.Pages
            For Each line In page.Lines
                For Each word In line.Words
                    ' Rectangle-based bounding box — no polygon math required
                    ' Confidence is already 0–100, matching the converted Azure scale
                    words.Add(New WordResult With {
                        .Text = word.Text,
                        .Confidence = word.Confidence,   ' 0–100, no conversion needed
                        .X = word.X,
                        .Y = word.Y,
                        .Width = word.Width,
                        .Height = word.Height
                    })
                Next
            Next
        Next

        Return words
    End Function

    ' Filter to only high-confidence words — common post-processing pattern
    Public Function ExtractHighConfidenceWords(imagePath As String, Optional threshold As Double = 80.0) As IEnumerable(Of String)
        Dim result = New IronTesseract().Read(imagePath)
        Return result.Words _
            .Where(Function(w) w.Confidence >= threshold) _
            .Select(Function(w) w.Text)
    End Function

End Class

Public Class WordResult
    Public Property Text As String
    Public Property Confidence As Double
    Public Property X As Integer
    Public Property Y As Integer
    Public Property Width As Integer
    Public Property Height As Integer
End Class
$vbLabelText   $csharpLabel

다각형을 직사각형으로 변환하는 기능이 사라집니다. Azure 0.0~1.0 값을 100으로 곱하면 신뢰도 값이 직접 일치합니다. 기존 임계값 논리는 이 한 번의 조정만 필요합니다. 구조화된 데이터 출력 가이드에는 전체 계층 구조와 좌표 속성이 문서화되어 있습니다. 특히 신뢰도 점수 모델에 대해서는 신뢰도 점수 가이드를 참조하십시오.

클라우드 업로드 없이 다중 페이지 TIFF 파일 처리

Azure Computer Vision의 ImageAnalysisClient는 단일 이미지를 허용합니다. 문서 스캔 워크플로, 팩스 아카이브 및 의료 영상 파이프라인에서 흔히 사용되는 다중 프레임 TIFF 파일은 업로드 전에 TIFF 파일을 개별 이미지로 분할하거나(프레임당 하나의 트랜잭션) 별도의 구성이 가능한 양식 인식기로 전환해야 합니다. 어느 길도 깨끗하지 않다.

Azure 컴퓨터 비전 접근 방식:

// Azure does not support multi-frame TIFF directly via ImageAnalysisClient
// Must split frames manually and upload each as a separate transaction
public async Task<string> ExtractMultiFrameTiffAsync(string tiffPath)
{
    // Load TIFF using System.Drawing or a third-party library
    using var bitmap = new System.Drawing.Bitmap(tiffPath);
    int frameCount = bitmap.GetFrameCount(
        System.Drawing.Imaging.FrameDimension.Page);

    var allText = new StringBuilder();

    for (int i = 0; i < frameCount; i++)
    {
        // Select frame, save to temporary PNG, upload to Azure
        bitmap.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Page, i);

        using var ms = new MemoryStream();
        bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
        ms.Position = 0;

        // Each frame = 1 Azure transaction = $0.001
        var imageData = BinaryData.FromStream(ms);
        var result = await _client.AnalyzeAsync(imageData, VisualFeatures.Read);

        foreach (var block in result.Value.Read.Blocks)
            foreach (var line in block.Lines)
                allText.AppendLine(line.Text);
    }

    return allText.ToString();
}
// Azure does not support multi-frame TIFF directly via ImageAnalysisClient
// Must split frames manually and upload each as a separate transaction
public async Task<string> ExtractMultiFrameTiffAsync(string tiffPath)
{
    // Load TIFF using System.Drawing or a third-party library
    using var bitmap = new System.Drawing.Bitmap(tiffPath);
    int frameCount = bitmap.GetFrameCount(
        System.Drawing.Imaging.FrameDimension.Page);

    var allText = new StringBuilder();

    for (int i = 0; i < frameCount; i++)
    {
        // Select frame, save to temporary PNG, upload to Azure
        bitmap.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Page, i);

        using var ms = new MemoryStream();
        bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
        ms.Position = 0;

        // Each frame = 1 Azure transaction = $0.001
        var imageData = BinaryData.FromStream(ms);
        var result = await _client.AnalyzeAsync(imageData, VisualFeatures.Read);

        foreach (var block in result.Value.Read.Blocks)
            foreach (var line in block.Lines)
                allText.AppendLine(line.Text);
    }

    return allText.ToString();
}
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.IO
Imports System.Text
Imports System.Threading.Tasks

Public Class TiffProcessor
    Private _client As ImageAnalysisClient

    Public Async Function ExtractMultiFrameTiffAsync(tiffPath As String) As Task(Of String)
        ' Load TIFF using System.Drawing or a third-party library
        Using bitmap As New Bitmap(tiffPath)
            Dim frameCount As Integer = bitmap.GetFrameCount(FrameDimension.Page)

            Dim allText As New StringBuilder()

            For i As Integer = 0 To frameCount - 1
                ' Select frame, save to temporary PNG, upload to Azure
                bitmap.SelectActiveFrame(FrameDimension.Page, i)

                Using ms As New MemoryStream()
                    bitmap.Save(ms, Imaging.ImageFormat.Png)
                    ms.Position = 0

                    ' Each frame = 1 Azure transaction = $0.001
                    Dim imageData As BinaryData = BinaryData.FromStream(ms)
                    Dim result = Await _client.AnalyzeAsync(imageData, VisualFeatures.Read)

                    For Each block In result.Value.Read.Blocks
                        For Each line In block.Lines
                            allText.AppendLine(line.Text)
                        Next
                    Next
                End Using
            Next

            Return allText.ToString()
        End Using
    End Function
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

//IronOCR handles multi-frame TIFF natively — single call, no frame splitting
public string ExtractMultiFrameTiff(string tiffPath)
{
    using var input = new OcrInput();
    input.LoadImageFrames(tiffPath);  // all frames loaded automatically
    var result = new IronTesseract().Read(input);
    return result.Text;
}

// Access per-page data for frame-level reporting
public void ExtractTiffWithPageStats(string tiffPath)
{
    using var input = new OcrInput();
    input.LoadImageFrames(tiffPath);

    var ocr = new IronTesseract();
    var result = ocr.Read(input);

    Console.WriteLine($"Total frames processed: {result.Pages.Length}");
    foreach (var page in result.Pages)
    {
        Console.WriteLine($"Frame {page.PageNumber}: " +
            $"{page.Words.Length} words, " +
            $"confidence {page.Confidence:F1}%");
    }
}

// Combine with preprocessing for scanned TIFF archives
public string ExtractLowQualityTiffArchive(string tiffPath)
{
    using var input = new OcrInput();
    input.LoadImageFrames(tiffPath);
    input.Deskew();
    input.DeNoise();
    input.Contrast();

    var result = new IronTesseract().Read(input);
    return result.Text;
}
//IronOCR handles multi-frame TIFF natively — single call, no frame splitting
public string ExtractMultiFrameTiff(string tiffPath)
{
    using var input = new OcrInput();
    input.LoadImageFrames(tiffPath);  // all frames loaded automatically
    var result = new IronTesseract().Read(input);
    return result.Text;
}

// Access per-page data for frame-level reporting
public void ExtractTiffWithPageStats(string tiffPath)
{
    using var input = new OcrInput();
    input.LoadImageFrames(tiffPath);

    var ocr = new IronTesseract();
    var result = ocr.Read(input);

    Console.WriteLine($"Total frames processed: {result.Pages.Length}");
    foreach (var page in result.Pages)
    {
        Console.WriteLine($"Frame {page.PageNumber}: " +
            $"{page.Words.Length} words, " +
            $"confidence {page.Confidence:F1}%");
    }
}

// Combine with preprocessing for scanned TIFF archives
public string ExtractLowQualityTiffArchive(string tiffPath)
{
    using var input = new OcrInput();
    input.LoadImageFrames(tiffPath);
    input.Deskew();
    input.DeNoise();
    input.Contrast();

    var result = new IronTesseract().Read(input);
    return result.Text;
}
Imports System

' IronOCR handles multi-frame TIFF natively — single call, no frame splitting
Public Function ExtractMultiFrameTiff(tiffPath As String) As String
    Using input As New OcrInput()
        input.LoadImageFrames(tiffPath)  ' all frames loaded automatically
        Dim result = New IronTesseract().Read(input)
        Return result.Text
    End Using
End Function

' Access per-page data for frame-level reporting
Public Sub ExtractTiffWithPageStats(tiffPath As String)
    Using input As New OcrInput()
        input.LoadImageFrames(tiffPath)

        Dim ocr As New IronTesseract()
        Dim result = ocr.Read(input)

        Console.WriteLine($"Total frames processed: {result.Pages.Length}")
        For Each page In result.Pages
            Console.WriteLine($"Frame {page.PageNumber}: " &
                              $"{page.Words.Length} words, " &
                              $"confidence {page.Confidence:F1}%")
        Next
    End Using
End Sub

' Combine with preprocessing for scanned TIFF archives
Public Function ExtractLowQualityTiffArchive(tiffPath As String) As String
    Using input As New OcrInput()
        input.LoadImageFrames(tiffPath)
        input.Deskew()
        input.DeNoise()
        input.Contrast()

        Dim result = New IronTesseract().Read(input)
        Return result.Text
    End Using
End Function
$vbLabelText   $csharpLabel

프레임당 거래 비용이 0으로 떨어집니다. System.Drawing 프레임 추출 루프와 임시 PNG 직렬화 단계가 완전히 제거됩니다. 문서 품질이 다양한 팩스 아카이브 워크플로의 경우, TIFF 및 GIF 입력 가이드 에서는 프레임 선택 옵션을 다루고, 이미지 품질 보정 가이드 에서는 전체 전처리 필터 세트를 설명합니다.

속도 제한 큐잉 없는 병렬 배치 처리

Azure Computer Vision의 S1 계층은 초당 최대 10건의 트랜잭션으로 처리량을 제한합니다. 이 속도를 초과하는 배치 작업은 HTTP 429 응답을 받습니다. 프로덕션 구현에는 제한 내에 머무르기 위해 속도 제한 래퍼, 세마포어 또는 큐잉 레이어가 필요합니다.IronOCR API는 설계상 스레드 안전합니다 — 스레드당 하나의 IronTesseract 인스턴스를 생성하고 Parallel.ForEach와 함께 실행합니다.

Azure 컴퓨터 비전 접근 방식:

// Must throttle to 10 TPS to avoid 429 errors on S1 tier
public class ThrottledAzureBatchProcessor
{
    private readonly ImageAnalysisClient _client;
    // Semaphore limits concurrent Azure calls to stay under 10 TPS
    private readonly SemaphoreSlim _throttle = new SemaphoreSlim(10, 10);

    public async Task<Dictionary<string, string>> ProcessBatchAsync(
        IEnumerable<string> imagePaths)
    {
        var results = new ConcurrentDictionary<string, string>();
        var tasks = imagePaths.Select(async path =>
        {
            await _throttle.WaitAsync();
            try
            {
                using var stream = File.OpenRead(path);
                var data = BinaryData.FromStream(stream);
                var response = await _client.AnalyzeAsync(data, VisualFeatures.Read);

                var text = string.Join("\n",
                    response.Value.Read.Blocks
                        .SelectMany(b => b.Lines)
                        .Select(l => l.Text));

                results[path] = text;

                // Respect 1-second window for the 10 TPS ceiling
                await Task.Delay(100);
            }
            catch (RequestFailedException ex) when (ex.Status == 429)
            {
                // Rate limited despite throttling — back off and retry
                await Task.Delay(2000);
                // Re-queue or log failure — simplified here
                results[path] = string.Empty;
            }
            finally
            {
                _throttle.Release();
            }
        });

        await Task.WhenAll(tasks);
        return new Dictionary<string, string>(results);
    }
}
// Must throttle to 10 TPS to avoid 429 errors on S1 tier
public class ThrottledAzureBatchProcessor
{
    private readonly ImageAnalysisClient _client;
    // Semaphore limits concurrent Azure calls to stay under 10 TPS
    private readonly SemaphoreSlim _throttle = new SemaphoreSlim(10, 10);

    public async Task<Dictionary<string, string>> ProcessBatchAsync(
        IEnumerable<string> imagePaths)
    {
        var results = new ConcurrentDictionary<string, string>();
        var tasks = imagePaths.Select(async path =>
        {
            await _throttle.WaitAsync();
            try
            {
                using var stream = File.OpenRead(path);
                var data = BinaryData.FromStream(stream);
                var response = await _client.AnalyzeAsync(data, VisualFeatures.Read);

                var text = string.Join("\n",
                    response.Value.Read.Blocks
                        .SelectMany(b => b.Lines)
                        .Select(l => l.Text));

                results[path] = text;

                // Respect 1-second window for the 10 TPS ceiling
                await Task.Delay(100);
            }
            catch (RequestFailedException ex) when (ex.Status == 429)
            {
                // Rate limited despite throttling — back off and retry
                await Task.Delay(2000);
                // Re-queue or log failure — simplified here
                results[path] = string.Empty;
            }
            finally
            {
                _throttle.Release();
            }
        });

        await Task.WhenAll(tasks);
        return new Dictionary<string, string>(results);
    }
}
Imports System.Collections.Concurrent
Imports System.IO
Imports System.Threading
Imports System.Threading.Tasks

' Must throttle to 10 TPS to avoid 429 errors on S1 tier
Public Class ThrottledAzureBatchProcessor
    Private ReadOnly _client As ImageAnalysisClient
    ' Semaphore limits concurrent Azure calls to stay under 10 TPS
    Private ReadOnly _throttle As New SemaphoreSlim(10, 10)

    Public Async Function ProcessBatchAsync(imagePaths As IEnumerable(Of String)) As Task(Of Dictionary(Of String, String))
        Dim results As New ConcurrentDictionary(Of String, String)()
        Dim tasks = imagePaths.Select(Async Function(path)
                                          Await _throttle.WaitAsync()
                                          Try
                                              Using stream = File.OpenRead(path)
                                                  Dim data = BinaryData.FromStream(stream)
                                                  Dim response = Await _client.AnalyzeAsync(data, VisualFeatures.Read)

                                                  Dim text = String.Join(vbLf,
                                                                         response.Value.Read.Blocks _
                                                                         .SelectMany(Function(b) b.Lines) _
                                                                         .Select(Function(l) l.Text))

                                                  results(path) = text

                                                  ' Respect 1-second window for the 10 TPS ceiling
                                                  Await Task.Delay(100)
                                              End Using
                                          Catch ex As RequestFailedException When ex.Status = 429
                                              ' Rate limited despite throttling — back off and retry
                                              Await Task.Delay(2000)
                                              ' Re-queue or log failure — simplified here
                                              results(path) = String.Empty
                                          Finally
                                              _throttle.Release()
                                          End Try
                                      End Function)

        Await Task.WhenAll(tasks)
        Return New Dictionary(Of String, String)(results)
    End Function
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

// 아니요 rate limiting needed — throughput is hardware-bound
public Dictionary<string, string> ProcessBatch(IEnumerable<string> imagePaths)
{
    var results = new ConcurrentDictionary<string, string>();

    Parallel.ForEach(imagePaths, imagePath =>
    {
        // Each thread gets its own IronTesseract instance — fully thread-safe
        var ocr = new IronTesseract();
        var result = ocr.Read(imagePath);
        results[imagePath] = result.Text;
    });

    return new Dictionary<string, string>(results);
}

// With controlled parallelism for memory-constrained environments
public Dictionary<string, string> ProcessBatchControlled(
    IEnumerable<string> imagePaths, int maxDegreeOfParallelism = 4)
{
    var results = new ConcurrentDictionary<string, string>();
    var options = new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism };

    Parallel.ForEach(imagePaths, options, imagePath =>
    {
        var ocr = new IronTesseract();
        var result = ocr.Read(imagePath);
        results[imagePath] = result.Text;
    });

    return new Dictionary<string, string>(results);
}
// 아니요 rate limiting needed — throughput is hardware-bound
public Dictionary<string, string> ProcessBatch(IEnumerable<string> imagePaths)
{
    var results = new ConcurrentDictionary<string, string>();

    Parallel.ForEach(imagePaths, imagePath =>
    {
        // Each thread gets its own IronTesseract instance — fully thread-safe
        var ocr = new IronTesseract();
        var result = ocr.Read(imagePath);
        results[imagePath] = result.Text;
    });

    return new Dictionary<string, string>(results);
}

// With controlled parallelism for memory-constrained environments
public Dictionary<string, string> ProcessBatchControlled(
    IEnumerable<string> imagePaths, int maxDegreeOfParallelism = 4)
{
    var results = new ConcurrentDictionary<string, string>();
    var options = new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism };

    Parallel.ForEach(imagePaths, options, imagePath =>
    {
        var ocr = new IronTesseract();
        var result = ocr.Read(imagePath);
        results[imagePath] = result.Text;
    });

    return new Dictionary<string, string>(results);
}
Imports System.Collections.Concurrent
Imports System.Collections.Generic
Imports System.Threading.Tasks

Public Class BatchProcessor
    ' 아니요 rate limiting needed — throughput is hardware-bound
    Public Function ProcessBatch(imagePaths As IEnumerable(Of String)) As Dictionary(Of String, String)
        Dim results = New ConcurrentDictionary(Of String, String)()

        Parallel.ForEach(imagePaths, Sub(imagePath)
                                         ' Each thread gets its own IronTesseract instance — fully thread-safe
                                         Dim ocr = New IronTesseract()
                                         Dim result = ocr.Read(imagePath)
                                         results(imagePath) = result.Text
                                     End Sub)

        Return New Dictionary(Of String, String)(results)
    End Function

    ' With controlled parallelism for memory-constrained environments
    Public Function ProcessBatchControlled(imagePaths As IEnumerable(Of String), Optional maxDegreeOfParallelism As Integer = 4) As Dictionary(Of String, String)
        Dim results = New ConcurrentDictionary(Of String, String)()
        Dim options = New ParallelOptions With {.MaxDegreeOfParallelism = maxDegreeOfParallelism}

        Parallel.ForEach(imagePaths, options, Sub(imagePath)
                                                  Dim ocr = New IronTesseract()
                                                  Dim result = ocr.Read(imagePath)
                                                  results(imagePath) = result.Text
                                              End Sub)

        Return New Dictionary(Of String, String)(results)
    End Function
End Class
$vbLabelText   $csharpLabel

세마포어, 100ms 지연, 그리고 HTTP 429 오류 처리 블록이 모두 제거되었습니다. 병렬 처리는 서비스 계층이 아니라 CPU 코어 수와 사용 가능한 메모리에 의해서만 제한됩니다. 멀티스레딩 예제는 타이밍 비교를 통해 전체 패턴을 보여주고, 속도 최적화 가이드는 배치 워크로드에 대한 엔진 구성 튜닝을 다룹니다.

Azure에서 거부하는 저품질 스캔 전처리

Azure Computer Vision은 서버 측 이미지 향상 기능을 제공하지만, 이 기능은 불투명하며 구성할 수 없습니다. 문서가 지나치게 왜곡되었거나, 노이즈가 많거나, 대비가 너무 낮은 경우 신뢰도가 낮은 결과가 반환되거나 수정할 방법 없이 빈 텍스트가 표시됩니다. IronOCR는 OcrInput에서 전처리 파이프라인을 직접 노출합니다.

Azure 컴퓨터 비전 접근 방식:

// 아니요 client-side preprocessing API — must preprocess externally before upload
public async Task<string> ExtractFromLowQualityScanAsync(string imagePath)
{
    // Option 1: Accept whatever Azure returns (may be empty or low-quality)
    using var stream = File.OpenRead(imagePath);
    var imageData = BinaryData.FromStream(stream);

    var result = await _client.AnalyzeAsync(imageData, VisualFeatures.Read);

    // 아니요 way to know if the server applied enhancement
    // 아니요 confidence on the overall result — only per-word
    var text = string.Join("\n",
        result.Value.Read.Blocks
            .SelectMany(b => b.Lines)
            .Select(l => l.Text));

    if (string.IsNullOrWhiteSpace(text))
    {
        // Option 2: Apply external preprocessing using System.Drawing, SkiaSharp,
        // or ImageMagick, re-serialize to stream, re-upload — second billable transaction
        throw new Exception("Azure returned empty result; manual preprocessing needed");
    }

    return text;
}
// 아니요 client-side preprocessing API — must preprocess externally before upload
public async Task<string> ExtractFromLowQualityScanAsync(string imagePath)
{
    // Option 1: Accept whatever Azure returns (may be empty or low-quality)
    using var stream = File.OpenRead(imagePath);
    var imageData = BinaryData.FromStream(stream);

    var result = await _client.AnalyzeAsync(imageData, VisualFeatures.Read);

    // 아니요 way to know if the server applied enhancement
    // 아니요 confidence on the overall result — only per-word
    var text = string.Join("\n",
        result.Value.Read.Blocks
            .SelectMany(b => b.Lines)
            .Select(l => l.Text));

    if (string.IsNullOrWhiteSpace(text))
    {
        // Option 2: Apply external preprocessing using System.Drawing, SkiaSharp,
        // or ImageMagick, re-serialize to stream, re-upload — second billable transaction
        throw new Exception("Azure returned empty result; manual preprocessing needed");
    }

    return text;
}
Imports System.IO
Imports System.Threading.Tasks

Public Class YourClassName
    Private _client As YourClientType ' Replace with the actual type of _client

    ' 아니요 client-side preprocessing API — must preprocess externally before upload
    Public Async Function ExtractFromLowQualityScanAsync(imagePath As String) As Task(Of String)
        ' Option 1: Accept whatever Azure returns (may be empty or low-quality)
        Using stream = File.OpenRead(imagePath)
            Dim imageData = BinaryData.FromStream(stream)

            Dim result = Await _client.AnalyzeAsync(imageData, VisualFeatures.Read)

            ' 아니요 way to know if the server applied enhancement
            ' 아니요 confidence on the overall result — only per-word
            Dim text = String.Join(vbLf, result.Value.Read.Blocks.SelectMany(Function(b) b.Lines).Select(Function(l) l.Text))

            If String.IsNullOrWhiteSpace(text) Then
                ' Option 2: Apply external preprocessing using System.Drawing, SkiaSharp,
                ' or ImageMagick, re-serialize to stream, re-upload — second billable transaction
                Throw New Exception("Azure returned empty result; manual preprocessing needed")
            End If

            Return text
        End Using
    End Function
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

// Preprocessing is part of the same call — no re-upload, no second transaction
public string ExtractFromLowQualityScan(string imagePath)
{
    using var input = new OcrInput();
    input.LoadImage(imagePath);

    // Correct common scanning defects before OCR
    input.Deskew();           // Fix rotated documents
    input.DeNoise();          // Remove scanner noise
    input.Contrast();         // Improve contrast for faded documents
    input.Binarize();         // Convert to black and white

    var ocr = new IronTesseract();
    var result = ocr.Read(input);

    Console.WriteLine($"Confidence after preprocessing: {result.Confidence:F1}%");
    return result.Text;
}

// For severely degraded documents
public string ExtractFromDegradedDocument(string imagePath)
{
    using var input = new OcrInput();
    input.LoadImage(imagePath);
    input.DeepCleanBackgroundNoise();  // Deep learning-based noise removal
    input.Deskew();
    input.Scale(150);                  // Upscale for better character resolution

    var result = new IronTesseract().Read(input);
    return result.Text;
}
// Preprocessing is part of the same call — no re-upload, no second transaction
public string ExtractFromLowQualityScan(string imagePath)
{
    using var input = new OcrInput();
    input.LoadImage(imagePath);

    // Correct common scanning defects before OCR
    input.Deskew();           // Fix rotated documents
    input.DeNoise();          // Remove scanner noise
    input.Contrast();         // Improve contrast for faded documents
    input.Binarize();         // Convert to black and white

    var ocr = new IronTesseract();
    var result = ocr.Read(input);

    Console.WriteLine($"Confidence after preprocessing: {result.Confidence:F1}%");
    return result.Text;
}

// For severely degraded documents
public string ExtractFromDegradedDocument(string imagePath)
{
    using var input = new OcrInput();
    input.LoadImage(imagePath);
    input.DeepCleanBackgroundNoise();  // Deep learning-based noise removal
    input.Deskew();
    input.Scale(150);                  // Upscale for better character resolution

    var result = new IronTesseract().Read(input);
    return result.Text;
}
Imports System

Public Class OcrProcessor
    ' Preprocessing is part of the same call — no re-upload, no second transaction
    Public Function ExtractFromLowQualityScan(imagePath As String) As String
        Using input As New OcrInput()
            input.LoadImage(imagePath)

            ' Correct common scanning defects before OCR
            input.Deskew()           ' Fix rotated documents
            input.DeNoise()          ' Remove scanner noise
            input.Contrast()         ' Improve contrast for faded documents
            input.Binarize()         ' Convert to black and white

            Dim ocr As New IronTesseract()
            Dim result = ocr.Read(input)

            Console.WriteLine($"Confidence after preprocessing: {result.Confidence:F1}%")
            Return result.Text
        End Using
    End Function

    ' For severely degraded documents
    Public Function ExtractFromDegradedDocument(imagePath As String) As String
        Using input As New OcrInput()
            input.LoadImage(imagePath)
            input.DeepCleanBackgroundNoise()  ' Deep learning-based noise removal
            input.Deskew()
            input.Scale(150)                  ' Upscale for better character resolution

            Dim result = New IronTesseract().Read(input)
            Return result.Text
        End Using
    End Function
End Class
$vbLabelText   $csharpLabel

외부 전처리 의존성 — System.Drawing, SkiaSharp 또는 ImageMagick — 이 제거됩니다. 재업로드 및 두 번째 거래 비용이 사라집니다. 전처리 파이프라인은 OcrInput 라이프사이클의 일부이며, 때문에 OCR 엔진이 이미지를 보기 전에 적용됩니다. 이미지 필터 튜토리얼에서는 각 필터에 대해 필터 적용 전후의 정확도 비교를 통해 자세히 설명합니다.

Azure 컴퓨터 비전 OCR API와IronOCR매핑 참조

Azure 컴퓨터 비전 IronOCR에 상응하는
ImageAnalysisClient IronTesseract
new AzureKeyCredential(apiKey) IronOcr.License.LicenseKey = key
new Uri(endpoint) 필요하지 않음
client.AnalyzeAsync(data, VisualFeatures.Read) ocr.Read(imagePath)
BinaryData.FromStream(stream) input.LoadImage(stream)
BinaryData.FromBytes(bytes) input.LoadImage(bytes)
result.Value.Read.Blocks result.Pages[i].Paragraphs
block.Lines result.Pages[i].Lines
line.Text line.Text
line.Words line.Words
word.Text word.Text
word.Confidence (0.0–1.0 float) word.Confidence (0–100 double)
word.BoundingPolygon word.X, word.Y, word.Width, word.Height
DocumentAnalysisClient IronTesseract + OcrInput
AnalyzeDocumentAsync(WaitUntil.Completed, "prebuilt-read", stream) ocr.Read(input) with input.LoadPdf(path)
operation.Value.Pages result.Pages
page.Lines[i].Content result.Lines[i].Text
UpdateStatusAsync() 폴링 루프 필수 아님 — 동기식 결과
RequestFailedException (상태 코드 429) 해당 사항 없음 - 요금 제한 없음
RequestFailedException (상태 코드 5xx) 해당 사항 없음 - 서비스 오류 없음
VisualFeatures.Read enum flag 암시적 — Read() 는 항상 텍스트를 추출합니다
Form Recognizer prebuilt-read 모델 내장형 OCR 엔진 (모델 선택 기능 없음)
appsettings.json에서 Azure 엔드포인트 URL 필요하지 않음
API 키 순환 절차 필요하지 않음

일반적인 마이그레이션 문제와 해결책

문제 1: 변경할 수 없는 비동기 인터페이스 계약

Azure Computer Vision: 서비스 인터페이스는 종종 Azure가 비동기를 요구하기 때문에 Task<string> 반환 유형을 선언합니다. 호출 코드, 컨트롤러 및 백그라운드 작업자는 모두 비동기 메서드로 작성됩니다.IronOCR로 전환하면 OCR 계층에서 비동기 처리가 필요 없어지지만, 대규모 코드베이스에서 모든 인터페이스 시그니처를 변경하는 것이 항상 가능한 것은 아닙니다.

솔루션: 기존 인터페이스를 만족시키기 위해 복잡한 리팩터 없이 비동기IronOCR호출을 Task.Run로 감쌉니다:

// Existing interface — do not change it
public interface IOcrService
{
    Task<string> ReadAsync(string imagePath);
}

// NewIronOCR implementation — fulfills the contract
public class IronOcrService : IOcrService
{
    private readonly IronTesseract _ocr;

    public IronOcrService(IronTesseract ocr) => _ocr = ocr;

    public Task<string> ReadAsync(string imagePath)
    {
        // Task.Run offloads to thread pool — no await chain needed
        return Task.Run(() => _ocr.Read(imagePath).Text);
    }
}
// Existing interface — do not change it
public interface IOcrService
{
    Task<string> ReadAsync(string imagePath);
}

// NewIronOCR implementation — fulfills the contract
public class IronOcrService : IOcrService
{
    private readonly IronTesseract _ocr;

    public IronOcrService(IronTesseract ocr) => _ocr = ocr;

    public Task<string> ReadAsync(string imagePath)
    {
        // Task.Run offloads to thread pool — no await chain needed
        return Task.Run(() => _ocr.Read(imagePath).Text);
    }
}
Imports System.Threading.Tasks

' Existing interface — do not change it
Public Interface IOcrService
    Function ReadAsync(imagePath As String) As Task(Of String)
End Interface

' NewIronOCR implementation — fulfills the contract
Public Class IronOcrService
    Implements IOcrService

    Private ReadOnly _ocr As IronTesseract

    Public Sub New(ocr As IronTesseract)
        _ocr = ocr
    End Sub

    Public Function ReadAsync(imagePath As String) As Task(Of String) Implements IOcrService.ReadAsync
        ' Task.Run offloads to thread pool — no await chain needed
        Return Task.Run(Function() _ocr.Read(imagePath).Text)
    End Function
End Class
$vbLabelText   $csharpLabel

이는 유효한 중간 단계입니다. 비동기 OCR 가이드에서는 완전한 비동기 통합이 요구되는 시나리오를 위해 IronOCR에 내장된 비동기 지원 기능을 다룹니다.

문제 2: 신뢰도 임계값 논리가 잘못된 결과를 도출함

Azure Computer Vision: Azure는 0.0에서 1.0 사이의 float으로 단어 신뢰도를 반환합니다. 기존 필터링 코드는 word.Confidence > 0.85f과 같은 임계값을 사용합니다. 마이그레이션 후에는IronOCR신뢰도 범위가 0~100이 아니라 0~1이기 때문에 이러한 비교는 항상 거짓으로 평가됩니다.

해결 방법: 필터링 로직을 업데이트할 때 기존 Azure 임계값에 100을 곱합니다.

// Before: Azure threshold (0.0 - 1.0 scale)
var highConfidenceWords = azureWords
    .Where(w => w.Confidence > 0.85f)
    .Select(w => w.Text);

// After:IronOCR threshold (0 - 100 scale)
var result = new IronTesseract().Read(imagePath);
var highConfidenceWords = result.Words
    .Where(w => w.Confidence > 85.0)
    .Select(w => w.Text);

// Overall document confidence — also on 0-100 scale
if (result.Confidence < 70.0)
{
    // Document may need preprocessing or manual review
}
// Before: Azure threshold (0.0 - 1.0 scale)
var highConfidenceWords = azureWords
    .Where(w => w.Confidence > 0.85f)
    .Select(w => w.Text);

// After:IronOCR threshold (0 - 100 scale)
var result = new IronTesseract().Read(imagePath);
var highConfidenceWords = result.Words
    .Where(w => w.Confidence > 85.0)
    .Select(w => w.Text);

// Overall document confidence — also on 0-100 scale
if (result.Confidence < 70.0)
{
    // Document may need preprocessing or manual review
}
Imports System.Linq

' Before: Azure threshold (0.0 - 1.0 scale)
Dim highConfidenceWords = azureWords _
    .Where(Function(w) w.Confidence > 0.85F) _
    .Select(Function(w) w.Text)

' After: IronOCR threshold (0 - 100 scale)
Dim result = New IronTesseract().Read(imagePath)
Dim highConfidenceWords = result.Words _
    .Where(Function(w) w.Confidence > 85.0) _
    .Select(Function(w) w.Text)

' Overall document confidence — also on 0-100 scale
If result.Confidence < 70.0 Then
    ' Document may need preprocessing or manual review
End If
$vbLabelText   $csharpLabel

문제 3: 폼 인식기 사전 구축 모델 필드 추출에는IronOCR직접적으로 상응하는 기능이 없습니다.

Azure Computer Vision: Form Recognizer의 사전 구축된 송장 및 영수증 모델은 이름이 지정된 필드를 자동으로 추출합니다 — InvoiceTotal, VendorName, InvoiceDate — 해당 필드가 페이지에서 어디에 나타나는지 지정할 필요 없이. 추출 로직은 Azure 모델에 내장되어 있습니다.

솔루션: 모델 기반 필드 추출을 CropRectangle을 사용한 영역 기반 OCR으로 대체합니다. 이를 위해서는 문서 레이아웃을 알아야 하지만, 대부분의 실제 배포 환경에서는 이미 고정된 템플릿이 있습니다.

var ocr = new IronTesseract();

// Define extraction zones for a known invoice template
var headerZone    = new CropRectangle(50,  40,  400, 60);
var totalZone     = new CropRectangle(350, 600, 250, 50);
var dateZone      = new CropRectangle(400, 100, 200, 40);

string header, total, date;

using (var input = new OcrInput())
{
    input.LoadImage("invoice.jpg", headerZone);
    header = ocr.Read(input).Text.Trim();
}

using (var input = new OcrInput())
{
    input.LoadImage("invoice.jpg", totalZone);
    total = ocr.Read(input).Text.Trim();
}

using (var input = new OcrInput())
{
    input.LoadImage("invoice.jpg", dateZone);
    date = ocr.Read(input).Text.Trim();
}
var ocr = new IronTesseract();

// Define extraction zones for a known invoice template
var headerZone    = new CropRectangle(50,  40,  400, 60);
var totalZone     = new CropRectangle(350, 600, 250, 50);
var dateZone      = new CropRectangle(400, 100, 200, 40);

string header, total, date;

using (var input = new OcrInput())
{
    input.LoadImage("invoice.jpg", headerZone);
    header = ocr.Read(input).Text.Trim();
}

using (var input = new OcrInput())
{
    input.LoadImage("invoice.jpg", totalZone);
    total = ocr.Read(input).Text.Trim();
}

using (var input = new OcrInput())
{
    input.LoadImage("invoice.jpg", dateZone);
    date = ocr.Read(input).Text.Trim();
}
Imports IronOcr

Dim ocr As New IronTesseract()

' Define extraction zones for a known invoice template
Dim headerZone As New CropRectangle(50, 40, 400, 60)
Dim totalZone As New CropRectangle(350, 600, 250, 50)
Dim dateZone As New CropRectangle(400, 100, 200, 40)

Dim header As String
Dim total As String
Dim date As String

Using input As New OcrInput()
    input.LoadImage("invoice.jpg", headerZone)
    header = ocr.Read(input).Text.Trim()
End Using

Using input As New OcrInput()
    input.LoadImage("invoice.jpg", totalZone)
    total = ocr.Read(input).Text.Trim()
End Using

Using input As New OcrInput()
    input.LoadImage("invoice.jpg", dateZone)
    date = ocr.Read(input).Text.Trim()
End Using
$vbLabelText   $csharpLabel

지역 기반 OCR 가이드 에서는 좌표계 세부 정보 및 다중 지역 일괄 처리에 대해 다룹니다.

문제 4: hOCR 및 구조화된 내보내기 누락

Azure Computer Vision: Azure는 hOCR 출력물을 제공하지 않습니다. 하위 문서 분석 도구에 필요한 표준화된 레이아웃 데이터를 필요로 하는 팀은 Azure 응답에서 경계 상자를 수동으로 추출하여 자체 출력 형식을 구성합니다.

해결책:IronOCR단 한 번의 호출로 표준을 준수하는 hOCR 결과를 생성합니다.

var result = new IronTesseract().Read("document.jpg");

// Write hOCR file — recognized by most document analysis tools
result.SaveAsHocrFile("document.hocr");

// Searchable PDF — alternative for archive and search indexing workflows
result.SaveAsSearchablePdf("document-searchable.pdf");
var result = new IronTesseract().Read("document.jpg");

// Write hOCR file — recognized by most document analysis tools
result.SaveAsHocrFile("document.hocr");

// Searchable PDF — alternative for archive and search indexing workflows
result.SaveAsSearchablePdf("document-searchable.pdf");
Dim result = (New IronTesseract()).Read("document.jpg")

' Write hOCR file — recognized by most document analysis tools
result.SaveAsHocrFile("document.hocr")

' Searchable PDF — alternative for archive and search indexing workflows
result.SaveAsSearchablePdf("document-searchable.pdf")
$vbLabelText   $csharpLabel

문제 5: Azure SDK 버전과 다른 Azure 패키지 간의 충돌

Azure Computer Vision: 여러 Azure SDK 패키지 (Azure.Storage.Blobs, Azure.Identity, Azure.KeyVault.Secrets)를 사용하는 프로젝트는 Azure.Core 전이 의존성 간의 버전 충돌에 직면할 수 있습니다. Azure SDK의 모노레포 버전 관리 정책은 충돌을 줄이는 데 도움이 되지만, 특히 일반 출시(GA) 버전과 미리 보기(Preview) 버전의 SDK를 혼합해서 사용할 경우 모든 충돌을 완전히 해결하지는 못합니다.

솔루션: Azure.AI.Vision.ImageAnalysisAzure.AI.FormRecognizer를 제거하면 그 SDK 패키지들이 의존성 트리에서 제거됩니다. 프로젝트가 Azure를 OCR에만 사용하는 경우, 전체 Azure.* 의존성 세트가 제거됩니다. 다른 Azure 서비스가 남아 있는 경우 패키지 수가 줄어들어 충돌 발생 가능성이 낮아집니다.

# Remove only the OCR-related Azure packages
dotnet remove package Azure.AI.Vision.ImageAnalysis
dotnet remove package Azure.AI.FormRecognizer

# Verify remaining Azure packages have no new conflicts
dotnet restore
dotnet build
# Remove only the OCR-related Azure packages
dotnet remove package Azure.AI.Vision.ImageAnalysis
dotnet remove package Azure.AI.FormRecognizer

# Verify remaining Azure packages have no new conflicts
dotnet restore
dotnet build
SHELL

문제 6: 스캔한 문서 품질이 Azure 허용 기준치 미달

Azure 컴퓨터 비전: 해상도가 매우 낮은 이미지(약 150 DPI 미만) 또는 심하게 왜곡된 스캔의 경우 Azure 서버 측 파이프라인에서 최소한의 텍스트 또는 빈 텍스트만 반환되며 어떤 개선 작업이 시도되었는지에 대한 피드백이 제공되지 않습니다. 호출자는 외부 전처리 없이는 결과를 개선할 방법이 없습니다.

해결 방법: IronOCR의 전처리 파이프라인을 사용하여 OCR을 수행하기 전에 이미지를 준비합니다.

using var input = new OcrInput();
input.LoadImage("low-quality-scan.jpg");
input.Deskew();
input.DeNoise();
input.Scale(200);       // Upscale to improve character resolution
input.Contrast();
input.Sharpen();

var result = new IronTesseract().Read(input);
Console.WriteLine($"Extraction confidence: {result.Confidence:F1}%");
using var input = new OcrInput();
input.LoadImage("low-quality-scan.jpg");
input.Deskew();
input.DeNoise();
input.Scale(200);       // Upscale to improve character resolution
input.Contrast();
input.Sharpen();

var result = new IronTesseract().Read(input);
Console.WriteLine($"Extraction confidence: {result.Confidence:F1}%");
Imports IronOcr

Using input As New OcrInput()
    input.LoadImage("low-quality-scan.jpg")
    input.Deskew()
    input.DeNoise()
    input.Scale(200) ' Upscale to improve character resolution
    input.Contrast()
    input.Sharpen()

    Dim result = New IronTesseract().Read(input)
    Console.WriteLine($"Extraction confidence: {result.Confidence:F1}%")
End Using
$vbLabelText   $csharpLabel

이미지 방향 보정 가이드는 특히 기울기 보정 및 회전 감지에 대해 다룹니다.

Azure 컴퓨터 비전 OCR 마이그레이션 체크리스트

사전 마이그레이션

코드베이스에서 Azure 컴퓨터 비전 및 폼 인식기 사용 부분을 모두 찾으십시오.

# Find all Azure OCR-related using statements
grep -r "Azure.AI.Vision.ImageAnalysis" --include="*.cs" .
grep -r "Azure.AI.FormRecognizer" --include="*.cs" .
grep -r "ImageAnalysisClient" --include="*.cs" .
grep -r "DocumentAnalysisClient" --include="*.cs" .
grep -r "AnalyzeAsync" --include="*.cs" .
grep -r "AnalyzeDocumentAsync" --include="*.cs" .
grep -r "VisualFeatures.Read" --include="*.cs" .
grep -r "WaitUntil.Completed" --include="*.cs" .
grep -r "UpdateStatusAsync" --include="*.cs" .
grep -r "AzureKeyCredential" --include="*.cs" .
# Find all Azure OCR-related using statements
grep -r "Azure.AI.Vision.ImageAnalysis" --include="*.cs" .
grep -r "Azure.AI.FormRecognizer" --include="*.cs" .
grep -r "ImageAnalysisClient" --include="*.cs" .
grep -r "DocumentAnalysisClient" --include="*.cs" .
grep -r "AnalyzeAsync" --include="*.cs" .
grep -r "AnalyzeDocumentAsync" --include="*.cs" .
grep -r "VisualFeatures.Read" --include="*.cs" .
grep -r "WaitUntil.Completed" --include="*.cs" .
grep -r "UpdateStatusAsync" --include="*.cs" .
grep -r "AzureKeyCredential" --include="*.cs" .
SHELL

Azure OCR 엔드포인트 및 키가 포함된 구성 파일을 식별합니다.

grep -r "cognitiveservices.azure.com" --include="*.json" .
grep -r "AzureComputerVision\|FormRecognizer" --include="*.json" .
grep -r "ComputerVision\|FormRecognizer" --include="appsettings*.json" .
grep -r "cognitiveservices.azure.com" --include="*.json" .
grep -r "AzureComputerVision\|FormRecognizer" --include="*.json" .
grep -r "ComputerVision\|FormRecognizer" --include="appsettings*.json" .
SHELL

코딩 시작 전 재고 품목:

  • Azure OCR 서비스 패턴을 구현하는 클래스 수
  • OCR 호출에서 전파되는 비동기 메서드 체인의 개수
  • Azure 척도(0.0~1.0)를 사용하여 단어 신뢰도 임계값을 설정합니다.
  • 지역 기반 교체가 필요한 양식 인식기 사전 구축 모델(송장, 영수증, 신분증) 사용 현황을 파악합니다.
  • 현재 프레임별 업로드를 위해 분할되어 있는 멀티프레임 TIFF 입력 파일을 식별합니다.
  • Docker 및 CI/CD 구성에서 더 이상 필요하지 않은 Azure 네트워크 아웃바운드 규칙을 확인하세요.

코드 마이그레이션

  1. 모든 프로젝트에서 Azure.AI.Vision.ImageAnalysis NuGet 패키지를 제거하십시오
  2. 모든 프로젝트에서 Azure.AI.FormRecognizer NuGet 패키지를 제거하십시오
  3. 영향을 받은 각 프로젝트에서 dotnet add package IronOcr를 실행하십시오
  4. 애플리케이션 시작 시 IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")를 추가하십시오
  5. Azure 사용을 대체합니다. using Azure.AI.Vision.ImageAnalysis;withusing IronOcr;`
  6. DI에서 싱글톤으로 IronTesseract를 등록하십시오 (services.AddSingleton<IronTesseract>())
  7. AzureComputerVisionOptions 또는 동등한 구성 클래스를 제거하세요; appsettings.json Azure OCR 섹션을 제거하십시오
  8. 모든 서비스 클래스에서 ImageAnalysisClient 생성자 주입을 IronTesseract 주입으로 대체하십시오
  9. AnalyzeAsync(BinaryData.FromStream(stream), VisualFeatures.Read) 호출을 경우에 따라 ocr.Read(imagePath) 또는 ocr.Read(input) + OcrInput로 대체하십시오
  10. 모든 UpdateStatusAsync 폴링 루프 및 WaitUntil.Completed 패턴을 제거하십시오; DocumentAnalysisClientIronTesseract + OcrInput.LoadPdf()으로 대체하십시오
  11. 단어 신뢰도 임계값 비교 업데이트: 모든 Azure 0.0~1.0 값에 100을 곱합니다.
  12. 다각형 경계 상자 접근 (word.BoundingPolygon.Min/Max) 를 직접 word.X, word.Y, word.Width, word.Height 속성으로 대체하십시오
  13. 다중 프레임 TIFF 프레임당 업로드 루프를 input.LoadImageFrames(tiffPath)로 대체하십시오
  14. Form Recognizer 사전 구축 모델 필드 추출을 알려진 문서 템플릿에 대한 CropRectangle 기반 영역 OCR로 변환하십시오
  15. HTTP 429 및 5xx를 위해 RequestFailedException 캐치 블록을 제거하십시오; 오류 처리를 파일 시스템 및 입력 유효성 검사 예외로만 간소화합니다

마이그레이션 이후

  • 20개 이상의 문서로 구성된 대표 샘플에 대해 일반 텍스트 추출 결과가 Azure 결과와 일치하거나 더 나은지 확인합니다.
  • 모니터링 시스템에서 페이지별 요금 청구 없이 여러 페이지로 구성된 PDF 처리 시 모든 페이지에 텍스트가 생성되는지 확인합니다.
  • 다중 프레임 TIFF 처리 테스트에서 원본 프레임 수와 동일한 페이지 수가 반환됩니다.
  • 단어 신뢰도 값이 0~100 범위 내에 있는지, 그리고 임계값 비교가 올바르게 작동하는지 검증합니다.
  • 단어 경계 상자 좌표가 원본 이미지 좌표계와 올바르게 정렬되었는지 확인합니다.
  • 일괄 처리 경로를 실행하고 속도 제한 오류나 세마포어 경합 없이 완료되는지 확인합니다.
  • 외부 인터넷 액세스가 없는 환경에서 테스트하여 Azure 엔드포인트 호출이 발생하지 않는지 확인합니다.
  • Docker 및 컨테이너 배포가 @cognitiveservices.azure.com를 위한 아웃바운드 방화벽 규칙 없이 성공적으로 시작되었는지 확인하십시오
  • DI 컨테이너 구성이 성공적으로 완료되었으며 IronTesseract 싱글톤이 올바르게 해결되었는지 확인하십시오
  • 모든 배포 환경에서 IRONOCR_LICENSE 환경 변수가 설정되어 있으며, OCR이 라이선스가 부착된(워터마크 없는) 출력을 생성하는지 확인합니다

IronOCR로 마이그레이션할 때의 주요 이점

비용을 예측할 수 있습니다. Azure Computer Vision의 트랜잭션별 비용 측정기는 지속적으로 작동합니다. 문서량이 많은 달에는IronOCR라이선스의 연간 비용을 초과할 수 있습니다. 마이그레이션 후 OCR 예산은 고정 항목이 됩니다. 처리 볼륨은 사업 확장, 대량 재처리 또는 일시적인 급증으로 인해 커질 수 있습니다 — 더 큰 송장을 생성하지 않고. IronOCR 라이선싱 페이지는 단일 개발자 라이선스의 $999부터 무제한 개발자의 경우 $2,999까지의 계층 옵션을 보여줍니다.

응용 프로그램 스택이 간단해집니다. Azure.AI.Vision.ImageAnalysisAzure.AI.FormRecognizer를 제거하면 두 개의 NuGet 패키지, 두 개의 Azure 리소스 구성, 두 세트의 자격 증명 및 전체 비동기 폴링 인프라가 제거됩니다. 클라우드 I/O 때문에 이전에 async Task<string> 서명을 필요로 했던 서비스 클래스가 동기 메서드가 됩니다. 오류 처리 범위는 네트워크 오류, 속도 제한, 서비스 가용성에서 파일 시스템 및 입력 유효성 검사로 좁혀집니다. 이 코드베이스에서 작업하는 모든 미래 개발자는 이해해야 할 코드가 훨씬 적어질 것입니다.

문서 데이터는 조직의 통제 하에 유지됩니다. 마이그레이션 후 OCR 처리는 로컬 작업으로 진행됩니다. 문서는 조직 경계를 넘지 않으며, Azure 원격 측정 데이터에 나타나지 않고, Microsoft의 데이터 보존 또는 처리 정책의 적용을 받지 않습니다. HIPAA 적용 대상 기관, ITAR 계약업체, GDPR 규제 대상 조직 및 데이터 상주 요건이 있는 모든 팀은 클라우드 제3자에 대한 규정 준수 검토 없이 문서를 처리할 수 있습니다. Linux 배포 가이드Docker 배포 가이드는 제한된 환경에서IronOCR배포하는 방법을 보여줍니다.

배치 처리량은 하드웨어에 따라 확장됩니다. Azure S1 계층의 10 TPS(초당 트랜잭션 수) 제한은 엄격한 제한이므로 이를 해결하려면 큐잉, 속도 제한 또는 계층 업그레이드가 필요합니다. 마이그레이션 후, 동시 OCR 작업은 서비스가 부과한 제한 없이 가용 CPU 코어를 포화시킵니다. 4코어 서버는 동시에 네 개의 IronTesseract 인스턴스를 실행할 수 있습니다. 멀티스레딩 예제는 이러한 패턴을 보여주고 코어 수에 따라 처리량이 확장됨을 나타냅니다.

전처리 과정의 결함은 코드에서 해결할 수 있습니다. Azure Computer Vision의 서버 측 이미지 향상 기능은 블랙박스처럼 작동합니다. 스캔이 빈 출력이나 신뢰도가 낮은 출력을 반환하는 경우, 수용하거나 추가 비용을 들여 재업로드하기 전에 외부에서 전처리하는 것 외에는 선택지가 없습니다. IronOCR의 OcrInput 파이프라인은 데스큐, 잡음 제거, 대비, 이진화, 스케일, 선명화 및 깊은 잡음 제거를 일급 메서드로 노출합니다. 문제가 있는 스캔 유형은 조정 가능한 매개변수가 됩니다. 전처리 기능 페이지에는 전체 필터 세트 목록과 각 필터가 어떤 스캔 결함을 해결하는지에 대한 안내가 나와 있습니다.

참고해 주세요Azure Computer Vision과 Tesseract는 각 소유자의 등록 상표입니다. 이 사이트는 Google이나 Microsoft와 제휴, 승인, 후원받지 않습니다. 모든 제품명, 로고 및 브랜드는 해당 소유자의 자산입니다. 비교는 정보 제공 목적으로만 사용되며, 작성 시점에 공개적으로 이용 가능한 정보를 반영합니다.

자주 묻는 질문

Azure 컴퓨터 비전 OCR에서 IronOCR로 마이그레이션해야 하는 이유는 무엇인가요?

일반적인 동인으로는 COM 상호 운용의 복잡성 제거, 파일 기반 라이선스 관리 대체, 페이지당 과금 방지, Docker/컨테이너 배포 활성화, 표준 .NET 툴링과 통합되는 NuGet 네이티브 워크플로 채택 등이 있습니다.

Azure Computer Vision OCR에서 IronOCR로 마이그레이션할 때 변경되는 주요 코드는 무엇인가요?

Azure Computer Vision 초기화 시퀀스를 IronTesseract 인스턴스화로 대체하고, COM 수명 주기 관리(명시적 만들기/로드/닫기 패턴)를 제거하고, 결과 속성 이름을 업데이트합니다. 그 결과 상용구 줄이 훨씬 줄어듭니다.

마이그레이션을 시작하려면 IronOCR을 어떻게 설치하나요?

패키지 관리자 콘솔에서 '설치-패키지 IronOcr'을 실행하거나 CLI에서 '닷넷 추가 패키지 IronOcr'을 실행하세요. 언어 팩은 별도의 패키지입니다: 예를 들어 프랑스어의 경우 '닷넷 추가 패키지 IronOcr.Languages.French'를 실행합니다.

IronOCR은 표준 비즈니스 문서에 대해 Azure Computer Vision OCR의 OCR 정확도와 일치하나요?

IronOCR은 송장, 계약서, 영수증, 타이핑된 양식 등 표준 비즈니스 콘텐츠에 대해 높은 정확도를 달성합니다. 이미지 전처리 필터(데스큐, 노이즈 제거, 대비 향상)는 품질이 저하된 입력에 대한 인식률을 더욱 향상시킵니다.

Azure 컴퓨터 비전 OCR이 별도로 설치하는 언어 데이터는 IronOCR에서 어떻게 처리하나요?

IronOCR의 언어 데이터는 NuGet 패키지로 배포됩니다. '닷넷 추가 패키지 IronOcr.Languages.German'은 독일어 지원을 설치합니다. 수동 파일 배치나 디렉터리 경로는 필요하지 않습니다.

Azure Computer Vision OCR에서 IronOCR로 마이그레이션하려면 배포 인프라를 변경해야 하나요?

IronOCR은 Azure 컴퓨터 비전 OCR보다 인프라 변경이 더 적게 필요합니다. SDK 바이너리 경로, 라이선스 파일 배치 또는 라이선스 서버 구성이 필요하지 않습니다. NuGet 패키지에는 전체 OCR 엔진이 포함되어 있으며 라이선스 키는 애플리케이션 코드에 설정된 문자열입니다.

마이그레이션 후 IronOCR 라이선싱은 어떻게 구성하나요?

애플리케이션 시작 코드에 IronOcr.License.LicenseKey = "YOUR-KEY"를 할당하세요. Docker 또는 Kubernetes에서 키를 환경 변수로 저장하고 시작 시 키를 읽습니다. 트래픽을 수락하기 전에 License.IsValidLicense를 사용하여 유효성을 검사하세요.

IronOCR은 Azure Computer Vision과 동일한 방식으로 PDF를 처리할 수 있나요?

예. IronOCR은 원본 및 스캔한 PDF를 모두 읽습니다. IronTesseract를 인스턴스화하고, 입력이 PDF 경로 또는 OcrPdfInput인 경우 ocr.Read(input)를 호출하고, OcrResult 페이지를 반복하면 됩니다. 별도의 PDF 렌더링 파이프라인이 필요하지 않습니다.

IronOCR은 대량 처리에서 스레딩을 어떻게 처리하나요?

IronTesseract는 스레드별로 인스턴스화해도 안전합니다. Parallel.ForEach 또는 Task 풀에서 스레드당 하나의 인스턴스를 스핀업하고, OCR을 동시에 실행하고, 완료되면 각 인스턴스를 폐기하면 됩니다. 전역 상태나 잠금이 필요하지 않습니다.

IronOCR은 텍스트 추출 후 어떤 출력 형식을 지원하나요?

IronOCR은 텍스트, 단어 좌표, 신뢰도 점수, 페이지 구조를 포함한 구조화된 결과를 반환합니다. 내보내기 옵션에는 일반 텍스트, 검색 가능한 PDF, 다운스트림 처리를 위한 구조화된 결과 개체가 포함됩니다.

워크로드 확장을 위해 Azure Computer Vision OCR보다 IronOCR 가격이 더 예측 가능한가요?

IronOCR은 페이지당 또는 볼륨당 요금이 없는 정액제 영구 라이선스를 사용합니다. 10,000페이지를 처리하든 1,000만 페이지를 처리하든 라이선스 비용은 일정하게 유지됩니다. 볼륨 및 팀 라이선스 옵션은 IronOCR 가격 페이지에서 확인할 수 있습니다.

Azure Computer Vision OCR에서 IronOCR로 마이그레이션한 후 기존 테스트는 어떻게 되나요?

추출된 텍스트 콘텐츠에 대해 어설트하는 테스트는 마이그레이션 후에도 계속 통과해야 합니다. API 호출 패턴 또는 COM 개체 수명 주기의 유효성을 검사하는 테스트는 IronOCR의 더 간단한 초기화 및 결과 모델을 반영하도록 업데이트해야 합니다.

칸나오팟 우돈판트
소프트웨어 엔지니어
카나팟은 소프트웨어 엔지니어가 되기 전 일본 홋카이도 대학교에서 환경 자원학 박사 학위를 취득했습니다. 학위 과정 중에는 생물생산공학과 소속 차량 로봇 연구실에서 활동하기도 했습니다. 2022년에는 C# 기술을 활용하여 Iron Software의 엔지니어링 팀에 합류했고, 현재 IronPDF 개발에 집중하고 있습니다. 카나팟은 IronPDF에 사용되는 대부분의 코드를 직접 작성하는 개발자로부터 배울 수 있다는 점에 만족하며, 동료들과의 소통을 통해 배우는 것 외에도 Iron Software에서 일하는 즐거움을 누리고 있습니다. 코딩이나 문서 작업을 하지 않을 때는 주로 PS5로 게임을 하거나 The Last of Us를 다시 시청하는 것을 즐깁니다.

아이언 서포트 팀

저희는 주 5일, 24시간 온라인으로 운영합니다.
채팅
이메일
전화해