mirror of
https://github.com/cyanfish/naps2.git
synced 2024-09-21 04:37:47 +03:00
Make BlankDetector static
No plans to actually have multiple implementations and it does overcomplicate object construction for tests. Can consider reverting later if requested...
This commit is contained in:
parent
ef1a6665d3
commit
9217b695fc
@ -62,7 +62,6 @@ namespace NAPS2.Modules
|
||||
Bind<ILogger>().To<NLogLogger>().InSingletonScope();
|
||||
Bind<ScannedImageList>().ToSelf().InSingletonScope();
|
||||
Bind<StillImage>().ToSelf().InSingletonScope();
|
||||
Bind<BlankDetector>().To<ThresholdBlankDetector>();
|
||||
Bind<AutoSaver>().ToSelf();
|
||||
Bind<BitmapRenderer>().ToSelf();
|
||||
Bind<ImageContext>().To<GdiImageContext>().InSingletonScope();
|
||||
|
@ -29,7 +29,7 @@ namespace NAPS2.Remoting.Network
|
||||
}
|
||||
|
||||
public NetworkScanServer(ImageContext imageContext, NetworkScanServerOptions options)
|
||||
: this(imageContext, options, new RemoteScanController(new ScanDriverFactory(imageContext), new RemotePostProcessor(imageContext, new ThresholdBlankDetector())))
|
||||
: this(imageContext, options, new RemoteScanController(imageContext))
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ namespace NAPS2.Sdk.Tests.Scan
|
||||
scanDriver.Setup(x => x.GetDeviceList(It.IsAny<ScanOptions>())).ReturnsAsync(new List<ScanDevice> { device, wiaDevice });
|
||||
var scanDriverFactory = new Mock<IScanDriverFactory>();
|
||||
scanDriverFactory.Setup(x => x.Create(It.IsAny<ScanOptions>())).Returns(scanDriver.Object);
|
||||
var controller = new RemoteScanController(scanDriverFactory.Object, new RemotePostProcessor(ImageContext, new ThresholdBlankDetector()));
|
||||
var controller = new RemoteScanController(scanDriverFactory.Object, new RemotePostProcessor(ImageContext));
|
||||
|
||||
var deviceList = await controller.GetDeviceList(new ScanOptions { Driver = Driver.Wia });
|
||||
Assert.Equal(2, deviceList.Count);
|
||||
|
@ -1,26 +1,67 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using NAPS2.Images.Storage;
|
||||
using NAPS2.Scan;
|
||||
using NAPS2.Util;
|
||||
|
||||
namespace NAPS2.Images
|
||||
{
|
||||
public abstract class BlankDetector
|
||||
public static class BlankDetector
|
||||
{
|
||||
private static BlankDetector _default = new ThresholdBlankDetector();
|
||||
// If the pixel value (0-255) >= white_threshold, then it counts as a white pixel.
|
||||
private const int WHITE_THRESHOLD_MIN = 1;
|
||||
private const int WHITE_THRESHOLD_MAX = 255;
|
||||
// If the fraction of non-white pixels > coverage_threshold, then it counts as a non-blank page.
|
||||
private const double COVERAGE_THRESHOLD_MIN = 0.00;
|
||||
private const double COVERAGE_THRESHOLD_MAX = 0.01;
|
||||
|
||||
public static BlankDetector Default
|
||||
public static bool IsBlank(IImage image, int whiteThresholdNorm, int coverageThresholdNorm)
|
||||
{
|
||||
get
|
||||
if (image.PixelFormat == StoragePixelFormat.BW1)
|
||||
{
|
||||
TestingContext.NoStaticDefaults();
|
||||
return _default;
|
||||
// TODO: Make more generic
|
||||
if (!(image is GdiImage gdiImage))
|
||||
{
|
||||
throw new InvalidOperationException("Patch code detection only supported for GdiStorage");
|
||||
}
|
||||
using var bitmap2 = BitmapHelper.CopyToBpp(gdiImage.Bitmap, 8);
|
||||
return IsBlankRGB(new GdiImage(bitmap2), whiteThresholdNorm, coverageThresholdNorm);
|
||||
}
|
||||
set => _default = value ?? throw new ArgumentNullException(nameof(value));
|
||||
if (image.PixelFormat != StoragePixelFormat.RGB24)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return IsBlankRGB(image, whiteThresholdNorm, coverageThresholdNorm);
|
||||
}
|
||||
|
||||
public abstract bool IsBlank(IImage image, int whiteThresholdNorm, int coverageThresholdNorm);
|
||||
private static bool IsBlankRGB(IImage image, int whiteThresholdNorm, int coverageThresholdNorm)
|
||||
{
|
||||
var whiteThreshold = (int)Math.Round(WHITE_THRESHOLD_MIN + (whiteThresholdNorm / 100.0) * (WHITE_THRESHOLD_MAX - WHITE_THRESHOLD_MIN));
|
||||
var coverageThreshold = COVERAGE_THRESHOLD_MIN + (coverageThresholdNorm / 100.0) * (COVERAGE_THRESHOLD_MAX - COVERAGE_THRESHOLD_MIN);
|
||||
|
||||
public abstract bool ExcludePage(IImage image, ScanProfile scanProfile);
|
||||
long totalPixels = image.Width * image.Height;
|
||||
long matchPixels = 0;
|
||||
|
||||
var data = image.Lock(LockMode.ReadOnly, out var scan0, out var stride);
|
||||
var bytes = new byte[stride * image.Height];
|
||||
Marshal.Copy(scan0, bytes, 0, bytes.Length);
|
||||
image.Unlock(data);
|
||||
for (int x = 0; x < image.Width; x++)
|
||||
{
|
||||
for (int y = 0; y < image.Height; y++)
|
||||
{
|
||||
int r = bytes[stride * y + x * 3];
|
||||
int g = bytes[stride * y + x * 3 + 1];
|
||||
int b = bytes[stride * y + x * 3 + 2];
|
||||
// Use standard values for grayscale conversion to weight the RGB values
|
||||
int luma = r * 299 + g * 587 + b * 114;
|
||||
if (luma < whiteThreshold * 1000)
|
||||
{
|
||||
matchPixels++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var coverage = matchPixels / (double)totalPixels;
|
||||
return coverage < coverageThreshold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,73 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using NAPS2.Images.Storage;
|
||||
using NAPS2.Scan;
|
||||
|
||||
namespace NAPS2.Images
|
||||
{
|
||||
public class ThresholdBlankDetector : BlankDetector
|
||||
{
|
||||
// If the pixel value (0-255) >= white_threshold, then it counts as a white pixel.
|
||||
private const int WHITE_THRESHOLD_MIN = 1;
|
||||
private const int WHITE_THRESHOLD_MAX = 255;
|
||||
// If the fraction of non-white pixels > coverage_threshold, then it counts as a non-blank page.
|
||||
private const double COVERAGE_THRESHOLD_MIN = 0.00;
|
||||
private const double COVERAGE_THRESHOLD_MAX = 0.01;
|
||||
|
||||
public override bool IsBlank(IImage image, int whiteThresholdNorm, int coverageThresholdNorm)
|
||||
{
|
||||
if (image.PixelFormat == StoragePixelFormat.BW1)
|
||||
{
|
||||
// TODO: Make more generic
|
||||
if (!(image is GdiImage gdiImage))
|
||||
{
|
||||
throw new InvalidOperationException("Patch code detection only supported for GdiStorage");
|
||||
}
|
||||
using var bitmap2 = BitmapHelper.CopyToBpp(gdiImage.Bitmap, 8);
|
||||
return IsBlankRGB(new GdiImage(bitmap2), whiteThresholdNorm, coverageThresholdNorm);
|
||||
}
|
||||
if (image.PixelFormat != StoragePixelFormat.RGB24)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return IsBlankRGB(image, whiteThresholdNorm, coverageThresholdNorm);
|
||||
}
|
||||
|
||||
private static bool IsBlankRGB(IImage image, int whiteThresholdNorm, int coverageThresholdNorm)
|
||||
{
|
||||
var whiteThreshold = (int)Math.Round(WHITE_THRESHOLD_MIN + (whiteThresholdNorm / 100.0) * (WHITE_THRESHOLD_MAX - WHITE_THRESHOLD_MIN));
|
||||
var coverageThreshold = COVERAGE_THRESHOLD_MIN + (coverageThresholdNorm / 100.0) * (COVERAGE_THRESHOLD_MAX - COVERAGE_THRESHOLD_MIN);
|
||||
|
||||
long totalPixels = image.Width * image.Height;
|
||||
long matchPixels = 0;
|
||||
|
||||
var data = image.Lock(LockMode.ReadOnly, out var scan0, out var stride);
|
||||
var bytes = new byte[stride * image.Height];
|
||||
Marshal.Copy(scan0, bytes, 0, bytes.Length);
|
||||
image.Unlock(data);
|
||||
for (int x = 0; x < image.Width; x++)
|
||||
{
|
||||
for (int y = 0; y < image.Height; y++)
|
||||
{
|
||||
int r = bytes[stride * y + x * 3];
|
||||
int g = bytes[stride * y + x * 3 + 1];
|
||||
int b = bytes[stride * y + x * 3 + 2];
|
||||
// Use standard values for grayscale conversion to weight the RGB values
|
||||
int luma = r * 299 + g * 587 + b * 114;
|
||||
if (luma < whiteThreshold * 1000)
|
||||
{
|
||||
matchPixels++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var coverage = (matchPixels / (double)totalPixels);
|
||||
return coverage < coverageThreshold;
|
||||
}
|
||||
|
||||
public override bool ExcludePage(IImage image, ScanProfile scanProfile)
|
||||
{
|
||||
return scanProfile.ExcludeBlankPages && IsBlank(image, scanProfile.BlankPageWhiteThreshold, scanProfile.BlankPageCoverageThreshold);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,8 +23,8 @@ namespace NAPS2.Remoting.Worker
|
||||
private readonly ThumbnailRenderer thumbnailRenderer;
|
||||
private readonly IMapiWrapper mapiWrapper;
|
||||
|
||||
public WorkerServiceImpl(ImageContext imageContext, ThumbnailRenderer thumbnailRenderer, IMapiWrapper mapiWrapper, BlankDetector blankDetector)
|
||||
: this(imageContext, new RemoteScanController(new ScanDriverFactory(imageContext), new RemotePostProcessor(imageContext, blankDetector)),
|
||||
public WorkerServiceImpl(ImageContext imageContext, ThumbnailRenderer thumbnailRenderer, IMapiWrapper mapiWrapper)
|
||||
: this(imageContext, new RemoteScanController(imageContext),
|
||||
thumbnailRenderer, mapiWrapper)
|
||||
{
|
||||
}
|
||||
|
@ -8,17 +8,15 @@ namespace NAPS2.Scan.Internal
|
||||
internal class RemotePostProcessor : IRemotePostProcessor
|
||||
{
|
||||
private readonly ImageContext imageContext;
|
||||
private readonly BlankDetector blankDetector;
|
||||
|
||||
public RemotePostProcessor()
|
||||
: this(ImageContext.Default, BlankDetector.Default)
|
||||
: this(ImageContext.Default)
|
||||
{
|
||||
}
|
||||
|
||||
public RemotePostProcessor(ImageContext imageContext, BlankDetector blankDetector)
|
||||
public RemotePostProcessor(ImageContext imageContext)
|
||||
{
|
||||
this.imageContext = imageContext;
|
||||
this.blankDetector = blankDetector;
|
||||
}
|
||||
|
||||
|
||||
@ -41,7 +39,7 @@ namespace NAPS2.Scan.Internal
|
||||
{
|
||||
using (image = DoInitialTransforms(image, options))
|
||||
{
|
||||
if (options.ExcludeBlankPages && blankDetector.IsBlank(image, options.BlankPageWhiteThreshold, options.BlankPageCoverageThreshold))
|
||||
if (options.ExcludeBlankPages && BlankDetector.IsBlank(image, options.BlankPageWhiteThreshold, options.BlankPageCoverageThreshold))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
@ -19,6 +19,11 @@ namespace NAPS2.Scan.Internal
|
||||
{
|
||||
}
|
||||
|
||||
public RemoteScanController(ImageContext imageContext)
|
||||
: this(new ScanDriverFactory(imageContext), new RemotePostProcessor(imageContext))
|
||||
{
|
||||
}
|
||||
|
||||
public RemoteScanController(IScanDriverFactory scanDriverFactory, IRemotePostProcessor remotePostProcessor)
|
||||
{
|
||||
this.scanDriverFactory = scanDriverFactory;
|
||||
|
Loading…
Reference in New Issue
Block a user