Github Copilot 實戰(zhàn): 從零開始用AI寫一個OCR工具 (1)
最近AI很火,咱也嘗試一下由淺入深探索一下 Github Copilot 的能力和底限.
環(huán)境是 Windows11 + Microsoft Visual Studio Enterprise 2022 (64 位) - Current 版本 17.13.7 + VS內(nèi)置的 Github Copilot Pro
項目目標(biāo)
本項目旨在利用 Github Copilot 輔助開發(fā),從零實現(xiàn)一個基于 AI 的 OCR(光學(xué)字符識別)工具。項目采用 .NET 9 和 WPF 技術(shù)棧,集成了 PaddleOCR 作為核心識別引擎,實現(xiàn)了圖片文字識別、區(qū)域選擇、結(jié)果高亮與復(fù)制等實用功能。
主要技術(shù)與依賴
? 開發(fā)語言與平臺:C#,.NET 9,WPF
? OCR引擎:Sdcb.OpenVINO.PaddleOCR
? 圖像處理:OpenCvSharp
? 界面交互:WPF,支持拖拽、粘貼、截圖等多種圖片輸入方式
? AI輔助開發(fā):Github Copilot 提供代碼建議與自動補(bǔ)全
核心功能
- 圖片輸入
? 支持文件選擇、拖拽、粘貼、屏幕截圖等多種方式加載圖片。 - OCR識別
? 調(diào)用 PaddleOCR 進(jìn)行文字識別,支持中文、英文等多語種。
? 識別結(jié)果實時顯示,支持區(qū)域高亮和文字復(fù)制。 - 用戶體驗優(yōu)化
? 首次模型下載時,異步回調(diào) UI,友好提示“正在初始化OCR模型,請稍候...”,避免用戶等待時無響應(yīng)。
? 識別過程有進(jìn)度提示,提升交互體驗。 - 結(jié)果交互
? 支持鼠標(biāo)框選圖片區(qū)域,提取并復(fù)制選中區(qū)域的文字。
? 右鍵點擊可復(fù)制單個識別文本。
關(guān)鍵實現(xiàn)思路
? AI驅(qū)動開發(fā):通過 Copilot 自動生成代碼骨架、方法實現(xiàn)和注釋,大幅提升開發(fā)效率。
? 異步與回調(diào):模型下載和識別過程均為異步,UI 通過回調(diào)及時反饋進(jìn)度和狀態(tài)。
? 圖像與坐標(biāo)映射:實現(xiàn)了圖片與控件坐標(biāo)的精準(zhǔn)映射,保證高亮和選區(qū)準(zhǔn)確。
源碼
https://github.com/densen2014/Blazor100/tree/master/AI/MiOcr
首先創(chuàng)建wpf工程
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
<PackageReference Include="Sdcb.OpenVINO.PaddleOCR" Version="0.6.8" />
<PackageReference Include="Sdcb.OpenVINO.PaddleOCR.Models.Online" Version="0.6.2" />
<PackageReference Include="Sdcb.OpenVINO.runtime.win-x64" Version="2025.0.0" />
</ItemGroup>
</Project>
簡單放置一個本地ocr服務(wù)
using OpenCvSharp;
using Sdcb.OpenVINO.PaddleOCR;
using Sdcb.OpenVINO.PaddleOCR.Models;
using Sdcb.OpenVINO.PaddleOCR.Models.Online;
using System.Diagnostics;
using System.Net.Http;
namespace JovenApi;
public class PaddleOCRService
{
public static bool IsUrl(string filename)
{
return Uri.TryCreate(filename, UriKind.Absolute, out var uriResult)
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
}
public async Task<(List<string> strings, PaddleOcrResult result)> StartOCR(string filename)
{
Mat src;
if (string.IsNullOrEmpty(filename))
{
throw new ArgumentNullException(nameof(filename));
}
if (IsUrl(filename))
{
src = Cv2.ImDecode(await new HttpClient().GetByteArrayAsync(filename), ImreadModes.Color);
}
else
{
src = Cv2.ImRead(filename);
}
return await StartOCR(src);
}
public async Task<(List<string> strings, PaddleOcrResult result)> StartOCR(byte[] imageData)
{
Mat src;
ArgumentNullException.ThrowIfNull(imageData);
src = Cv2.ImDecode(imageData, ImreadModes.Color);
return await StartOCR(src);
}
public async Task<(List<string> strings, PaddleOcrResult result)> StartOCR(Mat src)
{
PaddleOcrResult result;
var resultText = new List<string>();
FullOcrModel model = await OnlineFullModels.ChineseV3.DownloadAsync();
using (PaddleOcrAll all = new(model)
{
AllowRotateDetection = true,
Enable180Classification = true,
})
{
Stopwatch sw = Stopwatch.StartNew();
result = all.Run(src);
Console.WriteLine($"elapsed={sw.ElapsedMilliseconds} ms");
Console.WriteLine("Detected all texts: \n" + result.Text);
foreach (PaddleOcrResultRegion region in result.Regions)
{
Console.WriteLine($"Text: {region.Text}, Score: {region.Score}, RectCenter: {region.Rect.Center}, RectSize: {region.Rect.Size}, Angle: {region.Rect.Angle}");
resultText.Add(region.Text);
}
}
src.Dispose();
return (resultText, result);
}
}
A. 提示詞 "實現(xiàn)拖放,粘貼圖片或者點擊打開選擇文件獲取一張圖片, 并且調(diào)用ocr服務(wù),將結(jié)果繪圖到原圖上, 右邊顯示相應(yīng)的文本"
下面是一個完整的 WPF 方案,支持拖放圖片、粘貼圖片、點擊選擇圖片,并調(diào)用 OCR 服務(wù),將識別結(jié)果繪制到原圖上,右側(cè)顯示文本。代碼已適配你的 PaddleOCRService,并假設(shè)其 StartOCR(byte[] image) 返回識別結(jié)果(如文本框列表)。
- MainWindow.xaml(UI布局)
<Window x:Class="MiOcr.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MiOcr" Height="600" Width="900"
AllowDrop="True"
KeyDown="Window_KeyDown"
Drop="Window_Drop"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Margin="10" BorderBrush="Gray" BorderThickness="1" Background="#FFF"
MouseLeftButtonUp="ImageBorder_MouseLeftButtonUp"
AllowDrop="True"
Drop="ImageBorder_Drop"
>
<Image x:Name="MainImage" Stretch="Uniform" />
</Border>
<ScrollViewer Grid.Column="1" Margin="10">
<TextBox x:Name="OcrTextBox" AcceptsReturn="True" VerticalScrollBarVisibility="Auto" TextWrapping="Wrap" FontSize="16"/>
</ScrollViewer>
<Button Content="選擇圖片" Width="100" Height="30" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="20"
Click="SelectImageButton_Click"/>
</Grid>
</Window>
- MainWindow.xaml.cs(核心邏輯)
using JovenApi;
using Microsoft.Win32;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using System.Windows.Media;
using System.Windows.Controls;
using System.Collections.Generic;
namespace MiOcr;
public partial class MainWindow : Window
{
private BitmapImage? _currentImage;
private byte[]? _currentImageBytes;
public MainWindow()
{
InitializeComponent();
this.PreviewDragOver += (s, e) => e.Handled = true; // 允許拖放
this.PreviewKeyDown += Window_KeyDown;
}
// 拖放圖片
private async void Window_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
if (files.Length > 0 && IsImageFile(files[0]))
{
await LoadAndOcrImage(files[0]);
}
}
}
// Border區(qū)域拖放
private async void ImageBorder_Drop(object sender, DragEventArgs e)
{
await Window_Drop(sender, e);
}
// 粘貼圖片
private async void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
{
if (Clipboard.ContainsImage())
{
var img = Clipboard.GetImage();
if (img != null)
{
var bmp = BitmapFromClipboard(img);
using var ms = new MemoryStream();
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bmp));
encoder.Save(ms);
await LoadAndOcrImage(ms.ToArray());
}
}
}
}
// 點擊選擇圖片
private async void SelectImageButton_Click(object sender, RoutedEventArgs e)
{
var dlg = new OpenFileDialog
{
Filter = "圖片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif"
};
if (dlg.ShowDialog() == true)
{
await LoadAndOcrImage(dlg.FileName);
}
}
// 點擊圖片區(qū)域也可選擇圖片
private async void ImageBorder_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
await SelectImageButton_Click(sender, e);
}
// 加載圖片并調(diào)用OCR
private async Task LoadAndOcrImage(string filePath)
{
_currentImageBytes = await File.ReadAllBytesAsync(filePath);
_currentImage = new BitmapImage(new Uri(filePath));
MainImage.Source = _currentImage;
await RunOcrAndDraw(_currentImageBytes);
}
// 加載圖片并調(diào)用OCR(字節(jié)流)
private async Task LoadAndOcrImage(byte[] imageBytes)
{
_currentImageBytes = imageBytes;
using var ms = new MemoryStream(imageBytes);
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.StreamSource = ms;
bmp.EndInit();
bmp.Freeze();
_currentImage = bmp;
MainImage.Source = _currentImage;
await RunOcrAndDraw(imageBytes);
}
// OCR并繪制
private async Task RunOcrAndDraw(byte[] imageBytes)
{
OcrTextBox.Text = "識別中...";
var ocr = new PaddleOCRService();
var results = await ocr.StartOCR(imageBytes);
if (results == null || results.Count == 0)
{
OcrTextBox.Text = "未識別到文本";
return;
}
// 假設(shè)results為List<OcrResult>,每個OcrResult有Text和Box(左上、右下坐標(biāo))
// 你需要根據(jù)PaddleOCRService的返回類型適配
OcrTextBox.Text = string.Join(Environment.NewLine, results.Select(r => r.Text));
// 繪制識別框和文字
var drawed = DrawOcrResultsOnImage(_currentImage, results);
MainImage.Source = drawed;
}
// 判斷文件是否為圖片
private bool IsImageFile(string file)
{
var ext = System.IO.Path.GetExtension(file).ToLower();
return ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp" || ext == ".gif";
}
// 從ClipboardBitmapSource轉(zhuǎn)BitmapSource
private BitmapSource BitmapFromClipboard(BitmapSource src)
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(src));
using var ms = new MemoryStream();
encoder.Save(ms);
ms.Position = 0;
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.StreamSource = ms;
bmp.EndInit();
bmp.Freeze();
return bmp;
}
// 繪制OCR結(jié)果到圖片
private BitmapSource DrawOcrResultsOnImage(BitmapImage? image, List<OcrResult> results)
{
if (image == null) return null!;
int width = image.PixelWidth;
int height = image.PixelHeight;
var visual = new DrawingVisual();
using (var dc = visual.RenderOpen())
{
dc.DrawImage(image, new Rect(0, 0, width, height));
var pen = new Pen(Brushes.Red, 2);
var typeface = new Typeface("Arial");
foreach (var r in results)
{
// 假設(shè) r.Box 是 int[4] 或 Point[],請根據(jù)實際類型適配
var box = r.Box; // 例如: [x1, y1, x2, y2]
dc.DrawRectangle(null, pen, new Rect(box[0], box[1], box[2] - box[0], box[3] - box[1]));
var formattedText = new FormattedText(
r.Text,
System.Globalization.CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
20,
Brushes.Yellow,
1.25);
dc.DrawText(formattedText, new Point(box[0], box[1] - 24));
}
}
var bmp = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
bmp.Render(visual);
bmp.Freeze();
return bmp;
}
}
// 假設(shè)OCR結(jié)果類型如下,實際請根據(jù)PaddleOCRService返回類型調(diào)整
public class OcrResult
{
public string Text { get; set; }
public int[] Box { get; set; } // [x1, y1, x2, y2]
}
說明
? 拖放:支持將圖片文件拖到窗口或圖片區(qū)域。
? 粘貼:Ctrl+V 粘貼剪貼板圖片。
? 點擊選擇:點擊圖片區(qū)域或按鈕彈出文件選擇對話框。
? OCR調(diào)用:調(diào)用 PaddleOCRService.StartOCR(byte[]),并假設(shè)返回 List
? 繪圖:在圖片上繪制識別框和文字,右側(cè)顯示所有識別文本。
? UI:左側(cè)圖片,右側(cè)文本,頂部有“選擇圖片”按鈕。
如 PaddleOCRService 返回類型不同,請補(bǔ)充其定義,我可幫你適配。
如需支持多種圖片格式或更復(fù)雜的坐標(biāo),請補(bǔ)充說明。
到這里,一個基本框架居然就完成了. 運行起來也差不多達(dá)到預(yù)期. 真是驚為天人.
修正一下,提示詞 "返回的 results.result 類型為 PaddleOcrResult"
非常好,PaddleOCRService.StartOCR 返回的 results.result 類型為 PaddleOcrResult。
PaddleOcrResult 結(jié)構(gòu)如下(基于 Sdcb.OpenVINO.PaddleOCR 官方文檔和源碼):
public class PaddleOcrResult
{
public List<PaddleOcrResultRegion> Regions { get; set; }
}
public class PaddleOcrResultRegion
{
public string Text { get; set; }
public float Score { get; set; }
public RotatedRect Rect { get; set; }
// 還有 PolyPoints、Label 等屬性
}
? Rect 是 RotatedRect,包含中心點、尺寸、角度。
? PolyPoints 是多邊形點(通常為4個點,順時針/逆時針),可用于繪制識別框。
你的代碼應(yīng)這樣處理:
-
顯示文本
OcrTextBox.Text = string.Join(Environment.NewLine, results.result.Regions.Select(r => r.Text)); -
繪制識別框和文字
建議用 PolyPoints 繪制多邊形框,Rect.Center 作為文字起點。
private BitmapSource DrawOcrResultsOnImage(BitmapImage? image, PaddleOcrResult result)
{
if (image == null) return null!;
int width = image.PixelWidth;
int height = image.PixelHeight;
var visual = new DrawingVisual();
using (var dc = visual.RenderOpen())
{
dc.DrawImage(image, new Rect(0, 0, width, height));
var pen = new Pen(Brushes.Red, 2);
var typeface = new Typeface("Arial");
foreach (var region in result.Regions)
{
// 繪制多邊形框
if (region.PolyPoints != null && region.PolyPoints.Length >= 4)
{
var points = region.PolyPoints.Select(p => new System.Windows.Point(p.X, p.Y)).ToArray();
var polygon = new System.Windows.Media.PolyLineSegment(points, true);
var figure = new PathFigure(points[0], new[] { polygon }, true);
var geometry = new PathGeometry(new[] { figure });
dc.DrawGeometry(null, pen, geometry);
}
// 繪制文字
var formattedText = new FormattedText(
region.Text,
System.Globalization.CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
20,
Brushes.Yellow,
1.25);
dc.DrawText(formattedText, new System.Windows.Point(region.Rect.Center.X, region.Rect.Center.Y));
}
}
var bmp = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
bmp.Render(visual);
bmp.Freeze();
return bmp;
}
- 調(diào)用方式
var drawed = DrawOcrResultsOnImage(_currentImage, results.result);
MainImage.Source = drawed;
總結(jié)
? results.result 是 PaddleOcrResult,其 Regions 為每個識別區(qū)域。
? 推薦用 PolyPoints 繪制多邊形框,region.Text 作為識別文本。
? 右側(cè)文本框用 string.Join(Environment.NewLine, results.result.Regions.Select(r => r.Text))。
繼續(xù)修正,提示詞 "繪制多邊形框方法有點錯誤, 根據(jù) Console.WriteLine($"Text: {region.Text}, Score: {region.Score}, RectCenter: {region.Rect.Center}, RectSize: {region.Rect.Size}, Angle: {region.Rect.Angle}"); 修正一下"
你當(dāng)前的繪制方法有誤,因為 region.Rect 是 RotatedRect,它不是點集合,而是一個帶有中心點、尺寸和角度的旋轉(zhuǎn)矩形。
正確做法是:使用 region.Rect.GetVertices() 獲取多邊形的四個頂點,然后用這些點繪制多邊形框。
請將 DrawOcrResultsOnImage 方法中的多邊形繪制部分修正如下:
private BitmapSource DrawOcrResultsOnImage(BitmapImage? image, PaddleOcrResult result)
{
if (image == null) return null!;
int width = image.PixelWidth;
int height = image.PixelHeight;
var visual = new DrawingVisual();
using (var dc = visual.RenderOpen())
{
dc.DrawImage(image, new Rect(0, 0, width, height));
var pen = new Pen(Brushes.Red, 2);
var typeface = new Typeface("Arial");
foreach (var region in result.Regions)
{
// 正確獲取四個頂點
var vertices = region.Rect.GetVertices(); // 返回 PointF[],長度為4
var points = vertices.Select(p => new System.Windows.Point(p.X, p.Y)).ToArray();
// 繪制多邊形
var figure = new PathFigure(points[0], new[] { new PolyLineSegment(points.Skip(1), true) }, true);
var geometry = new PathGeometry(new[] { figure });
dc.DrawGeometry(null, pen, geometry);
// 繪制文字(以中心點為準(zhǔn))
var formattedText = new FormattedText(
region.Text,
System.Globalization.CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
20,
Brushes.Yellow,
1.25);
dc.DrawText(formattedText, new System.Windows.Point(region.Rect.Center.X, region.Rect.Center.Y));
}
}
var bmp = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
bmp.Render(visual);
bmp.Freeze();
return bmp;
}
關(guān)鍵修正說明:
? 用 region.Rect.GetVertices() 獲取四個頂點,而不是直接遍歷 Rect。
? 用 PolyLineSegment 和 PathFigure 繪制閉合多邊形。
? 文字仍然以 region.Rect.Center 為基準(zhǔn)繪制。
這個時候還是有錯誤, AI需要繼續(xù)引導(dǎo)一下 , 提示詞 "region.Rect 的定義是 Point2f center, Size2f size, float angle"
感謝補(bǔ)充,region.Rect 是 RotatedRect,其定義為:
public struct RotatedRect
{
public Point2f Center; // 中心點
public Size2f Size; // 寬高
public float Angle; // 旋轉(zhuǎn)角度(度)
}
正確繪制多邊形框的方法是:
? 使用 region.Rect.GetVertices() 獲取四個頂點(Point2f[],順序為左上、右上、右下、左下)。
? 用這些點繪制閉合多邊形。
你的代碼如下即可:
private BitmapSource DrawOcrResultsOnImage(BitmapImage? image, PaddleOcrResult result)
{
if (image == null) return null!;
int width = image.PixelWidth;
int height = image.PixelHeight;
var visual = new DrawingVisual();
using (var dc = visual.RenderOpen())
{
dc.DrawImage(image, new Rect(0, 0, width, height));
var pen = new Pen(Brushes.Red, 2);
var typeface = new Typeface("Arial");
foreach (var region in result.Regions)
{
// 獲取四個頂點
var vertices = region.Rect.GetVertices(); // Point2f[],長度4
var points = vertices.Select(p => new System.Windows.Point(p.X, p.Y)).ToArray();
// 繪制多邊形
var figure = new PathFigure(points[0], new[] { new PolyLineSegment(points.Skip(1), true) }, true);
var geometry = new PathGeometry(new[] { figure });
dc.DrawGeometry(null, pen, geometry);
// 繪制文字(以中心點為準(zhǔn))
var formattedText = new FormattedText(
region.Text,
System.Globalization.CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
20,
Brushes.Yellow,
1.25);
dc.DrawText(formattedText, new System.Windows.Point(region.Rect.Center.X, region.Rect.Center.Y));
}
}
var bmp = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
bmp.Render(visual);
bmp.Freeze();
return bmp;
}
要點說明:
? region.Rect.GetVertices() 是最安全、最準(zhǔn)確的方式獲取旋轉(zhuǎn)矩形的四個頂點。
? 直接用 Center、Size、Angle 計算頂點較為繁瑣,庫已封裝好。
? 文字建議以 region.Rect.Center 為錨點繪制。
這樣即可正確繪制 PaddleOCR 識別區(qū)域的旋轉(zhuǎn)矩形框和文本。
本節(jié)成果

關(guān)聯(lián)項目
FreeSql QQ群:4336577
BA & Blazor QQ群:795206915
Maui Blazor 中文社區(qū) QQ群:645660665
知識共享許可協(xié)議
本作品采用 知識共享署名-非商業(yè)性使用-相同方式共享 4.0 國際許可協(xié)議 進(jìn)行許可。歡迎轉(zhuǎn)載、使用、重新發(fā)布,但務(wù)必保留文章署名AlexChow(包含鏈接: https://github.com/densen2014 ),不得用于商業(yè)目的,基于本文修改后的作品務(wù)必以相同的許可發(fā)布。如有任何疑問,請與我聯(lián)系 。
轉(zhuǎn)載聲明
本文來自博客園,作者:周創(chuàng)琳 AlexChow,轉(zhuǎn)載請注明原文鏈接:http://www.rzrgm.cn/densen2014/p/18897768
AlexChow
今日頭條 | 博客園 | 知乎 | Gitee | GitHub


浙公網(wǎng)安備 33010602011771號