DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

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

Suppose you are a C# developer who wants to use Dynamsoft Document Normalizer to implement a desktop document scanner application. Since Dynamsoft Document Normalizer is a C/C++ library, you may encounter some interoperability issues when using it in your C# project. In this article, we will help you build a .NET document scanner SDK based on Dynamsoft C/C++ Document Normalizer. With the SDK, you can quickly implement desktop document scanner applications in C# 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
Enter fullscreen mode Exit fullscreen mode

Open DocumentScannerSDK.csproj to configure the runtime libraries that the SDK depends on:

<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>
Enter fullscreen mode Exit fullscreen mode

We put *.dll and *.so file respectively to runtimes/win-x64/native and runtimes/linux-x64/native folder.

The quantity of *.dll files looks overwhelming. Don't panic. It will be reduced in next version.

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
Enter fullscreen mode Exit fullscreen mode

To call these functions in C#, we need to import them with the attribute DllImport.

#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

Enter fullscreen mode Exit fullscreen mode

Why do we use macro definition _WINDOWS? Because the name of the shared library file is different on Windows and Linux. To make the macro definition work, we need to define the preprocessor variable in csproj file:

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

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;
}
Enter fullscreen mode Exit fullscreen mode

As we call the C/C++ functions, we copy unmanaged memory to managed memory by using Marshal.PtrToStructure. For example, we get the detected quadrilaterals from the C/C++ function DDN_DetectQuadFromFile:

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;
}
Enter fullscreen mode Exit fullscreen mode

Conversely, we can copy managed memory to unmanaged memory by using Marshal.StructureToPtr. For example, we pass quadrilateral points to the C/C++ function DDN_NormalizeFile:

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);
Enter fullscreen mode Exit fullscreen mode

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"" 
        }
    ]
}";
}
Enter fullscreen mode Exit fullscreen mode

For more advanced customization, please refer to the online documentation.

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;
}
Enter fullscreen mode Exit fullscreen mode

To show the normalized image in GUI application, we can construct a Bitmap object from the NormalizedImage object. For grayscale and color images, we can construct a Bitmap directly by image stride, width, height, pixel format and bytes. However, for binary images, we need to convert the binary image to grayscale image first because the binary image saves 8 1-bit pixels in one byte.

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;
}
Enter fullscreen mode Exit fullscreen mode

The final width of the grayscale image should be Stride * 8 rather than using Width, for Width may be less than Stride * 8. The following code snippet shows how to convert the normalized image to OpenCV Mat object, and then convert the Mat object to Bitmap object for Windows Forms display:

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);
}
Enter fullscreen mode Exit fullscreen mode

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

                }
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

The application only outputs the quadrilateral points of the detected document. You cannot see the effect until you save the normalized image to a file. To check the detection result straightforwardly, you can use OpenCV to display the normalized image.

Install OpenCvSharp4 package for Windows from NuGet:

<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" />
Enter fullscreen mode Exit fullscreen mode

For Linux, change the runtime package to OpenCVSharp4.runtime.ubuntu.18.04-x64.

Based on the code of the command-line tool, do some modifications to display the normalized image with OpenCV:

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

                }
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

.NET command-line document scanner

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.

WinForms UI design for document scanner

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

Finally, run the .NET desktop document scanner application.

dotnet run
Enter fullscreen mode Exit fullscreen mode

WinForms desktop .NET document scanner

Source Code

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

Top comments (0)