From bfe5119b89e92ed738bdf9cb375b3ef0d95d1a9d Mon Sep 17 00:00:00 2001 From: Tim Lodemann Date: Tue, 28 Jan 2025 21:41:05 +0100 Subject: [PATCH 1/2] Add PaddleOCR-Standalone support --- .../Forms/Ocr/DownloadPaddleOCR.Designer.cs | 78 ++++++++ src/ui/Forms/Ocr/DownloadPaddleOCR.cs | 140 +++++++++++++++ src/ui/Forms/Ocr/DownloadPaddleOCR.resx | 120 +++++++++++++ .../Ocr/DownloadPaddleOCRCPU.Designer.cs | 78 ++++++++ src/ui/Forms/Ocr/DownloadPaddleOCRCPU.cs | 140 +++++++++++++++ src/ui/Forms/Ocr/DownloadPaddleOCRCPU.resx | 120 +++++++++++++ src/ui/Forms/Ocr/VobSubOcr.Designer.cs | 2 +- src/ui/Forms/Ocr/VobSubOcr.cs | 168 ++++++++++++++---- src/ui/Logic/Ocr/PaddleOcr.cs | 13 +- 9 files changed, 827 insertions(+), 32 deletions(-) create mode 100644 src/ui/Forms/Ocr/DownloadPaddleOCR.Designer.cs create mode 100644 src/ui/Forms/Ocr/DownloadPaddleOCR.cs create mode 100644 src/ui/Forms/Ocr/DownloadPaddleOCR.resx create mode 100644 src/ui/Forms/Ocr/DownloadPaddleOCRCPU.Designer.cs create mode 100644 src/ui/Forms/Ocr/DownloadPaddleOCRCPU.cs create mode 100644 src/ui/Forms/Ocr/DownloadPaddleOCRCPU.resx diff --git a/src/ui/Forms/Ocr/DownloadPaddleOCR.Designer.cs b/src/ui/Forms/Ocr/DownloadPaddleOCR.Designer.cs new file mode 100644 index 0000000000..1ef419b4ed --- /dev/null +++ b/src/ui/Forms/Ocr/DownloadPaddleOCR.Designer.cs @@ -0,0 +1,78 @@ +namespace Nikse.SubtitleEdit.Forms.Ocr +{ + sealed partial class DownloadPaddleOCR + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.labelDescription1 = new System.Windows.Forms.Label(); + this.labelPleaseWait = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // labelDescription1 + // + this.labelDescription1.AutoSize = true; + this.labelDescription1.Location = new System.Drawing.Point(21, 27); + this.labelDescription1.Name = "labelDescription1"; + this.labelDescription1.Size = new System.Drawing.Size(145, 13); + this.labelDescription1.TabIndex = 29; + this.labelDescription1.Text = "Downloading PaddleOCR"; + // + // labelPleaseWait + // + this.labelPleaseWait.AutoSize = true; + this.labelPleaseWait.Location = new System.Drawing.Point(21, 59); + this.labelPleaseWait.Name = "labelPleaseWait"; + this.labelPleaseWait.Size = new System.Drawing.Size(70, 13); + this.labelPleaseWait.TabIndex = 28; + this.labelPleaseWait.Text = "Please wait..."; + // + // DownloadPaddleOCR + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(320, 93); + this.Controls.Add(this.labelDescription1); + this.Controls.Add(this.labelPleaseWait); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "DownloadPaddleOCR"; + this.ShowInTaskbar = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Download Tesseract"; + this.Shown += new System.EventHandler(this.DownloadTesseract5_Shown); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label labelDescription1; + private System.Windows.Forms.Label labelPleaseWait; + } +} \ No newline at end of file diff --git a/src/ui/Forms/Ocr/DownloadPaddleOCR.cs b/src/ui/Forms/Ocr/DownloadPaddleOCR.cs new file mode 100644 index 0000000000..efb4c87cd2 --- /dev/null +++ b/src/ui/Forms/Ocr/DownloadPaddleOCR.cs @@ -0,0 +1,140 @@ +using Nikse.SubtitleEdit.Core.Common; +using Nikse.SubtitleEdit.Core.Http; +using Nikse.SubtitleEdit.Logic; +using SevenZipExtractor; +using System; +using System.IO; +using System.Threading; +using System.Windows.Forms; +using MessageBox = Nikse.SubtitleEdit.Forms.SeMsgBox.MessageBox; + +namespace Nikse.SubtitleEdit.Forms.Ocr +{ + public sealed partial class DownloadPaddleOCR : Form + { + public const string DownloadUrl = "https://github.com/timminator/PaddleOCR-Standalone/releases/download/v.1.0.0/PaddleOCR-GPU-v1.0.0.7z"; + private readonly CancellationTokenSource _cancellationTokenSource; + + private string _tempFileName; + + public DownloadPaddleOCR() + { + UiUtil.PreInitialize(this); + InitializeComponent(); + UiUtil.FixFonts(this); + Text = LanguageSettings.Current.GetTesseractDictionaries.Download + " PaddleOCR"; + labelPleaseWait.Text = LanguageSettings.Current.General.PleaseWait; + labelDescription1.Text = LanguageSettings.Current.GetTesseractDictionaries.Download + " PaddleOCR"; + _cancellationTokenSource = new CancellationTokenSource(); + } + + private void DownloadTesseract5_Shown(object sender, EventArgs e) + { + try + { + _tempFileName = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".7z"); + using (var downloadStream = new FileStream(_tempFileName, FileMode.Create, FileAccess.Write)) + using (var httpClient = DownloaderFactory.MakeHttpClient()) + { + var downloadTask = httpClient.DownloadAsync(DownloadUrl, downloadStream, new Progress((progress) => + { + var pct = (int)Math.Round(progress * 100.0, MidpointRounding.AwayFromZero); + labelPleaseWait.Text = LanguageSettings.Current.General.PleaseWait + " " + pct + "%"; + labelPleaseWait.Refresh(); + }), _cancellationTokenSource.Token); + + while (!downloadTask.IsCompleted && !downloadTask.IsCanceled) + { + Application.DoEvents(); + } + + if (downloadTask.IsCanceled) + { + DialogResult = DialogResult.Cancel; + labelPleaseWait.Refresh(); + return; + } + + } + CompleteDownload(_tempFileName); + return; + } + catch (Exception exception) + { + labelPleaseWait.Text = string.Empty; + Cursor = Cursors.Default; + MessageBox.Show($"Unable to download {DownloadUrl}!" + Environment.NewLine + Environment.NewLine + + exception.Message + Environment.NewLine + Environment.NewLine + exception.StackTrace); + DialogResult = DialogResult.Cancel; + } + } + + private void CompleteDownload(string downloadStream) + { + if (downloadStream.Length == 0) + { + throw new Exception("No content downloaded - missing file or no internet connection!" + Environment.NewLine + + $"For more info see: {Path.Combine(Configuration.DataDirectory, "error_log.txt")}"); + } + + var dictionaryFolder = Configuration.PaddleOcrDirectory; + if (!Directory.Exists(dictionaryFolder)) + { + Directory.CreateDirectory(dictionaryFolder); + } + + Extract7Zip(downloadStream, dictionaryFolder, "PaddleOCR-GPU-v1.0.0"); + Cursor = Cursors.Default; + labelPleaseWait.Text = string.Empty; + Cursor = Cursors.Default; + DialogResult = DialogResult.OK; + File.Delete(_tempFileName); + } + + private void Extract7Zip(string tempFileName, string dir, string skipFolderLevel) + { + Text = string.Format(LanguageSettings.Current.Settings.ExtractingX, string.Empty); + labelDescription1.Text = string.Format(LanguageSettings.Current.Settings.ExtractingX, "PaddleOCR"); + labelDescription1.Refresh(); + + using (var archiveFile = new ArchiveFile(tempFileName)) + { + archiveFile.Extract(entry => + { + if (_cancellationTokenSource.IsCancellationRequested) + { + return null; + } + + var entryFullName = entry.FileName; + if (!string.IsNullOrEmpty(skipFolderLevel) && entryFullName.StartsWith(skipFolderLevel)) + { + entryFullName = entryFullName.Substring(skipFolderLevel.Length); + } + + entryFullName = entryFullName.Replace('/', Path.DirectorySeparatorChar); + entryFullName = entryFullName.TrimStart(Path.DirectorySeparatorChar); + + var fullFileName = Path.Combine(dir, entryFullName); + + var fullPath = Path.GetDirectoryName(fullFileName); + if (fullPath == null) + { + return null; + } + + var displayName = entryFullName; + if (displayName.Length > 30) + { + displayName = "..." + displayName.Remove(0, displayName.Length - 26).Trim(); + } + + labelPleaseWait.Text = string.Format(LanguageSettings.Current.Settings.ExtractingX, $"{displayName}"); + labelPleaseWait.Refresh(); + + return fullFileName; + }); + } + } + } +} diff --git a/src/ui/Forms/Ocr/DownloadPaddleOCR.resx b/src/ui/Forms/Ocr/DownloadPaddleOCR.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/src/ui/Forms/Ocr/DownloadPaddleOCR.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/ui/Forms/Ocr/DownloadPaddleOCRCPU.Designer.cs b/src/ui/Forms/Ocr/DownloadPaddleOCRCPU.Designer.cs new file mode 100644 index 0000000000..416bf5e4f7 --- /dev/null +++ b/src/ui/Forms/Ocr/DownloadPaddleOCRCPU.Designer.cs @@ -0,0 +1,78 @@ +namespace Nikse.SubtitleEdit.Forms.Ocr +{ + sealed partial class DownloadPaddleOCRCPU + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.labelDescription1 = new System.Windows.Forms.Label(); + this.labelPleaseWait = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // labelDescription1 + // + this.labelDescription1.AutoSize = true; + this.labelDescription1.Location = new System.Drawing.Point(21, 27); + this.labelDescription1.Name = "labelDescription1"; + this.labelDescription1.Size = new System.Drawing.Size(145, 13); + this.labelDescription1.TabIndex = 29; + this.labelDescription1.Text = "Downloading PaddleOCR"; + // + // labelPleaseWait + // + this.labelPleaseWait.AutoSize = true; + this.labelPleaseWait.Location = new System.Drawing.Point(21, 59); + this.labelPleaseWait.Name = "labelPleaseWait"; + this.labelPleaseWait.Size = new System.Drawing.Size(70, 13); + this.labelPleaseWait.TabIndex = 28; + this.labelPleaseWait.Text = "Please wait..."; + // + // DownloadPaddleOCR + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(320, 93); + this.Controls.Add(this.labelDescription1); + this.Controls.Add(this.labelPleaseWait); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "DownloadPaddleOCR"; + this.ShowInTaskbar = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Download Tesseract"; + this.Shown += new System.EventHandler(this.DownloadTesseract5_Shown); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label labelDescription1; + private System.Windows.Forms.Label labelPleaseWait; + } +} \ No newline at end of file diff --git a/src/ui/Forms/Ocr/DownloadPaddleOCRCPU.cs b/src/ui/Forms/Ocr/DownloadPaddleOCRCPU.cs new file mode 100644 index 0000000000..1ec61dde3c --- /dev/null +++ b/src/ui/Forms/Ocr/DownloadPaddleOCRCPU.cs @@ -0,0 +1,140 @@ +using Nikse.SubtitleEdit.Core.Common; +using Nikse.SubtitleEdit.Core.Http; +using Nikse.SubtitleEdit.Logic; +using SevenZipExtractor; +using System; +using System.IO; +using System.Threading; +using System.Windows.Forms; +using MessageBox = Nikse.SubtitleEdit.Forms.SeMsgBox.MessageBox; + +namespace Nikse.SubtitleEdit.Forms.Ocr +{ + public sealed partial class DownloadPaddleOCRCPU : Form + { + public const string DownloadUrl = "https://github.com/timminator/PaddleOCR-Standalone/releases/download/v.1.0.0/PaddleOCR-CPU-v1.0.0.7z"; + private readonly CancellationTokenSource _cancellationTokenSource; + + private string _tempFileName; + + public DownloadPaddleOCRCPU() + { + UiUtil.PreInitialize(this); + InitializeComponent(); + UiUtil.FixFonts(this); + Text = LanguageSettings.Current.GetTesseractDictionaries.Download + " PaddleOCR (CPU version)"; + labelPleaseWait.Text = LanguageSettings.Current.General.PleaseWait; + labelDescription1.Text = LanguageSettings.Current.GetTesseractDictionaries.Download + " PaddleOCR (CPU version)"; + _cancellationTokenSource = new CancellationTokenSource(); + } + + private void DownloadTesseract5_Shown(object sender, EventArgs e) + { + try + { + _tempFileName = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".7z"); + using (var downloadStream = new FileStream(_tempFileName, FileMode.Create, FileAccess.Write)) + using (var httpClient = DownloaderFactory.MakeHttpClient()) + { + var downloadTask = httpClient.DownloadAsync(DownloadUrl, downloadStream, new Progress((progress) => + { + var pct = (int)Math.Round(progress * 100.0, MidpointRounding.AwayFromZero); + labelPleaseWait.Text = LanguageSettings.Current.General.PleaseWait + " " + pct + "%"; + labelPleaseWait.Refresh(); + }), _cancellationTokenSource.Token); + + while (!downloadTask.IsCompleted && !downloadTask.IsCanceled) + { + Application.DoEvents(); + } + + if (downloadTask.IsCanceled) + { + DialogResult = DialogResult.Cancel; + labelPleaseWait.Refresh(); + return; + } + + } + CompleteDownload(_tempFileName); + return; + } + catch (Exception exception) + { + labelPleaseWait.Text = string.Empty; + Cursor = Cursors.Default; + MessageBox.Show($"Unable to download {DownloadUrl}!" + Environment.NewLine + Environment.NewLine + + exception.Message + Environment.NewLine + Environment.NewLine + exception.StackTrace); + DialogResult = DialogResult.Cancel; + } + } + + private void CompleteDownload(string downloadStream) + { + if (downloadStream.Length == 0) + { + throw new Exception("No content downloaded - missing file or no internet connection!" + Environment.NewLine + + $"For more info see: {Path.Combine(Configuration.DataDirectory, "error_log.txt")}"); + } + + var dictionaryFolder = Configuration.PaddleOcrDirectory; + if (!Directory.Exists(dictionaryFolder)) + { + Directory.CreateDirectory(dictionaryFolder); + } + + Extract7Zip(downloadStream, dictionaryFolder, "PaddleOCR-CPU-v1.0.0"); + Cursor = Cursors.Default; + labelPleaseWait.Text = string.Empty; + Cursor = Cursors.Default; + DialogResult = DialogResult.OK; + File.Delete(_tempFileName); + } + + private void Extract7Zip(string tempFileName, string dir, string skipFolderLevel) + { + Text = string.Format(LanguageSettings.Current.Settings.ExtractingX, string.Empty); + labelDescription1.Text = string.Format(LanguageSettings.Current.Settings.ExtractingX, "PaddleOCR"); + labelDescription1.Refresh(); + + using (var archiveFile = new ArchiveFile(tempFileName)) + { + archiveFile.Extract(entry => + { + if (_cancellationTokenSource.IsCancellationRequested) + { + return null; + } + + var entryFullName = entry.FileName; + if (!string.IsNullOrEmpty(skipFolderLevel) && entryFullName.StartsWith(skipFolderLevel)) + { + entryFullName = entryFullName.Substring(skipFolderLevel.Length); + } + + entryFullName = entryFullName.Replace('/', Path.DirectorySeparatorChar); + entryFullName = entryFullName.TrimStart(Path.DirectorySeparatorChar); + + var fullFileName = Path.Combine(dir, entryFullName); + + var fullPath = Path.GetDirectoryName(fullFileName); + if (fullPath == null) + { + return null; + } + + var displayName = entryFullName; + if (displayName.Length > 30) + { + displayName = "..." + displayName.Remove(0, displayName.Length - 26).Trim(); + } + + labelPleaseWait.Text = string.Format(LanguageSettings.Current.Settings.ExtractingX, $"{displayName}"); + labelPleaseWait.Refresh(); + + return fullFileName; + }); + } + } + } +} diff --git a/src/ui/Forms/Ocr/DownloadPaddleOCRCPU.resx b/src/ui/Forms/Ocr/DownloadPaddleOCRCPU.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/src/ui/Forms/Ocr/DownloadPaddleOCRCPU.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/ui/Forms/Ocr/VobSubOcr.Designer.cs b/src/ui/Forms/Ocr/VobSubOcr.Designer.cs index 922c7995c8..d375263f0a 100644 --- a/src/ui/Forms/Ocr/VobSubOcr.Designer.cs +++ b/src/ui/Forms/Ocr/VobSubOcr.Designer.cs @@ -2343,7 +2343,7 @@ private void InitializeComponent() this.checkBoxPaddleOcrUseGpu.Name = "checkBoxPaddleOcrUseGpu"; this.checkBoxPaddleOcrUseGpu.Size = new System.Drawing.Size(67, 17); this.checkBoxPaddleOcrUseGpu.TabIndex = 2; - this.checkBoxPaddleOcrUseGpu.Text = "Use GPU"; + this.checkBoxPaddleOcrUseGpu.Text = "Use GPU (Only affects GPU version)"; this.checkBoxPaddleOcrUseGpu.UseVisualStyleBackColor = true; // // VobSubOcr diff --git a/src/ui/Forms/Ocr/VobSubOcr.cs b/src/ui/Forms/Ocr/VobSubOcr.cs index 6939a3e224..73e60c000e 100644 --- a/src/ui/Forms/Ocr/VobSubOcr.cs +++ b/src/ui/Forms/Ocr/VobSubOcr.cs @@ -339,6 +339,9 @@ public OcrFix(int index, string oldLine, string newLine) private int _tesseractOcrAutoFixes; private string Tesseract5Version = "5.5.0"; + // Minimum driver version for CUDA 12.3 (PaddleOCR GPU version) + private double requiredDriverVersion = 545.84; + private Subtitle _bdnXmlOriginal; private Subtitle _bdnXmlSubtitle; private XmlDocument _bdnXmlDocument; @@ -4784,21 +4787,7 @@ private void ButtonStartOcrClick(object sender, EventArgs e) } else if (_ocrMethodIndex == _ocrMethodPaddle) { - if (!Directory.Exists(Configuration.PaddleOcrDirectory)) - { - if (MessageBox.Show(string.Format(LanguageSettings.Current.Settings.DownloadX, "Paddle OCR models"), "Subtitle Edit", MessageBoxButtons.YesNoCancel) != DialogResult.Yes) - { - return; - } - - using (var form = new DownloadPaddleOcrModels()) - { - if (form.ShowDialog(this) != DialogResult.OK) - { - return; - } - } - } + buttonDownloadPaddleOCRModels_Click(sender, e); } progressBar1.Maximum = max; @@ -6506,21 +6495,7 @@ private string OcrViaPaddle(Bitmap bitmap, int listViewIndex) var language = (nikseComboBoxPaddleLanguages.SelectedItem as OcrLanguage2)?.Code; string line; - try - { - line = _paddleOcr.Ocr(bitmap, language ?? "en", checkBoxPaddleOcrUseGpu.Checked); - } - catch (Exception exception) - { - MessageBox.Show(exception.Message + Environment.NewLine + Environment.NewLine + - "Make sure you have installed PaddleOCR" + Environment.NewLine + Environment.NewLine + - "Read more here: https://www.paddlepaddle.org.cn/en/install/quick?docurl=/documentation/docs/en/install/pip/windows-pip_en.html" + Environment.NewLine+ Environment.NewLine + - "Requires Python + pip." + Environment.NewLine + - _paddleOcr.Error); - - ButtonPauseClick(null, null); - return string.Empty; - } + line = _paddleOcr.Ocr(bitmap, language ?? "ch", checkBoxPaddleOcrUseGpu.Checked); if (checkBoxAutoFixCommonErrors.Checked && _ocrFixEngine != null) { @@ -7257,6 +7232,71 @@ private void LoadOcrFixEngine(string threeLetterIsoLanguageName, string hunspell } } + private bool IsNvidiaGpuPresentAndCudaCompatible() + { + try + { + // Execute nvidia-smi and capture its output + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "nvidia-smi", + Arguments = "--query-gpu=driver_version --format=csv,noheader", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + return false; // nvidia-smi command failed + } + + string rawOutput = output.Trim(); + + // Compare the version strings + if (String.Compare(rawOutput, requiredDriverVersion.ToString("F2", System.Globalization.CultureInfo.InvariantCulture), StringComparison.Ordinal) >= 0) + { + return true; // Driver version meets or exceeds the required version + } + + return false; + } + catch (Exception) + { + // Handle cases where nvidia-smi is not available or fails + return false; + } + } + + private bool IsExecutableInPath(string executableName) + { + var pathVariable = Environment.GetEnvironmentVariable("PATH"); + + if (string.IsNullOrEmpty(pathVariable)) + return false; + + var paths = pathVariable.Split(Path.PathSeparator); + + foreach (var path in paths) + { + var fullPath = Path.Combine(path, executableName); + if (File.Exists(fullPath)) + { + return true; + } + } + + return false; + } + private void ComboBoxOcrMethodSelectedIndexChanged(object sender, EventArgs e) { _abort = true; @@ -7369,6 +7409,58 @@ private void ComboBoxOcrMethodSelectedIndexChanged(object sender, EventArgs e) { ShowOcrMethodGroupBox(groupBoxPaddle); Configuration.Settings.VobSubOcr.LastOcrMethod = "PaddleOCR"; + if (Configuration.IsRunningOnWindows && !File.Exists(Path.Combine(Configuration.PaddleOcrDirectory, "paddleocr.exe")) && !IsExecutableInPath("paddleocr.exe")) + { + if (IntPtr.Size * 8 == 32) + { + MessageBox.Show("Sorry, PaddleOCR requires a 64-bit processor"); + comboBoxOcrMethod.SelectedIndex = _ocrMethodBinaryImageCompare; + return; + } + else if (!IsNvidiaGpuPresentAndCudaCompatible()) + { + var result = MessageBox.Show( + $"PaddleOCR with GPU is not supported on this system.{Environment.NewLine}" + + $"An NVIDIA graphics card with driver version {requiredDriverVersion.ToString("F2", System.Globalization.CultureInfo.InvariantCulture)} or higher is required.{Environment.NewLine}" + + $"{LanguageSettings.Current.GetTesseractDictionaries.Download} PaddleOCR (CPU version)?", + LanguageSettings.Current.General.Title, + MessageBoxButtons.YesNoCancel); + + if (result == DialogResult.Yes) + { + using (var form = new DownloadPaddleOCRCPU()) + { + if (form.ShowDialog(this) == DialogResult.OK) + { + buttonDownloadPaddleOCRModels_Click(sender, e); + } + } + _ocrFixEngine = null; + SubtitleListView1SelectedIndexChanged(null, null); + return; // Exit after handling CPU download + } + else if (result == DialogResult.No || result == DialogResult.Cancel) + { + comboBoxOcrMethod.SelectedIndex = _ocrMethodBinaryImageCompare; + return; + } + } + else if (MessageBox.Show($"{LanguageSettings.Current.GetTesseractDictionaries.Download} PaddleOCR (CPU and GPU version)?", LanguageSettings.Current.General.Title, MessageBoxButtons.YesNoCancel) == DialogResult.Yes) + { + using (var form = new DownloadPaddleOCR()) + { + if (form.ShowDialog(this) == DialogResult.OK) + { + buttonDownloadPaddleOCRModels_Click(sender, e); + } + } + } + else + { + comboBoxOcrMethod.SelectedIndex = _ocrMethodBinaryImageCompare; + return; + } + } } _ocrFixEngine = null; @@ -8947,6 +9039,22 @@ private void buttonGetTesseractDictionaries_Click(object sender, EventArgs e) } } + private void buttonDownloadPaddleOCRModels_Click(object sender, EventArgs e) + { + if (!Directory.Exists(Path.Combine(Configuration.PaddleOcrDirectory, "det")) || !Directory.Exists(Path.Combine(Configuration.PaddleOcrDirectory, "cls")) ||!Directory.Exists(Path.Combine(Configuration.PaddleOcrDirectory, "rec"))) + { + if (MessageBox.Show(string.Format(LanguageSettings.Current.Settings.DownloadX, "Paddle OCR models"), "Subtitle Edit", MessageBoxButtons.YesNoCancel) != DialogResult.Yes) + { + return; + } + + using (var form = new DownloadPaddleOcrModels()) + { + form.ShowDialog(this); + } + } + } + private void toolStripMenuItemInspectNOcrMatches_Click(object sender, EventArgs e) { if (subtitleListView1.SelectedItems.Count != 1) diff --git a/src/ui/Logic/Ocr/PaddleOcr.cs b/src/ui/Logic/Ocr/PaddleOcr.cs index c99210b224..b7e3a505e8 100644 --- a/src/ui/Logic/Ocr/PaddleOcr.cs +++ b/src/ui/Logic/Ocr/PaddleOcr.cs @@ -159,11 +159,22 @@ public string Ocr(Bitmap bitmap, string language, bool useGpu) var tempImage = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".png"); borderedBitmap.Save(tempImage, System.Drawing.Imaging.ImageFormat.Png); var parameters = $"--image_dir \"{tempImage}\" --ocr_version PP-OCRv4 --use_angle_cls true --use_gpu {useGpu.ToString().ToLowerInvariant()} --lang {language} --show_log false --det_model_dir \"{_detPath}\\{detFilePrefix}\" --rec_model_dir \"{_recPath}\\{recFilePrefix}\" --cls_model_dir \"{_clsPath}\\ch_ppocr_mobile_v2.0_cls_infer\""; + string PaddleOCRPath = null; + + if (File.Exists(Path.Combine(Configuration.PaddleOcrDirectory, "paddleocr.exe"))) + { + PaddleOCRPath = Path.GetFullPath(Path.Combine(Configuration.PaddleOcrDirectory, "paddleocr.exe")); + } + else + { + PaddleOCRPath = "paddleocr.exe"; + } + var process = new Process { StartInfo = new ProcessStartInfo { - FileName = "paddleocr", + FileName = PaddleOCRPath, Arguments = parameters, UseShellExecute = false, RedirectStandardOutput = true, From de61328f379f24d4ce8e7bef855c6f820b86eccb Mon Sep 17 00:00:00 2001 From: Tim Lodemann Date: Sat, 1 Feb 2025 20:12:10 +0100 Subject: [PATCH 2/2] Add Batch mode support and a few minor changes --- src/ui/Forms/Ocr/VobSubOcr.cs | 552 ++++++++++++++++++++++++++++++++-- src/ui/Logic/Ocr/PaddleOcr.cs | 223 ++++++++++++-- 2 files changed, 724 insertions(+), 51 deletions(-) diff --git a/src/ui/Forms/Ocr/VobSubOcr.cs b/src/ui/Forms/Ocr/VobSubOcr.cs index 73e60c000e..3a04faf60c 100644 --- a/src/ui/Forms/Ocr/VobSubOcr.cs +++ b/src/ui/Forms/Ocr/VobSubOcr.cs @@ -340,7 +340,13 @@ public OcrFix(int index, string oldLine, string newLine) private string Tesseract5Version = "5.5.0"; // Minimum driver version for CUDA 12.3 (PaddleOCR GPU version) - private double requiredDriverVersion = 545.84; + private static readonly string requiredDriverVersion = "545.84"; + + // Last PaddleOCR version that does not support batch mode + private static readonly string lastPaddleOcrVersionWithoutBatchMode = "2.9.1"; + + // Minimum PaddleOCR version with PP-OCRv4 model support + private static readonly string requiredPaddleOcrVersionForPPOCRv4 = "2.7.0"; private Subtitle _bdnXmlOriginal; private Subtitle _bdnXmlSubtitle; @@ -1028,6 +1034,58 @@ private void DoBatch() } textBoxCurrentText.TextChanged -= TextBoxCurrentTextTextChanged; + + bool hasPaddleBatchSupport = HasPaddleBatchSupport(); + + // Collect all remaining bitmaps for PaddleOCR batch processing + List bitmaps = null; + List paddleResults = null; + + if (_ocrMethodIndex == _ocrMethodPaddle && hasPaddleBatchSupport) + { + for (int i = 0; i < max; i++) + { + bitmaps.Add(GetSubtitleBitmap(i)); + } + + // Outside of background worker before OCRViaPaddleBatch! + if (_ocrFixEngine == null) + { + comboBoxDictionaries_SelectedIndexChanged(null, null); + } + + ManualResetEvent resetEvent = new ManualResetEvent(false); + IEnumerable results = null; + + BackgroundWorker worker = new BackgroundWorker(); + worker.DoWork += (sender, e) => + { + e.Result = OcrViaPaddleBatch(bitmaps, progress => + { + if (ProgressCallback != null) + { + var percent = (int)Math.Round(progress * 100.0 / max); + ProgressCallback?.Invoke($"{percent}%"); + } + }, () => _abort); + }; + + worker.RunWorkerCompleted += (sender, e) => + { + results = e.Result as IEnumerable; + resetEvent.Set(); + }; + + worker.RunWorkerAsync(); + + while (!resetEvent.WaitOne(100)) + { + Application.DoEvents(); + } + + paddleResults = ProcessResultsFromPaddleBatch(results, bitmaps, Enumerable.Range(0, max).ToList()); + }; + for (int i = 0; i < max; i++) { _selectedIndex = i; @@ -1043,7 +1101,7 @@ private void DoBatch() ProgressCallback?.Invoke($"{percent}%"); } - string text; + string text = string.Empty; if (_ocrMethodIndex == _ocrMethodNocr) { text = OcrViaNOCR(GetSubtitleBitmap(i), i); @@ -1052,7 +1110,11 @@ private void DoBatch() { text = OcrViaCloudVision(GetSubtitleBitmap(i), i); } - else if (_ocrMethodIndex == _ocrMethodPaddle) + else if (_ocrMethodIndex == _ocrMethodPaddle && hasPaddleBatchSupport) + { + text = paddleResults[i]; + } + else if (_ocrMethodIndex == _ocrMethodPaddle && !hasPaddleBatchSupport) { text = OcrViaPaddle(GetSubtitleBitmap(i), i); } @@ -5253,6 +5315,186 @@ private bool MainLoop(int max, int i) return false; } + private bool MainLoopPaddleBatch(int max, int i, List selectedIndices = null) + { + if (selectedIndices == null && i >= max) + { + SetButtonsEnabledAfterOcrDone(); + _mainOcrRunning = false; + return true; + } + + // Collect the bitmaps to process + List bitmaps = new List(); + if (selectedIndices != null) + { + foreach (int index in selectedIndices) + { + bitmaps.Add(ShowSubtitleImage(index)); + } + } + else + { + for (int index = i; index < max; index++) + { + bitmaps.Add(ShowSubtitleImage(index)); + } + } + + labelStatus.Text = $"Starting PaddleOCR... This can take a while..."; + labelStatus.Refresh(); + + // Outside of background worker before OCRViaPaddleBatch! + if (_ocrFixEngine == null) + { + comboBoxDictionaries_SelectedIndexChanged(null, null); + } + + ManualResetEvent resetEvent = new ManualResetEvent(false); + IEnumerable results = null; + + BackgroundWorker worker = new BackgroundWorker(); + worker.DoWork += (sender, e) => + { + e.Result = OcrViaPaddleBatch(bitmaps, progress => + { + labelStatus.Invoke(new Action(() => + { + labelStatus.Text = $"Step 1: Performing OCR on image {progress} of {bitmaps.Count}..."; + progressBar1.Maximum = bitmaps.Count; + progressBar1.Value = progress; + + if (ProgressCallback != null) + { + var percent = (int)Math.Round(progress * 100.0 / bitmaps.Count); + ProgressCallback?.Invoke($"{percent}%"); + } + + labelStatus.Refresh(); + progressBar1.Refresh(); + })); + }, () => _abort); + }; + + worker.RunWorkerCompleted += (sender, e) => + { + results = e.Result as IEnumerable; + resetEvent.Set(); + }; + + worker.RunWorkerAsync(); + + while (!resetEvent.WaitOne(100)) + { + Application.DoEvents(); + } + + List paddleResults = ProcessResultsFromPaddleBatch(results, bitmaps, selectedIndices ?? Enumerable.Range(i, max - i).ToList()); + + int totalCount = selectedIndices != null ? selectedIndices.Count : max - i; + + // Process each subtitle entry, ensuring index mapping is correct + for (int idx = 0; idx < totalCount && idx < paddleResults.Count; idx++) + { + int originalIndex = selectedIndices != null ? selectedIndices[idx] : i + idx; + + var bmp = ShowSubtitleImage(originalIndex); + GetSubtitleTime(originalIndex, out var startTime, out var endTime); + + labelStatus.Text = $"{idx + 1} / {totalCount}: {startTime} - {endTime}"; + progressBar1.Value = idx + 1; + + if (ProgressCallback != null) + { + var percent = (int)Math.Round((idx + 1) * 100.0 / totalCount); + ProgressCallback?.Invoke($"{percent}%"); + } + + labelStatus.Refresh(); + progressBar1.Refresh(); + + _mainOcrBitmap = bmp; + + int j = originalIndex; + subtitleListView1.Items[originalIndex].Selected = true; + subtitleListView1.Items[originalIndex].Focused = true; + if (j < max - 1) + { + j++; + } + if (j < max - 1) + { + j++; + } + + if (originalIndex % 3 == 0) + { + subtitleListView1.Items[j].EnsureVisible(); + } + + string text = paddleResults[idx]; // Get batch OCR result for this subtitle + + _lastLine = text; + + text = text.Replace("-", "-") + .Replace("a", "a") + .Replace(".", ".") + .Replace(",", ",") + .Replace(" ", " ") + .Trim(); + + text = text.Replace(" " + Environment.NewLine, Environment.NewLine) + .Replace(Environment.NewLine + " ", Environment.NewLine); + + // Max allow 2 lines + if (_autoBreakLines && Utilities.GetNumberOfLines(text) > 2) + { + text = text.Replace(" " + Environment.NewLine, Environment.NewLine) + .Replace(Environment.NewLine + " ", Environment.NewLine) + .RemoveRecursiveLineBreaks(); + + if (Utilities.GetNumberOfLines(text) > 2) + { + text = Utilities.AutoBreakLine(text); + } + } + + // Handle DVB subtitles color + if (_dvbSubtitles != null && _transportStreamUseColor) + { + if (_dvbSubColor[originalIndex] != Color.Transparent) + { + text = "" + text + ""; + } + } + + text = text.Trim() + .Replace(" ", " ") + .Replace(Environment.NewLine + Environment.NewLine, Environment.NewLine) + .Replace(" ", " ") + .Replace(Environment.NewLine + Environment.NewLine, Environment.NewLine); + + text = SetTopAlign(originalIndex, text); + + Paragraph p = _subtitle.GetParagraphOrDefault(originalIndex); + if (p != null) + { + p.Text = text; + } + + if (subtitleListView1.SelectedItems.Count == 1 && subtitleListView1.SelectedItems[0].Index == originalIndex) + { + textBoxCurrentText.Text = text; + } + else + { + subtitleListView1.SetText(originalIndex, text); + } + } + + return true; + } + private string SetTopAlign(int i, string text) { if (_captureTopAlign && _captureTopAlignHeight > 0) @@ -5289,10 +5531,67 @@ private bool MainLoopTesseract(int max, int i) return false; } + private bool HasPaddleBatchSupport() + { + if (File.Exists(Path.Combine(Configuration.PaddleOcrDirectory, "paddleocr.exe"))) + { + return true; // Standalone version supports batch processing + } + + try + { + using (var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "python", + Arguments = "-m pip show paddleocr", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }) + { + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + return false; + } + + foreach (var line in output.Split('\n')) + { + if (line.StartsWith("Version:", StringComparison.OrdinalIgnoreCase)) + { + string installedVersion = line.Split(':')[1].Trim(); + return String.Compare(installedVersion, lastPaddleOcrVersionWithoutBatchMode, StringComparison.Ordinal) > 0; + } + } + + return false; + } + } + catch (Exception) + { + return false; + } + } + private void mainOcrTimer_Tick(object sender, EventArgs e) { _mainOcrTimer.Stop(); - bool done = _ocrMethodIndex == _ocrMethodTesseract5 || _ocrMethodIndex == _ocrMethodTesseract302 ? MainLoopTesseract(_mainOcrTimerMax, _mainOcrIndex) : MainLoop(_mainOcrTimerMax, _mainOcrIndex); + + bool done = _ocrMethodIndex == _ocrMethodTesseract5 || _ocrMethodIndex == _ocrMethodTesseract302 + ? MainLoopTesseract(_mainOcrTimerMax, _mainOcrIndex) + : _ocrMethodIndex == _ocrMethodPaddle + ? (HasPaddleBatchSupport() + ? MainLoopPaddleBatch(_mainOcrTimerMax, _mainOcrIndex, _mainOcrSelectedIndices) + : MainLoop(_mainOcrTimerMax, _mainOcrIndex)) + : MainLoop(_mainOcrTimerMax, _mainOcrIndex); + if (done || _abort) { SetButtonsEnabledAfterOcrDone(); @@ -6484,7 +6783,7 @@ private string OcrViaCloudVision(Bitmap bitmap, int listViewIndex) return line; } - + private string OcrViaPaddle(Bitmap bitmap, int listViewIndex) { if (_ocrFixEngine == null) @@ -6495,7 +6794,7 @@ private string OcrViaPaddle(Bitmap bitmap, int listViewIndex) var language = (nikseComboBoxPaddleLanguages.SelectedItem as OcrLanguage2)?.Code; string line; - line = _paddleOcr.Ocr(bitmap, language ?? "ch", checkBoxPaddleOcrUseGpu.Checked); + line = _paddleOcr.Ocr(bitmap, language ?? "en", checkBoxPaddleOcrUseGpu.Checked); if (checkBoxAutoFixCommonErrors.Checked && _ocrFixEngine != null) { @@ -6565,6 +6864,98 @@ private string OcrViaPaddle(Bitmap bitmap, int listViewIndex) return line; } + private List OcrViaPaddleBatch(List bitmaps, Action progressCallback, Func abortCheck) + { + var language = (nikseComboBoxPaddleLanguages.SelectedItem as OcrLanguage2)?.Code ?? "en"; + + var results = _paddleOcr.OcrBatch(bitmaps, language, checkBoxPaddleOcrUseGpu.Checked, progressCallback, abortCheck); + + return results.Select(r => r.Item2).ToList(); + } + + private List ProcessResultsFromPaddleBatch(IEnumerable results, List bitmaps, List indices) + { + var finalResults = new List(); + int index = 0; + + foreach (var text in results) + { + int subtitleIndex = indices[index]; // Map to correct subtitle index + string processedText = text; + + if (checkBoxAutoFixCommonErrors.Checked && _ocrFixEngine != null) + { + var lastLastLine = GetLastLastText(subtitleIndex); + processedText = _ocrFixEngine.FixOcrErrorsViaHardcodedRules(processedText, _lastLine, lastLastLine, null); + } + + if (checkBoxRightToLeft.Checked) + { + processedText = ReverseNumberStrings(processedText); + } + + string textWithoutFixes = processedText; + + if (_ocrFixEngine != null && _ocrFixEngine.IsDictionaryLoaded) + { + var autoGuessLevel = OcrFixEngine.AutoGuessLevel.None; + if (checkBoxGuessUnknownWords.Checked) + { + autoGuessLevel = OcrFixEngine.AutoGuessLevel.Aggressive; + } + + if (checkBoxAutoFixCommonErrors.Checked) + { + var lastLastLine = GetLastLastText(subtitleIndex); + processedText = _ocrFixEngine.FixOcrErrors(processedText, _subtitle, subtitleIndex, _lastLine, lastLastLine, true, autoGuessLevel); + } + + int wordsNotFound = _ocrFixEngine.CountUnknownWordsViaDictionary(processedText, out var correctWords); + + if (wordsNotFound > 0 || correctWords == 0 || textWithoutFixes != null) + { + _ocrFixEngine.AutoGuessesUsed.Clear(); + _ocrFixEngine.UnknownWordsFound.Clear(); + processedText = _ocrFixEngine.FixUnknownWordsViaGuessOrPrompt( + out wordsNotFound, processedText, subtitleIndex, bitmaps[index], + checkBoxAutoFixCommonErrors.Checked, checkBoxPromptForUnknownWords.Checked, + true, autoGuessLevel); + } + + if (_ocrFixEngine.Abort) + { + ButtonPauseClick(null, null); + _ocrFixEngine.Abort = false; + return finalResults; // Return partial results if aborted + } + + // Log used word guesses + foreach (var guess in _ocrFixEngine.AutoGuessesUsed) + { + listBoxLogSuggestions.Items.Add(guess); + } + + _ocrFixEngine.AutoGuessesUsed.Clear(); + LogUnknownWords(); + + // Correctly color subtitle line + ColorLineByNumberOfUnknownWords(subtitleIndex, wordsNotFound, processedText); + } + + if (textWithoutFixes.Trim() != processedText.Trim()) + { + _tesseractOcrAutoFixes++; + labelFixesMade.Text = $" - {_tesseractOcrAutoFixes}"; + LogOcrFix(subtitleIndex, textWithoutFixes, processedText); + } + + finalResults.Add(processedText); + index++; + } + + return finalResults; + } + private void InitializeNOcrForBatch(string db) { _ocrMethodIndex = _ocrMethodNocr; @@ -7236,8 +7627,7 @@ private bool IsNvidiaGpuPresentAndCudaCompatible() { try { - // Execute nvidia-smi and capture its output - var process = new Process + using (var process = new Process { StartInfo = new ProcessStartInfo { @@ -7248,30 +7638,23 @@ private bool IsNvidiaGpuPresentAndCudaCompatible() UseShellExecute = false, CreateNoWindow = true, } - }; - - process.Start(); - string output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - - if (process.ExitCode != 0) + }) { - return false; // nvidia-smi command failed - } + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); - string rawOutput = output.Trim(); + if (process.ExitCode != 0) + { + return false; + } - // Compare the version strings - if (String.Compare(rawOutput, requiredDriverVersion.ToString("F2", System.Globalization.CultureInfo.InvariantCulture), StringComparison.Ordinal) >= 0) - { - return true; // Driver version meets or exceeds the required version + string rawOutput = output.Trim(); + return String.Compare(rawOutput, requiredDriverVersion, StringComparison.Ordinal) >= 0; } - - return false; } catch (Exception) { - // Handle cases where nvidia-smi is not available or fails return false; } } @@ -7297,6 +7680,55 @@ private bool IsExecutableInPath(string executableName) return false; } + private bool HasPaddlePPOCRv4Support() + { + if (File.Exists(Path.Combine(Configuration.PaddleOcrDirectory, "paddleocr.exe"))) + { + return true; // Standalone version supports batch processing + } + + try + { + using (var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "python", + Arguments = "-m pip show paddleocr", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }) + { + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + return false; + } + + foreach (var line in output.Split('\n')) + { + if (line.StartsWith("Version:", StringComparison.OrdinalIgnoreCase)) + { + string installedVersion = line.Split(':')[1].Trim(); + return String.Compare(installedVersion, requiredPaddleOcrVersionForPPOCRv4, StringComparison.Ordinal) > 0; + } + } + + return false; + } + } + catch (Exception) + { + return false; + } + } + private void ComboBoxOcrMethodSelectedIndexChanged(object sender, EventArgs e) { _abort = true; @@ -7409,6 +7841,70 @@ private void ComboBoxOcrMethodSelectedIndexChanged(object sender, EventArgs e) { ShowOcrMethodGroupBox(groupBoxPaddle); Configuration.Settings.VobSubOcr.LastOcrMethod = "PaddleOCR"; + + // Check if installed PaddleOCR version supports PP-OCRv4 + if (Configuration.IsRunningOnWindows && !File.Exists(Path.Combine(Configuration.PaddleOcrDirectory, "paddleocr.exe")) && IsExecutableInPath("paddleocr.exe")) + { + if(!HasPaddlePPOCRv4Support()) + { + var firstresult = MessageBox.Show( + $"The installed PaddleOCR version is to old!{Environment.NewLine}" + + $"PaddleOCR version {requiredPaddleOcrVersionForPPOCRv4} or higher is required.{Environment.NewLine}" + + $"Update PaddleOCR or download the Standalone PaddleOCR version.{Environment.NewLine}" + + $"{LanguageSettings.Current.GetTesseractDictionaries.Download} PaddleOCR?", + LanguageSettings.Current.General.Title, + MessageBoxButtons.YesNoCancel); + + if (firstresult == DialogResult.Yes) + { + if (!IsNvidiaGpuPresentAndCudaCompatible()) + { + var secondresult = MessageBox.Show( + $"PaddleOCR with GPU is not supported on this system.{Environment.NewLine}" + + $"An NVIDIA graphics card with driver version {requiredDriverVersion} or higher is required.{Environment.NewLine}" + + $"{LanguageSettings.Current.GetTesseractDictionaries.Download} PaddleOCR (CPU version)?", + LanguageSettings.Current.General.Title, + MessageBoxButtons.YesNoCancel); + + if (secondresult == DialogResult.Yes) + { + using (var form = new DownloadPaddleOCRCPU()) + { + if (form.ShowDialog(this) == DialogResult.OK) + { + buttonDownloadPaddleOCRModels_Click(sender, e); + } + } + _ocrFixEngine = null; + SubtitleListView1SelectedIndexChanged(null, null); + return; + } + else if (secondresult == DialogResult.No || secondresult == DialogResult.Cancel) + { + comboBoxOcrMethod.SelectedIndex = _ocrMethodBinaryImageCompare; + return; + } + } + using (var form = new DownloadPaddleOCR()) + { + if (form.ShowDialog(this) == DialogResult.OK) + { + buttonDownloadPaddleOCRModels_Click(sender, e); + } + } + _ocrFixEngine = null; + SubtitleListView1SelectedIndexChanged(null, null); + return; + } + else if (firstresult == DialogResult.No || firstresult == DialogResult.Cancel) + { + comboBoxOcrMethod.SelectedIndex = _ocrMethodBinaryImageCompare; + return; + } + } + } + + // If no version is installed prompt for download if (Configuration.IsRunningOnWindows && !File.Exists(Path.Combine(Configuration.PaddleOcrDirectory, "paddleocr.exe")) && !IsExecutableInPath("paddleocr.exe")) { if (IntPtr.Size * 8 == 32) @@ -7421,7 +7917,7 @@ private void ComboBoxOcrMethodSelectedIndexChanged(object sender, EventArgs e) { var result = MessageBox.Show( $"PaddleOCR with GPU is not supported on this system.{Environment.NewLine}" + - $"An NVIDIA graphics card with driver version {requiredDriverVersion.ToString("F2", System.Globalization.CultureInfo.InvariantCulture)} or higher is required.{Environment.NewLine}" + + $"An NVIDIA graphics card with driver version {requiredDriverVersion} or higher is required.{Environment.NewLine}" + $"{LanguageSettings.Current.GetTesseractDictionaries.Download} PaddleOCR (CPU version)?", LanguageSettings.Current.General.Title, MessageBoxButtons.YesNoCancel); @@ -7437,7 +7933,7 @@ private void ComboBoxOcrMethodSelectedIndexChanged(object sender, EventArgs e) } _ocrFixEngine = null; SubtitleListView1SelectedIndexChanged(null, null); - return; // Exit after handling CPU download + return; } else if (result == DialogResult.No || result == DialogResult.Cancel) { @@ -7445,7 +7941,7 @@ private void ComboBoxOcrMethodSelectedIndexChanged(object sender, EventArgs e) return; } } - else if (MessageBox.Show($"{LanguageSettings.Current.GetTesseractDictionaries.Download} PaddleOCR (CPU and GPU version)?", LanguageSettings.Current.General.Title, MessageBoxButtons.YesNoCancel) == DialogResult.Yes) + else if (MessageBox.Show($"{LanguageSettings.Current.GetTesseractDictionaries.Download} PaddleOCR?", LanguageSettings.Current.General.Title, MessageBoxButtons.YesNoCancel) == DialogResult.Yes) { using (var form = new DownloadPaddleOCR()) { diff --git a/src/ui/Logic/Ocr/PaddleOcr.cs b/src/ui/Logic/Ocr/PaddleOcr.cs index b7e3a505e8..c00f85f67a 100644 --- a/src/ui/Logic/Ocr/PaddleOcr.cs +++ b/src/ui/Logic/Ocr/PaddleOcr.cs @@ -170,7 +170,7 @@ public string Ocr(Bitmap bitmap, string language, bool useGpu) PaddleOCRPath = "paddleocr.exe"; } - var process = new Process + using (var process = new Process { StartInfo = new ProcessStartInfo { @@ -181,40 +181,217 @@ public string Ocr(Bitmap bitmap, string language, bool useGpu) RedirectStandardError = true, CreateNoWindow = true, }, - }; - - process.StartInfo.StandardOutputEncoding = Encoding.UTF8; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8"; - process.StartInfo.EnvironmentVariables["PYTHONUTF8"] = "1"; - process.OutputDataReceived += OutputHandler; - _textDetectionResults.Clear(); + }) + { + process.StartInfo.StandardOutputEncoding = Encoding.UTF8; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8"; + process.StartInfo.EnvironmentVariables["PYTHONUTF8"] = "1"; + process.OutputDataReceived += OutputHandler; + _textDetectionResults.Clear(); #pragma warning disable CA1416 // Validate platform compatibility - process.Start(); + process.Start(); #pragma warning restore CA1416 // Validate platform compatibility; - process.BeginOutputReadLine(); + process.BeginOutputReadLine(); + + process.WaitForExit(); + + borderedBitmap.Dispose(); - process.WaitForExit(); + if (process.ExitCode != 0) + { + Error = process.StandardError.ReadToEnd(); + return string.Empty; + } - borderedBitmap.Dispose(); + File.Delete(tempImage); - if (process.ExitCode != 0) - { - Error = process.StandardError.ReadToEnd(); - return string.Empty; + if (_textDetectionResults.Count == 0) + { + return string.Empty; + } + + var result = MakeResult(_textDetectionResults); + return result; } + } - File.Delete(tempImage); + public List<(string filePath, string text)> OcrBatch(List bitmaps, string language, bool useGpu, Action progressCallback, Func abortCheck) + { + var detFilePrefix = language; + if (language != "en" && language != "ch") + { + detFilePrefix = $"ml{Path.DirectorySeparatorChar}Multilingual_PP-OCRv3_det_infer"; + } + else if (language == "ch") + { + detFilePrefix = $"{language}{Path.DirectorySeparatorChar}{language}_PP-OCRv4_det_infer"; + } + else + { + detFilePrefix = $"{language}{Path.DirectorySeparatorChar}{language}_PP-OCRv3_det_infer"; + } - if (_textDetectionResults.Count == 0) + var recFilePrefix = language; + if (LatinLanguageCodes.Contains(language)) + { + recFilePrefix = $"latin{Path.DirectorySeparatorChar}latin_PP-OCRv3_rec_infer"; + } + else if (ArabicLanguageCodes.Contains(language)) { - return string.Empty; + recFilePrefix = $"arabic{Path.DirectorySeparatorChar}arabic_PP-OCRv4_rec_infer"; + } + else if (CyrillicLanguageCodes.Contains(language)) + { + recFilePrefix = $"cyrillic{Path.DirectorySeparatorChar}cyrillic_PP-OCRv3_rec_infer"; + } + else if (DevanagariLanguageCodes.Contains(language)) + { + recFilePrefix = $"devanagari{Path.DirectorySeparatorChar}devanagari_PP-OCRv4_rec_infer"; + } + else if (language == "chinese_cht") + { + recFilePrefix = $"{language}{Path.DirectorySeparatorChar}{language}_PP-OCRv3_rec_infer"; + } + else + { + recFilePrefix = $"{language}{Path.DirectorySeparatorChar}{language}_PP-OCRv4_rec_infer"; } - var result = MakeResult(_textDetectionResults); - return result; + var tempFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempFolder); + + try + { + + var imagePaths = new List(); + int width = bitmaps.Count.ToString().Length; // Determine the number of digits for padding + + for (int i = 0; i < bitmaps.Count; i++) + { + var borderedBitmapTemp = AddBorder(bitmaps[i], 10, Color.Black); + var borderedBitmap = AddBorder(borderedBitmapTemp, 10, Color.Transparent); + borderedBitmapTemp.Dispose(); + + var imagePath = Path.Combine(tempFolder, string.Format("{0:D" + width + "}.png", i)); borderedBitmap.Save(imagePath, System.Drawing.Imaging.ImageFormat.Png); + borderedBitmap.Dispose(); + imagePaths.Add(imagePath); + } + + var parameters = $"--image_dir \"{tempFolder}\" --ocr_version PP-OCRv4 --use_angle_cls true --use_gpu {useGpu.ToString().ToLowerInvariant()} --lang {language} --show_log false --det_model_dir \"{_detPath}\\{detFilePrefix}\" --rec_model_dir \"{_recPath}\\{recFilePrefix}\" --cls_model_dir \"{_clsPath}\\ch_ppocr_mobile_v2.0_cls_infer\""; + + string PaddleOCRPath = null; + + if (File.Exists(Path.Combine(Configuration.PaddleOcrDirectory, "paddleocr.exe"))) + { + PaddleOCRPath = Path.GetFullPath(Path.Combine(Configuration.PaddleOcrDirectory, "paddleocr.exe")); + } + else + { + PaddleOCRPath = "paddleocr.exe"; + } + + using (var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = PaddleOCRPath, + Arguments = parameters, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + } + }) + { + process.StartInfo.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8"; + process.StartInfo.EnvironmentVariables["PYTHONUTF8"] = "1"; + + var results = new Dictionary(); + int currentProgress = 0; + + process.OutputDataReceived += (sendingProcess, outLine) => + { + if (string.IsNullOrWhiteSpace(outLine.Data)) return; + + if (outLine.Data.Contains("ppocr INFO: **********")) + { + currentProgress++; + progressCallback?.Invoke(currentProgress); + + var fileName = outLine.Data.Split(new[] { "ppocr INFO: **********" }, StringSplitOptions.None)[1].Trim(); + + if (!results.ContainsKey(fileName)) + { + results[fileName] = string.Empty; + } + } + else if (outLine.Data.Contains("ppocr WARNING: No text found in image")) + { + return; + } + else if (outLine.Data.Contains("ppocr INFO:")) + { + if (results.Count > 0) + { + var lastFile = results.Keys.Last(); + + // Regex pattern to extract the text inside quotes + string textPattern = @"['""].*['""]"; + var textMatch = Regex.Match(outLine.Data, textPattern); + + if (textMatch.Success) + { + string extractedText = textMatch.Value.Trim('\'', '"'); + results[lastFile] += extractedText + Environment.NewLine; + } + } + } + + if (abortCheck != null && abortCheck.Invoke()) + { + try + { + process.CancelOutputRead(); + process.Kill(); // Terminate PaddleOCR process + process.WaitForExit(1000); + } + catch (Exception ex) + { + Error = $"Error terminating PaddleOCR: {ex.Message}"; + } + } + }; + +#pragma warning disable CA1416 // Validate platform compatibility + process.Start(); +#pragma warning restore CA1416 // Validate platform compatibility; + + process.BeginOutputReadLine(); + process.WaitForExit(); + + return results + .Select(r => (filePath: r.Key, text: r.Value)) + .ToList(); + } + } + finally + { + try + { + if (Directory.Exists(tempFolder)) + { + Directory.Delete(tempFolder, true); + } + } + catch (Exception ex) + { + Error = $"An unexpected error occurred during cleanup: {ex.Message}"; + } + } } public static Bitmap AddBorder(Bitmap originalBitmap, int borderWidth, Color borderColor) @@ -324,7 +501,7 @@ private void OutputHandler(object sendingProcess, DataReceivedEventArgs outLine) // Example: [[[92.0, 56.0], [735.0, 60.0], [734.0, 118.0], [91.0, 113.0]], ('My mommy always said', 0.9907816052436829)] } - + public static List GetLanguages() { return new List