Files
ctbjrj/成套报价软件/扒图/QianfanOcrClient.cs
T

279 lines
12 KiB
C#

// ============================================================
// QianfanOcrClient:百度千帆 PaddleOCR-VL-0.9B(版面解析)
// · POST https://qianfan.baidubce.com/v2/ocr/paddleocr
// Authorization: Bearer {QianfanApiKey}(千帆 ModelBuilder 的 API Key,永久有效)
// {"model":"paddleocr-vl-0.9b","file":"<纯base64>","fileType":1}
// · 响应 result.layoutParsingResults[0].prunedResult.parsing_res_list[]
// 每块 {block_label, block_content, block_bbox[x1,y1,x2,y2](像素)}
// → 文本块转 OcrItem;表格块逐行拆(与 GLM 处理一致)
// · Key 存 baidu_ocr.json 的 qianfan_api_key(OCR设置窗口写)
// ============================================================
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
namespace 成套报价软件
{
/// <summary>百度千帆 PaddleOCR-VL 客户端(单实例)。</summary>
internal sealed class QianfanOcrClient
{
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
private string _apiKey;
private const string BaseUrl = "https://qianfan.baidubce.com/v2/ocr/paddleocr";
/// <summary>状态提示,接 OcrClient.StatusChanged。</summary>
public event Action<string> StatusChanged;
public QianfanOcrClient()
{
try
{
ServicePointManager.SecurityProtocol =
ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
}
catch { }
}
public bool IsConfigured
{
get { return !string.IsNullOrWhiteSpace(_apiKey); }
}
/// <summary>Key 由 OcrClient 从 baidu_ocr.json 的 qianfan_api_key 注入(热更新)。</summary>
public void SetApiKey(string key)
{
_apiKey = key == null ? "" : key.Trim();
}
/// <summary>识别一块位图 → 文本块列表(带 bbox 像素坐标)。</summary>
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
{
if (crop == null || crop.Width <= 0 || crop.Height <= 0)
return Task.FromResult(new List<OcrItem>());
if (!IsConfigured)
{
Notify("千帆 PaddleOCR-VL 未配置 API Key(OCR 设置窗口里填千帆 API Key)");
var tcs0 = new TaskCompletionSource<List<OcrItem>>();
tcs0.SetException(new Exception(
"千帆 PaddleOCR-VL 未配置 API Key。\r\n" +
"请到百度智能云千帆平台(ModelBuilder)创建 API Key," +
"在 OCR 设置窗口选择引擎\"百度 PaddleOCR-VL\"并填入后保存。"));
return tcs0.Task;
}
return Task.Factory.StartNew(() =>
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var result = RequestAndParse(crop, timeoutMs);
Notify("PaddleOCR-VL·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms");
return result;
});
}
private List<OcrItem> RequestAndParse(Bitmap crop, int timeoutMs)
{
byte[] png;
using (var ms = new MemoryStream())
{
crop.Save(ms, ImageFormat.Png);
png = ms.ToArray();
}
// 千帆要求:base64 直传(无 data URI 前缀),fileType=1 图像
string body = _json.Serialize(new Dictionary<string, object>
{
{ "model", "paddleocr-vl-0.9b" },
{ "file", Convert.ToBase64String(png) },
{ "fileType", 1 }
});
var req = (HttpWebRequest)WebRequest.Create(BaseUrl);
req.Method = "POST";
req.ContentType = "application/json";
req.Headers.Add("Authorization", "Bearer " + _apiKey);
req.Timeout = timeoutMs;
req.ReadWriteTimeout = timeoutMs;
req.Proxy = null;
var bytes = Encoding.UTF8.GetBytes(body);
req.ContentLength = bytes.Length;
using (var rs = req.GetRequestStream())
{
rs.Write(bytes, 0, bytes.Length);
}
string respText;
try
{
using (var resp = (HttpWebResponse)req.GetResponse())
using (var sr = new StreamReader(resp.GetResponseStream(), new UTF8Encoding(false)))
{
respText = sr.ReadToEnd();
}
}
catch (WebException wex)
{
string detail = "";
try
{
if (wex.Response != null)
using (var sr = new StreamReader(wex.Response.GetResponseStream(), new UTF8Encoding(false)))
detail = sr.ReadToEnd();
}
catch { }
string hint = wex.Response == null
? ("网络不可达: " + wex.Message)
: ("HTTP " + (int)((HttpWebResponse)wex.Response).StatusCode + " " + detail);
throw new Exception("PaddleOCR-VL 请求失败: " + hint);
}
return ParseResponse(respText, crop.Width, crop.Height);
}
// ---- 响应解析(行拆分/清理逻辑与 GLM 一致,复用其 shared 方法) ----
private List<OcrItem> ParseResponse(string respText, int cropW, int cropH)
{
var msg = _json.Deserialize<Dictionary<string, object>>(respText);
if (msg == null) throw new Exception("千帆响应解析失败(空响应)");
if (msg.ContainsKey("error"))
{
var err = msg["error"] as Dictionary<string, object>;
string code = err != null && err.ContainsKey("code") ? Convert.ToString(err["code"]) : "?";
string message = err != null && err.ContainsKey("message") ? Convert.ToString(err["message"]) : respText;
throw new Exception("PaddleOCR-VL 错误 [" + code + "]: " + message);
}
var items = new List<OcrItem>();
// result.layoutParsingResults[0].prunedResult.parsing_res_list
var result = msg.ContainsKey("result") ? msg["result"] as Dictionary<string, object> : null;
var layouts = result != null && result.ContainsKey("layoutParsingResults")
? result["layoutParsingResults"] as System.Collections.ArrayList : null;
if (layouts == null || layouts.Count == 0)
throw new Exception("PaddleOCR-VL 响应没有 layoutParsingResults(内容: " +
(respText.Length > 300 ? respText.Substring(0, 300) : respText) + ")");
var first = layouts[0] as Dictionary<string, object>;
var pruned = first != null && first.ContainsKey("prunedResult")
? first["prunedResult"] as Dictionary<string, object> : null;
var blocks = pruned != null && pruned.ContainsKey("parsing_res_list")
? pruned["parsing_res_list"] as System.Collections.ArrayList : null;
if (blocks == null) return items;
foreach (Dictionary<string, object> b in blocks)
{
string label = b.ContainsKey("block_label") ? Convert.ToString(b["block_label"]) : "";
string content = b.ContainsKey("block_content") ? Convert.ToString(b["block_content"]) : "";
if (string.IsNullOrWhiteSpace(content)) continue;
var bbox = b.ContainsKey("block_bbox") ? b["block_bbox"] as System.Collections.ArrayList : null;
if (bbox == null || bbox.Count < 4) continue;
// block_bbox 像素坐标 [x1,y1,x2,y2];裁剪到图内
float x1 = Math.Max(0, Math.Min(cropW, Convert.ToSingle(bbox[0])));
float y1 = Math.Max(0, Math.Min(cropH, Convert.ToSingle(bbox[1])));
float x2 = Math.Max(0, Math.Min(cropW, Convert.ToSingle(bbox[2])));
float y2 = Math.Max(0, Math.Min(cropH, Convert.ToSingle(bbox[3])));
if (x2 - x1 < 2 || y2 - y1 < 2) continue;
if (label == "table")
{
AddTableItems(items, content, x1, y1, x2, y2);
continue;
}
if (label != "text" && label != "title" && label != "paragraph")
continue; // 图片/公式/图表块跳过(title 也是文字,收)
content = GlmOcrClient.CleanHtmlShared(content);
if (string.IsNullOrWhiteSpace(content)) continue;
string[] lines = content.Split(new[] { '\r', '\n' },
StringSplitOptions.RemoveEmptyEntries);
float lineH = (y2 - y1) / lines.Length;
float pad = Math.Min(lineH * 0.15f, 3f);
for (int i = 0; i < lines.Length; i++)
{
string text = GlmOcrClient.CleanMarkdownShared(lines[i]).Trim();
if (text.Length == 0) continue;
float ty1 = y1 + lineH * i + pad;
float ty2 = y1 + lineH * (i + 1) - pad;
if (ty2 - ty1 < 2) { ty1 = y1 + lineH * i; ty2 = ty1 + Math.Max(2, lineH); }
items.Add(new OcrItem
{
Text = text,
Box = new[]
{
new PointF(x1, ty1),
new PointF(x2, ty1),
new PointF(x2, ty2),
new PointF(x1, ty2)
},
Score = 1f
});
}
}
return items;
}
private static readonly System.Text.RegularExpressions.Regex _trRegex =
new System.Text.RegularExpressions.Regex("<tr[^>]*>(.*?)</tr>",
System.Text.RegularExpressions.RegexOptions.Singleline |
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
System.Text.RegularExpressions.RegexOptions.Compiled);
private static readonly System.Text.RegularExpressions.Regex _tdRegex =
new System.Text.RegularExpressions.Regex("<t[dh][^>]*>(.*?)</t[dh]>",
System.Text.RegularExpressions.RegexOptions.Singleline |
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
System.Text.RegularExpressions.RegexOptions.Compiled);
/// <summary>表格块 → 每 tr 一条 item(td 内容空格拼接),行高均分 bbox。</summary>
private static void AddTableItems(List<OcrItem> items, string tableHtml,
float x1, float y1, float x2, float y2)
{
var rows = new List<string>();
foreach (System.Text.RegularExpressions.Match tr in _trRegex.Matches(tableHtml))
{
var cells = new List<string>();
foreach (System.Text.RegularExpressions.Match td in _tdRegex.Matches(tr.Groups[1].Value))
{
string cell = GlmOcrClient.CleanHtmlShared(td.Groups[1].Value).Trim();
if (cell.Length > 0) cells.Add(cell);
}
if (cells.Count > 0) rows.Add(string.Join(" ", cells.ToArray()));
}
if (rows.Count == 0) return;
float rowH = (y2 - y1) / rows.Count;
float pad = Math.Min(rowH * 0.15f, 3f);
for (int i = 0; i < rows.Count; i++)
{
float ty1 = y1 + rowH * i + pad;
float ty2 = y1 + rowH * (i + 1) - pad;
if (ty2 - ty1 < 2) { ty1 = y1 + rowH * i; ty2 = ty1 + Math.Max(2, rowH); }
items.Add(new OcrItem
{
Text = rows[i],
Box = new[]
{
new PointF(x1, ty1),
new PointF(x2, ty1),
new PointF(x2, ty2),
new PointF(x1, ty2)
},
Score = 1f
});
}
}
private void Notify(string s)
{
var h = StatusChanged;
if (h != null) h(s);
}
}
}