Skip to content

Commit 134e17c

Browse files
committed
refactor(Avalonia): revamp OCR model download progress
It is now more transparent and more human.
1 parent 0d8978a commit 134e17c

3 files changed

Lines changed: 75 additions & 111 deletions

File tree

SnapX.Avalonia/Views/OCR.axaml.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using FluentAvalonia.UI.Controls;
1313
using FluentAvalonia.UI.Windowing;
1414
using SixLabors.ImageSharp;
15+
using SixLabors.ImageSharp.Formats.Webp;
1516
using SixLabors.ImageSharp.PixelFormats;
1617
using SixLabors.ImageSharp.Processing;
1718
using SnapX.Avalonia.ViewModels;
@@ -134,7 +135,11 @@ private async Task RunOCRAsync(string languageCode, CancellationToken cts = defa
134135

135136
// Resize the image to match the Canvas exactly otherwise text boxes aren't where they should be.
136137
response.AnnotatedImage.Mutate(x => x.Resize((int)TextOverlayCanvas.Width, (int)TextOverlayCanvas.Height, KnownResamplers.Bicubic));
137-
await response.AnnotatedImage.SaveAsWebpAsync(ms, cts).ConfigureAwait(true);
138+
await response.AnnotatedImage.SaveAsWebpAsync(ms, new WebpEncoder()
139+
{
140+
Quality = 85
141+
},
142+
cts).ConfigureAwait(true);
138143

139144
ms.Seek(0, SeekOrigin.Begin);
140145
var bitmap = new Bitmap(ms);

SnapX.Core/Job/TaskHelpers.cs

Lines changed: 68 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: GPL-3.0-or-later
22

33

4+
using System.Collections.Concurrent;
45
using System.Diagnostics;
56
using System.Diagnostics.CodeAnalysis;
67
using System.Reflection;
@@ -992,126 +993,109 @@ private static OcrResponse MapToResponse(OcrResult result, Image visualImage)
992993
#endif
993994

994995
public static async Task<OcrResponse> OCRImageDetailed(
995-
Image? image = null,
996-
string? filePath = null,
997-
TaskSettings? taskSettings = null,
998-
string? languageCode = null,
999-
IProgress<OCRProgress>? progress = null,
1000-
CancellationToken cts = default
1001-
)
1002-
996+
Image? image = null,
997+
string? filePath = null,
998+
TaskSettings? taskSettings = null,
999+
string? languageCode = null,
1000+
IProgress<OCRProgress>? progress = null,
1001+
CancellationToken cts = default
1002+
)
10031003
{
10041004
#if DISABLE_OCR
1005-
DebugHelper.WriteException(
1006-
1007-
new Exception("This build of SnapX was built with DISABLE_OCR build time constant.")
1008-
1009-
);
1010-
1011-
return new OcrResponse { FullText = "OCR Disabled in this build." };
1012-
1005+
DebugHelper.WriteException(new Exception("This build of SnapX was built with DISABLE_OCR build time constant."));
1006+
return new OcrResponse { FullText = "OCR Disabled in this build." };
10131007
#else
1014-
1015-
progress?.Report(
1016-
new OCRProgress(
1017-
10,
1018-
$"Loading model for {languageCode}... (This may take awhile, I MAY download model files from internet)"
1019-
)
1020-
);
1021-
1022-
await Task.Yield();
1023-
1024-
progress?.Report(new OCRProgress(40, "Running OCR inference..."));
1025-
1026-
await Task.Yield();
1027-
1008+
progress?.Report(new(5, "Initializing OCR engine..."));
10281009

10291010
var imageConfig = Configuration.Default;
1030-
10311011
imageConfig.ImageFormatsManager.SetEncoder(AVIFFormat.Instance, AVIFEncoder.Instance);
1032-
10331012
imageConfig.ImageFormatsManager.SetDecoder(AVIFFormat.Instance, AVIFDecoder.Instance);
1034-
1035-
imageConfig.ImageFormatsManager.AddImageFormatDetector(
1036-
new PatchedAVIFImageFormatDetector()
1037-
);
1038-
1013+
imageConfig.ImageFormatsManager.AddImageFormatDetector(new PatchedAVIFImageFormatDetector());
10391014

10401015
try
1041-
10421016
{
10431017
if (filePath is not null && image is null)
1044-
10451018
{
1046-
progress?.Report(new OCRProgress(45, "Reading image from disk..."));
1047-
1048-
await Task.Yield();
1049-
1019+
progress?.Report(new(10, "Reading image from disk..."));
10501020
image = await Image.LoadAsync(filePath, cts);
10511021
}
10521022
}
1053-
10541023
catch (Exception ex)
1055-
10561024
{
10571025
var issue = "Failed to load image for OCR";
1058-
10591026
DebugHelper.Logger?.Warning(issue);
1060-
10611027
DebugHelper.WriteException(ex);
1062-
1063-
return new OcrResponse
1064-
{
1065-
FullText = issue + Environment.NewLine + ex.Message
1066-
};
1028+
return new OcrResponse { FullText = $"{issue}{Environment.NewLine}{ex.Message}" };
10671029
}
10681030

1069-
if (image is null)
1070-
1071-
return new OcrResponse
1072-
{
1073-
FullText = "SNAPX ERROR: PASSED NULL IMAGE AND NULL FILEPATH."
1074-
};
1031+
if (image is null) return new OcrResponse { FullText = "SNAPX ERROR: PASSED NULL IMAGE AND NULL FILEPATH." };
10751032

1076-
progress?.Report(new OCRProgress(55, "Preprocessing image for Paddle..."));
1033+
var modelDir = Path.Combine(BaseDirectory.CacheHome, SnapX.AppName, "PaddleOCRModels");
1034+
var model = await GetModelForLanguage(languageCode ?? "eng");
1035+
var ocrEngine = new RapidOcr();
10771036

1078-
await Task.Yield();
1037+
var progressValue = 15;
1038+
var fileUrls = new ConcurrentDictionary<string, int>();
1039+
var fileProgressValues = new ConcurrentDictionary<string, int>();
10791040

1080-
DebugHelper.WriteLine(filePath);
1041+
EventHandler<HttpProgressEventArgs> progressHandler = async (sender, args) =>
1042+
{
1043+
var url = args.UserState as string;
1044+
if (string.IsNullOrEmpty(url)) return;
1045+
if (!url.Contains("PP-OCR", StringComparison.OrdinalIgnoreCase) &&
1046+
!url.Contains("ppocr", StringComparison.OrdinalIgnoreCase)) return;
10811047

1048+
fileUrls.TryAdd(url, fileUrls.Count);
10821049

1083-
progress?.Report(new OCRProgress(65, "Initializing Paddle Inference device..."));
1050+
var fileIndex = fileUrls[url];
1051+
var fileCount = Math.Max(1, fileUrls.Count);
1052+
var perFileRange = 70 / fileCount;
10841053

1085-
await Task.Yield();
1054+
var fileStartPct = 15 + (fileIndex * perFileRange);
1055+
var fileEndPct = fileStartPct + perFileRange;
10861056

1087-
DebugHelper.WriteLine($"OCR image bytes: ??");
1057+
var currentFileProgress = fileStartPct + (int)((args.ProgressPercentage / 100.0) * (fileEndPct - fileStartPct));
1058+
var lastFileProgress = fileProgressValues.GetOrAdd(url, 0);
10881059

1089-
progress?.Report(new OCRProgress(75, "Performing Text Detection & Recognition..."));
1060+
var uri = new Uri(url);
1061+
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
1062+
var filename = query["filename"] ?? query["name"] ?? query["response-content-disposition"];
10901063

1091-
await Task.Yield();
1064+
if (string.IsNullOrEmpty(filename)) filename = Path.GetFileName(uri.LocalPath);
1065+
if (!string.IsNullOrEmpty(filename) && filename.Contains("filename="))
1066+
filename = filename.Split("filename=").Last().Trim('"');
10921067

1093-
var ocrEngine = new RapidOcr();
1094-
var modelDir = Path.Combine(
1068+
filename ??= "model.onnx";
10951069

1096-
BaseDirectory.CacheHome,
1070+
var displayProgress = Math.Max(progressValue, currentFileProgress);
1071+
progress?.Report(new(displayProgress, $"Downloading {filename} ({args.ProgressPercentage}%)..."));
10971072

1098-
SnapX.AppName,
1073+
if (currentFileProgress > lastFileProgress)
1074+
{
1075+
fileProgressValues[url] = currentFileProgress;
1076+
if (currentFileProgress > progressValue) progressValue = currentFileProgress;
1077+
}
1078+
await Task.Yield();
1079+
};
10991080

1100-
"PaddleOCRModels"
1081+
if (HttpClientFactory._ph != null) HttpClientFactory._ph.HttpReceiveProgress += progressHandler;
11011082

1102-
);
1103-
var model = await GetModelForLanguage(languageCode ?? "eng");
1083+
try
1084+
{
1085+
progress?.Report(new(15, "Checking and loading OCR models..."));
1086+
await ocrEngine.LoadModelAsync(model, modelDir, HttpClientFactory.Get(), ct: cts);
1087+
}
1088+
finally
1089+
{
1090+
if (HttpClientFactory._ph != null) HttpClientFactory._ph.HttpReceiveProgress -= progressHandler;
1091+
}
11041092

1105-
await ocrEngine.LoadModelAsync(model, modelDir, HttpClientFactory.Get(), ct: cts);
1093+
progress?.Report(new(85, "Preprocessing image for Paddle..."));
11061094
var originalLongSide = Math.Max(image.Width, image.Height);
11071095
var targetLimit = 1504;
1096+
var finalResize = originalLongSide < targetLimit ? (int)(Math.Ceiling(originalLongSide / 32.0) * 32) : targetLimit;
11081097

1109-
// This logic ensures we don't upscale tiny images,
1110-
// but we still snap the result to a multiple of 32.
1111-
var finalResize = originalLongSide < targetLimit
1112-
? (int)(Math.Ceiling(originalLongSide / 32.0) * 32)
1113-
: targetLimit;
1114-
1098+
progress?.Report(new(90, "Performing Text Detection & Recognition..."));
11151099
var ocrResult = ocrEngine.Detect(image, new RapidOcrOptions
11161100
{
11171101
BoxScoreThresh = 0.5f,
@@ -1120,31 +1104,17 @@ public static async Task<OcrResponse> OCRImageDetailed(
11201104
MostAngle = true,
11211105
UnClipRatio = 1.6f,
11221106
ImgResize = finalResize,
1123-
Padding = 10, // Reduced to keep text from getting lost in margins
1107+
Padding = 10,
11241108
});
1125-
var visualDebugImage = image.Clone(_ => { });
1126-
11271109

1110+
var visualDebugImage = image.Clone(_ => { });
11281111
foreach (var block in ocrResult.TextBlocks)
1129-
11301112
{
1131-
DebugHelper.WriteLine(block.ToString());
1132-
1133-
var points = block.BoxPoints;
1134-
1135-
1136-
visualDebugImage.Mutate(ctx => { ctx.DrawPolygon(Color.Red, 4f, points); });
1113+
visualDebugImage.Mutate(ctx => { ctx.DrawPolygon(Color.Red, 4f, block.BoxPoints); });
11371114
}
11381115

1139-
1140-
progress?.Report(new OCRProgress(95, "Finalizing results..."));
1141-
1142-
await Task.Yield();
1143-
1144-
DebugHelper.WriteAlways("Detected all texts: \n" + ocrResult.StrRes);
1145-
1116+
progress?.Report(new(98, "Finalizing results..."));
11461117
return MapToResponse(ocrResult, visualDebugImage);
1147-
11481118
#endif
11491119
}
11501120

SnapX.Core/SnapX.cs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -486,18 +486,7 @@ public static void InitSentry(Telemetry telemetry)
486486
options.Environment = "production";
487487
options.CreateHttpMessageHandler = () => HttpClientFactory.Handler;
488488
#endif
489-
options.ConfigureClient = client =>
490-
{
491-
var snapXHttpClient = HttpClientFactory.Get();
492-
493-
foreach (var header in snapXHttpClient.DefaultRequestHeaders)
494-
{
495-
client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value);
496-
}
497-
498-
client.DefaultRequestVersion = snapXHttpClient.DefaultRequestVersion;
499-
client.DefaultVersionPolicy = snapXHttpClient.DefaultVersionPolicy;
500-
};
489+
options.ConfigureClient = HttpClientFactory.ConfigureClient;
501490
// VLCException includes multiple paths with username
502491
// For full transparency, I discovered this issue on my computer.
503492
// No other users are effected to my knowledge.

0 commit comments

Comments
 (0)