Fixed live scanner adjustments
This commit is contained in:
@@ -42,9 +42,4 @@ public class ScreenshotScanner
|
||||
Lookup.Add(kv.Word, kv.Image);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Clear()
|
||||
{
|
||||
Lookup.Clear();
|
||||
}
|
||||
}
|
||||
@@ -161,52 +161,6 @@
|
||||
x:Name="EnableThreshold"
|
||||
Content="Apply Threshold"
|
||||
IsChecked="{Binding ProcessorConfig.EnableThresholding}" />
|
||||
|
||||
<Grid
|
||||
Margin="4"
|
||||
IsEnabled="{Binding ElementName=EnableThreshold,
|
||||
Path=IsChecked}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0">Block Width:</TextBlock>
|
||||
<Slider
|
||||
x:Name="SldThreshold1"
|
||||
Grid.Column="1"
|
||||
Margin="4,0"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Thumb.DragCompleted="SldThreshold1_OnDragCompleted"
|
||||
Value="15" />
|
||||
<TextBlock Grid.Column="2">
|
||||
<Run Text="{Binding Value, ElementName=SldThreshold1, FallbackValue=0, StringFormat=0.00}" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<Grid
|
||||
Margin="4"
|
||||
IsEnabled="{Binding ElementName=EnableThreshold,
|
||||
Path=IsChecked}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0">Block Height:</TextBlock>
|
||||
<Slider
|
||||
x:Name="SldThreshold2"
|
||||
Grid.Column="1"
|
||||
Margin="4,0"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Thumb.DragCompleted="SldThreshold2_OnDragCompleted"
|
||||
Value="17" />
|
||||
<TextBlock Grid.Column="2">
|
||||
<Run Text="{Binding Value, ElementName=SldThreshold2, FallbackValue=0, StringFormat=0.00}" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<CheckBox
|
||||
Content="Resize"
|
||||
IsChecked="{Binding ProcessorConfig.EnableResizing}" />
|
||||
@@ -229,13 +183,6 @@
|
||||
<Run Text="{Binding Value, ElementName=SldBorder, FallbackValue=0, StringFormat=0.00}" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<Rectangle />
|
||||
|
||||
|
||||
<CheckBox
|
||||
Content="Filter connected components"
|
||||
IsChecked="{Binding ProcessorConfig.FilterConnectedComponents}" />
|
||||
</UniformGrid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -25,19 +25,6 @@ public partial class ImageView : Window
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void SldThreshold1_OnDragCompleted(object sender, DragCompletedEventArgs args)
|
||||
{
|
||||
var vm = ViewModel;
|
||||
vm.ProcessorConfig.ThresholdWidth = (int)Math.Round(((Slider)sender).Value);
|
||||
}
|
||||
|
||||
private void SldThreshold2_OnDragCompleted(object sender, DragCompletedEventArgs args)
|
||||
{
|
||||
var vm = ViewModel;
|
||||
vm.ProcessorConfig.ThresholdHeight = (int)Math.Round(((Slider)sender).Value);
|
||||
}
|
||||
|
||||
|
||||
private void SldBorder_OnDragCompleted(object sender, DragCompletedEventArgs e)
|
||||
{
|
||||
var vm = ViewModel;
|
||||
|
||||
@@ -22,271 +22,277 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Ocr.Tesseract.Extensions;
|
||||
|
||||
namespace Ocr.Gui.Views;
|
||||
|
||||
internal class ImageViewModel : INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// The internally used <see cref="ScreenshotScanner"/>
|
||||
/// </summary>
|
||||
public ScreenshotScanner Scanner { get; private set; }
|
||||
/// <summary>
|
||||
/// Tesseract engine configuration
|
||||
/// </summary>
|
||||
public static readonly ITesseractConfiguration TesseractConfig =
|
||||
new TesseractScreenshotConfiguration
|
||||
{
|
||||
DataPath = "tessdata",
|
||||
Languages = new[] { "eng", "deu" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Tesseract engine configuration
|
||||
/// </summary>
|
||||
public static readonly ITesseractConfiguration TesseractConfig =
|
||||
new TesseractScreenshotConfiguration
|
||||
{
|
||||
DataPath = "tessdata",
|
||||
Languages = new[] { "eng", "deu" }
|
||||
};
|
||||
public ScreenshotProcessorConfiguration ProcessorConfig { get; } = new();
|
||||
|
||||
public ScreenshotProcessorConfiguration ProcessorConfig { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Regex"/> expression for extracting whole words from scan results
|
||||
/// </summary>
|
||||
public static readonly Regex WordRegex = new(
|
||||
@"[\w'\-]{2,}",
|
||||
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase
|
||||
);
|
||||
|
||||
public ImageViewModel()
|
||||
{
|
||||
Scanner = new ScreenshotScanner(MakeProcessor());
|
||||
ProcessorConfig.PropertyChanged += (sender, args) => Task.Run(UpdateImage);
|
||||
|
||||
OpenFileCommand = new Command(OpenFile);
|
||||
SaveEditedImageCommand = new Command(SaveEditedImage);
|
||||
}
|
||||
|
||||
public ImageViewModel(MagickImage image) : this()
|
||||
{
|
||||
Image = image;
|
||||
}
|
||||
|
||||
private IProcessorChain<MagickImage, ScanResult> MakeProcessor()
|
||||
{
|
||||
var threshold =
|
||||
new ThresholdAdaptiveProcessor(
|
||||
ProcessorConfig.ThresholdWidth,
|
||||
ProcessorConfig.ThresholdHeight
|
||||
);
|
||||
|
||||
var preprocessing = new ProcessorChainConfiguration<MagickImage, MagickImage>()
|
||||
.Use(new CloneImageProcessor())
|
||||
.Use(new ResizeProcessor(FilterType.Lanczos2Sharp, PixelInterpolateMethod.Mesh))
|
||||
.Use(new NormalizeProcessor())
|
||||
.Use(threshold)
|
||||
.Use(new AddBorderProcessor(ProcessorConfig.Border))
|
||||
.Use(new BinarizeProcessor())
|
||||
.Complete(new NegateCloneProcessor());
|
||||
|
||||
var postprocessing = new ProcessorChainConfiguration<ScanResult, ScanResult>()
|
||||
.Use(new ConfidenceFilter(50))
|
||||
.Use(new ToLowerProcessor())
|
||||
.Use(new DuplicateFilter())
|
||||
.Complete(new RegexFilter(WordRegex));
|
||||
|
||||
var scan = new TesseractProcessor(TesseractConfig);
|
||||
|
||||
return new ProcessorChainConfiguration<MagickImage, ScanResult>()
|
||||
.Use(preprocessing)
|
||||
.Use(new ProcessingEvent<MagickImage>(OnProcessing))
|
||||
.Use(scan)
|
||||
.Use(new ProcessingEvent<ScanResult>(OnProcessed))
|
||||
.Complete(postprocessing);
|
||||
}
|
||||
|
||||
#region Overrides of Scanner
|
||||
|
||||
/// <inheritdoc />
|
||||
protected void OnProcessing(IProcessor sender, ICollection<MagickImage> inputs)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
foreach (var image in inputs)
|
||||
{
|
||||
Edited.Add(image);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected void OnProcessed(IProcessor sender, ICollection<ScanResult> inputs)
|
||||
{
|
||||
ScannedText = $"[{inputs.Count} words] " + string.Join(' ', inputs);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear()
|
||||
{
|
||||
Scanner.Clear();
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
ScannedText = string.Empty;
|
||||
Words.Clear();
|
||||
Edited.Clear();
|
||||
}
|
||||
/// <summary>
|
||||
/// <see cref="Regex"/> expression for extracting whole words from scan results
|
||||
/// </summary>
|
||||
public static readonly Regex WordRegex = new(
|
||||
@"[\w'\-]{2,}",
|
||||
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Handling
|
||||
|
||||
private void OpenFile()
|
||||
{
|
||||
var dialog = new OpenFileDialog()
|
||||
public ImageViewModel()
|
||||
{
|
||||
InitialDirectory = Directory.GetCurrentDirectory()
|
||||
};
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
Image = new MagickImage(dialog.FileName);
|
||||
UpdateImage();
|
||||
}
|
||||
}
|
||||
ProcessorConfig.PropertyChanged += (sender, args) => Task.Run(UpdateImage);
|
||||
|
||||
private void SaveEditedImage()
|
||||
{
|
||||
var basePath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
for (var i = 0; i < Edited.Count; i++)
|
||||
{
|
||||
Edited[i].Write(Path.Combine(basePath, $"edited_{i}.png"));
|
||||
OpenFileCommand = new Command(OpenFile);
|
||||
SaveEditedImageCommand = new Command(SaveEditedImage);
|
||||
}
|
||||
|
||||
Log.Information($"Saved image to '{basePath}'");
|
||||
System.Diagnostics.Process.Start(
|
||||
"explorer.exe",
|
||||
Path.GetDirectoryName(basePath) ?? string.Empty
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Updating data
|
||||
|
||||
private void UpdateConfidence()
|
||||
{
|
||||
Confidence = Scanner.Lookup.Keys.Any()
|
||||
? Scanner.Lookup.Keys.Sum(key => key.Confidence) / Scanner.Lookup.Keys.Count
|
||||
: 0;
|
||||
}
|
||||
|
||||
private void UpdateImage()
|
||||
{
|
||||
Task.Run(() =>
|
||||
public ImageViewModel(MagickImage image) : this()
|
||||
{
|
||||
IsIdle = false;
|
||||
|
||||
Clear();
|
||||
if (Image != null)
|
||||
{
|
||||
Scanner.Process(new[] { Image });
|
||||
}
|
||||
|
||||
UpdateWords();
|
||||
UpdateConfidence();
|
||||
|
||||
IsIdle = true;
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateWords()
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
foreach (var word in Scanner.Lookup.Keys)
|
||||
{
|
||||
Words.Add(word);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
private float _confidence;
|
||||
private MagickImage? _image;
|
||||
private bool _isIdle;
|
||||
|
||||
private string _scannedText = string.Empty;
|
||||
|
||||
public string ScannedText
|
||||
{
|
||||
get => _scannedText;
|
||||
set
|
||||
{
|
||||
if (value == _scannedText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_scannedText = value;
|
||||
OnPropertyChanged();
|
||||
Image = image;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsIdle
|
||||
{
|
||||
get => _isIdle;
|
||||
set
|
||||
private IProcessorChain<MagickImage, ScanResult> MakeProcessor()
|
||||
{
|
||||
if (value == _isIdle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var threshold =
|
||||
new ThresholdAdaptiveProcessor(
|
||||
ProcessorConfig.ThresholdWidth,
|
||||
ProcessorConfig.ThresholdHeight
|
||||
);
|
||||
|
||||
_isIdle = value;
|
||||
OnPropertyChanged();
|
||||
var chainConfig = new ProcessorChainConfiguration<MagickImage, MagickImage>()
|
||||
.Use(new CloneImageProcessor());
|
||||
|
||||
if (ProcessorConfig.EnableResizing)
|
||||
{
|
||||
chainConfig
|
||||
.Use(new ResizeProcessor(FilterType.Lanczos2Sharp, PixelInterpolateMethod.Mesh))
|
||||
.Use(new ProcessingEvent<MagickImage>(OnProcessing));
|
||||
}
|
||||
|
||||
chainConfig = chainConfig
|
||||
.Use(new NormalizeProcessor())
|
||||
.Use(new ProcessingEvent<MagickImage>(OnProcessing));
|
||||
|
||||
if (ProcessorConfig.EnableThresholding)
|
||||
{
|
||||
chainConfig = chainConfig
|
||||
.Use(threshold)
|
||||
.Use(new ProcessingEvent<MagickImage>(OnProcessing));
|
||||
}
|
||||
|
||||
var preprocessing = chainConfig
|
||||
.Use(new AddBorderProcessor(ProcessorConfig.Border))
|
||||
.Use(new BinarizeProcessor())
|
||||
.Use(new ProcessingEvent<MagickImage>(OnProcessing))
|
||||
.Complete(new NegateCloneProcessor());
|
||||
|
||||
var postprocessing = new ProcessorChainConfiguration<ScanResult, ScanResult>()
|
||||
.Use(new ConfidenceFilter(50))
|
||||
.Use(new ToLowerProcessor())
|
||||
.Use(new DuplicateFilter())
|
||||
.Complete(new RegexFilter(WordRegex));
|
||||
|
||||
var scan = new TesseractProcessor(TesseractConfig);
|
||||
|
||||
return new ProcessorChainConfiguration<MagickImage, ScanResult>()
|
||||
.Use(preprocessing)
|
||||
.Use(scan)
|
||||
.Use(new ProcessingEvent<ScanResult>(OnProcessed))
|
||||
.Complete(postprocessing);
|
||||
}
|
||||
}
|
||||
|
||||
public float Confidence
|
||||
{
|
||||
get => _confidence;
|
||||
set
|
||||
#region Overrides of Scanner
|
||||
|
||||
/// <inheritdoc />
|
||||
protected void OnProcessing(IProcessor sender, ICollection<MagickImage> inputs)
|
||||
{
|
||||
_confidence = value;
|
||||
OnPropertyChanged();
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
foreach (var image in inputs)
|
||||
{
|
||||
Edited.Add(image.CloneImage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<MagickImage> Edited { get; } = new();
|
||||
|
||||
public MagickImage? Image
|
||||
{
|
||||
get => _image;
|
||||
set
|
||||
/// <inheritdoc />
|
||||
protected void OnProcessed(IProcessor sender, ICollection<ScanResult> inputs)
|
||||
{
|
||||
_image = value;
|
||||
OnPropertyChanged();
|
||||
var wordStr = string.Join("\", \"", inputs);
|
||||
ScannedText = $"{inputs.Count} words:{Environment.NewLine}[ \"{wordStr}\" ]";
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<Word> Words { get; } = new();
|
||||
/// <inheritdoc />
|
||||
public void Clear()
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
ScannedText = string.Empty;
|
||||
Words.Clear();
|
||||
Edited.Clear();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
#endregion
|
||||
|
||||
#region Commands
|
||||
#region File Handling
|
||||
|
||||
public ICommand OpenFileCommand { get; private set; }
|
||||
public ICommand SaveEditedImageCommand { get; private set; }
|
||||
private void OpenFile()
|
||||
{
|
||||
var dialog = new OpenFileDialog()
|
||||
{
|
||||
InitialDirectory = Directory.GetCurrentDirectory()
|
||||
};
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
Image = new MagickImage(dialog.FileName);
|
||||
UpdateImage();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Commands
|
||||
private void SaveEditedImage()
|
||||
{
|
||||
var basePath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
#region INotifyPropertyChanged
|
||||
for (var i = 0; i < Edited.Count; i++)
|
||||
{
|
||||
Edited[i].Write(Path.Combine(basePath, $"edited_{i}.png"));
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
Log.Information($"Saved image to '{basePath}'");
|
||||
System.Diagnostics.Process.Start(
|
||||
"explorer.exe",
|
||||
Path.GetDirectoryName(basePath) ?? string.Empty
|
||||
);
|
||||
}
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#endregion INotifyPropertyChanged
|
||||
}
|
||||
#region Updating data
|
||||
|
||||
private void UpdateImage()
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
IsIdle = false;
|
||||
var scanner = new ScreenshotScanner(MakeProcessor());
|
||||
|
||||
Clear();
|
||||
|
||||
if (Image != null)
|
||||
{
|
||||
scanner.Process(new[] { Image });
|
||||
}
|
||||
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var confidence = 0f;
|
||||
foreach (var word in scanner.Lookup.Keys)
|
||||
{
|
||||
confidence += word.Confidence;
|
||||
Words.Add(word);
|
||||
}
|
||||
|
||||
Confidence = confidence / scanner.Lookup.Keys.Count;
|
||||
}
|
||||
);
|
||||
|
||||
IsIdle = true;
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
private float _confidence;
|
||||
private MagickImage? _image;
|
||||
private bool _isIdle;
|
||||
|
||||
private string _scannedText = string.Empty;
|
||||
|
||||
public string ScannedText
|
||||
{
|
||||
get => _scannedText;
|
||||
set
|
||||
{
|
||||
if (value == _scannedText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_scannedText = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsIdle
|
||||
{
|
||||
get => _isIdle;
|
||||
set
|
||||
{
|
||||
if (value == _isIdle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isIdle = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float Confidence
|
||||
{
|
||||
get => _confidence;
|
||||
set
|
||||
{
|
||||
_confidence = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<MagickImage> Edited { get; } = new();
|
||||
|
||||
public MagickImage? Image
|
||||
{
|
||||
get => _image;
|
||||
set
|
||||
{
|
||||
_image = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<Word> Words { get; } = new();
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Commands
|
||||
|
||||
public ICommand OpenFileCommand { get; private set; }
|
||||
public ICommand SaveEditedImageCommand { get; private set; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#region INotifyPropertyChanged
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
#endregion INotifyPropertyChanged
|
||||
}
|
||||
Reference in New Issue
Block a user