如何在 C# 中使用异步和多线程
从 明迪 迁移到IronOCR将文档处理从外部云服务器转移到您自己的基础设施上——消除了按页计费、第三方数据传输和运行时网络可用性依赖性。 以下各节涵盖了包替换、命名空间更新以及四个实际的前后代码场景:发票区域提取、多页收据 PDF 处理、财务文档预处理和可搜索 PDF 归档。
为什么要从 明迪 迁移
Mindee 以优雅的方式解决了一个实际问题:提交文档,接收结构化的 JSON 字段。 对于可以接受将财务文件传输到外部 API 且文件量适中的团队来说,这种方法可以快速取得成果。 当若干条件之一发生变化时,通常会触发迁移。
财务数据跨越合规界限。 Mindee的ParseAsync<InvoiceV4>将完整的文档上传到外部服务器。 IBAN 代码、路由号码、供应商 EIN 和明细项目通过公共互联网传输,并在您无法控制的硬件上进行处理。 SOC 2 II 型认证确认 明迪 遵循安全规范; 它不会将您的文件保留在您的安全边界内。 当合规性审查、新的客户合同或数据驻留法规对第三方云处理划定界限时,无论 明迪 的提取准确性如何,其架构都会失去资格。
**按页计费,费用根据您的需求而定。**入门套餐每月提供 1000 页打印量,价格为 49 美元/月。 专业版套餐每月提供 5,000 页,价格为 499 美元/月。 多页PDF文件会计算每一页。 开发运行次数计入配额。 一个团队每月处理 4,000 张发票,每月支付 499 美元,每年支付 5,988 美元,该数字每年重置,并随着业务量的增长而增加。 每月 24,000 页的打印量,您是在Enterprise定价。IronOCR的永久许可证售价为 1,499 美元(Professional版),涵盖无限量的文档,并且不到四个月即可通过 明迪 Pro 收回成本。
异步轮询贯穿整个技术栈。Mindee的每个操作都是异步的,因为每个操作都是一个网络请求。这种强制性的异步链会向上层级传递,依次经过控制器、服务层和工作类。 当网络不可用或 明迪 的服务中断时,连锁反应会导致故障,且没有本地回退方案。 IronOCR同步处理本地CPU工作,并在需要异步接口以保持架构一致性时封装在Task.Run中。
预构建的API目录有一个硬性限制。 Mindee涵盖了InvoiceV4, ReceiptV5, PassportV1,以及一个小型的其他文档类型目录。任何在该列表之外的文档类型需要企业计划的自定义模型训练:样本收集、标注、训练时间以及在API可用之前的准备时间。 贵公司每需要一种新的文档类型,就需要与一家供应商单独合作。 IronOCR使用相同的IronTesseract().Read()调用处理任何文档类型; 添加新的文档类型意味着编写提取模式,而不是协商培训合同。
API密钥管理增加了操作界面。Mindee凭证必须安全地存储在服务器端,定期轮换,并防止泄露。 网络出口规则必须允许向 明迪 端点发出 HTTPS 出站流量。 重试逻辑必须处理来自不可避免的网络故障的MindeeException。 移除 明迪 会移除所有这些操作表面:没有凭证,没有出口规则,也没有围绕网络 I/O 的重试包装器。
**物理隔离和受限环境不适用。**政府承包商、国防分包商、对出站互联网访问有严格控制的医疗保健网络,以及部署在缺乏可靠互联网连接的设施中的工业系统,均无法按设计使用 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
// 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与 Mindee:功能对比
下表列出了驱动迁移决策的架构和功能差异。
| 特征 | 明迪 | IronOCR |
|---|---|---|
| 加工地点 | 明迪 云服务器 | 本地计算机/您的基础架构 |
| 运行时需要互联网连接。 | 总是 | 绝不 |
| 数据传输 | 每次通话都会上传完整文档 | None |
| 单份文件成本 | 是的(按页计费,按计划计费) | 否(永久许可) |
| 起价 | 每月 49 美元(1000 页) | $999 一次性 (Lite) |
| 离线/气隙支持 | 不支持 | 全面支持 |
| 本地部署 | 不可用 | 唯一选项 |
| 发票解析 | 预构建结构化API (InvoiceV4) | OCR + 基于区域或正则表达式的提取 |
| 收据解析 | 预构建结构化API (ReceiptV5) | OCR + 提取模式 |
| 任意文档类型 | 没有定制培训则不支持 | 提供全面支持,任何文件 |
| 自定义模型训练 | Enterprise计划 + 提前期 | 不要求 |
| API 调用模式 | 异步强制(网络 I/O) | 同步(异步可选) |
| 限速 | 取决于计划 | None |
| PDF 输入 | 是 | 是的(原生,无需转换) |
| 多页PDF处理 | 是 | 是 |
| 可搜索的 PDF 输出 | 否 | 是 (result.SaveAsSearchablePdf()) |
| 图像预处理 | 未公开 | 校正倾斜、降噪、增强对比度、二值化、锐化、缩放 |
| 基于区域的提取 | 响应中的场坐标 | CropRectangle 在输入上 |
| 支持 125 种以上的语言 | 有限的 | 是的(已捆绑,无需管理 Tess 数据) |
| 条形码读取 | 否 | 是的(与OCR同时进行) |
| 结构化词坐标 | 否 | 是的(页数、段落数、行数、字数) |
| 线程安全 | 不适用(每次呼叫的网络 I/O) | 内置 |
| 跨平台 | 云计算(平台无关) | Windows、Linux、macOS、Docker、Azure、AWS |
| .NET兼容性 | .NET Standard 2.0+ | .NET Framework 4.6.2+,. .NET 5/6/7/8/9 |
| 商业支持 | 是 | 是 |
快速入门:Mindee 到IronOCR 的迁移
步骤 1:替换 NuGet 软件包
移除 明迪 软件包:
dotnet remove package Mindee
从NuGet安装IronOCR :
步骤 2:更新命名空间
将 明迪 命名空间块替换为IronOCR命名空间:
// Before (Mindee)
using Mindee;
using Mindee.Input;
using Mindee.Product.Invoice;
using Mindee.Product.Receipt;
using Mindee.Product.FinancialDocument;
// After (IronOCR)
using IronOcr;
步骤 3:初始化许可证
在应用程序启动时(首次 OCR 调用之前)添加许可证密钥:
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"代码迁移示例
利用文档区域提取发票字段
Mindee 会返回已知位置的预解析字段,因为它的服务器端模型知道发票号码、日期和总额通常出现在哪里。 IronOCR等效于使用CropRectangle读取文档的特定区域,以匹配Mindee提取的空间感,而不传输文档。
Mindee 方法:
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
);
}
}
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);
}
}
基于区域的 OCR 与 明迪 的空间场检测的意图相符:读取文档的特定区域,而不是将正则表达式应用于整个页面。 CropRectangle(x, y, width, height) 接受像素坐标,因此调节需要根据代表性样本发票进行测量。基于区域的OCR指南涵盖坐标测量和区域重叠策略。 对于布局可变的发票,将区域提取与整个页面的正则表达式在result.Text上结合使用可产生最可靠的结果。
多页费用报告 PDF 处理
Mindee 接受 PDF 文件,并将每一页作为独立的文档单元进行处理,每页返回一个结构化的结果。 IronOCR通过result.Pages,其中包含每页内容和单词坐标。
Mindee 方法:
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;
}
}
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 明迪 处理 10 页费用报告 PDF 会产生 10 次 API 调用和 10 页计费页。 IronOCR只需一次本地调用即可处理同一个文件,无需按页付费,也无需按页进行网络往返。 result.Pages 集合提供每页文本、单词级包围框和用于布局感知提取的线条位置。 有关受密码保护的 PDF 加载和页面范围选择,请参阅PDF 输入指南;有关使用单词坐标,请参阅结构化读取结果指南。
扫描发票预处理流程
Mindee 的云处理在运行场提取之前应用了自己的图像归一化。 这种归一化的质量是不透明的——你无法控制它,也无法了解运行了哪些预处理步骤。 当扫描的发票出现倾斜、阴影或对比度低等情况时,Mindee 会返回较低的置信度评分,而且没有机制让您在提交之前改进扫描。 IronOCR明确地公开了预处理流程,允许您在识别运行之前应用给定文档所需的精确校正。
Mindee 方法:
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
};
}
}
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; }
}
两遍扫描模式——先快速读取,再对低置信度数据进行预处理——避免了对干净扫描数据进行完全预处理的开销。 对于在应付账款中最常见的扫描质量场景(略微倾斜的平板扫描、传真伪影、复印发票),DeNoise()共同恢复大部分的识别准确性。 图像质量校正指南详细记录了所有可用的滤镜,并指导用户哪些组合最适合针对不同的扫描缺陷进行校正。 图像方向校正指南涵盖了对倒置扫描的文档进行自动旋转的功能。
财务文档可搜索PDF归档
Mindee 不会生成任何 PDF 输出。 它的架构是只输入式的:提交文档,接收 JSON 字段。 将扫描发票归档为可搜索格式的团队必须将该流程与 明迪 的提取流程分开维护。 IronOCR直接从用于字段提取的同一识别过程中生成可搜索的 PDF——无需额外的库,也无需第二个处理步骤。
Mindee 方法:
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}");
}
}
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; }
}
result.SaveAsSearchablePdf() 直接将识别的文本层嵌入输出PDF中,生成的文件每个单词都可以在任何PDF查看器或企业内容管理系统中选择和搜索。 字段提取和归档在同一过程中运行——一次result.SaveAsSearchablePdf()用于归档。可搜索的PDF指南涵盖了包括HOCR导出在内的输出选项,PDF OCR用例页面涵盖了用于文档归档管道的生产部署模式。
移除 FinancialDocumentV1 自定义端点
Mindee的FinancialDocumentV1 API通过自动检测文档类型来处理发票和收据。 使用该技术的团队可以消除分支逻辑,但代价是更高的云依赖性——所有文档,无论类型如何,都会被上传。 IronOCR取代了它,它使用词级位置数据来检测文档结构和路由提取逻辑,而无需任何云调用。
Mindee 方法:
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
};
}
}
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; }
}
文档类型检测逻辑用简单的关键字扫描替换了Mindee服务器端的DocumentType字段。 对于绝大多数财务文件而言,"发票"或"账单接收方"字样的存在可以明确地识别发票; 缺失标识为收据。result.Pages中的单词级位置数据如果您的文档集中包含边缘案例,则可以启用更复杂的基于布局的检测。 读取结果指南解释了完整的对象模型,包括Paragraphs及其边界坐标。
明迪 API 到IronOCR映射参考
| 明迪 | IronOCR当量 |
|---|---|
new MindeeClient(apiKey) | new IronTesseract() — 不需要API密钥 |
new LocalInputSource(filePath) | new OcrInput() + input.LoadImage(filePath) or 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:**空间字段坐标由服务器端处理。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()}");
}
基于区域的 OCR 示例在一个完整的工作示例中展示了这种动态区域模式。
问题 2:移除 明迪 后异步调用站点失效
**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;
});
}
对于高吞吐量的ASP.NET场景,在决定在Task.Run封装和本地异步路径之间进行选择之前,请查看异步OCR指南。
问题三:低质量扫描图像提取准确率下降
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);
对于严重劣化的扫描,input.DeepCleanBackgroundNoise()适用更重的背景去除通道。 图像质量校正指南记录了阈值,而滤镜向导则有助于确定哪些滤镜可以提高给定文档样本的准确性。
问题 4:API 密钥和网络出口配置已移除
Mindee: 存储在应用程序设置中的API密钥、允许到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
问题 5:多页 PDF 页数计费
**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");
问题 6:FinancialDocumentV1 依赖项不能用单个模式替代
Mindee: FinancialDocumentV1自动检测文档类型并提取字段,无需调用代码在文档类型上分支。
**解决方案:**构建一个基于关键词存在性的轻量级检测器。 财务文件清晰地分为发票(包含"发票"、"账单接收方"或"到期日")和收据(包含"收据"、"感谢信"或缺少发票标记)。 两种提取方法Plus一个三行检测器,取代了 明迪 API 调用,同时又不牺牲对格式良好文档的准确性:
var result = _ocr.Read(filePath);
var summary = 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" .
调查结果:
- 使用
ParseAsync<t>计数唯一的调用站点 - 注意使用了哪些文档类型 (
InvoiceV4,ReceiptV5,PassportV1,FinancialDocumentV1) - 识别所有从 明迪 调用传播的异步方法链
- 在配置文件和密钥库中查找 API 密钥引用
- 识别 明迪 网络调用的重试逻辑
- 确定允许出站流量到达 明迪 端点的网络出口规则
代码迁移
- 从项目文件中移除
MindeeNuGet包 - 运行
dotnet add package IronOcr安装IronOCR - 在应用程序启动中添加
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY" - 移除所有
using Mindee.Product.*指令 - 用
MindeeClient构造函数注入 - 将每个
_ocr.Read(filePath)加上发票提取方法 - 用
ParseAsync<ReceiptV5>调用 - 用使用
ParseAsync<PassportV1>调用 - 用文档类型检测器和路由模式替换每个
ParseAsync<FinancialDocumentV1>调用 - 移除所有
await Task.Delay(...)速率限制合规行 - 从OCR路径中移除所有
catch (HttpRequestException)块 - 为扫描文档路径添加
OcrInput预处理调用 (Deskew,DeNoise,Contrast) - 将
result.SaveAsSearchablePdf(archivePath)添加到任何文档归档路径中 - 从所有配置文件和密钥库中移除 明迪 API 密钥
- 更新集成测试,使其同步运行,无需网络模拟。
后迁移
- 使用涵盖每种文档类型的 20-30 个文档样本集来验证提取准确性
- 确认代表性干净扫描上的
result.Confidence值高于80; 如果符合以下条件,则应用预处理 - 在倾斜和被降级的扫描样本上测试预处理管道 (
Deskew,DeNoise) - 验证多页PDF生成正确的
result.Pages.Count和每页内容 - 确认
result.SaveAsSearchablePdf()输出在Adobe Acrobat和系统PDF查看器中可搜索 - 针对您的供应商集中文档布局差异测试所有基于区域的提取 (
CropRectangle) - 在没有
Task.Delay调用的情况下运行批处理并验证没有线程问题 - 确认应用程序能够正常启动和运行,且无出站互联网访问
- 验证许可证密钥初始化在每个入口点的第一次
_ocr.Read()调用之前运行 - 检查配置、环境变量或密钥中是否残留任何 明迪 API 密钥引用。
迁移到IronOCR的主要优势
**对财务文档拥有完全的数据主权。**迁移后,供应商的 IBAN 代码、路由号码、客户的 EIN 以及详细的交易明细将始终保留在您的基础架构中。 合规范围仅涵盖您的组织。 与第三方人工智能供应商签订的处理协议将从您的审计跟踪中删除。 遵循 GLBA、CMMC、FedRAMP 或数据驻留要求的团队可以处理财务文件,而无需像外部云传输那样进行合规性审查。
**可预测的成本,无论数量如何。**IronOCRLite许可证在$999涵盖了一个开发人员的无限文档处理。 从每月 500 张发票扩展到每月 50,000 张发票,无需额外费用。 年末处理高峰、批量修复运行和开发测试周期不会增加任何计费计数器。 对于每月处理 5,000 页的团队来说,三年总拥有成本从大约 17,964 美元(Mindee Pro)降至 1,499 美元至 2,999 美元(IronOCR永久许可证)。 有关等级详情,请参阅完整的IronOCR产品页面。
永久拥有您的提取逻辑。IronOCR提取模式是IronOCR在您代码库中的纯 C# 代码。 它们具有版本控制、可审查、可测试和可部署性,不受任何外部供应商的发布计划或 API 版本控制决策的影响。 当Mindee发布InvoiceV4时,即为供应商强加的迁移截止日期。 当您维护提取模式时,改进是git commit。
**针对实际扫描质量的预处理控制。**应付账款部门会收到来自数十家甚至数百家供应商的账单,每家供应商的账单都使用不同的设备和不同的设置进行扫描。 IronOCR的预处理管道—Deskew(), DeNoise(), Contrast(), Sharpen(), Binarize()—解决了每个扫描类别中的特定缺陷。 热敏收据照片的缺陷与平板扫描的多页发票的缺陷不同;您需要针对每种途径应用相应的过滤器。 明迪 的预处理是不可见的,且不可配置。 请参阅预处理功能页面以获取完整的过滤器目录。
一步即可实现可搜索的PDF归档。 通过IronOCR处理的每张发票都可以通过result.SaveAsSearchablePdf()归档为可搜索的PDF。 识别出的文本层嵌入到输出文件中,使得每个单词都可以在文档管理系统、SharePoint 库和Enterprise内容存储库中进行全文搜索。 明迪 完全不提供 PDF 输出; 以前,可搜索的存档需要单独的图书馆、单独的处理流程和额外的维护。 迁移后,提取和归档合并为一个_ocr.Read(input)调用。
无需架构变更即可部署到任何地方。IronOCRIronOCR相同的NuGet包和相同的初始化方法,即可部署在 Windows、Linux、macOS、Docker、Azure 应用服务、AWS Lambda 和 Google Cloud Run 上。 气隙环境、工厂车间系统和政府安全设施均无需修改即可运行。 Docker 部署指南、 Azure 指南和AWS 指南涵盖了每个平台的特定环境配置。
