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:
Ben Olden-Cooligan 2020-02-03 18:26:24 -05:00
parent ef1a6665d3
commit 9217b695fc
8 changed files with 64 additions and 94 deletions

View File

@ -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();

View File

@ -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))
{
}

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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)
{
}

View File

@ -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;
}

View File

@ -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;