【Halcon】Halcon联合C/C++及打包SO库

前言

为了将halcon编写的算法用在WEB端的产品,需要导出为C++代码,并在Linux打包成SO库,最终由JAVA的JNA调用…在折腾了一天后解决连“高级JAVA工程师”都无法解决的问题了↘呢→

我 们 联 合 !

正文

先放参考链接,因为之前没打包,思路也大多时从网上找的:

【1】超详细:halcon 与 VS 联合编程教程 - 知乎 【2】使用VS生成本地静态库文件以及静态库文件的使用_WHEgqing的专栏-CSDN博客 【3】Ubuntu18.04+Halcon18.11安装教程 【4】linux下java调用.so动态库方法

文章以文字为主(主要是贴图太麻烦),我尽量把操作步骤写明白;

用来示范的算法是 二维码识别,halcon代码如下:

* main文件
action ('123.png', result)

------分割线------

* action(::path:result)
read_image (Image, path)
**输入图像
**输出二维码的结果
get_2dcode_result (Image, DecodedDataStrings)
result := DecodedDataStrings
return ()

------分割线------

* get_2dcode_result(tdcode:::DecodedDataStrings)

* *输入二维码区域的图像变量Image
* *输出二维码的结果
* **二维码识别需要使用原图   多换几个参数来识别。

create_data_code_2d_model ('QR Code', 'default_parameters', 'enhanced_recognition', DataCodeHandleQR)

set_data_code_2d_param (DataCodeHandleQR, 'default_parameters', 'enhanced_recognition')
set_data_code_2d_param (DataCodeHandleQR, 'timeout', 200)
set_data_code_2d_param (DataCodeHandleQR, 'polarity', 'dark_on_light')

* 码粒个数设置(有几种二维码设置这个参数非法)
* set_data_code_2d_param (DataCodeHandleQR, 'symbol_size_max', 11)
* set_data_code_2d_param (DataCodeHandleQR, 'symbol_size_max', 31)

* 增强图像对比度(后文有该函数说明)
* enhanced_contrast (ImagePart, Image2, 40, 180)
Max := 254
Min := 1

Mult := 255.0 / (Max - Min)

Add := -Mult * Min
scale_image (tdcode, Image2, Mult, Add)


* 码粒像素设置
set_data_code_2d_param (DataCodeHandleQR, ['module_size_min','module_size_max'], [1,40])

* 如果GenParamNames, GenParamValues不填,那么默认只找一个二维码,将'stop_after_result_num'设置为3指最多找3个
find_data_code_2d (Image2, SymbolXLDs, DataCodeHandleQR, [], [], ResultHandles, DecodedDataStrings)
find_data_code_2d (Image2, SymbolXLDs, DataCodeHandleQR, 'stop_after_result_num', 3, ResultHandles, DecodedDataStrings)

tuple_length (DecodedDataStrings, Length)

* 找不到的话,改一下参数再找一遍
if (Length == 0)
    set_data_code_2d_param (DataCodeHandleQR, ['module_size_min','module_size_max'], [1,40])
    find_data_code_2d (Image2, SymbolXLDs, DataCodeHandleQR, [], [], ResultHandles, DecodedDataStrings)
endif

tuple_length (DecodedDataStrings, Length)

if (Length=0)
    Max := 180
    Min := 80

    Mult := 255.0 / (Max - Min)
    Add := -Mult * Min

    scale_image (tdcode, Image2, Mult, Add)

    * 码粒像素设置
    set_data_code_2d_param (DataCodeHandleQR, ['module_size_min','module_size_max'], [1,40])

    * 如果GenParamNames, GenParamValues不填,那么默认只找一个二维码,将'stop_after_result_num'设置为3指最多找3个
    find_data_code_2d (Image2, SymbolXLDs, DataCodeHandleQR, [], [], ResultHandles, DecodedDataStrings)
    find_data_code_2d (Image2, SymbolXLDs, DataCodeHandleQR, 'stop_after_result_num', 3, ResultHandles, DecodedDataStrings)

    tuple_length (DecodedDataStrings, Length)

    * 找不到的话,改一下参数再找一遍
    if (Length == 0)
        set_data_code_2d_param (DataCodeHandleQR, ['module_size_min','module_size_max'], [1,40])
        find_data_code_2d (Image2, SymbolXLDs, DataCodeHandleQR, [], [], ResultHandles, DecodedDataStrings)
    endif

    tuple_length (DecodedDataStrings, Length)

    if (Length == 0)
        DecodedDataStrings := ''
    endif

endif
return ()

从代码不难看出,程序是通过调用action() 来得到二维码识别结果的,根据热心网友提醒可能发生的ABI错误,函数声明时要添加 extern 关键字,同时为了测试方便,文件名采用绝对路径的形式:

cpp
///////////////////////////////////////////////////////////////////////////////
// File generated by HDevelop for HALCON/C++ Version 20.11.1.2
// Non-ASCII strings in this file are encoded in UTF-8.
// 
// Please note that non-ASCII characters in string constants are exported
// as octal codes in order to guarantee that the strings are correctly
// created on all systems, independent on any compiler settings.
// 
// Source files with different encoding should not be mixed in one project.
///////////////////////////////////////////////////////////////////////////////



#ifndef __APPLE__
#  include "HalconCpp.h"
#  include "HDevThread.h"
#else
#  ifndef HC_LARGE_IMAGES
#    include <HALCONCpp/HalconCpp.h>
#    include <HALCONCpp/HDevThread.h>
#    include <HALCON/HpThread.h>
#  else
#    include <HALCONCppxl/HalconCpp.h>
#    include <HALCONCppxl/HDevThread.h>
#    include <HALCONxl/HpThread.h>
#  endif
#  include <stdio.h>
#  include <CoreFoundation/CFRunLoop.h>
#endif

using namespace HalconCpp;

extern "C"
{
    // Procedure declarations 
    // Local procedures 
    void action(HTuple hv_path, HTuple* hv_result);
    void get_2dcode_result(HObject ho_tdcode, HTuple* hv_DecodedDataStrings);
}

// Procedures 
// Local procedures 
void action(HTuple hv_path, HTuple* hv_result)
{

    // Local iconic variables
    HObject  ho_Image;

    // Local control variables
    HTuple  hv_DecodedDataStrings;

    ReadImage(&ho_Image, hv_path);

    //*输入图像
    //*输出二维码的结果
    get_2dcode_result(ho_Image, &hv_DecodedDataStrings);
    //zoom_image_factor (Image, ImageZoomed, 2, 2, 'constant')

    //get_2dcode_result (ImageZoomed, DecodedDataStrings2)
    //zoom_image_factor (Image, ImageZoomed, 2, 2, 'constant')
    //write_image (ImageZoomed, 'jpg', 0, '19.jpg')


    (*hv_result) = hv_DecodedDataStrings;

    return;
}

void get_2dcode_result(HObject ho_tdcode, HTuple* hv_DecodedDataStrings)
{

    // Local iconic variables
    HObject  ho_Image2, ho_SymbolXLDs;

    // Local control variables
    HTuple  hv_DataCodeHandleQR, hv_Max, hv_Min, hv_Mult;
    HTuple  hv_Add, hv_ResultHandles, hv_Length;


    //*输入二维码区域的图像变量Image
    //*输出二维码的结果
    //stop ()
    //**二维码识别需要使用原图   多换几个参数来识别。

    CreateDataCode2dModel("QR Code", "default_parameters", "enhanced_recognition",
        &hv_DataCodeHandleQR);

    SetDataCode2dParam(hv_DataCodeHandleQR, "default_parameters", "enhanced_recognition");
    SetDataCode2dParam(hv_DataCodeHandleQR, "timeout", 200);
    SetDataCode2dParam(hv_DataCodeHandleQR, "polarity", "dark_on_light");

    //码粒个数设置(有几种二维码设置这个参数非法)
    //set_data_code_2d_param (DataCodeHandleQR, 'symbol_size_max', 11)
    //set_data_code_2d_param (DataCodeHandleQR, 'symbol_size_max', 31)

    //增强图像对比度(后文有该函数说明)
    //enhanced_contrast (ImagePart, Image2, 40, 180)
    hv_Max = 254;
    hv_Min = 1;

    hv_Mult = 255.0 / (hv_Max - hv_Min);

    hv_Add = (-hv_Mult) * hv_Min;
    ScaleImage(ho_tdcode, &ho_Image2, hv_Mult, hv_Add);


    //码粒像素设置
    SetDataCode2dParam(hv_DataCodeHandleQR, (HTuple("module_size_min").Append("module_size_max")),
        (HTuple(1).Append(40)));

    //如果GenParamNames, GenParamValues不填,那么默认只找一个二维码,将'stop_after_result_num'设置为3指最多找3个
    FindDataCode2d(ho_Image2, &ho_SymbolXLDs, hv_DataCodeHandleQR, HTuple(), HTuple(),
        &hv_ResultHandles, &(*hv_DecodedDataStrings));
    FindDataCode2d(ho_Image2, &ho_SymbolXLDs, hv_DataCodeHandleQR, "stop_after_result_num",
        3, &hv_ResultHandles, &(*hv_DecodedDataStrings));

    TupleLength((*hv_DecodedDataStrings), &hv_Length);

    //找不到的话,改一下参数再找一遍
    if (0 != (int(hv_Length == 0)))
    {
        SetDataCode2dParam(hv_DataCodeHandleQR, (HTuple("module_size_min").Append("module_size_max")),
            (HTuple(1).Append(40)));
        FindDataCode2d(ho_Image2, &ho_SymbolXLDs, hv_DataCodeHandleQR, HTuple(), HTuple(),
            &hv_ResultHandles, &(*hv_DecodedDataStrings));
    }

    TupleLength((*hv_DecodedDataStrings), &hv_Length);

    if (0 != (int(hv_Length == 0)))
    {
        hv_Max = 180;
        hv_Min = 80;

        hv_Mult = 255.0 / (hv_Max - hv_Min);
        hv_Add = (-hv_Mult) * hv_Min;

        ScaleImage(ho_tdcode, &ho_Image2, hv_Mult, hv_Add);

        //码粒像素设置
        SetDataCode2dParam(hv_DataCodeHandleQR, (HTuple("module_size_min").Append("module_size_max")),
            (HTuple(1).Append(40)));

        //如果GenParamNames, GenParamValues不填,那么默认只找一个二维码,将'stop_after_result_num'设置为3指最多找3个
        FindDataCode2d(ho_Image2, &ho_SymbolXLDs, hv_DataCodeHandleQR, HTuple(), HTuple(),
            &hv_ResultHandles, &(*hv_DecodedDataStrings));
        FindDataCode2d(ho_Image2, &ho_SymbolXLDs, hv_DataCodeHandleQR, "stop_after_result_num",
            3, &hv_ResultHandles, &(*hv_DecodedDataStrings));

        TupleLength((*hv_DecodedDataStrings), &hv_Length);

        //找不到的话,改一下参数再找一遍
        if (0 != (int(hv_Length == 0)))
        {
            SetDataCode2dParam(hv_DataCodeHandleQR, (HTuple("module_size_min").Append("module_size_max")),
                (HTuple(1).Append(40)));
            FindDataCode2d(ho_Image2, &ho_SymbolXLDs, hv_DataCodeHandleQR, HTuple(), HTuple(),
                &hv_ResultHandles, &(*hv_DecodedDataStrings));
        }

        TupleLength((*hv_DecodedDataStrings), &hv_Length);

        if (0 != (int(hv_Length == 0)))
        {
            (*hv_DecodedDataStrings) = "";
        }
    }
    return;
}

#ifndef NO_EXPORT_MAIN
// Main procedure 
void action()
{

    // Local control variables
    HTuple  hv_result;

    action("/home/sknp/Documents/JProject/diaoso/src/123.png", &hv_result);

    std::cout << hv_result << std::endl;
}


#ifndef NO_EXPORT_APP_MAIN

#ifdef __APPLE__
// On OS X systems, we must have a CFRunLoop running on the main thread in
// order for the HALCON graphics operators to work correctly, and run the
// action function in a separate thread. A CFRunLoopTimer is used to make sure
// the action function is not called before the CFRunLoop is running.
// Note that starting with macOS 10.12, the run loop may be stopped when a
// window is closed, so we need to put the call to CFRunLoopRun() into a loop
// of its own.
HTuple      gStartMutex;
H_pthread_t gActionThread;
HBOOL       gTerminate = FALSE;

static void timer_callback(CFRunLoopTimerRef timer, void* info)
{
    UnlockMutex(gStartMutex);
}

static Herror apple_action(void** parameters)
{
    // Wait until the timer has fired to start processing.
    LockMutex(gStartMutex);
    UnlockMutex(gStartMutex);

    try
    {
        action();
    }
    catch (HException& exception)
    {
        fprintf(stderr, "  Error #%u in %s: %s\n", exception.ErrorCode(),
            exception.ProcName().TextA(),
            exception.ErrorMessage().TextA());
    }

    // Tell the main thread to terminate itself.
    LockMutex(gStartMutex);
    gTerminate = TRUE;
    UnlockMutex(gStartMutex);
    CFRunLoopStop(CFRunLoopGetMain());
    return H_MSG_OK;
}

static int apple_main(int argc, char* argv[])
{
    Herror                error;
    CFRunLoopTimerRef     Timer;
    CFRunLoopTimerContext TimerContext = { 0, 0, 0, 0, 0 };

    CreateMutex("type", "sleep", &gStartMutex);
    LockMutex(gStartMutex);

    error = HpThreadHandleAlloc(&gActionThread);
    if (H_MSG_OK != error)
    {
        fprintf(stderr, "HpThreadHandleAlloc failed: %d\n", error);
        exit(1);
    }

    error = HpThreadCreate(gActionThread, 0, apple_action);
    if (H_MSG_OK != error)
    {
        fprintf(stderr, "HpThreadCreate failed: %d\n", error);
        exit(1);
    }

    Timer = CFRunLoopTimerCreate(kCFAllocatorDefault,
        CFAbsoluteTimeGetCurrent(), 0, 0, 0,
        timer_callback, &TimerContext);
    if (!Timer)
    {
        fprintf(stderr, "CFRunLoopTimerCreate failed\n");
        exit(1);
    }
    CFRunLoopAddTimer(CFRunLoopGetCurrent(), Timer, kCFRunLoopCommonModes);

    for (;;)
    {
        HBOOL terminate;

        CFRunLoopRun();

        LockMutex(gStartMutex);
        terminate = gTerminate;
        UnlockMutex(gStartMutex);

        if (terminate)
            break;
    }

    CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), Timer, kCFRunLoopCommonModes);
    CFRelease(Timer);

    error = HpThreadHandleFree(gActionThread);
    if (H_MSG_OK != error)
    {
        fprintf(stderr, "HpThreadHandleFree failed: %d\n", error);
        exit(1);
    }

    ClearMutex(gStartMutex);
    return 0;
}
#endif

int main(int argc, char* argv[])
{
    int ret = 0;

    try
    {
#if defined(_WIN32)
        SetSystem("use_window_thread", "true");
#endif

        // Default settings used in HDevelop (can be omitted)
        SetSystem("width", 512);
        SetSystem("height", 512);

#ifndef __APPLE__
        action();
        std::getchar();
#else
        ret = apple_main(argc, argv);
#endif
    }
    catch (HException& exception)
    {
        fprintf(stderr, "  Error #%u in %s: %s\n", exception.ErrorCode(),
            exception.ProcName().TextA(),
            exception.ErrorMessage().TextA());
        ret = 1;
    }
    return ret;
}
#endif
#endif

**P.S. halcon在linux与windows导出的语言文件完全一致,但是在不同平台编译的方式有所不同,后文细嗦; **

在windows下编译

详情在参考链接【1】中,完全按照参考教程即可;

使用Visual Studio编译项目:

  • 项目 属性页
    • VC++目录 --> 包含目录 :%halcon安装目录%/include
    • VC++目录 --> 库目录 :%halcon安装目录%/lib/x64-win64
    • 链接器 --> 附加依赖项 :手动输入 halconcpp.lib (cpp项目依赖的库文件)

在我编译的过程中,出现了非语法问题,来回倒腾了几次莫名其妙就好了,如果遇到相同问题不妨留言讨论;

在linux下编译

接下来是困扰我很长时间的问题,上面说到在windows下编译需要 include (一系列头文件)和 .lib 文件,但是在linux下显然没有lib文件可用;在找了大量资料后,我悟了,可以这样理解:头文件和库文件是一起的,头文件是声明库文件函数的文件,也就是说在linux下找到头文件和库文件就能编译,这里的库文件也就是SO库!

好的,思路已经有了,开始实操~

首先要在linux环境下安装halcon,参考链接【3】可以顺利的安装,我选择的版本是 halcon 20.11.1

配置好后,终端运行:

bash
echo $LD_LIBRARY_PATH

显示结果:/opt/halcon/lib/x64-linux 代表安装成功,记住这个路径,里面的文件有大用;

开始编译,依次输入两条命令:

bash
clang++ -c qr.cpp -I include/ -I include/halconcpp/ -fPIC
clang++ -shared -fPIC qrcode.o -o libqrcode.so -lhalcon -lhalconcpp

简单说说参数,为了方便,我把 include 文件夹复制到qr.cpp的同目录,使用-I 引入上述路径,也可以根据实际情况选择路径;

-l 命令需要输入SO文件的libxxxx.so的中间部分,这里用到了 libhalcon.solibhalconcpp.so

还有段插曲啊,在我没安装halcon前,-l 默认会在 /usr/lib 中寻找;好,那我把SO文件复制到目录下再引入,但!是!源文件全名是 libhalcon.so.20.11.1libhalconcpp.so.20.11.1 ,带后缀的SO库不能被编译器找到(超坑!)于是使用软连接:

bash
ln -s libhalcon.so.20.11.1 libhalcon.so
ln -s libhalconcpp.so.20.11.1 libhalconcpp.so

个人建议安装halcon,以防缺这缺那的;

再看看打包的 libqrcode.so 有没有正确引用:

bash
sknp@ubuntu:~/Documents/JProject/diaoso/src$ ldd libqrcode.so 
	linux-vdso.so.1 (0x00007ffcba34d000)
	libhalcon.so.20.11.1 => /opt/halcon/lib/x64-linux/libhalcon.so.20.11.1 (0x00007fd3ee12a000)
	libhalconcpp.so.20.11.1 => /opt/halcon/lib/x64-linux/libhalconcpp.so.20.11.1 (0x00007fd3edb65000)
	libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fd3ed971000)
	libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fd3ed822000)
	libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fd3ed807000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd3ed615000)
	libX11.so.6 => /lib/x86_64-linux-gnu/libX11.so.6 (0x00007fd3ed4d6000)
	libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fd3ed4b3000)
	librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007fd3ed4a8000)
	libXext.so.6 => /lib/x86_64-linux-gnu/libXext.so.6 (0x00007fd3ed493000)
	libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fd3ed48d000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fd3f2693000)
	libxcb.so.1 => /lib/x86_64-linux-gnu/libxcb.so.1 (0x00007fd3ed461000)
	libXau.so.6 => /lib/x86_64-linux-gnu/libXau.so.6 (0x00007fd3ed45b000)
	libXdmcp.so.6 => /lib/x86_64-linux-gnu/libXdmcp.so.6 (0x00007fd3ed453000)
	libbsd.so.0 => /lib/x86_64-linux-gnu/libbsd.so.0 (0x00007fd3ed439000)

主要看这两条:

  • libhalcon.so.20.11.1 => /opt/halcon/lib/x64-linux/libhalcon.so.20.11.1 (0x00007fd3ee12a000)
  • libhalconcpp.so.20.11.1 => /opt/halcon/lib/x64-linux/libhalconcpp.so.20.11.1 (0x00007fd3edb65000)

OKK下一步;

JNA调用SO库

好家伙,从halcon联合C#到JAVA调SO,把C#、C++、JAVA折腾个遍;

调用方式参考链接【4】,JAVA项目的配置就不多说了,上代码:

java
import com.sun.jna.Library;
import com.sun.jna.Native;

public class Jtest {

    // 继承Library,用于加载库文件
    public interface Clibrary extends Library {
        // 加载libhello.so链接库
        Clibrary INSTANTCE = (Clibrary) Native.loadLibrary("qrcode",
                Clibrary.class);
        // 此方法为链接库中的方法
        int main();
    }

    public static void main(String[] args) {
        // 调用
        Clibrary.INSTANTCE.main();
    }
}

最后

踩坑史:

  • 在编译的时候提示undefine symbol错误,不明所以,后来在windows平台编译后才发现缺少halconcpp文件(巨坑,一开始的方向便是错的);

  • 若不是之前网友提醒加上 extern "C" ,恐怕会更费劲;

总的来说,花了一天时间解决了“JAVA工程师”几天都无法解决的问题,还是有点小成就感的(

经过了这一天,我捡起了许久未用的C/C++和JAVA,朝着全干工程师更近了一步😂

随后升职加薪、迎娶白富美,走上人生巅峰…

【玩转群晖】FRP内网穿透
【C#】根据文件名排序