Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
60c2298
Initial plan
Copilot Oct 12, 2025
2e9ba98
Add priority rectangle OCR feature - data models and translations
Copilot Oct 12, 2025
48d077c
Fix PriorityRectFilter to use IServiceProvider for IOcrModule resolution
Copilot Oct 12, 2025
84a3cb0
Add documentation for Priority Rectangle OCR feature
Copilot Oct 12, 2025
0b701d7
Add configuration examples for Priority Rectangle OCR feature
Copilot Oct 12, 2025
c7efcd3
Add implementation summary for Priority Rectangle OCR feature
Copilot Oct 12, 2025
fc5d66f
Refactor: Implement priority rectangles inside OCR modules
Copilot Oct 12, 2025
84582fc
Update documentation to reflect architectural changes
Copilot Oct 12, 2025
3ccfd86
実装整理
Freeesia Oct 26, 2025
bb3085e
Fix scale, rotation, and crop timing in priority rectangle OCR
Copilot Oct 26, 2025
67222d3
Merge branch 'master' into copilot/add-text-recognition-rectangle-fea…
Freeesia Jan 3, 2026
583fdae
コード整理
Freeesia Jan 3, 2026
10514bd
Merge remote-tracking branch 'origin/master' into freeesia-effective-…
Freeesia Aug 5, 2026
213a3a4
優先矩形OCRの実装を整理してUIを統合
Freeesia Aug 5, 2026
89d4f91
矩形選択ウィンドウのコメントの文字化けを修正
Freeesia Aug 5, 2026
507d3e7
優先矩形で認識した領域のテキストが消える問題を修正
Freeesia Aug 5, 2026
e4c7da6
優先矩形の内外の文字が結合された結果が重複表示される問題を修正
Freeesia Aug 5, 2026
6222a62
矩形選択時に少し広めに囲むよう案内を追加
Freeesia Aug 5, 2026
42957db
LLM/Gemini OCRプラグインに優先矩形機能を統合
Freeesia Aug 8, 2026
e972c66
指定範囲のみOCRするよう変更
Freeesia Aug 11, 2026
6d4ee7b
Merge pull request #667 from Freeesia/freeesia-effective-carnival
Freeesia Aug 11, 2026
52cbbaf
PriorityRectResources.cs削除・D&Dによる順序変更に変更
Copilot Aug 11, 2026
53d2aed
指定範囲OCRのレビュー指摘を修正
Freeesia Aug 11, 2026
8a96f2f
指定範囲OCRの重複判定と検証を改善
Freeesia Aug 11, 2026
bdb0df7
未承認の画像形式ガードを削除
Freeesia Aug 11, 2026
7d9564c
指定範囲OCRの座標処理と不要実装を整理
Freeesia Aug 11, 2026
8ed551c
優先矩形の重複仕様と座標精度を整理
Freeesia Aug 14, 2026
ea0a7c4
リソーステストを改行コードに依存させない
Freeesia Aug 14, 2026
b1c68fe
OCR対象範囲の処理をキャプチャー単位に統合
Freeesia Aug 21, 2026
d5e8cf8
PriorityRectsEditor のUI調整・リソース文言整理
Freeesia Aug 23, 2026
bc26da9
選択範囲の表示と多言語リソースを追加
Freeesia Aug 23, 2026
11d9798
Merge remote-tracking branch 'origin/master' into copilot/add-text-re…
Freeesia Aug 23, 2026
ff94431
OCR対象範囲の処理責務を整理
Freeesia Aug 23, 2026
ae721a9
OCR前処理をユーティリティへ集約
Freeesia Aug 23, 2026
967cef4
拡大後の画像サイズ計算を共通化
Freeesia Aug 24, 2026
701a650
OCR範囲認識の共通メソッドを統合
Freeesia Aug 25, 2026
7b90e3e
ビルドエラー修正
Freeesia Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PackageVersion Include="DeepL.net" Version="1.15.0" />
<PackageVersion Include="Emoji.Wpf" Version="0.3.4" />
<PackageVersion Include="GitHub.Copilot.SDK" Version="1.0.7" />
<PackageVersion Include="gong-wpf-dragdrop" Version="4.0.0" />
<PackageVersion Include="Google.Apis.Auth" Version="1.76.0" />
<PackageVersion Include="Google.Apis.Script.v1" Version="1.68.0.3294" />
<PackageVersion Include="Google.Cloud.Storage.V1" Version="4.15.0" />
Expand Down
8 changes: 6 additions & 2 deletions Plugins/WindowTranslator.Plugin.GoogleAIPlugin/GoogleAIOcr.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ 4. 座標値は画像ごとに0~1000に正規化してください。
systemInstruction: system);
}

public async ValueTask<IEnumerable<TextRect>> RecognizeAsync(SoftwareBitmap bitmap)
public ValueTask<IReadOnlyList<TextRect>> RecognizeAsync(OcrCaptureInput input)
=> OcrUtility.RecognizeRegionsAsync(input, (bitmap, _) => RecognizeRegionAsync(bitmap));

private async ValueTask<IReadOnlyList<TextRect>> RecognizeRegionAsync(SoftwareBitmap bitmap)
{
var base64 = await bitmap.EncodeToJpegBase64().ConfigureAwait(false);
var req = new GenerateContentRequest();
Expand All @@ -71,7 +74,8 @@ public async ValueTask<IEnumerable<TextRect>> RecognizeAsync(SoftwareBitmap bitm
var widthPx = xMaxPx - xMinPx;
var heightPx = yMaxPx - yMinPx;
return new TextRect(rect.Text, xMinPx, yMinPx, widthPx, heightPx, heightPx, false);
});
})
.ToArray();
}

private record Rect(int[] Box2d, string Text);
Expand Down
11 changes: 7 additions & 4 deletions Plugins/WindowTranslator.Plugin.LLMPlugin/LLMOcr.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ public LLMOcr(IOptionsSnapshot<LanguageOptions> langOptions, IOptionsSnapshot<LL
{
var options = llmOptions.Value;
this.logger = logger;

if (string.IsNullOrEmpty(options.ApiKey) || string.IsNullOrEmpty(options.Model))
{
throw new AppUserException("LLM機能が初期化されていません。設定ダイアログからLLMオプションを設定してください");
Expand Down Expand Up @@ -105,7 +104,10 @@ 6. 数字のみのテキストは認識しないでください。
clientOptions);
}

public async ValueTask<IEnumerable<TextRect>> RecognizeAsync(SoftwareBitmap bitmap)
public ValueTask<IReadOnlyList<TextRect>> RecognizeAsync(OcrCaptureInput input)
=> OcrUtility.RecognizeRegionsAsync(input, (bitmap, _) => RecognizeRegionAsync(bitmap));

private async ValueTask<IReadOnlyList<TextRect>> RecognizeRegionAsync(SoftwareBitmap bitmap)
{
var bytes = await bitmap.EncodeToJpegBytes().ConfigureAwait(false);
var image = BinaryData.FromBytes(bytes);
Expand Down Expand Up @@ -147,7 +149,8 @@ public async ValueTask<IEnumerable<TextRect>> RecognizeAsync(SoftwareBitmap bitm
var widthPx = xMaxPx - xMinPx;
var heightPx = yMaxPx - yMinPx;
return new TextRect(rect.Text, xMinPx, yMinPx, widthPx, heightPx, heightPx, false);
});
})
.ToArray();
}
catch (Exception ex)
{
Expand All @@ -158,4 +161,4 @@ public async ValueTask<IEnumerable<TextRect>> RecognizeAsync(SoftwareBitmap bitm

private record Rect([property: JsonPropertyName("box_2d")] int[] Box2d, string Text);
private record RecognizedTexts(Rect[] Texts);
}
}
54 changes: 19 additions & 35 deletions Plugins/WindowTranslator.Plugin.OneOcrPlugin/OneOcr.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,37 +128,31 @@ public void Dispose()
this.fastText?.Dispose();
}

public async ValueTask<IEnumerable<TextRect>> RecognizeAsync(SoftwareBitmap bitmap)
{
// リサイズ処理(scale != 1.0 の場合は新しいビットマップを生成)
var workingBitmap = await bitmap.ResizeSoftwareBitmapAsync(this.scale);
public ValueTask<IReadOnlyList<TextRect>> RecognizeAsync(OcrCaptureInput input)
=> OcrUtility.RecognizeRegionsAsync(
input,
RecognizeRegionAsync,
this.scale,
this.brightness,
this.contrast);

// 明るさ・コントラスト調整(インプレース)
// scale == 1.0 の場合はリサイズで元のビットマップが返るため、コピーを作成してから調整
if (this.brightness != 0 || this.contrast != 0)
{
if (workingBitmap == bitmap)
{
// 元のビットマップを変更しないようにコピーを作成
#pragma warning disable CA1416 // プラットフォームの互換性を検証
workingBitmap = SoftwareBitmap.Copy(bitmap);
#pragma warning restore CA1416 // プラットフォームの互換性を検証
}
workingBitmap.AdjustBrightnessContrastInPlace(this.brightness, this.contrast);
}
/// <summary>
/// 指定した画像のテキストを認識する
/// </summary>
/// <param name="workingBitmap">認識対象の画像</param>
/// <param name="sourceSize">拡大後の全体画像サイズ</param>
private async ValueTask<IReadOnlyList<TextRect>> RecognizeRegionAsync(
SoftwareBitmap workingBitmap,
System.Drawing.Size sourceSize)
{

// テキスト認識処理をバックグラウンドで実行
var textRects = await Task.Run(() => Recognize(workingBitmap)).ConfigureAwait(false);

// 認識したテキスト矩形の補正と結合処理を実行
textRects = ProcessTextRects(textRects, workingBitmap.PixelWidth, workingBitmap.PixelHeight);

if (bitmap != workingBitmap)
{
workingBitmap.Dispose();
}
textRects = ProcessTextRects(textRects, sourceSize.Width, sourceSize.Height);

var wFat = bitmap.PixelWidth * 0.004;
var wFat = sourceSize.Width * 0.004;

return textRects
// マージ後に少なすぎる文字も認識ミス扱い
Expand Down Expand Up @@ -391,23 +385,13 @@ private TextRect ToTextRect(TextRectMerger mergedRect)
{
var (x, y, width, height, fontSize, text) = mergedRect;

// スケールに応じた座標変換
if (this.scale != 1.0)
{
x /= scale;
y /= scale;
width /= scale;
height /= scale;
fontSize /= scale;
}

// 高さがフォントサイズの2倍以上の場合は複数行とみなす
var lines = height / fontSize >= 2;

// 結合された矩形の平均角度を計算
var angle = mergedRect.Rects.Average(r => r.Angle);

return new(text, x, y, width, height, fontSize, lines) { Angle = angle };
return new TextRect(text, x, y, width, height, fontSize, lines) { Angle = angle };
}

/// <summary>
Expand Down
62 changes: 24 additions & 38 deletions Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/TesseractOcr.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,30 +43,26 @@ public sealed class TesseractOcr(
private readonly int brightness = ocrParam.Value.Brightness;
private readonly int contrast = ocrParam.Value.Contrast;

public async ValueTask<IEnumerable<TextRect>> RecognizeAsync(SoftwareBitmap bitmap)
public ValueTask<IReadOnlyList<TextRect>> RecognizeAsync(OcrCaptureInput input)
=> OcrUtility.RecognizeRegionsAsync(
input,
RecognizeRegionAsync,
this.scale,
this.brightness,
this.contrast,
this.cts.Token);

/// <summary>
/// 指定した画像のテキストを認識する
/// </summary>
/// <param name="bitmap">認識対象の画像</param>
/// <param name="sourceSize">拡大後の全体画像サイズ</param>
private async ValueTask<IReadOnlyList<TextRect>> RecognizeRegionAsync(SoftwareBitmap bitmap, System.Drawing.Size sourceSize)
{
// リサイズ処理(scale != 1.0 の場合は新しいビットマップを生成)
var workingBitmap = await bitmap.ResizeSoftwareBitmapAsync(this.scale, this.cts.Token);
this.cts.Token.ThrowIfCancellationRequested();

// 明るさ・コントラスト調整(インプレース)
// scale == 1.0 の場合はリサイズで元のビットマップが返るため、コピーを作成してから調整
if (this.brightness != 0 || this.contrast != 0)
{
if (workingBitmap == bitmap)
{
// 元のビットマップを変更しないようにコピーを作成
#pragma warning disable CA1416 // プラットフォームの互換性を検証
workingBitmap = SoftwareBitmap.Copy(bitmap);
#pragma warning restore CA1416 // プラットフォームの互換性を検証
}
workingBitmap.AdjustBrightnessContrastInPlace(this.brightness, this.contrast);
}
this.cts.Token.ThrowIfCancellationRequested();

var sw = Stopwatch.StartNew();
// テキスト認識処理をバックグラウンドで実行
var textRects = await Task.Run(async () => await Recognize(workingBitmap).ConfigureAwait(false), this.cts.Token).ConfigureAwait(false);
var textRects = await Task.Run(async () => await Recognize(bitmap).ConfigureAwait(false), this.cts.Token).ConfigureAwait(false);
this.cts.Token.ThrowIfCancellationRequested();
this.logger.LogDebug($"Recognize: {sw.Elapsed}");

Expand All @@ -76,8 +72,9 @@ public async ValueTask<IEnumerable<TextRect>> RecognizeAsync(SoftwareBitmap bitm
}

// マージ処理
var xt = xPosThreshold * bitmap.PixelWidth;
var yt = yPosThreshold * bitmap.PixelHeight;
// 認識結果はスケール後画像の座標系なので、マージ閾値も同じ座標系に揃える
var xt = xPosThreshold * sourceSize.Width;
var yt = yPosThreshold * sourceSize.Height;

var results = new List<TempMergeRect>(textRects.Length);
var queue = new RemovableQueue<TextRect>(textRects.OrderBy(r => r.Y));
Expand Down Expand Up @@ -114,18 +111,14 @@ public async ValueTask<IEnumerable<TextRect>> RecognizeAsync(SoftwareBitmap bitm
results.Add(temp);
}

if (bitmap != workingBitmap)
{
workingBitmap.Dispose();
}

return results
.Select(r => ToTextRect(r, this.scale))
.Select(ToTextRect)
// マージ後に少なすぎる文字も認識ミス扱い
// 特殊なグリフの言語は対象外(日本語、中国語、韓国語、ロシア語)
.Where(w => IsSpecialLang(this.source) || w.SourceText.Length > 2)
// 全部数字・記号なら対象外
.Where(w => !AllSymbolOrSpace().IsMatch(w.SourceText));
.Where(w => !AllSymbolOrSpace().IsMatch(w.SourceText))
.ToArray();
}

private async ValueTask<TextRect[]> Recognize(SoftwareBitmap bitmap)
Expand Down Expand Up @@ -298,17 +291,10 @@ public TextRect ToRect()
}
}

private static TextRect ToTextRect(TempMergeRect combinedRect, double scale)
private static TextRect ToTextRect(TempMergeRect combinedRect)
{
var (x, y, width, height, fontSize, _) = combinedRect;
var text = combinedRect.Text;
// 元の画像座標に変換
x /= scale;
y /= scale;
width /= scale;
height /= scale;
fontSize /= scale;

// 高さがフォントサイズの2倍以上の場合は複数行とみなす
var lines = height / fontSize >= 2;

Expand All @@ -319,7 +305,7 @@ private static TextRect ToTextRect(TempMergeRect combinedRect, double scale)
height += fontSize * fat;
y -= fontSize * fat * .5;

return new(text, x, y, width, height, fontSize, lines);
return new TextRect(text, x, y, width, height, fontSize, lines);
}

public void Dispose()
Expand Down
2 changes: 1 addition & 1 deletion Sandbox/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ static async Task ClipTextRect([Argument] string imagePath, [FromServices] ILogg
var bitmap = await decoder.GetSoftwareBitmapAsync();

// OCRの実行
var textRects = await ocr.RecognizeAsync(bitmap);
var textRects = await ocr.RecognizeAsync(new(bitmap, []));

// 画像からテキスト矩形を切り抜き
var outputDir = Path.Combine(Path.GetDirectoryName(imagePath)!, "clipped");
Expand Down
37 changes: 37 additions & 0 deletions WindowTranslator.Abstractions/BitmapUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,43 @@ public static async ValueTask TrySaveImage(this SoftwareBitmap source, string pa
// ここで何かログを残すことも可能ですが、今回は省略します
}
}

/// <summary>
/// 画像を切り出す
/// </summary>
/// <param name="source">元の画像</param>
/// <param name="x">切り出し開始位置のX座標</param>
/// <param name="y">切り出し開始位置のY座標</param>
/// <param name="width">切り出す幅</param>
/// <param name="height">切り出す高さ</param>
/// <returns>切り出された画像</returns>
internal static unsafe SoftwareBitmap Crop(this SoftwareBitmap source, int x, int y, int width, int height)
{
var cropped = new SoftwareBitmap(source.BitmapPixelFormat, width, height, source.BitmapAlphaMode);

using var sourceBuffer = source.LockBuffer(BitmapBufferAccessMode.Read);
using var croppedBuffer = cropped.LockBuffer(BitmapBufferAccessMode.Write);
using var sourceReference = sourceBuffer.CreateReference();
using var croppedReference = croppedBuffer.CreateReference();

sourceReference.As<IMemoryBufferByteAccess>().GetBuffer(out var sourceData, out _);
croppedReference.As<IMemoryBufferByteAccess>().GetBuffer(out var croppedData, out _);

var bytesPerPixel = 4; // BGRA8
var sourceStride = sourceBuffer.GetPlaneDescription(0).Stride;
var croppedStride = croppedBuffer.GetPlaneDescription(0).Stride;

for (int row = 0; row < height; row++)
{
var sourceOffset = ((y + row) * sourceStride) + (x * bytesPerPixel);
var croppedOffset = row * croppedStride;

new ReadOnlySpan<byte>(sourceData + sourceOffset, width * bytesPerPixel)
.CopyTo(new Span<byte>(croppedData + croppedOffset, width * bytesPerPixel));
}

return cropped;
}
}

[ComImport]
Expand Down
31 changes: 29 additions & 2 deletions WindowTranslator.Abstractions/Modules/IOcrModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,30 @@ public interface IOcrModule

#if WINDOWS
/// <summary>
/// 画像からテキストを認識する
/// 1回のキャプチャーに含まれるOCR対象画像からテキストを認識する
/// </summary>
ValueTask<IEnumerable<TextRect>> RecognizeAsync(Windows.Graphics.Imaging.SoftwareBitmap bitmap);
ValueTask<IReadOnlyList<TextRect>> RecognizeAsync(OcrCaptureInput input);
#endif
}

#if WINDOWS
/// <summary>
/// 1回のキャプチャーに対するOCR入力
/// </summary>
/// <param name="Source">全体のコンテキストを把握するための元画像</param>
/// <param name="Regions">実際にOCRする範囲の一覧</param>
public sealed record OcrCaptureInput(
Windows.Graphics.Imaging.SoftwareBitmap Source,
IReadOnlyList<OcrRegionInput> Regions);

/// <summary>
/// 1つのOCR対象範囲
/// </summary>
/// <param name="Bounds">全体画像上の切り出し範囲</param>
/// <param name="Keyword">翻訳コンテキストに設定するキーワード。未指定の場合は認識結果のコンテキストを維持する</param>
public sealed record OcrRegionInput(RectInfo Bounds, string? Keyword = null);
#endif

/// <summary>
/// 基本的なOCRパラメータ
/// </summary>
Expand Down Expand Up @@ -94,4 +112,13 @@ public class BasicOcrParam : IPluginParam
[Category("MergeThrethold")]
public bool IsAvoidMergeList { get; set; } = false;

/// <summary>
/// OCR対象範囲のリスト
/// </summary>
/// <remarks>
/// 1件以上設定されている場合は、画像全体ではなく指定範囲内だけをOCRする。
/// 範囲が重なる場合は、リストの順序が優先度を表す(前方が高優先度)。
/// </remarks>
[Category("PriorityRect")]
public List<PriorityRect> PriorityRects { get; set; } = [];
}
Loading
Loading