C#에서 비동기 및 멀티스레딩을 사용하는 방법
Mindee에서 IronOCR 로 마이그레이션하면 문서 처리가 외부 클라우드 서버에서 자체 인프라로 이전되어 페이지당 요금 청구, 타사 데이터 전송 및 런타임 시 네트워크 가용성 종속성이 사라집니다. 아래 섹션에서는 패키지 교체, 네임스페이스 업데이트, 그리고 송장 영역 추출, 다중 페이지 영수증 PDF 처리, 재무 문서 사전 처리, 검색 가능한 PDF 아카이빙 등 네 가지 실제 코드 변경 전후 시나리오를 다룹니다.
Mindee에서 다른 곳으로 옮겨야 하는 이유는 무엇일까요?
Mindee는 실제 문제를 우아하게 해결합니다. 문서를 제출하면 구조화된 JSON 필드를 받아볼 수 있습니다. 재무 문서를 외부 API로 전송하는 것이 허용되고 전송량이 적당한 수준인 팀의 경우, 이 방법은 빠른 결과를 제공합니다. 마이그레이션 트리거는 일반적으로 여러 조건 중 하나가 변경될 때 발생합니다.
재무 데이터는 준수 경계를 넘습니다. Mindee의 ParseAsync<InvoiceV4>은(는) 전체 문서를 외부 서버에 업로드합니다. IBAN 코드, 라우팅 번호, 공급업체 EIN 및 상세 품목 정보는 공용 인터넷을 통해 전송되며 사용자가 제어할 수 없는 하드웨어에서 처리됩니다. SOC 2 Type II 인증은 Mindee가 보안 관행을 준수함을 확인시켜 줍니다. 이 기능은 문서를 보안 영역 내에 안전하게 보관하지 않습니다. 규정 준수 검토, 신규 고객 계약 또는 데이터 상주 규정에서 제3자 클라우드 처리를 금지하는 경우, Mindee의 아키텍처는 데이터 추출 정확도와 관계없이 부적격 판정을 받게 됩니다.
페이지당 비용은 사용량에 따라 불리하게 작용합니다. 스타터 플랜은 월 49달러에 월 1,000페이지를 제공합니다. 프로 플랜은 월 499달러에 월 5,000페이지를 제공합니다. 여러 페이지로 구성된 PDF 파일은 모든 페이지 수를 계산합니다. 개발 실행 횟수는 할당량에 포함됩니다. 한 달에 4,000건의 송장을 처리하는 팀은 월 499달러, 연간 5,988달러를 지불하며, 이 금액은 매년 재설정되고 처리량이 증가할 때마다 인상됩니다. 월 24,000페이지 분량이라면 Enterprise 가격으로 협상하시는 겁니다. IronOCR의 영구 라이선스는 1,499달러(Professional 등급)이며, 무제한 문서 용량을 지원하고 민디 Pro 대비 4개월 이내에 투자 비용을 회수할 수 있습니다.
비동기 폴링은 스택 전체에 전파됩니다. Mindee의 모든 작업은 네트워크 요청이므로 비동기적으로 처리됩니다. 이러한 필수적인 비동기 처리는 컨트롤러, 서비스 계층, 워커 클래스를 거쳐 상위 계층으로 전파됩니다. 네트워크를 사용할 수 없거나 민디 서비스에 장애가 발생하면, 연쇄적인 오류로 인해 로컬 백업 시스템 없이 전체 시스템이 마비됩니다. IronOCR은 동기식으로 처리되며, 필요할 경우 아키텍처 일관성을 위해 Task.Run로 래핑합니다.
사전 구축된 API 카탈로그는 한계가 있습니다. Mindee는 InvoiceV4, ReceiptV5, PassportV1 및 기타 문서 유형의 작은 카탈로그를 포함합니다. 그 목록 외의 문서 유형은 Enterprise 플랜 사용자 정의 모델 학습: 샘플 수집, 라벨링, 학습 시간, API 사용 가능 이전의 리드 타임이 필요합니다. 귀사에서 필요로 하는 새로운 문서 유형이 생길 때마다 별도의 업체와 계약을 체결해야 합니다. IronOCR은 동일한 IronTesseract().Read() 호출을 사용하여 모든 문서 유형을 처리합니다. 새로운 문서 유형을 추가한다는 것은 추출 패턴을 작성하는 것을 의미하며, 교육 계약을 협상하는 것을 의미하지 않습니다.
API 키 관리는 운영 측면을 강화합니다. 민디 자격 증명은 서버 측에 안전하게 저장하고, 주기적으로 갱신하며, 노출되지 않도록 보호해야 합니다. 네트워크 송신 규칙은 민디 엔드포인트로의 HTTPS 아웃바운드 전송을 허용해야 합니다. 재시도 로직은 피할 수 없는 네트워크 실패로 인한 HttpRequestException 및 MindeeException를 처리해야 합니다. Mindee를 제거하면 자격 증명, 송신 규칙, 네트워크 I/O에 대한 재시도 래퍼 등 운영상의 표면적인 문제가 모두 사라집니다.
인터넷 연결이 차단된 환경 및 제한된 환경에서는 Mindee를 사용할 수 없습니다. 정부 계약업체, 방위산업 하청업체, 엄격한 인터넷 차단이 적용되는 의료 네트워크, 안정적인 인터넷 연결이 없는 시설에 설치된 산업 시스템은 Mindee를 설계된 대로 사용할 수 없습니다.IronOCR외부 액세스가 없는 Docker 컨테이너, 송신 제한이 있는 Azure Functions, 그리고 완전히 격리된 환경에서 동일하게 실행됩니다. 실행 시 클라우드 종속성이 없습니다. 배포 등급에 대한 자세한 내용은 IronOCR 라이선스 페이지를 참조하십시오.
근본적인 문제
Mindee에서 처리하는 모든 금융 문서는 사용자가 제어할 수 없는 네트워크 경계를 통과합니다.
// Mindee: invoice with bank details leaves your infrastructure on this line
var response = await _client.ParseAsync<InvoiceV4>(new LocalInputSource("invoice.pdf"));
// IBAN, routing number, EIN, line items — all transmitted to 민디 cloud
// Mindee: invoice with bank details leaves your infrastructure on this line
var response = await _client.ParseAsync<InvoiceV4>(new LocalInputSource("invoice.pdf"));
// IBAN, routing number, EIN, line items — all transmitted to 민디 cloud
Imports System.Threading.Tasks
' Mindee: invoice with bank details leaves your infrastructure on this line
Dim response = Await _client.ParseAsync(Of InvoiceV4)(New LocalInputSource("invoice.pdf"))
' IBAN, routing number, EIN, line items — all transmitted to 민디 cloud
// IronOCR: same invoice, same result, zero data transmission
var result = new IronTesseract().Read("invoice.pdf");
// All processing local — bank details never leave your machine
// IronOCR: same invoice, same result, zero data transmission
var result = new IronTesseract().Read("invoice.pdf");
// All processing local — bank details never leave your machine
' IronOCR: same invoice, same result, zero data transmission
Dim result = New IronTesseract().Read("invoice.pdf")
' All processing local — bank details never leave your machine
IronOCR과 Mindee: 기능 비교
다음 표는 마이그레이션 결정에 영향을 미치는 아키텍처 및 기능상의 차이점을 보여줍니다.
| 기능 | 민디 | IronOCR |
|---|---|---|
| 처리 위치 | 민디 클라우드 서버 | 로컬 머신 / 인프라 |
| 실행 시 인터넷 연결이 필요합니다. | 항상 | 절대 |
| 데이터 전송 | 통화당 전체 문서가 업로드됩니다. | None |
| 문서당 비용 | 예 (페이지당, 요금제 기준) | 아니요 (영구 라이선스) |
| 시작 가격 | 월 49달러 (1,000페이지) | $999 일회성 (Lite) |
| 오프라인/에어갭 지원 | 지원되지 않음 | 전체 지원 |
| 온프레미스 배포 | 사용 불가 | 유일한 선택 |
| 송장 분석 | 사전 구축된 구조화된 API (InvoiceV4) |
OCR + 영역 기반 또는 정규 표현식 추출 |
| 영수증 분석 | 사전 구축된 구조화된 API (ReceiptV5) |
OCR + 추출 패턴 |
| 임의의 문서 유형 | 맞춤형 학습 없이는 지원되지 않습니다. | 모든 문서에 대한 완벽한 지원 |
| 사용자 지정 모델 학습 | Enterprise 계획 + 소요 기간 | 필요하지 않음 |
| API 호출 패턴 | 비동기 필수(네트워크 I/O) | 동기식 (비동기식 선택 사항) |
| 속도 제한 | 계획에 따름 | None |
| PDF 입력 | 예 | 예 (네이티브, 변환 없음) |
| 다중 페이지 PDF 처리 | 예 | 예 |
| 검색 가능한 PDF 출력 | 아니요 | 예 (result.SaveAsSearchablePdf()) |
| 이미지 전처리 | 노출된 것 없음 | 기울기 보정, 노이즈 제거, 대비 조정, 이진화, 선명도 향상, 크기 조정 |
| 영역 기반 추출 | 응답에 대한 필드 좌표 | CropRectangle 입력 시 |
| 125개 이상의 언어 지원 | 제한적 | 예 (번들형이며, tessdata 관리 기능은 포함되지 않습니다) |
| 바코드 판독 | 아니요 | 예 (OCR과 동시에 진행됨) |
| 구조화된 단어 좌표 | 아니요 | 예 (페이지, 단락, 줄, 단어) |
| 스레드 안전성 | 해당 없음 (통화당 네트워크 I/O) | 내장형 |
| 크로스 플랫폼 | 클라우드(플랫폼 독립적) | 윈도우, 리눅스, macOS, Docker, Azure, AWS |
| .NET 호환성 | .NET Standard 2.0 이상 | .NET Framework 4.6.2 이상, .NET 5/6/7/8/9 |
| 상업적 지원 | 예 | 예 |
빠른 시작: Mindee에서IronOCR로 마이그레이션
1단계: NuGet 패키지 교체
Mindee 패키지를 제거하세요:
dotnet remove package Mindee
dotnet remove package Mindee
NuGet 에서IronOCR설치하세요.
dotnet add package IronOcr
단계 2: 네임스페이스 업데이트
Mindee 네임스페이스 블록을IronOCR네임스페이스로 교체하십시오.
// Before (Mindee)
using Mindee;
using Mindee.Input;
using Mindee.Product.Invoice;
using Mindee.Product.Receipt;
using Mindee.Product.FinancialDocument;
// After (IronOCR)
using IronOcr;
// Before (Mindee)
using Mindee;
using Mindee.Input;
using Mindee.Product.Invoice;
using Mindee.Product.Receipt;
using Mindee.Product.FinancialDocument;
// After (IronOCR)
using IronOcr;
Imports Mindee
Imports Mindee.Input
Imports Mindee.Product.Invoice
Imports Mindee.Product.Receipt
Imports Mindee.Product.FinancialDocument
Imports IronOcr
단계 3: 라이선스 초기화
애플리케이션 시작 시(첫 번째 OCR 호출 전) 라이선스 키를 추가하십시오.
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"
코드 마이그레이션 예제
문서 영역을 이용한 송장 필드 추출
Mindee는 서버 측 모델이 송장 번호, 날짜 및 합계가 일반적으로 나타나는 위치를 알고 있기 때문에 미리 구문 분석된 필드를 알려진 위치에 반환합니다.IronOCR등가는 문서를 전송하지 않고 Mindee의 추출을 공간 인식성과 일치시키기 위해 특정 영역을 읽기 위해 CropRectangle을 사용합니다.
돌봄 제공자 접근 방식:
using Mindee;
using Mindee.Input;
using Mindee.Product.Invoice;
public class MindeeZoneExtractor
{
private readonly MindeeClient _client;
public MindeeZoneExtractor(string apiKey)
{
_client = new MindeeClient(apiKey);
}
public async Task<(string invoiceNumber, string supplierName, decimal? total)>
ExtractKeyFieldsAsync(string filePath)
{
// Document transmitted to 민디 — spatial field detection happens server-side
var inputSource = new LocalInputSource(filePath);
var response = await _client.ParseAsync<InvoiceV4>(inputSource);
var prediction = response.Document.Inference.Prediction;
// Fields arrive pre-parsed; no zone configuration required on your end
return (
prediction.InvoiceNumber?.Value,
prediction.SupplierName?.Value,
prediction.TotalAmount?.Value
);
}
}
using Mindee;
using Mindee.Input;
using Mindee.Product.Invoice;
public class MindeeZoneExtractor
{
private readonly MindeeClient _client;
public MindeeZoneExtractor(string apiKey)
{
_client = new MindeeClient(apiKey);
}
public async Task<(string invoiceNumber, string supplierName, decimal? total)>
ExtractKeyFieldsAsync(string filePath)
{
// Document transmitted to 민디 — spatial field detection happens server-side
var inputSource = new LocalInputSource(filePath);
var response = await _client.ParseAsync<InvoiceV4>(inputSource);
var prediction = response.Document.Inference.Prediction;
// Fields arrive pre-parsed; no zone configuration required on your end
return (
prediction.InvoiceNumber?.Value,
prediction.SupplierName?.Value,
prediction.TotalAmount?.Value
);
}
}
Imports Mindee
Imports Mindee.Input
Imports Mindee.Product.Invoice
Imports System.Threading.Tasks
Public Class MindeeZoneExtractor
Private ReadOnly _client As MindeeClient
Public Sub New(apiKey As String)
_client = New MindeeClient(apiKey)
End Sub
Public Async Function ExtractKeyFieldsAsync(filePath As String) As Task(Of (invoiceNumber As String, supplierName As String, total As Decimal?))
' Document transmitted to 민디 — spatial field detection happens server-side
Dim inputSource = New LocalInputSource(filePath)
Dim response = Await _client.ParseAsync(Of InvoiceV4)(inputSource)
Dim prediction = response.Document.Inference.Prediction
' Fields arrive pre-parsed; no zone configuration required on your end
Return (
prediction.InvoiceNumber?.Value,
prediction.SupplierName?.Value,
prediction.TotalAmount?.Value
)
End Function
End Class
IronOCR 접근 방식:
using IronOcr;
using System.Text.RegularExpressions;
public class IronOcrZoneExtractor
{
private readonly IronTesseract _ocr = new IronTesseract();
public (string invoiceNumber, string supplierName, string total)
ExtractKeyFields(string filePath)
{
// Zone 1: supplier name — top 12% of document, full width
var headerRegion = new CropRectangle(0, 0, 850, 120);
using var headerInput = new OcrInput();
headerInput.LoadImage(filePath, headerRegion);
var supplierResult = _ocr.Read(headerInput);
// Zone 2: invoice number and date — upper-right quadrant
var metaRegion = new CropRectangle(450, 80, 400, 100);
using var metaInput = new OcrInput();
metaInput.LoadImage(filePath, metaRegion);
var metaResult = _ocr.Read(metaInput);
// Zone 3: totals block — bottom-right 20%
var totalsRegion = new CropRectangle(500, 800, 350, 200);
using var totalsInput = new OcrInput();
totalsInput.LoadImage(filePath, totalsRegion);
var totalsResult = _ocr.Read(totalsInput);
var invoiceNumber = Regex.Match(
metaResult.Text,
@"Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups[1].Value;
var total = Regex.Match(
totalsResult.Text,
@"Total\s*(?:Due)?\s*:?\s*\$?([\d,]+\.?\d*)",
RegexOptions.IgnoreCase).Groups[1].Value;
// Supplier name is the first substantial text line in the header zone
var supplierName = supplierResult.Text
.Split('\n')
.FirstOrDefault(l => l.Trim().Length > 4)
?.Trim();
return (invoiceNumber, supplierName, total);
}
}
using IronOcr;
using System.Text.RegularExpressions;
public class IronOcrZoneExtractor
{
private readonly IronTesseract _ocr = new IronTesseract();
public (string invoiceNumber, string supplierName, string total)
ExtractKeyFields(string filePath)
{
// Zone 1: supplier name — top 12% of document, full width
var headerRegion = new CropRectangle(0, 0, 850, 120);
using var headerInput = new OcrInput();
headerInput.LoadImage(filePath, headerRegion);
var supplierResult = _ocr.Read(headerInput);
// Zone 2: invoice number and date — upper-right quadrant
var metaRegion = new CropRectangle(450, 80, 400, 100);
using var metaInput = new OcrInput();
metaInput.LoadImage(filePath, metaRegion);
var metaResult = _ocr.Read(metaInput);
// Zone 3: totals block — bottom-right 20%
var totalsRegion = new CropRectangle(500, 800, 350, 200);
using var totalsInput = new OcrInput();
totalsInput.LoadImage(filePath, totalsRegion);
var totalsResult = _ocr.Read(totalsInput);
var invoiceNumber = Regex.Match(
metaResult.Text,
@"Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups[1].Value;
var total = Regex.Match(
totalsResult.Text,
@"Total\s*(?:Due)?\s*:?\s*\$?([\d,]+\.?\d*)",
RegexOptions.IgnoreCase).Groups[1].Value;
// Supplier name is the first substantial text line in the header zone
var supplierName = supplierResult.Text
.Split('\n')
.FirstOrDefault(l => l.Trim().Length > 4)
?.Trim();
return (invoiceNumber, supplierName, total);
}
}
Imports IronOcr
Imports System.Text.RegularExpressions
Public Class IronOcrZoneExtractor
Private ReadOnly _ocr As New IronTesseract()
Public Function ExtractKeyFields(filePath As String) As (invoiceNumber As String, supplierName As String, total As String)
' Zone 1: supplier name — top 12% of document, full width
Dim headerRegion As New CropRectangle(0, 0, 850, 120)
Using headerInput As New OcrInput()
headerInput.LoadImage(filePath, headerRegion)
Dim supplierResult = _ocr.Read(headerInput)
' Zone 2: invoice number and date — upper-right quadrant
Dim metaRegion As New CropRectangle(450, 80, 400, 100)
Using metaInput As New OcrInput()
metaInput.LoadImage(filePath, metaRegion)
Dim metaResult = _ocr.Read(metaInput)
' Zone 3: totals block — bottom-right 20%
Dim totalsRegion As New CropRectangle(500, 800, 350, 200)
Using totalsInput As New OcrInput()
totalsInput.LoadImage(filePath, totalsRegion)
Dim totalsResult = _ocr.Read(totalsInput)
Dim invoiceNumber = Regex.Match(metaResult.Text, "Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)", RegexOptions.IgnoreCase).Groups(1).Value
Dim total = Regex.Match(totalsResult.Text, "Total\s*(?:Due)?\s*:?\s*\$?([\d,]+\.?\d*)", RegexOptions.IgnoreCase).Groups(1).Value
' Supplier name is the first substantial text line in the header zone
Dim supplierName = supplierResult.Text.Split(vbLf).FirstOrDefault(Function(l) l.Trim().Length > 4)?.Trim()
Return (invoiceNumber, supplierName, total)
End Using
End Using
End Using
End Function
End Class
영역 기반 OCR은 Mindee의 공간 필드 감지 의도와 일치합니다. 즉, 전체 페이지에 정규식을 적용하는 대신 문서의 특정 영역을 읽습니다. CropRectangle(x, y, width, height)은(는) 픽셀 좌표를 가져오기 때문에 조정은 샘플 송장에 대해 측정해야 합니다. 영역 기반 OCR 가이드는 좌표 측정과 영역 겹침 전략을 다룹니다. 레이아웃이 가변적인 송장의 경우, 영역 추출을 result.Text에서 전체 페이지 정규식과 결합하면 가장 신뢰할 수 있는 결과가 생성됩니다.
여러 페이지로 구성된 경비 보고서 PDF 처리
Mindee는 PDF 파일을 입력받아 각 페이지를 개별 문서 단위로 처리하고, 페이지별로 구조화된 결과를 반환합니다. IronOCR은 다페이지 PDF를 OcrInput.LoadPdf()을 통해 본래적으로 읽고, 모든 페이지를 한 번의 호출로 처리하며, 페이지별 콘텐츠와 단어 좌표가 포함된 result.Pages을 반환합니다.
돌봄 제공자 접근 방식:
using Mindee;
using Mindee.Input;
using Mindee.Product.Receipt;
public class MindeeExpenseReportProcessor
{
private readonly MindeeClient _client;
public MindeeExpenseReportProcessor(string apiKey)
{
_client = new MindeeClient(apiKey);
}
public async Task<List<ExpenseSummary>> ProcessExpenseReportAsync(string pdfPath)
{
var summaries = new List<ExpenseSummary>();
// 민디 processes per-document; each page of a PDF is a separate upload
// For a 10-page expense report: 10 API calls, 10 pages billed
var inputSource = new LocalInputSource(pdfPath);
var response = await _client.ParseAsync<ReceiptV5>(inputSource);
var prediction = response.Document.Inference.Prediction;
summaries.Add(new ExpenseSummary
{
MerchantName = prediction.SupplierName?.Value,
Date = prediction.Date?.Value?.ToString(),
TotalAmount = prediction.TotalAmount?.Value ?? 0m,
Category = prediction.Category?.Value
});
return summaries;
}
}
using Mindee;
using Mindee.Input;
using Mindee.Product.Receipt;
public class MindeeExpenseReportProcessor
{
private readonly MindeeClient _client;
public MindeeExpenseReportProcessor(string apiKey)
{
_client = new MindeeClient(apiKey);
}
public async Task<List<ExpenseSummary>> ProcessExpenseReportAsync(string pdfPath)
{
var summaries = new List<ExpenseSummary>();
// 민디 processes per-document; each page of a PDF is a separate upload
// For a 10-page expense report: 10 API calls, 10 pages billed
var inputSource = new LocalInputSource(pdfPath);
var response = await _client.ParseAsync<ReceiptV5>(inputSource);
var prediction = response.Document.Inference.Prediction;
summaries.Add(new ExpenseSummary
{
MerchantName = prediction.SupplierName?.Value,
Date = prediction.Date?.Value?.ToString(),
TotalAmount = prediction.TotalAmount?.Value ?? 0m,
Category = prediction.Category?.Value
});
return summaries;
}
}
Imports Mindee
Imports Mindee.Input
Imports Mindee.Product.Receipt
Imports System.Threading.Tasks
Public Class MindeeExpenseReportProcessor
Private ReadOnly _client As MindeeClient
Public Sub New(apiKey As String)
_client = New MindeeClient(apiKey)
End Sub
Public Async Function ProcessExpenseReportAsync(pdfPath As String) As Task(Of List(Of ExpenseSummary))
Dim summaries As New List(Of ExpenseSummary)()
' 민디 processes per-document; each page of a PDF is a separate upload
' For a 10-page expense report: 10 API calls, 10 pages billed
Dim inputSource As New LocalInputSource(pdfPath)
Dim response = Await _client.ParseAsync(Of ReceiptV5)(inputSource)
Dim prediction = response.Document.Inference.Prediction
summaries.Add(New ExpenseSummary With {
.MerchantName = prediction.SupplierName?.Value,
.Date = prediction.Date?.Value?.ToString(),
.TotalAmount = If(prediction.TotalAmount?.Value, 0D),
.Category = prediction.Category?.Value
})
Return summaries
End Function
End Class
IronOCR 접근 방식:
using IronOcr;
using System.Text.RegularExpressions;
public class IronOcrExpenseReportProcessor
{
private readonly IronTesseract _ocr = new IronTesseract();
public List<ExpenseSummary> ProcessExpenseReport(string pdfPath)
{
// Load entire multi-page PDF in one call — no per-page upload
using var input = new OcrInput();
input.LoadPdf(pdfPath); // all pages loaded locally
var result = _ocr.Read(input);
var summaries = new List<ExpenseSummary>();
// Each page maps to one receipt in the expense report
foreach (var page in result.Pages)
{
var pageText = page.Text;
// Skip pages without receipt content
if (!pageText.Contains("Total", StringComparison.OrdinalIgnoreCase))
continue;
var merchantName = page.Lines
.FirstOrDefault(l => l.Text.Trim().Length > 4)
?.Text.Trim();
var totalMatch = Regex.Match(
pageText,
@"(?:Total|Amount\s+Due|Grand\s+Total)\s*:?\s*\$?([\d,]+\.\d{2})",
RegexOptions.IgnoreCase);
var dateMatch = Regex.Match(
pageText,
@"\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\b");
var categoryMatch = Regex.Match(
pageText,
@"(?:Category|Dept|Department)\s*:?\s*(.+)",
RegexOptions.IgnoreCase);
summaries.Add(new ExpenseSummary
{
PageNumber = page.PageNumber,
MerchantName = merchantName,
Date = dateMatch.Success ? dateMatch.Value : null,
TotalAmount = totalMatch.Success
? decimal.Parse(totalMatch.Groups[1].Value.Replace(",", ""))
: 0m,
Category = categoryMatch.Success
? categoryMatch.Groups[1].Value.Trim()
: null,
Confidence = page.Words.Any()
? page.Words.Average(w => (double)w.Confidence)
: 0
});
}
return summaries;
}
}
public class ExpenseSummary
{
public int PageNumber { get; set; }
public string MerchantName { get; set; }
public string Date { get; set; }
public decimal TotalAmount { get; set; }
public string Category { get; set; }
public double Confidence { get; set; }
}
using IronOcr;
using System.Text.RegularExpressions;
public class IronOcrExpenseReportProcessor
{
private readonly IronTesseract _ocr = new IronTesseract();
public List<ExpenseSummary> ProcessExpenseReport(string pdfPath)
{
// Load entire multi-page PDF in one call — no per-page upload
using var input = new OcrInput();
input.LoadPdf(pdfPath); // all pages loaded locally
var result = _ocr.Read(input);
var summaries = new List<ExpenseSummary>();
// Each page maps to one receipt in the expense report
foreach (var page in result.Pages)
{
var pageText = page.Text;
// Skip pages without receipt content
if (!pageText.Contains("Total", StringComparison.OrdinalIgnoreCase))
continue;
var merchantName = page.Lines
.FirstOrDefault(l => l.Text.Trim().Length > 4)
?.Text.Trim();
var totalMatch = Regex.Match(
pageText,
@"(?:Total|Amount\s+Due|Grand\s+Total)\s*:?\s*\$?([\d,]+\.\d{2})",
RegexOptions.IgnoreCase);
var dateMatch = Regex.Match(
pageText,
@"\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\b");
var categoryMatch = Regex.Match(
pageText,
@"(?:Category|Dept|Department)\s*:?\s*(.+)",
RegexOptions.IgnoreCase);
summaries.Add(new ExpenseSummary
{
PageNumber = page.PageNumber,
MerchantName = merchantName,
Date = dateMatch.Success ? dateMatch.Value : null,
TotalAmount = totalMatch.Success
? decimal.Parse(totalMatch.Groups[1].Value.Replace(",", ""))
: 0m,
Category = categoryMatch.Success
? categoryMatch.Groups[1].Value.Trim()
: null,
Confidence = page.Words.Any()
? page.Words.Average(w => (double)w.Confidence)
: 0
});
}
return summaries;
}
}
public class ExpenseSummary
{
public int PageNumber { get; set; }
public string MerchantName { get; set; }
public string Date { get; set; }
public decimal TotalAmount { get; set; }
public string Category { get; set; }
public double Confidence { get; set; }
}
Imports IronOcr
Imports System.Text.RegularExpressions
Public Class IronOcrExpenseReportProcessor
Private ReadOnly _ocr As New IronTesseract()
Public Function ProcessExpenseReport(pdfPath As String) As List(Of ExpenseSummary)
' Load entire multi-page PDF in one call — no per-page upload
Using input As New OcrInput()
input.LoadPdf(pdfPath) ' all pages loaded locally
Dim result = _ocr.Read(input)
Dim summaries As New List(Of ExpenseSummary)()
' Each page maps to one receipt in the expense report
For Each page In result.Pages
Dim pageText = page.Text
' Skip pages without receipt content
If Not pageText.Contains("Total", StringComparison.OrdinalIgnoreCase) Then
Continue For
End If
Dim merchantName = page.Lines _
.FirstOrDefault(Function(l) l.Text.Trim().Length > 4) _
?.Text.Trim()
Dim totalMatch = Regex.Match(
pageText,
"(?:Total|Amount\s+Due|Grand\s+Total)\s*:?\s*\$?([\d,]+\.\d{2})",
RegexOptions.IgnoreCase)
Dim dateMatch = Regex.Match(
pageText,
"\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\b")
Dim categoryMatch = Regex.Match(
pageText,
"(?:Category|Dept|Department)\s*:?\s*(.+)",
RegexOptions.IgnoreCase)
summaries.Add(New ExpenseSummary With {
.PageNumber = page.PageNumber,
.MerchantName = merchantName,
.Date = If(dateMatch.Success, dateMatch.Value, Nothing),
.TotalAmount = If(totalMatch.Success, Decimal.Parse(totalMatch.Groups(1).Value.Replace(",", "")), 0D),
.Category = If(categoryMatch.Success, categoryMatch.Groups(1).Value.Trim(), Nothing),
.Confidence = If(page.Words.Any(), page.Words.Average(Function(w) CDbl(w.Confidence)), 0)
})
Next
Return summaries
End Using
End Function
End Class
Public Class ExpenseSummary
Public Property PageNumber As Integer
Public Property MerchantName As String
Public Property Date As String
Public Property TotalAmount As Decimal
Public Property Category As String
Public Property Confidence As Double
End Class
Mindee를 사용하여 10페이지 분량의 경비 보고서 PDF 파일을 처리하면 10번의 API 호출이 발생하고 10페이지 분량의 비용이 청구됩니다.IronOCR페이지당 비용이나 페이지당 네트워크 왕복 없이 단일 로컬 호출로 동일한 파일을 처리합니다. result.Pages 컬렉션은 레이아웃 기반 추출을 위한 페이지별 텍스트, 단어 레벨 경계 상자 및 줄 위치를 제공합니다. 암호로 보호된 PDF 파일을 불러오거나 페이지 범위를 선택하는 방법에 대해서는 PDF 입력 가이드를 , 단어 좌표를 사용하는 방법에 대해서는 구조화된 읽기 결과 가이드를 참조하십시오.
스캔된 송장 사전 처리 파이프라인
Mindee의 클라우드 처리 기능은 필드 추출을 실행하기 전에 자체적인 이미지 정규화를 적용합니다. 해당 정규화의 품질은 불투명합니다. 사용자는 이를 제어할 수 없으며 어떤 전처리 과정이 실행되었는지도 확인할 수 없습니다. 스캔한 송장에 기울어짐, 그림자 또는 낮은 대비가 있는 경우 Mindee는 낮은 신뢰도 점수를 반환하며, 제출 전에 스캔 품질을 개선할 수 있는 방법이 없습니다.IronOCR전처리 파이프라인을 명시적으로 보여주므로, 문서 인식 실행 전에 특정 문서에 필요한 수정 사항을 정확하게 적용할 수 있습니다.
돌봄 제공자 접근 방식:
using Mindee;
using Mindee.Input;
using Mindee.Product.Invoice;
public class MindeeScanProcessor
{
private readonly MindeeClient _client;
public MindeeScanProcessor(string apiKey)
{
_client = new MindeeClient(apiKey);
}
public async Task<ScanResult> ProcessScannedInvoiceAsync(string scanPath)
{
// 민디 applies internal normalization — no developer control over preprocessing
var inputSource = new LocalInputSource(scanPath);
var response = await _client.ParseAsync<InvoiceV4>(inputSource);
var prediction = response.Document.Inference.Prediction;
return new ScanResult
{
InvoiceNumber = prediction.InvoiceNumber?.Value,
Total = prediction.TotalAmount?.Value,
// Per-field confidence: only proxy for scan quality
InvoiceNumberConfidence = prediction.InvoiceNumber?.Confidence,
TotalConfidence = prediction.TotalAmount?.Confidence
};
}
}
using Mindee;
using Mindee.Input;
using Mindee.Product.Invoice;
public class MindeeScanProcessor
{
private readonly MindeeClient _client;
public MindeeScanProcessor(string apiKey)
{
_client = new MindeeClient(apiKey);
}
public async Task<ScanResult> ProcessScannedInvoiceAsync(string scanPath)
{
// 민디 applies internal normalization — no developer control over preprocessing
var inputSource = new LocalInputSource(scanPath);
var response = await _client.ParseAsync<InvoiceV4>(inputSource);
var prediction = response.Document.Inference.Prediction;
return new ScanResult
{
InvoiceNumber = prediction.InvoiceNumber?.Value,
Total = prediction.TotalAmount?.Value,
// Per-field confidence: only proxy for scan quality
InvoiceNumberConfidence = prediction.InvoiceNumber?.Confidence,
TotalConfidence = prediction.TotalAmount?.Confidence
};
}
}
Imports Mindee
Imports Mindee.Input
Imports Mindee.Product.Invoice
Imports System.Threading.Tasks
Public Class MindeeScanProcessor
Private ReadOnly _client As MindeeClient
Public Sub New(apiKey As String)
_client = New MindeeClient(apiKey)
End Sub
Public Async Function ProcessScannedInvoiceAsync(scanPath As String) As Task(Of ScanResult)
' 민디 applies internal normalization — no developer control over preprocessing
Dim inputSource = New LocalInputSource(scanPath)
Dim response = Await _client.ParseAsync(Of InvoiceV4)(inputSource)
Dim prediction = response.Document.Inference.Prediction
Return New ScanResult With {
.InvoiceNumber = prediction.InvoiceNumber?.Value,
.Total = prediction.TotalAmount?.Value,
' Per-field confidence: only proxy for scan quality
.InvoiceNumberConfidence = prediction.InvoiceNumber?.Confidence,
.TotalConfidence = prediction.TotalAmount?.Confidence
}
End Function
End Class
IronOCR 접근 방식:
using IronOcr;
using System.Text.RegularExpressions;
public class IronOcrScanProcessor
{
private readonly IronTesseract _ocr = new IronTesseract();
public ScanResult ProcessScannedInvoice(string scanPath)
{
// First pass: read without preprocessing
var quickResult = _ocr.Read(scanPath);
OcrResult result;
if (quickResult.Confidence < 75)
{
// Low confidence — apply full preprocessing pipeline
using var input = new OcrInput();
input.LoadImage(scanPath);
input.Deskew(); // correct page rotation up to ±45 degrees
input.DeNoise(); // remove scanner artifacts and speckle
input.Contrast(); // normalize contrast for faded or overexposed scans
input.Sharpen(); // sharpen blurred text edges
result = _ocr.Read(input);
}
else
{
result = quickResult;
}
var invoiceNumber = Regex.Match(
result.Text,
@"Invoice\s*(?:Number|#|No\.?)?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups[1].Value;
var total = Regex.Match(
result.Text,
@"(?:Total|Amount\s+Due)\s*:?\s*\$?([\d,]+\.\d{2})",
RegexOptions.IgnoreCase).Groups[1].Value;
return new ScanResult
{
InvoiceNumber = invoiceNumber,
Total = total,
DocumentConfidence = result.Confidence,
PreprocessingApplied = quickResult.Confidence < 75
};
}
}
public class ScanResult
{
public string InvoiceNumber { get; set; }
public string Total { get; set; }
public double DocumentConfidence { get; set; }
public bool PreprocessingApplied { get; set; }
}
using IronOcr;
using System.Text.RegularExpressions;
public class IronOcrScanProcessor
{
private readonly IronTesseract _ocr = new IronTesseract();
public ScanResult ProcessScannedInvoice(string scanPath)
{
// First pass: read without preprocessing
var quickResult = _ocr.Read(scanPath);
OcrResult result;
if (quickResult.Confidence < 75)
{
// Low confidence — apply full preprocessing pipeline
using var input = new OcrInput();
input.LoadImage(scanPath);
input.Deskew(); // correct page rotation up to ±45 degrees
input.DeNoise(); // remove scanner artifacts and speckle
input.Contrast(); // normalize contrast for faded or overexposed scans
input.Sharpen(); // sharpen blurred text edges
result = _ocr.Read(input);
}
else
{
result = quickResult;
}
var invoiceNumber = Regex.Match(
result.Text,
@"Invoice\s*(?:Number|#|No\.?)?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups[1].Value;
var total = Regex.Match(
result.Text,
@"(?:Total|Amount\s+Due)\s*:?\s*\$?([\d,]+\.\d{2})",
RegexOptions.IgnoreCase).Groups[1].Value;
return new ScanResult
{
InvoiceNumber = invoiceNumber,
Total = total,
DocumentConfidence = result.Confidence,
PreprocessingApplied = quickResult.Confidence < 75
};
}
}
public class ScanResult
{
public string InvoiceNumber { get; set; }
public string Total { get; set; }
public double DocumentConfidence { get; set; }
public bool PreprocessingApplied { get; set; }
}
Imports IronOcr
Imports System.Text.RegularExpressions
Public Class IronOcrScanProcessor
Private ReadOnly _ocr As New IronTesseract()
Public Function ProcessScannedInvoice(scanPath As String) As ScanResult
' First pass: read without preprocessing
Dim quickResult = _ocr.Read(scanPath)
Dim result As OcrResult
If quickResult.Confidence < 75 Then
' Low confidence — apply full preprocessing pipeline
Using input As New OcrInput()
input.LoadImage(scanPath)
input.Deskew() ' correct page rotation up to ±45 degrees
input.DeNoise() ' remove scanner artifacts and speckle
input.Contrast() ' normalize contrast for faded or overexposed scans
input.Sharpen() ' sharpen blurred text edges
result = _ocr.Read(input)
End Using
Else
result = quickResult
End If
Dim invoiceNumber = Regex.Match(
result.Text,
"Invoice\s*(?:Number|#|No\.?)?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups(1).Value
Dim total = Regex.Match(
result.Text,
"(?:Total|Amount\s+Due)\s*:?\s*\$?([\d,]+\.\d{2})",
RegexOptions.IgnoreCase).Groups(1).Value
Return New ScanResult With {
.InvoiceNumber = invoiceNumber,
.Total = total,
.DocumentConfidence = result.Confidence,
.PreprocessingApplied = quickResult.Confidence < 75
}
End Function
End Class
Public Class ScanResult
Public Property InvoiceNumber As String
Public Property Total As String
Public Property DocumentConfidence As Double
Public Property PreprocessingApplied As Boolean
End Class
먼저 빠르게 읽고 신뢰도가 낮은 데이터에 대해 전처리하는 2단계 패턴은 깨끗한 데이터에 대해 전체 전처리를 수행하는 데 드는 오버헤드를 방지합니다. 가장 일반적인 스캔 품질 시나리오 (약간 기울어진 평판 스캔, 팩스 아티팩트, 복사된 송장)에서는 Deskew() 및 DeNoise()이(가) 대부분의 인식 정확도를 복구합니다. 이미지 품질 보정 가이드 에는 사용 가능한 모든 필터에 대한 설명과 다양한 스캔 결함에 가장 적합한 필터 조합에 대한 지침이 포함되어 있습니다. 이미지 방향 보정 가이드는 거꾸로 스캔한 문서의 자동 회전 기능을 다룹니다.
재무 문서 검색 가능한 PDF 아카이빙
Mindee는 PDF 출력물을 생성하지 않습니다. 이 시스템의 아키텍처는 입력 전용입니다. 문서를 제출하면 JSON 필드를 수신합니다. 스캔한 송장을 검색 가능한 형식으로 보관하는 팀은 Mindee의 추출 기능과 별도로 해당 파이프라인을 관리해야 합니다.IronOCR필드 추출에 사용된 동일한 인식 과정을 통해 검색 가능한 PDF를 직접 생성합니다. 추가 라이브러리나 두 번째 처리 단계가 필요하지 않습니다.
돌봄 제공자 접근 방식:
using Mindee;
using Mindee.Input;
using Mindee.Product.Invoice;
public class MindeeArchiveProcessor
{
private readonly MindeeClient _client;
public MindeeArchiveProcessor(string apiKey)
{
_client = new MindeeClient(apiKey);
}
public async Task ArchiveInvoiceAsync(string scanPath, string archivePath)
{
// Extract fields via 민디 (document transmitted to cloud)
var inputSource = new LocalInputSource(scanPath);
var response = await _client.ParseAsync<InvoiceV4>(inputSource);
var prediction = response.Document.Inference.Prediction;
// 민디 returns no PDF output — searchable PDF requires a separate library
// (e.g., iTextSharp, PdfSharp, or a separate OCR library)
// The scan at scanPath is the only PDF available for archiving;
// it remains image-only with no embedded text layer
Console.WriteLine($"Fields extracted. Searchable PDF: not available from Mindee.");
Console.WriteLine($"Invoice: {prediction.InvoiceNumber?.Value}");
}
}
using Mindee;
using Mindee.Input;
using Mindee.Product.Invoice;
public class MindeeArchiveProcessor
{
private readonly MindeeClient _client;
public MindeeArchiveProcessor(string apiKey)
{
_client = new MindeeClient(apiKey);
}
public async Task ArchiveInvoiceAsync(string scanPath, string archivePath)
{
// Extract fields via 민디 (document transmitted to cloud)
var inputSource = new LocalInputSource(scanPath);
var response = await _client.ParseAsync<InvoiceV4>(inputSource);
var prediction = response.Document.Inference.Prediction;
// 민디 returns no PDF output — searchable PDF requires a separate library
// (e.g., iTextSharp, PdfSharp, or a separate OCR library)
// The scan at scanPath is the only PDF available for archiving;
// it remains image-only with no embedded text layer
Console.WriteLine($"Fields extracted. Searchable PDF: not available from Mindee.");
Console.WriteLine($"Invoice: {prediction.InvoiceNumber?.Value}");
}
}
Imports Mindee
Imports Mindee.Input
Imports Mindee.Product.Invoice
Imports System.Threading.Tasks
Public Class MindeeArchiveProcessor
Private ReadOnly _client As MindeeClient
Public Sub New(apiKey As String)
_client = New MindeeClient(apiKey)
End Sub
Public Async Function ArchiveInvoiceAsync(scanPath As String, archivePath As String) As Task
' Extract fields via 민디 (document transmitted to cloud)
Dim inputSource = New LocalInputSource(scanPath)
Dim response = Await _client.ParseAsync(Of InvoiceV4)(inputSource)
Dim prediction = response.Document.Inference.Prediction
' 민디 returns no PDF output — searchable PDF requires a separate library
' (e.g., iTextSharp, PdfSharp, or a separate OCR library)
' The scan at scanPath is the only PDF available for archiving;
' it remains image-only with no embedded text layer
Console.WriteLine($"Fields extracted. Searchable PDF: not available from Mindee.")
Console.WriteLine($"Invoice: {prediction.InvoiceNumber?.Value}")
End Function
End Class
IronOCR 접근 방식:
using IronOcr;
using System.Text.RegularExpressions;
public class IronOcrArchiveProcessor
{
private readonly IronTesseract _ocr = new IronTesseract();
public ArchiveResult ArchiveInvoice(string scanPath, string archiveOutputPath)
{
// Apply preprocessing before recognition and archiving
using var input = new OcrInput();
input.LoadPdf(scanPath); // works with both PDF scans and image files
input.Deskew();
input.DeNoise();
var result = _ocr.Read(input);
// Extract fields from the same recognition pass
var invoiceNumber = Regex.Match(
result.Text,
@"Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups[1].Value;
var vendor = result.Pages.FirstOrDefault()?.Lines
.FirstOrDefault(l => l.Text.Trim().Length > 4)
?.Text.Trim();
var totalMatch = Regex.Match(
result.Text,
@"Total\s*(?:Due)?\s*:?\s*\$?([\d,]+\.\d{2})",
RegexOptions.IgnoreCase);
// Write searchable PDF to archive in one call — no separate library needed
result.SaveAsSearchablePdf(archiveOutputPath);
return new ArchiveResult
{
InvoiceNumber = invoiceNumber,
VendorName = vendor,
Total = totalMatch.Success ? totalMatch.Groups[1].Value : null,
ArchivePath = archiveOutputPath,
DocumentConfidence = result.Confidence,
PageCount = result.Pages.Count()
};
}
}
public class ArchiveResult
{
public string InvoiceNumber { get; set; }
public string VendorName { get; set; }
public string Total { get; set; }
public string ArchivePath { get; set; }
public double DocumentConfidence { get; set; }
public int PageCount { get; set; }
}
using IronOcr;
using System.Text.RegularExpressions;
public class IronOcrArchiveProcessor
{
private readonly IronTesseract _ocr = new IronTesseract();
public ArchiveResult ArchiveInvoice(string scanPath, string archiveOutputPath)
{
// Apply preprocessing before recognition and archiving
using var input = new OcrInput();
input.LoadPdf(scanPath); // works with both PDF scans and image files
input.Deskew();
input.DeNoise();
var result = _ocr.Read(input);
// Extract fields from the same recognition pass
var invoiceNumber = Regex.Match(
result.Text,
@"Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups[1].Value;
var vendor = result.Pages.FirstOrDefault()?.Lines
.FirstOrDefault(l => l.Text.Trim().Length > 4)
?.Text.Trim();
var totalMatch = Regex.Match(
result.Text,
@"Total\s*(?:Due)?\s*:?\s*\$?([\d,]+\.\d{2})",
RegexOptions.IgnoreCase);
// Write searchable PDF to archive in one call — no separate library needed
result.SaveAsSearchablePdf(archiveOutputPath);
return new ArchiveResult
{
InvoiceNumber = invoiceNumber,
VendorName = vendor,
Total = totalMatch.Success ? totalMatch.Groups[1].Value : null,
ArchivePath = archiveOutputPath,
DocumentConfidence = result.Confidence,
PageCount = result.Pages.Count()
};
}
}
public class ArchiveResult
{
public string InvoiceNumber { get; set; }
public string VendorName { get; set; }
public string Total { get; set; }
public string ArchivePath { get; set; }
public double DocumentConfidence { get; set; }
public int PageCount { get; set; }
}
Imports IronOcr
Imports System.Text.RegularExpressions
Imports System.Linq
Public Class IronOcrArchiveProcessor
Private ReadOnly _ocr As New IronTesseract()
Public Function ArchiveInvoice(scanPath As String, archiveOutputPath As String) As ArchiveResult
' Apply preprocessing before recognition and archiving
Using input As New OcrInput()
input.LoadPdf(scanPath) ' works with both PDF scans and image files
input.Deskew()
input.DeNoise()
Dim result = _ocr.Read(input)
' Extract fields from the same recognition pass
Dim invoiceNumber = Regex.Match(result.Text, "Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)", RegexOptions.IgnoreCase).Groups(1).Value
Dim vendor = result.Pages.FirstOrDefault()?.Lines.FirstOrDefault(Function(l) l.Text.Trim().Length > 4)?.Text.Trim()
Dim totalMatch = Regex.Match(result.Text, "Total\s*(?:Due)?\s*:?\s*\$?([\d,]+\.\d{2})", RegexOptions.IgnoreCase)
' Write searchable PDF to archive in one call — no separate library needed
result.SaveAsSearchablePdf(archiveOutputPath)
Return New ArchiveResult With {
.InvoiceNumber = invoiceNumber,
.VendorName = vendor,
.Total = If(totalMatch.Success, totalMatch.Groups(1).Value, Nothing),
.ArchivePath = archiveOutputPath,
.DocumentConfidence = result.Confidence,
.PageCount = result.Pages.Count()
}
End Using
End Function
End Class
Public Class ArchiveResult
Public Property InvoiceNumber As String
Public Property VendorName As String
Public Property Total As String
Public Property ArchivePath As String
Public Property DocumentConfidence As Double
Public Property PageCount As Integer
End Class
result.SaveAsSearchablePdf() 은(는) 인식된 텍스트 레이어를 직접 출력 PDF에 내장하여, 모든 PDF 뷰어나 Enterprise 콘텐츠 관리 시스템에서 선택 가능하고 검색 가능한 파일을 생성합니다. 필드 추출과 아카이브가 동일한 패스로 실행되며, 하나의 _ocr.Read(input) 호출이 패턴 매칭을 위한 result.Text 및 아카이브를 위한 result.SaveAsSearchablePdf() 모두 제공합니다. 검색 가능한 PDF 가이드는 HOCR 내보내기 및 PDF OCR 사용 사례 페이지를 포함한 출력 옵션을 다루며 문서 아카이브 파이프라인을 위한 생산 배포 패턴을 다룹니다.
FinancialDocumentV1 사용자 지정 엔드포인트 제거
Mindee의 FinancialDocumentV1 API는 문서 유형을 자동 감지하여 송장과 영수증 모두를 처리합니다. 이 기능을 사용하는 팀은 분기 논리를 제거할 수 있지만 클라우드 의존도가 높아집니다. 문서 유형에 관계없이 모든 문서가 업로드되기 때문입니다.IronOCR로 교체하면 단어 수준의 위치 데이터를 사용하여 클라우드 호출 없이 문서 구조와 추출 로직을 감지할 수 있습니다.
돌봄 제공자 접근 방식:
using Mindee;
using Mindee.Input;
using Mindee.Product.FinancialDocument;
public class MindeeFinancialRouter
{
private readonly MindeeClient _client;
public MindeeFinancialRouter(string apiKey)
{
_client = new MindeeClient(apiKey);
}
public async Task<FinancialSummary> ProcessFinancialDocumentAsync(string filePath)
{
// All financial documents uploaded for auto-detection and parsing
var inputSource = new LocalInputSource(filePath);
var response = await _client.ParseAsync<FinancialDocumentV1>(inputSource);
var prediction = response.Document.Inference.Prediction;
return new FinancialSummary
{
DocumentType = prediction.DocumentType?.Value, // "INVOICE" or "RECEIPT"
InvoiceNumber = prediction.InvoiceNumber?.Value,
Date = prediction.Date?.Value?.ToString(),
TotalAmount = prediction.TotalAmount?.Value,
SupplierName = prediction.SupplierName?.Value,
CustomerName = prediction.CustomerName?.Value
};
}
}
using Mindee;
using Mindee.Input;
using Mindee.Product.FinancialDocument;
public class MindeeFinancialRouter
{
private readonly MindeeClient _client;
public MindeeFinancialRouter(string apiKey)
{
_client = new MindeeClient(apiKey);
}
public async Task<FinancialSummary> ProcessFinancialDocumentAsync(string filePath)
{
// All financial documents uploaded for auto-detection and parsing
var inputSource = new LocalInputSource(filePath);
var response = await _client.ParseAsync<FinancialDocumentV1>(inputSource);
var prediction = response.Document.Inference.Prediction;
return new FinancialSummary
{
DocumentType = prediction.DocumentType?.Value, // "INVOICE" or "RECEIPT"
InvoiceNumber = prediction.InvoiceNumber?.Value,
Date = prediction.Date?.Value?.ToString(),
TotalAmount = prediction.TotalAmount?.Value,
SupplierName = prediction.SupplierName?.Value,
CustomerName = prediction.CustomerName?.Value
};
}
}
Imports Mindee
Imports Mindee.Input
Imports Mindee.Product.FinancialDocument
Imports System.Threading.Tasks
Public Class MindeeFinancialRouter
Private ReadOnly _client As MindeeClient
Public Sub New(apiKey As String)
_client = New MindeeClient(apiKey)
End Sub
Public Async Function ProcessFinancialDocumentAsync(filePath As String) As Task(Of FinancialSummary)
' All financial documents uploaded for auto-detection and parsing
Dim inputSource = New LocalInputSource(filePath)
Dim response = Await _client.ParseAsync(Of FinancialDocumentV1)(inputSource)
Dim prediction = response.Document.Inference.Prediction
Return New FinancialSummary With {
.DocumentType = prediction.DocumentType?.Value, ' "INVOICE" or "RECEIPT"
.InvoiceNumber = prediction.InvoiceNumber?.Value,
.Date = prediction.Date?.Value?.ToString(),
.TotalAmount = prediction.TotalAmount?.Value,
.SupplierName = prediction.SupplierName?.Value,
.CustomerName = prediction.CustomerName?.Value
}
End Function
End Class
IronOCR 접근 방식:
using IronOcr;
using System.Text.RegularExpressions;
public class IronOcrFinancialRouter
{
private readonly IronTesseract _ocr = new IronTesseract();
public FinancialSummary ProcessFinancialDocument(string filePath)
{
var result = _ocr.Read(filePath);
var text = result.Text;
// Detect document type using structural keywords from word data
var isInvoice = DetectInvoice(result);
if (isInvoice)
{
return new FinancialSummary
{
DocumentType = "INVOICE",
InvoiceNumber = Regex.Match(text,
@"Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups[1].Value,
Date = Regex.Match(text,
@"(?:Invoice\s+)?Date\s*:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})",
RegexOptions.IgnoreCase).Groups[1].Value,
TotalAmount = ParseAmount(text,
@"Total\s*(?:Due|Amount)?\s*:?\s*\$?([\d,]+\.\d{2})"),
SupplierName = ExtractTopLine(result),
CustomerName = Regex.Match(text,
@"Bill\s+To\s*:?\s*\n?(.+)",
RegexOptions.IgnoreCase).Groups[1].Value.Trim()
};
}
else
{
// Receipt structure: merchant at top, no "Bill To" section
return new FinancialSummary
{
DocumentType = "RECEIPT",
Date = Regex.Match(text,
@"\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\b").Value,
TotalAmount = ParseAmount(text,
@"(?:Total|Amount\s+Due|Grand\s+Total)\s*:?\s*\$?([\d,]+\.\d{2})"),
SupplierName = ExtractTopLine(result)
};
}
}
private bool DetectInvoice(OcrResult result)
{
// Invoice indicators: "Invoice", "Bill To", "Due Date", line items with quantities
var text = result.Text;
var invoiceSignals = new[] { "Invoice", "Bill To", "Due Date", "Purchase Order" };
return invoiceSignals.Any(s =>
text.IndexOf(s, StringComparison.OrdinalIgnoreCase) >= 0);
}
private string ExtractTopLine(OcrResult result)
{
return result.Pages
.FirstOrDefault()
?.Lines
.FirstOrDefault(l => l.Text.Trim().Length > 4)
?.Text.Trim();
}
private decimal? ParseAmount(string text, string pattern)
{
var match = Regex.Match(text, pattern, RegexOptions.IgnoreCase);
if (match.Success &&
decimal.TryParse(match.Groups[1].Value.Replace(",", ""), out var val))
return val;
return null;
}
}
public class FinancialSummary
{
public string DocumentType { get; set; }
public string InvoiceNumber { get; set; }
public string Date { get; set; }
public decimal? TotalAmount { get; set; }
public string SupplierName { get; set; }
public string CustomerName { get; set; }
}
using IronOcr;
using System.Text.RegularExpressions;
public class IronOcrFinancialRouter
{
private readonly IronTesseract _ocr = new IronTesseract();
public FinancialSummary ProcessFinancialDocument(string filePath)
{
var result = _ocr.Read(filePath);
var text = result.Text;
// Detect document type using structural keywords from word data
var isInvoice = DetectInvoice(result);
if (isInvoice)
{
return new FinancialSummary
{
DocumentType = "INVOICE",
InvoiceNumber = Regex.Match(text,
@"Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups[1].Value,
Date = Regex.Match(text,
@"(?:Invoice\s+)?Date\s*:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})",
RegexOptions.IgnoreCase).Groups[1].Value,
TotalAmount = ParseAmount(text,
@"Total\s*(?:Due|Amount)?\s*:?\s*\$?([\d,]+\.\d{2})"),
SupplierName = ExtractTopLine(result),
CustomerName = Regex.Match(text,
@"Bill\s+To\s*:?\s*\n?(.+)",
RegexOptions.IgnoreCase).Groups[1].Value.Trim()
};
}
else
{
// Receipt structure: merchant at top, no "Bill To" section
return new FinancialSummary
{
DocumentType = "RECEIPT",
Date = Regex.Match(text,
@"\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\b").Value,
TotalAmount = ParseAmount(text,
@"(?:Total|Amount\s+Due|Grand\s+Total)\s*:?\s*\$?([\d,]+\.\d{2})"),
SupplierName = ExtractTopLine(result)
};
}
}
private bool DetectInvoice(OcrResult result)
{
// Invoice indicators: "Invoice", "Bill To", "Due Date", line items with quantities
var text = result.Text;
var invoiceSignals = new[] { "Invoice", "Bill To", "Due Date", "Purchase Order" };
return invoiceSignals.Any(s =>
text.IndexOf(s, StringComparison.OrdinalIgnoreCase) >= 0);
}
private string ExtractTopLine(OcrResult result)
{
return result.Pages
.FirstOrDefault()
?.Lines
.FirstOrDefault(l => l.Text.Trim().Length > 4)
?.Text.Trim();
}
private decimal? ParseAmount(string text, string pattern)
{
var match = Regex.Match(text, pattern, RegexOptions.IgnoreCase);
if (match.Success &&
decimal.TryParse(match.Groups[1].Value.Replace(",", ""), out var val))
return val;
return null;
}
}
public class FinancialSummary
{
public string DocumentType { get; set; }
public string InvoiceNumber { get; set; }
public string Date { get; set; }
public decimal? TotalAmount { get; set; }
public string SupplierName { get; set; }
public string CustomerName { get; set; }
}
Imports IronOcr
Imports System.Text.RegularExpressions
Public Class IronOcrFinancialRouter
Private ReadOnly _ocr As New IronTesseract()
Public Function ProcessFinancialDocument(filePath As String) As FinancialSummary
Dim result = _ocr.Read(filePath)
Dim text = result.Text
' Detect document type using structural keywords from word data
Dim isInvoice = DetectInvoice(result)
If isInvoice Then
Return New FinancialSummary With {
.DocumentType = "INVOICE",
.InvoiceNumber = Regex.Match(text,
"Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups(1).Value,
.Date = Regex.Match(text,
"(?:Invoice\s+)?Date\s*:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})",
RegexOptions.IgnoreCase).Groups(1).Value,
.TotalAmount = ParseAmount(text,
"Total\s*(?:Due|Amount)?\s*:?\s*\$?([\d,]+\.\d{2})"),
.SupplierName = ExtractTopLine(result),
.CustomerName = Regex.Match(text,
"Bill\s+To\s*:?\s*\n?(.+)",
RegexOptions.IgnoreCase).Groups(1).Value.Trim()
}
Else
' Receipt structure: merchant at top, no "Bill To" section
Return New FinancialSummary With {
.DocumentType = "RECEIPT",
.Date = Regex.Match(text,
"\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\b").Value,
.TotalAmount = ParseAmount(text,
"(?:Total|Amount\s+Due|Grand\s+Total)\s*:?\s*\$?([\d,]+\.\d{2})"),
.SupplierName = ExtractTopLine(result)
}
End If
End Function
Private Function DetectInvoice(result As OcrResult) As Boolean
' Invoice indicators: "Invoice", "Bill To", "Due Date", line items with quantities
Dim text = result.Text
Dim invoiceSignals = {"Invoice", "Bill To", "Due Date", "Purchase Order"}
Return invoiceSignals.Any(Function(s) text.IndexOf(s, StringComparison.OrdinalIgnoreCase) >= 0)
End Function
Private Function ExtractTopLine(result As OcrResult) As String
Return result.Pages _
.FirstOrDefault() _
?.Lines _
.FirstOrDefault(Function(l) l.Text.Trim().Length > 4) _
?.Text.Trim()
End Function
Private Function ParseAmount(text As String, pattern As String) As Decimal?
Dim match = Regex.Match(text, pattern, RegexOptions.IgnoreCase)
If match.Success AndAlso
Decimal.TryParse(match.Groups(1).Value.Replace(",", ""), val) Then
Return val
End If
Return Nothing
End Function
End Class
Public Class FinancialSummary
Public Property DocumentType As String
Public Property InvoiceNumber As String
Public Property Date As String
Public Property TotalAmount As Decimal?
Public Property SupplierName As String
Public Property CustomerName As String
End Class
문서 유형 감지 로직은 Mindee의 서버 측 DocumentType 필드를 result.Text에 대한 단순 키워드 스캔으로 대체합니다. 대부분의 재무 문서에서 "송장" 또는 "청구 대상"이라는 문구는 송장을 명확하게 식별하는 데 사용됩니다. 부재는 영수증을 식별합니다. result.Pages의 워드레벨 위치 데이터는 문서 세트에 엣지 케이스가 포함된 경우 더 정교한 레이아웃 기반 감지를 가능하게 합니다. 결과 읽기 가이드는 Words, Lines, Paragraphs 및 그들의 경계 좌표를 포함한 전체 객체 모델을 설명합니다.
민디 API와IronOCR매핑 참조
| 민디 | IronOCR에 상응하는 |
|---|---|
new MindeeClient(apiKey) |
new IronTesseract() - API 키 불필요 |
new LocalInputSource(filePath) |
new OcrInput() + input.LoadImage(filePath) 또는 ocr.Read(filePath) |
_client.ParseAsync<InvoiceV4>(input) |
_ocr.Read(filePath) + result.Text 에서의 정규식 추출 |
_client.ParseAsync<ReceiptV5>(input) |
_ocr.Read(filePath) + 영수증 추출 패턴 |
_client.ParseAsync<PassportV1>(input) |
_ocr.Read(filePath) + CropRectangle 을 통한 MRZ 영역 추출 |
_client.ParseAsync<FinancialDocumentV1>(input) |
_ocr.Read(filePath) + result.Text 에 대한 문서 유형 감지 |
response.Document.Inference.Prediction |
result.Text, result.Pages, result.Lines, result.Words |
prediction.InvoiceNumber?.Value |
Regex.Match(result.Text, @"Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)") |
prediction.SupplierName?.Value |
첫 페이지 위쪽 라인 result.Pages[0].Lines.First() 통해 |
prediction.TotalAmount?.Value |
Regex.Match(result.Text, @"Total\s*:?\s*\$?([\d,]+\.\d{2})") |
prediction.LineItems |
result.Lines 줄 아이템 정규식 패턴 필터링 |
prediction.SupplierPaymentDetails[].Iban |
Regex.Match(result.Text, @"IBAN\s*:?\s*([\w\s]+)") |
prediction.InvoiceDate?.Value |
Regex.Match(result.Text, @"Date\s*:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{4})") |
field.Confidence |
result.Confidence (문서 레벨) 또는 word.Confidence (단어별) |
catch (MindeeException ex) |
동등한 항목 없음 — 인식 시점에 네트워크 오류 없음 |
await Task.Delay(100) (속도 제한) |
필수 사항 아님 - 요금 제한 없음 |
input.LoadPdf(path) |
ocrInput.LoadPdf(path) - 동일한 작업, 완전 로컬 |
| 구성의 API 키 | IronOcr.License.LicenseKey = "..." 을 통한 라이선스 키 - 회전 불필요 |
일반적인 마이그레이션 문제와 해결책
문제 1: 자르기 사각형 좌표가 문서 레이아웃과 일치하지 않음
Mindee: 공간 필드 좌표는 서버 측에서 처리됩니다. 민디 모델은 개발자의 좌표 설정 없이도 다양한 송장 레이아웃에 맞춰 조정됩니다.
해결 방법: 공급업체 문서 세트에서 추출한 대표 샘플을 기준으로 영역 좌표를 측정합니다. 아무 이미지 편집기에서 샘플 송장을 열고, 픽셀 크기를 읽고, CropRectangle(x, y, width, height) 을 정의합니다. 레이아웃이 가변적인 송장의 경우, 먼저 전체 문서를 읽고 단어 위치 데이터를 사용하여 영역을 동적으로 찾으세요.
var result = _ocr.Read("invoice.jpg");
var allWords = result.Pages.First().Words;
// Find the word "Total" and read the region 50px to its right
var totalLabel = allWords.FirstOrDefault(w =>
w.Text.Equals("Total", StringComparison.OrdinalIgnoreCase));
if (totalLabel != null)
{
var valueRegion = new CropRectangle(
totalLabel.X + totalLabel.Width + 5,
totalLabel.Y - 5,
200,
totalLabel.Height + 10);
using var valueInput = new OcrInput();
valueInput.LoadImage("invoice.jpg", valueRegion);
var valueResult = _ocr.Read(valueInput);
Console.WriteLine($"Total: {valueResult.Text.Trim()}");
}
var result = _ocr.Read("invoice.jpg");
var allWords = result.Pages.First().Words;
// Find the word "Total" and read the region 50px to its right
var totalLabel = allWords.FirstOrDefault(w =>
w.Text.Equals("Total", StringComparison.OrdinalIgnoreCase));
if (totalLabel != null)
{
var valueRegion = new CropRectangle(
totalLabel.X + totalLabel.Width + 5,
totalLabel.Y - 5,
200,
totalLabel.Height + 10);
using var valueInput = new OcrInput();
valueInput.LoadImage("invoice.jpg", valueRegion);
var valueResult = _ocr.Read(valueInput);
Console.WriteLine($"Total: {valueResult.Text.Trim()}");
}
Imports System
Imports System.Linq
Dim result = _ocr.Read("invoice.jpg")
Dim allWords = result.Pages.First().Words
' Find the word "Total" and read the region 50px to its right
Dim totalLabel = allWords.FirstOrDefault(Function(w) w.Text.Equals("Total", StringComparison.OrdinalIgnoreCase))
If totalLabel IsNot Nothing Then
Dim valueRegion = New CropRectangle(totalLabel.X + totalLabel.Width + 5, totalLabel.Y - 5, 200, totalLabel.Height + 10)
Using valueInput As New OcrInput()
valueInput.LoadImage("invoice.jpg", valueRegion)
Dim valueResult = _ocr.Read(valueInput)
Console.WriteLine($"Total: {valueResult.Text.Trim()}")
End Using
End If
영역 기반 OCR 예시는 이러한 동적 영역 패턴을 완벽하게 작동하는 예시로 보여줍니다.
문제 2: Mindee를 제거한 후 비동기 호출 사이트가 작동하지 않음
민디(Mindee): 민디 API의 전체 표면은 네트워크 I/O를 사용하기 때문에 비동기식으로 작동합니다. await _client.ParseAsync<t>() 을(를) 기반으로 구축된 컨트롤러와 서비스 메서드는 호출 체인 전반에 비동기입니다.
솔루션: IronOCR의 Read() 메서드는 동기식입니다. 기존의 비동기 호출 사이트는 Task.Run으로 동기 호출을 래핑하여 즉시 작동합니다.
// Existing async signature preserved for callers
public async Task<string> ExtractInvoiceNumberAsync(string filePath)
{
return await Task.Run(() =>
{
var result = new IronTesseract().Read(filePath);
return Regex.Match(result.Text,
@"Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups[1].Value;
});
}
// Existing async signature preserved for callers
public async Task<string> ExtractInvoiceNumberAsync(string filePath)
{
return await Task.Run(() =>
{
var result = new IronTesseract().Read(filePath);
return Regex.Match(result.Text,
@"Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups[1].Value;
});
}
Imports System.Text.RegularExpressions
Imports System.Threading.Tasks
' Existing async signature preserved for callers
Public Async Function ExtractInvoiceNumberAsync(filePath As String) As Task(Of String)
Return Await Task.Run(Function()
Dim result = New IronTesseract().Read(filePath)
Return Regex.Match(result.Text,
"Invoice\s*#?\s*:?\s*([A-Z0-9\-]+)",
RegexOptions.IgnoreCase).Groups(1).Value
End Function)
End Function
고속 처리 ASP.NET 시나리오의 경우, Task.Run 래퍼와 네이티브 비동기 경로 중에서 결정하기 전에 비동기 OCR 가이드를 검토하세요.
문제 3: 저품질 스캔에서 추출 정확도가 떨어짐
민디(Mindee): 민디의 전처리 과정은 내부적으로 자동으로 이루어집니다. 스캔 품질 문제로 인해 현장 신뢰도 점수가 낮아지며, 재제출 전에 개발자의 개입이 불가능합니다.
해결 방법: 인식 전에 IronOCR의 전처리 파이프라인을 적용합니다. Deskew() 과 DeNoise() 의 조합은 계정 지급 워크플로에서 일반적으로 볼 수 있는 평판 스캔 결함에 대해 대부분의 정확도를 복구합니다.
using var input = new OcrInput();
input.LoadImage("faded-invoice.jpg");
input.Deskew();
input.DeNoise();
input.Contrast(); // for faded or low-contrast thermal paper receipts
input.Binarize(); // convert to clean black-and-white before recognition
var result = new IronTesseract().Read(input);
using var input = new OcrInput();
input.LoadImage("faded-invoice.jpg");
input.Deskew();
input.DeNoise();
input.Contrast(); // for faded or low-contrast thermal paper receipts
input.Binarize(); // convert to clean black-and-white before recognition
var result = new IronTesseract().Read(input);
Imports IronOcr
Using input As New OcrInput()
input.LoadImage("faded-invoice.jpg")
input.Deskew()
input.DeNoise()
input.Contrast() ' for faded or low-contrast thermal paper receipts
input.Binarize() ' convert to clean black-and-white before recognition
Dim result = New IronTesseract().Read(input)
End Using
심하게 열악한 스캔의 경우, input.DeepCleanBackgroundNoise() 은(는) 보다 강력한 배경 제거 패스를 적용합니다. 이미지 품질 보정 가이드에는 임계값이 명시되어 있으며, 필터 마법사는 주어진 문서 샘플에서 정확도를 향상시키는 필터를 식별하는 데 도움을 줍니다.
문제 4: API 키 및 네트워크 송신 구성이 제거되었습니다.
Mindee: 앱 설정에 저장된 API 키, api.mindee.net으로의 아웃바운드 HTTPS를 허용하는 네트워크 출구 규칙, 및 HttpRequestException을 래핑하는 재시도 로직은 모두 필요한 인프라입니다.
해결 방법: 세 가지를 모두 함께 제거합니다. 민디 API 키 항목이 설정에서 삭제되었습니다. 네트워크 송신 규칙이 취소되었습니다. try/catch (HttpRequestException) 블록이 제거됩니다. 남은 유일한 자격 증명은 시작 시 한 번 설정되는IronOCR라이선스 키입니다.
// appsettings.json: remove "Mindee:ApiKey"
// Firewall: revoke egress rule for api.mindee.net
// Startup.cs or Program.cs:
IronOcr.License.LicenseKey = Configuration["IronOcr:LicenseKey"];
// 아니요 rotation schedule, no secure storage beyond standard config secret management
// appsettings.json: remove "Mindee:ApiKey"
// Firewall: revoke egress rule for api.mindee.net
// Startup.cs or Program.cs:
IronOcr.License.LicenseKey = Configuration["IronOcr:LicenseKey"];
// 아니요 rotation schedule, no secure storage beyond standard config secret management
' appsettings.json: remove "Mindee:ApiKey"
' Firewall: revoke egress rule for api.mindee.net
' Startup.vb or Program.vb:
IronOcr.License.LicenseKey = Configuration("IronOcr:LicenseKey")
' 아니요 rotation schedule, no secure storage beyond standard config secret management
제5호: 다중 페이지 PDF 페이지 수 청구
Mindee: Mindee에 업로드된 12페이지 공급업체 명세서는 월별 할당량에 비해 12페이지를 소비합니다. 프로 등급 초과 사용 요금의 경우, 월 사용량 한도에 근접했을 때 해당 문서 하나당 월 1.20달러의 추가 비용이 발생합니다.
해결책:IronOCR페이지당 비용 없이 여러 페이지로 구성된 PDF 파일을 처리합니다. 전체 문서를 불러온 후 페이지를 순차적으로 처리하세요.
using var input = new OcrInput();
input.LoadPdf("vendor-statement.pdf"); // 12 pages — no billing unit increment
var result = new IronTesseract().Read(input);
foreach (var page in result.Pages)
Console.WriteLine($"Page {page.PageNumber}: {page.Text.Length} characters recognized");
using var input = new OcrInput();
input.LoadPdf("vendor-statement.pdf"); // 12 pages — no billing unit increment
var result = new IronTesseract().Read(input);
foreach (var page in result.Pages)
Console.WriteLine($"Page {page.PageNumber}: {page.Text.Length} characters recognized");
Imports IronOcr
Using input As New OcrInput()
input.LoadPdf("vendor-statement.pdf") ' 12 pages — no billing unit increment
Dim result = New IronTesseract().Read(input)
For Each page In result.Pages
Console.WriteLine($"Page {page.PageNumber}: {page.Text.Length} characters recognized")
Next
End Using
문제 6: FinancialDocumentV1 종속성을 단일 패턴으로 대체할 수 없습니다.
Mindee: FinancialDocumentV1이(가) 문서 유형을 자동 감지하고 문서 유형에 대한 호출 코드 가지 분기 없이 필드를 추출합니다.
해결책: 키워드 존재 여부를 이용하여 경량화된 탐지기를 구축합니다. 재무 문서는 "송장", "청구 대상" 또는 "만기일"이 포함된 송장과 "영수증", "감사합니다"가 포함되거나 송장 표시가 없는 영수증으로 명확하게 구분됩니다. 두 가지 추출 방법 Plus 세 줄짜리 탐지기가 민디 API 호출을 대체하여, 형식이 올바른 문서에 대한 정확도를 저하시키지 않습니다.
var result = _ocr.Read(filePath);
var summary = result.Text.IndexOf("Invoice", StringComparison.OrdinalIgnoreCase) >= 0
? ExtractInvoiceFields(result)
: ExtractReceiptFields(result);
var result = _ocr.Read(filePath);
var summary = result.Text.IndexOf("Invoice", StringComparison.OrdinalIgnoreCase) >= 0
? ExtractInvoiceFields(result)
: ExtractReceiptFields(result);
Dim result = _ocr.Read(filePath)
Dim summary = If(result.Text.IndexOf("Invoice", StringComparison.OrdinalIgnoreCase) >= 0, ExtractInvoiceFields(result), ExtractReceiptFields(result))
민디 이주 체크리스트
사전 마이그레이션
코드베이스 전체에서 민디 사용 내역을 모두 감사합니다.
grep -r "using Mindee" --include="*.cs" .
grep -r "MindeeClient\|ParseAsync\|InvoiceV4\|ReceiptV5\|PassportV1\|FinancialDocumentV1" --include="*.cs" .
grep -r "LocalInputSource\|MindeeException" --include="*.cs" .
grep -r "Mindee:ApiKey\|mindee_api_key\|MINDEE_API_KEY" --include="*.json" --include="*.env" --include="*.yml" .
grep -r "using Mindee" --include="*.cs" .
grep -r "MindeeClient\|ParseAsync\|InvoiceV4\|ReceiptV5\|PassportV1\|FinancialDocumentV1" --include="*.cs" .
grep -r "LocalInputSource\|MindeeException" --include="*.cs" .
grep -r "Mindee:ApiKey\|mindee_api_key\|MINDEE_API_KEY" --include="*.json" --include="*.env" --include="*.yml" .
재고 조사 결과:
ParseAsync<t>을(를) 사용하여 고유 호출 사이트 수를 세십시오- 어떤 문서 유형이 사용되는지 확인 (
InvoiceV4,ReceiptV5,PassportV1,FinancialDocumentV1) - 민디 호출에서 전파되는 모든 비동기 메서드 체인을 식별합니다.
- 구성 파일 및 비밀 저장소에서 API 키 참조를 찾습니다.
- 민디 네트워크 호출을 감싸는 재시도 로직을 식별합니다.
- 민디 엔드포인트로의 아웃바운드 트래픽을 허용하는 네트워크 송신 규칙을 식별합니다.
코드 마이그레이션
- 프로젝트 파일에서
MindeeNuGet 패키지를 제거합니다 - IronOCR을 설치하기 위해
dotnet add package IronOcr을(를) 실행합니다 - 애플리케이션 시작에
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"을 추가합니다 - 모든
using Mindee,using Mindee.Input및using Mindee.Product.*지시어를 제거합니다 MindeeClient생성자 주입을IronTesseract인스턴스화 또는 DI 등록으로 대체합니다- 각
ParseAsync<InvoiceV4>호출을_ocr.Read(filePath)및 송장 추출 메서드로 교체합니다 - 각
ParseAsync<ReceiptV5>호출을_ocr.Read(filePath)및 영수증 추출 메서드로 교체합니다 - 각
ParseAsync<PassportV1>호출을CropRectangle을 사용한 영역 기반 OCR로 교체합니다 - 각
ParseAsync<FinancialDocumentV1>호출을 문서 유형 감지기 + 라우팅 패턴으로 교체합니다 - 모든
await Task.Delay(...)속도 제한 준수 라인을 제거합니다 - OCR 경로에서 모든
catch (MindeeException)및catch (HttpRequestException)블록을 제거합니다 OcrInput전처리 호출을 추가합니다 (Deskew,DeNoise,Contrast) 스캔 문서 경로에 대해- 문서 아카이브 경로에
result.SaveAsSearchablePdf(archivePath)을 추가합니다 - 모든 설정 파일과 비밀 저장소에서 민디 API 키를 제거합니다.
- 네트워크 모킹 없이 동기적으로 실행되도록 통합 테스트를 업데이트합니다.
마이그레이션 이후
- 각 문서 유형을 포괄하는 20~30개의 문서 샘플 세트를 사용하여 추출 정확도를 검증합니다.
- 대표적인 깨끗한 스캔에서의
result.Confidence값이 80 이상임을 확인합니다; 다음과 같은 경우 전처리를 적용하세요 - 비뚤어진 및 열화된 스캔 샘플에서의 전처리 파이프라인 (
Deskew,DeNoise)을 테스트합니다 - 다중 페이지 PDFs가 올바른
result.Pages.Count을 생성하고 페이지별 콘텐츠를 제공하는지 확인합니다 result.SaveAsSearchablePdf()출력이 Adobe Acrobat 및 시스템 PDF 뷰어에서 검색 가능한 것인지 확인합니다- 공급업체 세트의 문서 레이아웃 변형에 대한 모든 영역 기반 추출 (
CropRectangle)을 테스트합니다 Task.Delay호출 없이 배치 처리를 실행하고 스레딩 문제 없음을 확인합니다- 인터넷 연결이 없는 상태에서 애플리케이션이 정상적으로 시작되고 실행되는지 확인하십시오.
- 각 진입점에서 첫 번째
_ocr.Read()호출 전에 라이선스 키 초기화가 실행되는지 확인합니다 - 구성, 환경 변수 또는 비밀 키에 민디 API 키 참조가 남아 있지 않은지 확인하십시오.
IronOCR로 마이그레이션할 때의 주요 이점
금융 문서에 대한 완벽한 데이터 주권을 확보하세요. 마이그레이션 후, 공급업체 IBAN 코드, 라우팅 번호, 고객 EIN 및 상세 내역은 귀사 인프라 외부로 유출되지 않습니다. 규정 준수 범위는 귀사 조직에만 적용됩니다. 타사 AI 공급업체와의 프로세서 계약은 감사 추적에서 삭제됩니다. GLBA, CMMC, FedRAMP 또는 데이터 상주 요건을 준수하는 팀은 외부 클라우드 전송에 필요한 규정 준수 검토 없이도 재무 문서를 처리할 수 있습니다.
사람이 볼륨과 관계없이 일정한 비용. $999에서IronOCR Lite 라이선스는 무제한 문서 처리가 가능한 개발자 한 명을 포함합니다. 월 500건에서 50,000건으로 송장 발행량을 늘리는 데 추가 비용은 전혀 들지 않습니다. 연말 처리량 급증, 일괄 수정 실행 및 개발 테스트 주기는 청구 카운터를 증가시키지 않습니다. 한 달에 5,000페이지를 처리하는 팀의 3년 총 소유 비용은 민디 Pro의 경우 약 17,964달러에서IronOCR영구 라이선스의 경우 1,499~2,999달러로 감소합니다. 자세한 등급 정보는 IronOCR 제품 페이지를 참조하세요.
추출 로직을 영구적으로 소유하세요.IronOCR추출 패턴은 저장소에 있는 일반 C# 코드입니다. 이러한 코드는 외부 공급업체의 출시 일정이나 API 버전 관리 결정과 관계없이 버전 관리, 검토, 테스트 및 배포가 가능합니다. Mindee가 InvoiceV5을 출시하고 InvoiceV4을 더 이상 사용하지 않게 될 때, 공급업체에 의해 이주 기한이 부과됩니다. 추출 패턴을 유지할 때, 개선은 git commit입니다.
실제 스캔 품질을 위한 전처리 제어. 회계 부서는 수십 또는 수백 개의 공급업체로부터 송장을 받는데, 각 송장은 서로 다른 장비와 설정으로 스캔됩니다. IronOCR의 전처리 파이프라인 — Deskew(), DeNoise(), Contrast(), Sharpen(), Binarize() — 은 각 스캔 범주의 특정 결함을 해결합니다. 감열식 영수증 사진은 평판 스캐너로 스캔한 여러 페이지짜리 청구서와는 다른 결함이 있습니다. 따라서 각 경로에 맞는 필터를 적용해야 합니다. Mindee의 전처리 과정은 보이지 않으며 구성할 수도 없습니다. 전체 필터 목록은 전처리 기능 페이지를 참조하십시오.
단계 하나로 검색 가능한 PDF 아카이브. IronOCR을 통해 처리된 모든 송장은 result.SaveAsSearchablePdf()을 사용하여 검색 가능한 PDF로 아카이브될 수 있습니다. 인식된 텍스트 레이어가 출력 파일에 포함되어 문서 관리 시스템, SharePoint 라이브러리 및 Enterprise 콘텐츠 저장소에서 모든 단어를 전체 텍스트로 검색할 수 있습니다. Mindee는 PDF 출력 기능을 전혀 제공하지 않습니다. 이전에는 검색 가능한 아카이빙을 위해 별도의 라이브러리, 별도의 처리 과정, 그리고 추가적인 유지 관리가 필요했습니다. 마이그레이션 후, 추출 및 아카이브는 하나의 _ocr.Read(input) 호출로 축소됩니다.
아키텍처 변경 없이 모든 곳에 배포 가능.IronOCR동일한 NuGet 패키지와 초기화를 사용하여 Windows, Linux, macOS, Docker, Azure App Service, AWS Lambda 및 Google Cloud Run에 배포할 수 있습니다. 밀폐된 환경, 공장 현장 시스템, 정부 보안 시설 모두 수정 없이 작동합니다. Docker 배포 가이드 , Azure 가이드 및 AWS 가이드는 각 플랫폼에 대한 환경별 구성을 다룹니다.
자주 묻는 질문
Mindee OCR API에서 IronOCR로 마이그레이션해야 하는 이유는 무엇인가요?
일반적인 동인으로는 COM 상호 운용의 복잡성 제거, 파일 기반 라이선스 관리 대체, 페이지당 과금 방지, Docker/컨테이너 배포 활성화, 표준 .NET 툴링과 통합되는 NuGet 네이티브 워크플로 채택 등이 있습니다.
Mindee OCR API에서 IronOCR로 마이그레이션할 때 주요 코드 변경 사항은 무엇인가요?
Mindee 초기화 시퀀스를 IronTesseract 인스턴스화로 대체하고, COM 수명 주기 관리(명시적 생성/로드/폐쇄 패턴)를 제거하고, 결과 속성 이름을 업데이트합니다. 그 결과 상용구 줄이 훨씬 줄어듭니다.
마이그레이션을 시작하려면 IronOCR을 어떻게 설치하나요?
패키지 관리자 콘솔에서 '설치-패키지 IronOcr'을 실행하거나 CLI에서 '닷넷 추가 패키지 IronOcr'을 실행하세요. 언어 팩은 별도의 패키지입니다: 예를 들어 프랑스어의 경우 '닷넷 추가 패키지 IronOcr.Languages.French'를 실행합니다.
IronOCR은 표준 비즈니스 문서에 대해 Mindee OCR API의 OCR 정확도와 일치하나요?
IronOCR은 송장, 계약서, 영수증, 타이핑된 양식 등 표준 비즈니스 콘텐츠에 대해 높은 정확도를 달성합니다. 이미지 전처리 필터(데스큐, 노이즈 제거, 대비 향상)는 품질이 저하된 입력에 대한 인식률을 더욱 향상시킵니다.
Mindee OCR API가 별도로 설치하는 언어 데이터는 어떻게 처리하나요?
IronOCR의 언어 데이터는 NuGet 패키지로 배포됩니다. '닷넷 추가 패키지 IronOcr.Languages.German'은 독일어 지원을 설치합니다. 수동 파일 배치나 디렉터리 경로는 필요하지 않습니다.
Mindee OCR API에서 IronOCR로 마이그레이션하려면 배포 인프라를 변경해야 하나요?
IronOCR은 Mindee OCR API보다 인프라 변경이 덜 필요합니다. SDK 바이너리 경로, 라이선스 파일 배치 또는 라이선스 서버 구성이 필요하지 않습니다. NuGet 패키지에는 완전한 OCR 엔진이 포함되어 있으며 라이선스 키는 애플리케이션 코드에 설정된 문자열입니다.
마이그레이션 후 IronOCR 라이선싱은 어떻게 구성하나요?
애플리케이션 시작 코드에 IronOcr.License.LicenseKey = "YOUR-KEY"를 할당하세요. Docker 또는 Kubernetes에서 키를 환경 변수로 저장하고 시작 시 키를 읽습니다. 트래픽을 수락하기 전에 License.IsValidLicense를 사용하여 유효성을 검사하세요.
IronOCR은 Mindee와 동일한 방식으로 PDF를 처리할 수 있나요?
예. IronOCR은 원본 및 스캔한 PDF를 모두 읽습니다. IronTesseract를 인스턴스화하고, 입력이 PDF 경로 또는 OcrPdfInput인 경우 ocr.Read(input)를 호출하고, OcrResult 페이지를 반복하면 됩니다. 별도의 PDF 렌더링 파이프라인이 필요하지 않습니다.
IronOCR은 대량 처리에서 스레딩을 어떻게 처리하나요?
IronTesseract는 스레드별로 인스턴스화해도 안전합니다. Parallel.ForEach 또는 Task 풀에서 스레드당 하나의 인스턴스를 스핀업하고, OCR을 동시에 실행하고, 완료되면 각 인스턴스를 폐기하면 됩니다. 전역 상태나 잠금이 필요하지 않습니다.
IronOCR은 텍스트 추출 후 어떤 출력 형식을 지원하나요?
IronOCR은 텍스트, 단어 좌표, 신뢰도 점수, 페이지 구조를 포함한 구조화된 결과를 반환합니다. 내보내기 옵션에는 일반 텍스트, 검색 가능한 PDF, 다운스트림 처리를 위한 구조화된 결과 개체가 포함됩니다.
워크로드 확장을 위해 IronOCR 가격이 Mindee OCR API보다 더 예측 가능한가요?
IronOCR은 페이지당 또는 볼륨당 요금이 없는 정액제 영구 라이선스를 사용합니다. 10,000페이지를 처리하든 1,000만 페이지를 처리하든 라이선스 비용은 일정하게 유지됩니다. 볼륨 및 팀 라이선스 옵션은 IronOCR 가격 페이지에서 확인할 수 있습니다.
Mindee OCR API에서 IronOCR로 마이그레이션한 후 기존 테스트는 어떻게 되나요?
추출된 텍스트 콘텐츠에 대해 어설트하는 테스트는 마이그레이션 후에도 계속 통과해야 합니다. API 호출 패턴 또는 COM 개체 수명 주기의 유효성을 검사하는 테스트는 IronOCR의 더 간단한 초기화 및 결과 모델을 반영하도록 업데이트해야 합니다.

