How to Build Desktop .NET Document Scanner Application on Windows and Linux

NuGet Package

https://www.nuget.org/packages/DocumentScannerSDK

Building .NET Document Scanner SDK with Dynamsoft C/C++ Document Normalizer

In this section, you will see how to package a .NET library with third-party C/C++ *.dll and *.so files, as well as how to glue C# and C/C++ code.

Create a .NET Library Project with Native Dependencies

Create a new C# library project named DocumentScannerSDK via dotnet CLI:

dotnet new classlib -o DocumentScannerSDK
<ItemGroup>
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-core-file-l1-2-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-core-file-l1-2-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-core-file-l2-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-core-file-l2-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-core-localization-l1-2-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-core-localization-l1-2-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-core-processthreads-l1-1-1.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-core-processthreads-l1-1-1.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-core-synch-l1-2-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-core-synch-l1-2-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-core-timezone-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-core-timezone-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-conio-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-conio-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-convert-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-convert-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-environment-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-environment-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-filesystem-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-filesystem-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-heap-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-heap-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-locale-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-locale-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-math-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-math-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-multibyte-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-multibyte-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-runtime-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-runtime-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-stdio-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-stdio-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-string-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-string-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-time-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-time-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-utility-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-utility-l1-1-0.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/concrt140.dll" Pack="true" PackagePath="runtimes/win-x64/native/concrt140.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/DynamsoftCorex64.dll" Pack="true" PackagePath="runtimes/win-x64/native/DynamsoftCorex64.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/DynamsoftDocumentNormalizerx64.dll" Pack="true" PackagePath="runtimes/win-x64/native/DynamsoftDocumentNormalizerx64.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/DynamsoftImageProcessingx64.dll" Pack="true" PackagePath="runtimes/win-x64/native/DynamsoftImageProcessingx64.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/DynamsoftIntermediateResultx64.dll" Pack="true" PackagePath="runtimes/win-x64/native/DynamsoftIntermediateResultx64.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/msvcp140.dll" Pack="true" PackagePath="runtimes/win-x64/native/msvcp140.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/msvcp140_1.dll" Pack="true" PackagePath="runtimes/win-x64/native/msvcp140_1.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/msvcp140_2.dll" Pack="true" PackagePath="runtimes/win-x64/native/msvcp140_2.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/ucrtbase.dll" Pack="true" PackagePath="runtimes/win-x64/native/ucrtbase.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/vccorlib140.dll" Pack="true" PackagePath="runtimes/win-x64/native/vccorlib140.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/vcomp140.dll" Pack="true" PackagePath="runtimes/win-x64/native/vcomp140.dll" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/vcruntime140.dll" Pack="true" PackagePath="runtimes/win-x64/native/vcruntime140.dll" />

<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/linux/libDynamsoftCore.so" Pack="true" PackagePath="runtimes/linux-x64/native/libDynamsoftCore.so" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/linux/libDynamsoftDocumentNormalizer.so" Pack="true" PackagePath="runtimes/linux-x64/native/libDynamsoftDocumentNormalizer.so" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/linux/libDynamsoftImageProcessing.so" Pack="true" PackagePath="runtimes/linux-x64/native/libDynamsoftImageProcessing.so" />
<None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/linux/libDynamsoftIntermediateResult.so" Pack="true" PackagePath="runtimes/linux-x64/native/libDynamsoftIntermediateResult.so" />
</ItemGroup>

Import Unmanaged Native Functions with DLLImport in C#

Let’s take a glimpse at the header file of Dynamsoft C/C++ Document Normalizer:

#ifdef __cplusplus
extern "C" {
#endif

DDN_API const char* DDN_GetVersion();

DDN_API void* DDN_CreateInstance();

DDN_API void DDN_DestroyInstance(void* normalizer);

DDN_API int DDN_InitRuntimeSettingsFromString(void* normalizer, const char* content, char errorMsgBuffer[], const int errorMsgBufferLen);

DDN_API int DDN_DetectQuadFromFile(void* normalizer, const char* sourceFilePath, const char* templateName, DetectedQuadResultArray** result);

DDN_API int DDN_DetectQuadFromBuffer(void* normalizer, const ImageData* sourceImage, const char* templateName, DetectedQuadResultArray** result);

DDN_API int DDN_NormalizeFile(void* normalizer, const char* sourceFilePath, const char* templateName, const Quadrilateral *quad, NormalizedImageResult** result);

DDN_API int DDN_NormalizeBuffer(void* normalizer, const ImageData* sourceImage, const char* templateName, const Quadrilateral *quad, NormalizedImageResult** result);

DDN_API void DDN_FreeNormalizedImageResult(NormalizedImageResult** result);

DDN_API void DDN_FreeDetectedQuadResultArray(DetectedQuadResultArray** results);

DDN_API int DDN_SaveImageDataToFile(const ImageData* imageData, const char * filePath);

#ifdef __cplusplus
}
#endif
#if _WINDOWS
[DllImport("DynamsoftCorex64")]
static extern int DC_InitLicense(string license, [Out] byte[] errorMsg, int errorMsgSize);

[DllImport("DynamsoftDocumentNormalizerx64")]
static extern IntPtr DDN_CreateInstance();

[DllImport("DynamsoftDocumentNormalizerx64")]
static extern void DDN_DestroyInstance(IntPtr handler);

[DllImport("DynamsoftDocumentNormalizerx64")]
static extern IntPtr DDN_GetVersion();

[DllImport("DynamsoftDocumentNormalizerx64")]
static extern int DDN_InitRuntimeSettingsFromString(IntPtr handler, string settings, [Out] byte[] errorMsg, int errorMsgSize);

[DllImport("DynamsoftDocumentNormalizerx64")]
static extern int DDN_DetectQuadFromFile(IntPtr handler, string sourceFilePath, string templateName, ref IntPtr pResultArray);

[DllImport("DynamsoftDocumentNormalizerx64")]
static extern int DDN_FreeDetectedQuadResultArray(ref IntPtr pResultArray);

[DllImport("DynamsoftDocumentNormalizerx64")]
static extern int DDN_NormalizeFile(IntPtr handler, string sourceFilePath, string templateName, IntPtr quad, ref IntPtr result);

[DllImport("DynamsoftDocumentNormalizerx64")]
static extern int DDN_FreeNormalizedImageResult(ref IntPtr result);

[DllImport("DynamsoftDocumentNormalizerx64")]
static extern int DDN_SaveImageDataToFile(IntPtr image, string filename);

[DllImport("DynamsoftDocumentNormalizerx64")]
static extern int DDN_DetectQuadFromBuffer(IntPtr handler, IntPtr sourceImage, string templateName, ref IntPtr pResultArray);

[DllImport("DynamsoftDocumentNormalizerx64")]
static extern int DDN_NormalizeBuffer(IntPtr handler, IntPtr sourceImage, string templateName, IntPtr quad, ref IntPtr result);

#else
[DllImport("DynamsoftCore")]
static extern int DC_InitLicense(string license, [Out] byte[] errorMsg, int errorMsgSize);

[DllImport("DynamsoftDocumentNormalizer")]
static extern IntPtr DDN_CreateInstance();

[DllImport("DynamsoftDocumentNormalizer")]
static extern void DDN_DestroyInstance(IntPtr handler);

[DllImport("DynamsoftDocumentNormalizer")]
static extern IntPtr DDN_GetVersion();

[DllImport("DynamsoftDocumentNormalizer")]
static extern int DDN_InitRuntimeSettingsFromString(IntPtr handler, string settings, [Out] byte[] errorMsg, int errorMsgSize);

[DllImport("DynamsoftDocumentNormalizer")]
static extern int DDN_DetectQuadFromFile(IntPtr handler, string sourceFilePath, string templateName, ref IntPtr pResultArray);

[DllImport("DynamsoftDocumentNormalizer")]
static extern int DDN_FreeDetectedQuadResultArray(ref IntPtr pResultArray);

[DllImport("DynamsoftDocumentNormalizer")]
static extern int DDN_NormalizeFile(IntPtr handler, string sourceFilePath, string templateName, IntPtr quad, ref IntPtr result);

[DllImport("DynamsoftDocumentNormalizer")]
static extern int DDN_FreeNormalizedImageResult(ref IntPtr result);

[DllImport("DynamsoftDocumentNormalizer")]
static extern int DDN_SaveImageDataToFile(IntPtr image, string filename);

[DllImport("DynamsoftDocumentNormalizer")]
static extern int DDN_DetectQuadFromBuffer(IntPtr handler, IntPtr sourceImage, string templateName, ref IntPtr pResultArray);

[DllImport("DynamsoftDocumentNormalizer")]
static extern int DDN_NormalizeBuffer(IntPtr handler, IntPtr sourceImage, string templateName, IntPtr quad, ref IntPtr result);

#endif
<!-- https://stackoverflow.com/questions/30153797/c-sharp-preprocessor-differentiate-between-operating-systems -->
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT' ">
<DefineConstants>_WINDOWS</DefineConstants>
</PropertyGroup>

Use StructLayout to Marshal C/C++ Structs

Define following structs in C#:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct NormalizedImageResult
{
public IntPtr ImageData;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct ImageData
{
public int bytesLength;

public IntPtr bytes;

public int width;

public int height;

public int stride;

public ImagePixelFormat format;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct DetectedQuadResultArray
{
public int resultsCount;
public IntPtr results;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct DetectedQuadResult
{
public IntPtr location;
public int confidenceAsDocumentBoundary;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct Quadrilateral
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public DM_Point[] points;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct DM_Point
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
public int[] coordinate;
}
public Result[]? DetectFile(string filename)
{
if (handler == IntPtr.Zero) return null;

IntPtr pResultArray = IntPtr.Zero;

int ret = DDN_DetectQuadFromFile(handler, filename, "", ref pResultArray);
return GetResults(pResultArray);
}

private Result[]? GetResults(IntPtr pResultArray)
{
Result[]? resultArray = null;
if (pResultArray != IntPtr.Zero)
{
DetectedQuadResultArray? results = (DetectedQuadResultArray?)Marshal.PtrToStructure(pResultArray, typeof(DetectedQuadResultArray));
if (results != null)
{
int count = results.Value.resultsCount;
if (count > 0)
{
IntPtr[] documents = new IntPtr[count];
Marshal.Copy(results.Value.results, documents, 0, count);
resultArray = new Result[count];

for (int i = 0; i < count; i++)
{
DetectedQuadResult? result = (DetectedQuadResult?)Marshal.PtrToStructure(documents[i], typeof(DetectedQuadResult));
if (result != null)
{
Result r = new Result();
resultArray[i] = r;
r.Confidence = result.Value.confidenceAsDocumentBoundary;
Quadrilateral? Quadrilateral = (Quadrilateral?)Marshal.PtrToStructure(result.Value.location, typeof(Quadrilateral));
if (Quadrilateral != null)
{
DM_Point[] points = Quadrilateral.Value.points;
r.Points = new int[8] { points[0].coordinate[0], points[0].coordinate[1], points[1].coordinate[0], points[1].coordinate[1], points[2].coordinate[0], points[2].coordinate[1], points[3].coordinate[0], points[3].coordinate[1] };
}
}
}
}
}
DDN_FreeDetectedQuadResultArray(ref pResultArray);
}

return resultArray;
}
Quadrilateral quad = new Quadrilateral();
quad.points = new DM_Point[4];
quad.points[0].coordinate = new int[2] { points[0], points[1] };
quad.points[1].coordinate = new int[2] { points[2], points[3] };
quad.points[2].coordinate = new int[2] { points[4], points[5] };
quad.points[3].coordinate = new int[2] { points[6], points[7] };

IntPtr pQuad = Marshal.AllocHGlobal(Marshal.SizeOf(quad));
Marshal.StructureToPtr(quad, pQuad, false);
int ret = DDN_NormalizeFile(handler, filename, "", pQuad, ref pResult);

Custom Templates for Document Normalization

Dynamsoft Document Normalizer allows developers to customize parameters for document normalization algorithm and output results. Here we create three built-in templates for changing image color mode:

public class Templates
{
public static string binary = @"{
""GlobalParameter"":{
""Name"":""GP""
},
""ImageParameterArray"":[
{
""Name"":""IP-1"",
""NormalizerParameterName"":""NP-1""
}
],
""NormalizerParameterArray"":[
{
""Name"":""NP-1"",
""ColourMode"": ""ICM_BINARY""
}
]
}";

public static string color = @"{
""GlobalParameter"":{
""Name"":""GP""
},
""ImageParameterArray"":[
{
""Name"":""IP-1"",
""NormalizerParameterName"":""NP-1""
}
],
""NormalizerParameterArray"":[
{
""Name"":""NP-1"",
""ColourMode"": ""ICM_COLOUR""
}
]
}";

public static string grayscale = @"{
""GlobalParameter"":{
""Name"":""GP""
},
""ImageParameterArray"":[
{
""Name"":""IP-1"",
""NormalizerParameterName"":""NP-1""
}
],
""NormalizerParameterArray"":[
{
""Name"":""NP-1"",
""ColourMode"": ""ICM_GRAYSCALE""
}
]
}";
}

Convert Binary Image to Grayscale Image

We save normalized Image to NormalizedImage class:

public class NormalizedImage
{
public int Width;
public int Height;
public int Stride;
public ImagePixelFormat Format;
public byte[] Data = new byte[0];

public IntPtr _dataPtr = IntPtr.Zero;
}
public byte[] Binary2Grayscale()
{
byte[] data = new byte[Data.Length * 8];
int index = 0;
foreach (byte b in Data)
{
int byteCount = 7;
while (byteCount >= 0)
{
int tmp = (b & (1 << byteCount)) >> byteCount;
if (tmp == 1)
data[index] = 255;
else
data[index] = 0;

byteCount -= 1;
index += 1;
}
}
return data;
}
NormalizedImage image = scanner.NormalizeBuffer(_mat.Data, _mat.Cols, _mat.Rows, (int)_mat.Step(), DocumentScanner.ImagePixelFormat.IPF_RGB_888, result.Points);
if (image != null && image.Data != null)
{
Mat mat2;
if (image.Stride < image.Width)
{
// binary
byte[] data = image.Binary2Grayscale();
mat2 = new Mat(image.Height, image.Stride * 8, MatType.CV_8UC1, data);
}
else if (image.Stride >= image.Width * 3)
{
// color
mat2 = new Mat(image.Height, image.Width, MatType.CV_8UC3, image.Data);
}
else
{
// grayscale
mat2 = new Mat(image.Height, image.Stride, MatType.CV_8UC1, image.Data);
}
pictureBox2.Image = BitmapConverter.ToBitmap(mat2);
}

Command-line Tool for Detecting and Normalizing Documents

Now we can create a simple command-line document scanning tool for Windows and Linux. You must get a valid license key from Dynamsoft customer portal to make the app work.

using System;
using System.Runtime.InteropServices;
using Dynamsoft;

namespace Test
{
class Program
{
static void Main(string[] args)
{
DocumentScanner.InitLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="); // Get a license key from https://www.dynamsoft.com/customer/license/trialLicense?product=ddn
Console.WriteLine("Version: " + DocumentScanner.GetVersionInfo());
DocumentScanner scanner = DocumentScanner.Create();
scanner.SetParameters(DocumentScanner.Templates.color);
DocumentScanner.Result[]? resultArray = scanner.DetectFile("<image-file>");
if (resultArray != null)
{
foreach (DocumentScanner.Result result in resultArray)
{
Console.WriteLine("Confidence: " + result.Confidence);
if (result.Points != null)
{
foreach (int point in result.Points)
{
Console.WriteLine("Point: " + point);
}

DocumentScanner.NormalizedImage image = scanner.NormalizeFile("1.png", result.Points);
if (image != null)
{
image.Save(DateTime.Now.ToFileTimeUtc() + ".png");
}
}

}
}
}
}
}
<PackageReference Include="OpenCvSharp4" Version="4.6.0.20220608" />
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.5.5.20211231" />
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.6.0.20220608" />
  1. Call DetectBuffer() instead of DetectFile() to detect the document from a byte array.
  2. Call NormalizeBuffer() instead of NormalizeFile() to normalize the document from a byte array.
  3. Call Cv2.ImShow() to display the normalized image.
using System;
using System.Runtime.InteropServices;
using Dynamsoft;
using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace Test
{
class Program
{
static void Main(string[] args)
{
DocumentScanner.InitLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="); // Get a license key from https://www.dynamsoft.com/customer/license/trialLicense?product=ddn
Console.WriteLine("Version: " + DocumentScanner.GetVersionInfo());
DocumentScanner scanner = DocumentScanner.Create();
scanner.SetParameters(DocumentScanner.Templates.binary);

Mat mat = Cv2.ImRead("<image-file>", ImreadModes.Color);
Mat copy = new Mat(mat.Rows, mat.Cols, MatType.CV_8UC3);
mat.CopyTo(copy);

DocumentScanner.Result[]? resultArray = scanner.DetectBuffer(copy.Data, copy.Cols, copy.Rows, (int)copy.Step(), DocumentScanner.ImagePixelFormat.IPF_RGB_888);
if (resultArray != null)
{
foreach (DocumentScanner.Result result in resultArray)
{
Console.WriteLine("Confidence: " + result.Confidence);
if (result.Points != null)
{
Point[] points = new Point[4];
for (int i = 0; i < 4; i++)
{
points[i] = new Point(result.Points[i * 2], result.Points[i * 2 + 1]);
}
Cv2.DrawContours(mat, new Point[][] { points }, 0, Scalar.Red, 2);
Cv2.ImShow("Source Image", mat);

DocumentScanner.NormalizedImage image = scanner.NormalizeBuffer(mat.Data, mat.Cols, mat.Rows, (int)mat.Step(), DocumentScanner.ImagePixelFormat.IPF_RGB_888, result.Points);
if (image != null && image.Data != null)
{
Mat mat2;
if (image.Stride < image.Width) {
// binary
byte[] data = image.Binary2Grayscale();
mat2 = new Mat(image.Height, image.Stride * 8, MatType.CV_8UC1, data);
}
else if (image.Stride >= image.Width * 3) {
// color
mat2 = new Mat(image.Height, image.Width, MatType.CV_8UC3, image.Data);
}
else {
// grayscale
mat2 = new Mat(image.Height, image.Stride, MatType.CV_8UC1, image.Data);
}
Cv2.ImShow("Normalized Document Image", mat2);
Cv2.WaitKey(0);
Cv2.DestroyAllWindows();
image.Save(DateTime.Now.ToFileTimeUtc() + ".png");
}
}

}
}
}
}
}

Desktop Document Scanner with WinForms

If you want to do more UI operations, you can create a WinForms application. Use the form designer to add the UI elements.

  • There are two picture boxes: one for the source image and the other for the normalized image.
  • The Load File button is used to load an image file from the local file system.
using (OpenFileDialog dlg = new OpenFileDialog())
{
dlg.Title = "Open Image";
dlg.Filter = "Image files (*.bmp, *.jpg, *.png) | *.bmp; *.jpg; *.png";

if (dlg.ShowDialog() == DialogResult.OK)
{
try
{
_mat = Cv2.ImRead(dlg.FileName, ImreadModes.Color);
Mat copy = new Mat(_mat.Rows, _mat.Cols, MatType.CV_8UC3);
_mat.CopyTo(copy);
pictureBox1.Image = DecodeMat(copy);
PreviewNormalizedImage();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
  • The Camera Scan button is used to open camera stream and scan documents in real-time. A worker thread is used to capture frames from the camera stream and detect documents. The detected document is normalized and displayed in the picture box.
private void StartScan() {
button2.Text = "Stop";
isCapturing = true;
thread = new Thread(new ThreadStart(FrameCallback));
thread.Start();
}

private void StopScan() {
button2.Text = "Camera Scan";
isCapturing = false;
if (thread != null) thread.Join();
}

private void FrameCallback() {
while (isCapturing) {
capture.Read(_mat);
Mat copy = new Mat(_mat.Rows, _mat.Cols, MatType.CV_8UC3);
_mat.CopyTo(copy);
pictureBox1.Image = DecodeMat(copy);
}
}
  • The Save Document button is used to save normalized document images to the local file system. Considering there may be multiple documents detected from the source image, we call FolderBrowserDialog() to let the user select a folder to save the normalized document images.
FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog();
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{
if (_results != null)
{
foreach (Result result in _results)
{
NormalizedImage image = scanner.NormalizeBuffer(_mat.Data, _mat.Cols, _mat.Rows, (int)_mat.Step(), DocumentScanner.ImagePixelFormat.IPF_RGB_888, result.Points);
if (image != null && image.Data != null)
{
Mat mat2;
if (image.Stride < image.Width)
{
// binary
byte[] data = image.Binary2Grayscale();
mat2 = new Mat(image.Height, image.Stride * 8, MatType.CV_8UC1, data);
}
else if (image.Stride >= image.Width * 3)
{
// color
mat2 = new Mat(image.Height, image.Width, MatType.CV_8UC3, image.Data);
}
else
{
// grayscale
mat2 = new Mat(image.Height, image.Stride, MatType.CV_8UC1, image.Data);
}
image.Save(Path.Join(folderBrowserDialog.SelectedPath, DateTime.Now.ToFileTimeUtc() + _color + ".png"));
}
}

MessageBox.Show("Saved normalized document images to " + folderBrowserDialog.SelectedPath);
}
}
  • The template radio buttons are useful for checking the different effects of normalization algorithms.
scanner.SetParameters(DocumentScanner.Templates.binary);
scanner.SetParameters(DocumentScanner.Templates.color);
scanner.SetParameters(DocumentScanner.Templates.grayscale);
scanner.SetParameters(richTextBox1.Text);
dotnet run

Source Code

https://github.com/yushulx/dotnet-document-scanner-sdk

--

--

Manager of Dynamsoft Open Source Projects | Tech Lover

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store