如何构建Java边缘检测应用程序以扫描和标准化文档
#编程 #java #jni #document

Dynamsoft Document Normalizer SDK帮助开发人员快速构建文档扫描应用程序。它提供了一组API来检测文档边缘并标准化文档图像。目前,SDK仅支持C/C++AndroidiOSXamarin.FormsJavaScript。尽管尚无Java版本可下载,但我们可以自己制作。本文旨在将DynamSoft文档标准器C ++库封装到Java Jar软件包中。 JAR软件包可以在Windows和Linux上的Java应用程序中使用。

先决条件

如何使用CMAKE构建Java JNI项目

首先,我们启动一个新的Java项目,并创建一个定义某些本机方法的NativeDocumentScanner.java文件。本机方法用于加载本机库并桥接C ++ API。

package com.dynamsoft.ddn;

import java.util.ArrayList;

public class NativeDocumentScanner {

    private long nativePtr = 0;

    static {
        try {
            if (NativeLoader.load()) {
                System.out.println("Successfully loaded Dynamsoft Document Normalizer.");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public NativeDocumentScanner() {
        nativePtr = nativeCreateInstance();
    }

    public void destroyInstance() {
        if (nativePtr != 0)
            nativeDestroyInstance(nativePtr);
    }

    public static int setLicense(String license) {
        return nativeInitLicense(license);
    }

    public ArrayList<DocumentResult> detectFile(String fileName) {
        return nativeDetectFile(nativePtr, fileName);
    }

    public String getVersion() {
        return nativeGetVersion();
    }

    public NormalizedImage normalizeFile(String fileName, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4) {
        return nativeNormalizeFile(nativePtr, fileName, x1, y1, x2, y2, x3, y3, x4, y4);    
    }

    public int setParameters(String parameters) {
        return nativeSetParameters(nativePtr, parameters);
    }

    public int saveImage(NormalizedImage image, String fileName) {
        return nativeSaveImage(image, fileName);
    }

    private native static int nativeInitLicense(String license);

    private native long nativeCreateInstance();

    private native void nativeDestroyInstance(long nativePtr);

    private native ArrayList<DocumentResult> nativeDetectFile(long nativePtr, String fileName);

    private native String nativeGetVersion();

    private native NormalizedImage nativeNormalizeFile(long nativePtr, String fileName, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4);

    private native int nativeSetParameters(long nativePtr, String parameters);

    private native int nativeSaveImage(NormalizedImage image, String fileName);
}

然后,我们使用javah工具为Java类NativeDocumentScanner生成标头文件。

cd src/main/java
javah -o ../../../jni/NativeDocumentScanner.h com.dynamsoft.ddn.NativeDocumentScanner

该工具无需手动编写标头文件。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_dynamsoft_ddn_NativeDocumentScanner */

#ifndef _Included_com_dynamsoft_ddn_NativeDocumentScanner
#define _Included_com_dynamsoft_ddn_NativeDocumentScanner
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeInitLicense
 * Signature: (Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeInitLicense
  (JNIEnv *, jclass, jstring);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeCreateInstance
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeCreateInstance
  (JNIEnv *, jobject);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeDestroyInstance
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeDestroyInstance
  (JNIEnv *, jobject, jlong);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeDetectFile
 * Signature: (JLjava/lang/String;)Ljava/util/ArrayList;
 */
JNIEXPORT jobject JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeDetectFile
  (JNIEnv *, jobject, jlong, jstring);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeGetVersion
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeGetVersion
  (JNIEnv *, jobject);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeNormalizeFile
 * Signature: (JLjava/lang/String;IIIIIIII)Lcom/dynamsoft/ddn/NormalizedImage;
 */
JNIEXPORT jobject JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeNormalizeFile
  (JNIEnv *, jobject, jlong, jstring, jint, jint, jint, jint, jint, jint, jint, jint);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeSetParameters
 * Signature: (JLjava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeSetParameters
  (JNIEnv *, jobject, jlong, jstring);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeSaveImage
 * Signature: (Lcom/dynamsoft/ddn/NormalizedImage;Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeSaveImage
  (JNIEnv *, jobject, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

创建一个NativeDocumentScanner.cxx文件以实现本机方法。我们将在后面的部分中讨论实施。

现在,打开CMakeLists.txt文件并添加以下构建配置:

cmake_minimum_required (VERSION 2.6)
project (ddn)
MESSAGE( STATUS "PROJECT_NAME: " ${PROJECT_NAME} )

find_package(JNI REQUIRED)
include_directories(${JNI_INCLUDE_DIRS})

MESSAGE( STATUS "JAVA_INCLUDE: " ${JAVA_INCLUDE})

# Check lib
if (CMAKE_HOST_WIN32)
    set(WINDOWS 1)
elseif(CMAKE_HOST_APPLE)
    set(MACOS 1)
elseif(CMAKE_HOST_UNIX)
    set(LINUX 1)
endif()

# Set RPATH
if(CMAKE_HOST_UNIX)
    SET(CMAKE_CXX_FLAGS "-std=c++11 -O3 -Wl,-rpath=$ORIGIN")
    SET(CMAKE_INSTALL_RPATH "$ORIGIN")
    SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
endif()

# Add search path for include and lib files
if(WINDOWS)
    link_directories("${PROJECT_SOURCE_DIR}/lib/win/" ${JNI_LIBRARIES}) 
elseif(LINUX)
    link_directories("${PROJECT_SOURCE_DIR}/lib/linux/" ${JNI_LIBRARIES})
endif()
include_directories("${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/include/")


# Add the library
add_library(ddn SHARED NativeDocumentScanner.cxx)
if(WINDOWS)
    target_link_libraries (${PROJECT_NAME} "DynamsoftCorex64" "DynamsoftDocumentNormalizerx64")
else()
    target_link_libraries (${PROJECT_NAME} "DynamsoftCore" "DynamsoftDocumentNormalizer" pthread)
endif()

# Set installation directory
set(CMAKE_INSTALL_PREFIX "${PROJECT_SOURCE_DIR}/../src/main/")
set(LIBRARY_PATH "java/com/dynamsoft/ddn/native")
if(WINDOWS)
    install (DIRECTORY "${PROJECT_SOURCE_DIR}/lib/win/" DESTINATION "${CMAKE_INSTALL_PREFIX}${LIBRARY_PATH}/win")
    install (TARGETS ddn DESTINATION "${CMAKE_INSTALL_PREFIX}${LIBRARY_PATH}/win")
elseif(LINUX)
    install (DIRECTORY "${PROJECT_SOURCE_DIR}/lib/linux/" DESTINATION "${CMAKE_INSTALL_PREFIX}${LIBRARY_PATH}/linux")
    install (TARGETS ddn DESTINATION "${CMAKE_INSTALL_PREFIX}${LIBRARY_PATH}/linux")
endif()
  • find_package(JNI REQUIRED):在系统中找到JNI库。
  • include_directories:添加JNI库和DynamSoft文档标准器SDK的包含路径。
  • link_directories:添加C ++库文件的搜索路径。
  • add_library:构建共享库。
  • target_link_libraries:将库与DynamSoft文档标准器SDK链接。
  • install:将库文件复制到目标文件夹。

运行以下命令以在Windows和Linux上构建JNI项目:

# Windows
mkdir build
cd build
cmake -DCMAKE_GENERATOR_PLATFORM=x64 ..
cmake --build . --config Release --target install

# Linux
mkdir build
cd build
cmake .. 
cmake --build . --config Release --target install

它将生成用于Windows的dnn.dll和Linux的libdnn.so。在下一部分中,我们将将本机库包装到JAR文件中。

如何使用Maven使用C ++库来构建Java Jar软件包

创建一个pom.xml文件,在该文件中,我们定义了本机库文件所在的资源路径,并使用Maven Assembly Plugin将本机库包装到JAR文件中。

<project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.dynamsoft</groupId>
    <artifactId>ddn</artifactId>
    <version>1.0.0</version>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <excludes>
                    <exclude>**/*.md</exclude>
                    <exclude>**/*.h</exclude>
                    <exclude>**/*.lib</exclude>
                    <exclude>**/*.java</exclude>
                </excludes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.5.1</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

之后,运行以下命令生成ddn-1.0.0.jar

mvn package

如何从Jar包装加载共享库

JAR文件包含类文件和特定于平台的库。要在Java应用程序中调用这些本机库,我们首先需要将它们提取到临时文件夹,然后致电System.load()加载本机库。

private static boolean extractResourceFiles(String ddnNativeLibraryPath, String ddnNativeLibraryName,
            String tempFolder) throws IOException {
  String[] filenames = null;
  if (Utils.isWindows()) {
    filenames = new String[] {"api-ms-win-core-file-l1-2-0.dll",
    "api-ms-win-core-file-l2-1-0.dll",
    "api-ms-win-core-localization-l1-2-0.dll",
    "api-ms-win-core-processthreads-l1-1-1.dll",
    "api-ms-win-core-synch-l1-2-0.dll",
    "api-ms-win-core-timezone-l1-1-0.dll",
    "api-ms-win-crt-conio-l1-1-0.dll",
    "api-ms-win-crt-convert-l1-1-0.dll",
    "api-ms-win-crt-environment-l1-1-0.dll",
    "api-ms-win-crt-filesystem-l1-1-0.dll",
    "api-ms-win-crt-heap-l1-1-0.dll",
    "api-ms-win-crt-locale-l1-1-0.dll",
    "api-ms-win-crt-math-l1-1-0.dll",
    "api-ms-win-crt-multibyte-l1-1-0.dll",
    "api-ms-win-crt-runtime-l1-1-0.dll",
    "api-ms-win-crt-stdio-l1-1-0.dll",
    "api-ms-win-crt-string-l1-1-0.dll",
    "api-ms-win-crt-time-l1-1-0.dll",
    "api-ms-win-crt-utility-l1-1-0.dll",
    "concrt140.dll",
    "DynamicImagex64.dll",
    "DynamicPdfCorex64.dll",
    "DynamicPdfx64.dll",
    "DynamsoftCorex64.dll",
    "DynamsoftImageProcessingx64.dll",
    "DynamsoftIntermediateResultx64.dll",
    "msvcp140.dll",
    "msvcp140_1.dll",
    "msvcp140_2.dll",
    "ucrtbase.dll",
    "vccorlib140.dll",
    "vcomp140.dll",
    "vcruntime140.dll", "DynamsoftDocumentNormalizerx64.dll", "ddn.dll"};
  }
  else if (Utils.isLinux()) {
    filenames = new String[] {"libddn.so", "libDynamicImage.so", "libDynamicPdf.so", "libDynamicPdfCore.so", "libDynamsoftCore.so", "libDynamsoftDocumentNormalizer.so", "libDynamsoftImageProcessing.so", "libDynamsoftIntermediateResult.so"};
  }

  boolean ret = true;

  for (String file : filenames) {
    ret &= extractAndLoadLibraryFile(ddnNativeLibraryPath, file, tempFolder);
  }

  return ret;
}

private static boolean extractAndLoadLibraryFile(String libFolderForCurrentOS, String libraryFileName,
            String targetFolder) {
  String nativeLibraryFilePath = libFolderForCurrentOS + "/" + libraryFileName;

  String extractedLibFileName = libraryFileName;
  File extractedLibFile = new File(targetFolder, extractedLibFileName);

  try {
    if (extractedLibFile.exists()) {
      // test md5sum value
      String md5sum1 = md5sum(NativeDocumentScanner.class.getResourceAsStream(nativeLibraryFilePath));
      String md5sum2 = md5sum(new FileInputStream(extractedLibFile));

      if (md5sum1.equals(md5sum2)) {
        return loadNativeLibrary(targetFolder, extractedLibFileName);
      } else {
        // remove old native library file
        boolean deletionSucceeded = extractedLibFile.delete();
        if (!deletionSucceeded) {
          throw new IOException(
              "failed to remove existing native library file: " + extractedLibFile.getAbsolutePath());
        }
      }
    }

    // Extract file into the current directory
    InputStream reader = NativeDocumentScanner.class.getResourceAsStream(nativeLibraryFilePath);
    FileOutputStream writer = new FileOutputStream(extractedLibFile);
    byte[] buffer = new byte[1024];
    int bytesRead = 0;
    while ((bytesRead = reader.read(buffer)) != -1) {
      writer.write(buffer, 0, bytesRead);
    }

    writer.close();
    reader.close();

    if (!System.getProperty("os.name").contains("Windows")) {
      try {
        Runtime.getRuntime().exec(new String[] { "chmod", "755", extractedLibFile.getAbsolutePath() })
            .waitFor();
      } catch (Throwable e) {
      }
    }

    return loadNativeLibrary(targetFolder, extractedLibFileName);
  } catch (IOException e) {
    System.err.println(e.getMessage());
    return false;
  }

}

private static synchronized boolean loadNativeLibrary(String path, String name) {
  File libPath = new File(path, name);
  if (libPath.exists()) {
    try {
      System.load(new File(path, name).getAbsolutePath());
      return true;
    } catch (UnsatisfiedLinkError e) {
      System.err.println(e);
      return false;
    }

  } else
    return false;
}

注意:在Windows上加载DLL文件时,加载DLL文件的顺序很重要。首先加载的DLL文件不应取决于其他DLL文件。例如,ddn.dll取决于DynamsoftDocumentNormalizerx64.dll,因此应首先加载DynamsoftDocumentNormalizerx64.dll。如果您错误的加载顺序,您将看到unsatisfiedLinkError

如何实施JNI API进行文档边缘检测和归一化

在本节中,您将看到如何在Java和C ++中实现API来进行文档边缘检测,透视校正和图像增强。

初始化DynamSoft文档标准器

由于许可证在全球范围内工作,因此我们创建了一个静态方法来设置许可证。该方法只需要一次调用。

private native static int nativeInitLicense(String license);
JNIEXPORT jint JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeInitLicense(JNIEnv *env, jclass, jstring license)
{
  const char *pszLicense = env->GetStringUTFChars(license, NULL);
  char errorMsgBuffer[512];
  // Click https://www.dynamsoft.com/customer/license/trialLicense/?product=ddn to get a trial license.
  int ret = DC_InitLicense(pszLicense, errorMsgBuffer, 512);
  printf("DC_InitLicense: %s\n", errorMsgBuffer);
  env->ReleaseStringUTFChars(license, pszLicense);
  return ret;
}

GetStringUTFChars方法用于将Java字符串转换为C字符串。使用后,不要忘记释放内存。

创建和破坏文档扫描仪实例

在C/C ++中,我们使用DDN_CreateInstance创建文档扫描仪实例并使用DDN_DestroyInstance销毁该实例。

JNIEXPORT jlong JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeCreateInstance(JNIEnv *, jobject)
{
  return (jlong)DDN_CreateInstance();
}

JNIEXPORT void JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeDestroyInstance(JNIEnv *, jobject, jlong handler)
{
  if (handler)
  {
    DDN_DestroyInstance((void *)handler);
  }
}

创建文档扫描仪实例时,nativeCreateInstance()方法在DocumentScanner类的构造函数中调用。本机指针保存在Java中。

private long nativePtr = 0;

public NativeDocumentScanner() {
  nativePtr = nativeCreateInstance();
}

C ++对象一直保持在内存中,直到调用destroyInstance()方法为止。

public void destroyInstance() {
  if (nativePtr != 0)
    nativeDestroyInstance(nativePtr);
}

配置文档标准器的参数

参数配置允许您更改文档扫描仪的行为。

private native int nativeSetParameters(long nativePtr, String parameters);
JNIEXPORT jint JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeSetParameters(JNIEnv *env, jobject, jlong ptr, jstring parameters)
{
  if (ptr)
  {
    void *handler = (void *)ptr;
    const char *params = env->GetStringUTFChars(parameters, NULL);
    char errorMsgBuffer[512];
    int ret = DDN_InitRuntimeSettingsFromString(handler, params, errorMsgBuffer, 512);
    printf("Init runtime settings: %s\n", errorMsgBuffer);

    env->ReleaseStringUTFChars(parameters, params);
    return ret;
  }

  return -1;
}

例如,您可以更改归一化的图像颜色模式:

public final static String binary = "{\"GlobalParameter\":{\"Name\":\"GP\"},\"ImageParameterArray\":[{\"Name\":\"IP-1\",\"NormalizerParameterName\":\"NP-1\"}],\"NormalizerParameterArray\":[{\"Name\":\"NP-1\",\"ColourMode\": \"ICM_BINARY\" }]}";

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

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

文档边缘检测

使用DDN_DetectQuadFromFile()方法来检测图像文件的文档边缘。

private native ArrayList<DocumentResult> nativeDetectFile(long nativePtr, String fileName);
JNIEXPORT jobject JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeDetectFile(JNIEnv *env, jobject, jlong ptr, jstring fileName)
{
  jobject arrayList = NULL;
  if (ptr)
  {
    jclass documentResultClass = env->FindClass("com/dynamsoft/ddn/DocumentResult");
    if (NULL == documentResultClass)
      printf("FindClass failed\n");

    jmethodID documentResultConstructor = env->GetMethodID(documentResultClass, "<init>", "(IIIIIIIII)V");
    if (NULL == documentResultConstructor)
      printf("GetMethodID failed\n");

    jclass arrayListClass = env->FindClass("java/util/ArrayList");
    if (NULL == arrayListClass)
      printf("FindClass failed\n");

    jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "<init>", "()V");
    if (NULL == arrayListConstructor)
      printf("GetMethodID failed\n");

    jmethodID arrayListAdd = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
    if (NULL == arrayListAdd)
      printf("GetMethodID failed\n");

    void *handler = (void *)ptr;
    const char *pszFileName = env->GetStringUTFChars(fileName, NULL);

    DetectedQuadResultArray *pResults = NULL;

    int ret = DDN_DetectQuadFromFile(handler, pszFileName, "", &pResults);
    if (ret)
    {
      printf("Detection error: %s\n", DC_GetErrorString(ret));
    }

    if (pResults)
    {
      int count = pResults->resultsCount;
      arrayList = env->NewObject(arrayListClass, arrayListConstructor);

      for (int i = 0; i < count; i++)
      {
        DetectedQuadResult *quadResult = pResults->detectedQuadResults[i];
        int confidence = quadResult->confidenceAsDocumentBoundary;
        DM_Point *points = quadResult->location->points;
        int x1 = points[0].coordinate[0];
        int y1 = points[0].coordinate[1];
        int x2 = points[1].coordinate[0];
        int y2 = points[1].coordinate[1];
        int x3 = points[2].coordinate[0];
        int y3 = points[2].coordinate[1];
        int x4 = points[3].coordinate[0];
        int y4 = points[3].coordinate[1];

        jobject object = env->NewObject(documentResultClass, documentResultConstructor, confidence, x1, y1, x2, y2, x3, y3, x4, y4);

        env->CallBooleanMethod(arrayList, arrayListAdd, object);
      }
    }

    if (pResults != NULL)
          DDN_FreeDetectedQuadResultArray(&pResults);

    env->ReleaseStringUTFChars(fileName, pszFileName);
  }

  return arrayList;
}

我们需要创建一个DocumentResult类来存储检测结果。

public class DocumentResult {
    public int confidence;
    public int x1, y1, x2, y2, x3, y3, x4, y4;

    public DocumentResult(int confidence, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4) {
        this.confidence = confidence;
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
        this.x3 = x3;
        this.y3 = y3;
        this.x4 = x4;
        this.y4 = y4;
    }
}

记录归一化

获得文档的四边形坐标后,我们致电DDN_NormalizeFile()使文档正常化。

private native NormalizedImage nativeNormalizeFile(long nativePtr, String fileName, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4);
JNIEXPORT jobject JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeNormalizeFile(JNIEnv *env, jobject, jlong ptr, jstring fileName, jint x1, jint y1, jint x2, jint y2, jint x3, jint y3, jint x4, jint y4)
{
  if (ptr)
  {

    jclass normalizedImageClass = env->FindClass("com/dynamsoft/ddn/NormalizedImage");
    if (NULL == normalizedImageClass)
      printf("FindClass failed\n");

    jmethodID normalizedImageConstructor = env->GetMethodID(normalizedImageClass, "<init>", "(IIII[BII)V");
    if (NULL == normalizedImageConstructor)
      printf("GetMethodID failed\n");

    const char *pszFileName = env->GetStringUTFChars(fileName, NULL);

    void *handler = (void *)ptr;

    Quadrilateral quad;
    quad.points[0].coordinate[0] = x1;
    quad.points[0].coordinate[1] = y1;
    quad.points[1].coordinate[0] = x2;
    quad.points[1].coordinate[1] = y2;
    quad.points[2].coordinate[0] = x3;
    quad.points[2].coordinate[1] = y3;
    quad.points[3].coordinate[0] = x4;
    quad.points[3].coordinate[1] = y4;

    NormalizedImageResult* normalizedResult = NULL;

    int errorCode = DDN_NormalizeFile(handler, pszFileName, "", &quad, &normalizedResult);
    if (errorCode != DM_OK)
      printf("%s\r\n", DC_GetErrorString(errorCode));

    ImageData *imageData = normalizedResult->image;

    int width = imageData->width;
    int height = imageData->height;
    int stride = imageData->stride;
    int format = (int)imageData->format;
    unsigned char* data = imageData->bytes;
    int orientation = imageData->orientation;
    int length = imageData->bytesLength;

    jbyteArray byteArray = env->NewByteArray(length);
    env->SetByteArrayRegion(byteArray, 0, length, (jbyte *)data);
    jobject object = env->NewObject(normalizedImageClass, normalizedImageConstructor, width, height, stride, format, byteArray, orientation, length);
    env->ReleaseStringUTFChars(fileName, pszFileName);

    if (normalizedResult != NULL)
      DDN_FreeNormalizedImageResult(&normalizedResult);

    return object;
  }
  return NULL;
}   

归一化图像数据存储在NormalizedImage类中。

public class NormalizedImage {
    public int width;
    public int height;
    public int stride;
    public int format;
    public byte[] data;
    public int orientation;
    public int length;

    public NormalizedImage(int width, int height, int stride, int format, byte[] data, int orientation, int length) {
        this.width = width;
        this.height = height;
        this.stride = stride;
        this.format = format;
        this.data = data;
        this.orientation = orientation;
        this.length = length;
    }
}

保存归一化文档

NormalizedImage类包含图像数据和图像格式。因此,我们可以将NormalizedImage转换为BufferedImage并使用ImageIO将图像数据保存到文件。

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;

public void saveImage(String formatName, String fileName) {
    BufferedImage image = null;
    byte[] imageData = null;
    int[] pixels = new int[width * height];
    image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

    if (format == ImagePixelFormat.IPF_RGB_888) {

        imageData = data;

        for (int i = 0; i < width * height; i++) {
            int r = imageData[i * 3] & 0xFF;
            int g = imageData[i * 3 + 1] & 0xFF;
            int b = imageData[i * 3 + 2] & 0xFF;
            pixels[i] = (r << 16) | (g << 8) | b;
        }
    }
    else if (format == ImagePixelFormat.IPF_GRAYSCALED) {
        imageData = data;

        for (int i = 0; i < width * height; i++) {
            int gray = imageData[i] & 0xFF;
            pixels[i] = (gray << 16) | (gray << 8) | gray;
        }
    }
    else if (format == ImagePixelFormat.IPF_BINARY) {
        imageData = binary2Grayscale();

        for (int i = 0; i < width * height; i++) {
            int gray = imageData[i] & 0xFF;
            pixels[i] = (gray << 16) | (gray << 8) | gray;
        }
    }

    image.setRGB(0, 0, width, height, pixels, 0, width);
    Utils.display(image, "Normalized Image");
    try {
        ImageIO.write(image, formatName, new java.io.File(fileName));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

ImageIO类不支持PDF,但是DynamSoft Document normorizer确实可以。本机方法NormalizedImageResult_SaveToFile()可以将归一化文档保存为BMP,PNG,JPEG和PDF文件。该代码有点复杂,我们需要从Java NormalizedImage对象构造C ++ ImageData类型。

private native int nativeSaveImage(NormalizedImage image, String fileName);
JNIEXPORT jint JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeSaveImage(JNIEnv *env, jobject, jobject obj, jstring fileName)
{
  jclass normalizedImageClass = env->FindClass("com/dynamsoft/ddn/NormalizedImage");
  if (NULL == normalizedImageClass)
    printf("FindClass failed\n");

  jfieldID fid = env->GetFieldID(normalizedImageClass, "width", "I");
  if (NULL == fid)
    printf("Get width failed\n");

  jint width = env->GetIntField(obj, fid);

  fid = env->GetFieldID(normalizedImageClass, "height", "I");
  if (NULL == fid)
    printf("Ge height failed\n");

  jint height = env->GetIntField(obj, fid);

  fid = env->GetFieldID(normalizedImageClass, "stride", "I");
  if (NULL == fid)
    printf("Get stride failed\n");

  jint stride = env->GetIntField(obj, fid);

  fid = env->GetFieldID(normalizedImageClass, "format", "I");
  if (NULL == fid)
    printf("Get format failed\n");

  jint format = env->GetIntField(obj, fid);

  fid = env->GetFieldID(normalizedImageClass, "data", "[B");
  if (NULL == fid)
    printf("Get data failed\n");

  jbyteArray byteArray = (jbyteArray)env->GetObjectField(obj, fid);
  jbyte *bytes = env->GetByteArrayElements(byteArray, NULL);

  fid = env->GetFieldID(normalizedImageClass, "orientation", "I");
  if (NULL == fid)
    printf("Get orientation failed\n");

  jint orientation = env->GetIntField(obj, fid);

  fid = env->GetFieldID(normalizedImageClass, "length", "I");
  if (NULL == fid)
    printf("Get length failed\n");

  jint length = env->GetIntField(obj, fid);

  ImageData data;
  data.bytes = (unsigned char *)bytes;
  data.width = width;
  data.height = height;
  data.stride = stride;
  data.format = (ImagePixelFormat)format;
  data.orientation = orientation;
  data.bytesLength = length;

  const char *pszFileName = env->GetStringUTFChars(fileName, NULL);

  NormalizedImageResult normalizedResult;
  normalizedResult.image = &data;
  int ret = NormalizedImageResult_SaveToFile(&normalizedResult, pszFileName);
  if (ret != DM_OK)
    printf("NormalizedImageResult_SaveToFile: %s\r\n", DC_GetErrorString(ret));

  env->ReleaseStringUTFChars(fileName, pszFileName);
  env->ReleaseByteArrayElements(byteArray, bytes, 0);

  return ret;
}

除了将图像保存到文件外,我们还可以使用JFrame在屏幕上显示它们:

public static void display(BufferedImage image, String title) {
  JFrame frame = new JFrame();
  frame.getContentPane().setLayout(new FlowLayout());
  frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
  frame.setTitle(title);
  frame.getContentPane().add(new JLabel(new ImageIcon(image)));
  frame.pack();
  frame.setVisible(true);
}

如何在Windows和Linux上构建Java文档扫描仪应用程序

  1. 获取30-day free trial license并致电setLicense()激活SDK。

    int ret = NativeDocumentScanner.setLicense(license);
    
  2. 创建一个NativeDocumentScanner对象:

    NativeDocumentScanner scanner = new NativeDocumentScanner();
    
  3. 设置参数。默认情况下,归一化图像是二进制图像。您可以通过致电setParameters()将其更改为灰度或颜色。

    String template = "{\"GlobalParameter\":{\"Name\":\"GP\"},\"ImageParameterArray\":[{\"Name\":\"IP-1\",\"NormalizerParameterName\":\"NP-1\"}],\"NormalizerParameterArray\":[{\"Name\":\"NP-1\",\"ColourMode\": \"ICM_COLOUR\" }]}";
    scanner.setParameters(template);
    
  4. 检测文档边缘:

    ArrayList<DocumentResult> results = (ArrayList<DocumentResult>)scanner.detectFile(fileName);
    
  5. 根据四边形对文档进行标准化:

    NormalizedImage normalizedImage = scanner.normalizeFile(fileName, result.x1, result.y1, result.x2, result.y2, result.x3, result.y3, result.x4, result.y4);
    
  6. 将归一化图像保存为PDF,JPEG或PNG:

    scanner.saveImage(normalizedImage, "normalized.pdf");
    

构建和测试Java文档扫描仪SDK的整个步骤

cd jni
mkdir build
cd build
cmake ..
cmake --build . --config Release --target install
cd ../../
mvn package
java -cp target/ddn-1.0.0.jar com.dynamsoft.ddn.Test images/sample-image.png <optional: template.json> <optional: license key>

Java document scanner: document edge detection and normalization

源代码

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