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
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>
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
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
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>
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;
}
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;
}
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);
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""
}
]
}";
}
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;
}
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;
}
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);
}
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");
}
}
}
}
}
}
}
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" />
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:
- Call
DetectBuffer()
instead ofDetectFile()
to detect the document from a byte array. - Call
NormalizeBuffer()
instead ofNormalizeFile()
to normalize the document from a byte array. - 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 callFolderBrowserDialog()
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
Top comments (0)