在出差的过程中,曾经出现了一个微小人脸检测的需求,该算法的主要工作流程是从一张图片上通过算法识别出图片上的微小人脸。撇去算法如何实现,在这个微小人脸检测执行的过程中,图片的来源经过协商,希望通过海康威视摄像头的抓拍功能来实现,毕竟做安放平台摄像头是必不可少的设备,现场也采购了几个海康威视的人脸相机。这样也就定下来了实现的方案,即通过调用海康威视的设备SDK来实现摄像头的抓图的功能。然后通过HTTP连接的方式把该图片传送给算法识别,作为算法识别的数据来源,以满足微小人脸的测试需求。 而海康威视设备SDK为C++开发而成,而当时开发主要是使用Spring Boot框架,使用了Java开发语言,因此这便涉及到使用Java调用C++程序以调用C++的功能。
动态链接库(Dynamic Link Library)是一个包含可由多个程序,同时使用的代码和数据的库。
在百科上说,Comdlg32.dll执行与对话框有关的常见函数。因此,每个程序都可以使用该DLL中包含的功能来实现“打开对话框”。这有助于避免代码重写和存进内存的有效使用。通过使用dll,程序可以实现模块化,由相对独立的组件组成。因为模块是相互独立的,所以程序的加载速度更快,而且模块只在相应的功能被请求时才加载。
JNA框架由JNI发展而来,JNI最早是用来进行跨语言通信的框架,尤其使用Java调用C/C++交互的应用场景,当然这个文档要解决的通过Java程序调用C++开发的设备SDK也符合这样的场景。
JNI调用C/C++的过程如下图所示:
从上图可知,JNI调用的过程很复杂。
JNA(Java Native Access)框架是一个开源的Java框架,是SUN公司主导开发的,建立在经典的JNI的基础之上的一个框架。使用JNI调用共享类库(.dll/.so文件)是非常麻烦的事情,既需要编写java代码,又要编写C语言的代理方法,这其中需要很多数据类型的转换,是让人非常头痛。JNA框架就是为了解决这些问题和繁琐的事情而开发的,它提供一组Java工具类用于在运行期动态访问系统本地共享类库而不需要编写任何Native/JNI代码。开发人员只要在一个java接口中描述目标native library的函数与结构,JNA将自动实现Java接口到native function的映射,大大降低了Java调用本体共享库的开发难度。JNA与.NET平台上的P/Invoke机制一样简单和方便。
简而言之,就是JNA在跨语言交互更有竞争力。
开始时,以为服务器会是Windows Server,因此在海康威视官网下载了win64的设备网络sdk包。笔者下载的win64下的SDK,解压之后目录结构如下:
在设备网络SDK使用手册中,可以看到设备网络SDK调用的主要流程包括:
初始化→注册→抓拍→反注册→反初始化
上述图示中虚线框是可选部分,不会影响其他流程和模块的功能使用。按实现功能的不同可以分成十个模块,实现每个模块的功能时初始化SDK、用户设备注册、注销设备和释放SDK资源这4个流程是必不可少的。
该模块支持的功能从设备取实时码流,解码显示以及播放控制、抓图等功能。
可以通过按时间和按文件名的方式远程回放或者下载设备的录像文件,后续可以进行解码或者存储。同时还支持断点续传功能。
设置和获取设备的参数,主要包括设备参数、网络参数、通道压缩参数、串口参数、报警参数、异常参数、交易信息和用户配置等参数信息。
实现关闭设备、重启设备、恢复默认值、远程硬盘格式化、远程升级和配置文件导入/导出等维护工作。
实现和设备的语音数据对讲和语音数据获取,音频编码格式可以指定
处理设备上传的各种报警信号。报警分为“布防”和“监听”两种方式,在采用监听方式并且不需要获取用户ID的情况下,报警模块可以无需进行“用户注册”操作步骤。
透明通道是将IP数据报文解析后直接发送到串行口的一种技术。实际上起到了延伸串行设备控制距离的作用。可利用IP网络控制多种串行设备,如控制解码器、矩阵、报警主机、门禁、仪器仪表等串行设备,对用户来说,只看到点对点传输,无须关心网络传输过程,所以称为串口透明通道。 SDK提供485和232串口作为透明通道功能,其中要将232串口作为透明通道使用,首先必须在232串口的配置信息(NET_DVR_RS232CFG)中将工作模式选为透明通道,这样232串口才可作为透明通道使用。
实现对云台的基本操作、预置点、巡航、轨迹和透明云台的控制。SDK将云台控制分为两种模式:一种是通过图像预览返回的句柄进行控制;另一种是无预览限制,通过用户注册ID号进行云台控制。
实现解码器设备的配置、解码控制等功能。SDK支持单路解码器和多路解码器,但目前以多路解码器为主流产品
实现对智能产品的参数配置、报警上传和能力集获取等功能
由此可以到,摄像机抓图需要依赖预览模块来实现。
其中,预览录像抓图模块流程图如下:
抓图实现分成两种方式:预览抓图和设备抓图,在实现时采用了设备抓图的方式,具体区别可以参见设备网络SDK使用手册.chm
相关的抓图接口如下:
BOOL NET_DVR_CaptureJPEGPicture(
LONG lUserID,
LONG lChannel, LPNET_DVR_JPEGPARA lpJpegPara,
char *sPicFileName);
在源码实现时,显然要在Java程序中导入相关的dll,供程序JNA模块主动加载。由于在实现时服务器是固定的,可以把相关dll放在指定目录,供JNA模块加载即可。
在解压后的设备SDK文件夹可以看到如下的结构
txt文件内容如下:
【注意事项】
------------------------------------
1. 更新设备网络SDK时,SDK开发包【库文件】里的HCNetSDK.dll、HCCore.dll、PlayCtrl.dll、SuperRender.dll、AudioRender.dll、HCNetSDKCom文件夹等文件均要加载到程序里面,【HCNetSDKCom文件夹】(包含里面的功能组件dll库文件)需要和HCNetSDK.dll、HCCore.dll一起加载,放在同一个目录下,且HCNetSDKCom文件夹名不能修改。
2. 如果自行开发软件不能正常实现相应功能,而且程序没有指定加载的dll库路径,请在程序运行的情况下尝试删除HCNetSDK.dll。如果可以删除,说明程序可能调用到系统盘Windows->System32目录下的dll文件,建议删除或者更新该目录下的相关dll文件;如果不能删除,dll文件右键选择属性确认SDK库版本。
3. 如按上述步骤操作后还是不能实现相应功能,请根据NET_DVR_GetLastError返回的错误号判断原因。
HCNetSDK.java文件存在于Java Demo中,但需要修改载入的路径。
package com.example.platform.sdk;
import com.sun.jna.Native;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.Union;
import com.sun.jna.examples.win32.GDI32.RECT;
import com.sun.jna.examples.win32.W32API;
import com.sun.jna.examples.win32.W32API.HWND;
import com.sun.jna.ptr.ByteByReference;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.NativeLongByReference;
import com.sun.jna.ptr.ShortByReference;
//SDK接口说明,HCNetSDK.dll
public interface HCNetSDK extends StdCallLibrary {
//TODO 如果把dll打入jar包,则需要在运行时首先通过流把dll相关内容拷贝到本地目录,然后加载
//TODO 比如说拷贝到系统本地目录C:\Windows\System32,此时加载目录为:
//TODO C:\Windows\System32\lib\HCNetSDK
HCNetSDK INSTANCE = (HCNetSDK) Native.loadLibrary("D:\\Git\\screenshot\\src\\main\\java\\com\\example\\platform\\sdk\\lib\\HCNetSDK", HCNetSDK.class);
/***宏定义***/
//常量
public static final int MAX_NAMELEN = 16; //DVR本地登陆名
public static final int MAX_RIGHT = 32; //设备支持的权限(1 -12表示本地权限,13-32表示远程权限)
public static final int NAME_LEN = 32; //用户名长度
public static final int PASSWD_LEN = 16; //密码长度
…
}
在这个过程中,有一个步骤较为关键,即Nativi.loadLibrary()函数的参数要通过绝对路径方式载入HCNetSDK,对应的文件HCNetSDK.dll文件。
这样在程序运行时,INSTANCE即为设备网络SDK的实例,通过该实例可以实现需要的操作。
package com.example.platform.controller;
import com.alibaba.fastjson.JSONObject;
import com.example.platform.common.CommonReturn;
import com.example.platform.model.LoginInfo;
import com.example.platform.sdk.HCNetSDK;
import com.example.platform.service.AsyncTask;
import com.sun.jna.NativeLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 抓图
*
* @Owner: SongQuanHeng
* @Time: 2019/3/14-14:44
* @Version:
* @Change:
*/
@RestController
public class ScreenShot {
private final static Logger logger = LoggerFactory.getLogger(ScreenShot.class);
private LoginInfo loginInfo;
private String dirPath;
private HCNetSDK.NET_DVR_DEVICEINFO_V30 deviceInfo;
private NativeLong userID = new NativeLong(-1);
static HCNetSDK hCNetSDK = HCNetSDK.INSTANCE;
private final List<String> validKeys = new ArrayList<>(Arrays.asList("ip", "port", "userName", "password", "dirPath"));
@Autowired
private AsyncTask asyncTask;
// 负责初始化
private boolean init() {
logger.debug("Enter init");
return hCNetSDK.NET_DVR_Init();
}
// 反初始化
private boolean unInit() {
return hCNetSDK.NET_DVR_Cleanup();
}
@RequestMapping("/connect")
public String connect(@RequestBody JSONObject param) throws Exception {
if (!hasValidKey(param)) {
return CommonReturn.httpReturnFailure("参数有误");
}
if (userID.intValue()>0) {
return CommonReturn.httpReturnFailure("设备已连接, 请断开连接然后重新连接");
}
this.getLoginInfoAndDir(param);
if (!isExistDir(dirPath)) {
return CommonReturn.httpReturnFailure("文件夹目录不存在");
}
// 去掉传入的dirPath末尾的\\\\
while (dirPath.endsWith("\\")) {
dirPath = dirPath.substring(0, dirPath.length()-1);
}
if (!init()) {
int errorCode = hCNetSDK.NET_DVR_GetLastError();
return CommonReturn.httpReturn(String.valueOf(errorCode), "初始化SDK失败");
}
deviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V30();
userID = hCNetSDK.NET_DVR_Login_V30(loginInfo.getIp(), loginInfo.getPort(), loginInfo.getUserName(), loginInfo.getPassword(), deviceInfo);
asyncTask.setConnected(true);
asyncTask.executeAsyncTask(userID, dirPath);
return CommonReturn.httpReturnSuccess();
}
@RequestMapping("/disconnect")
public String disconnect() {
asyncTask.setConnected(false);
if (!hCNetSDK.NET_DVR_Logout(userID)){
int errorCode = hCNetSDK.NET_DVR_GetLastError();
logger.info("In disconnect NET_DVR_Logout errorCode: "+errorCode);
return CommonReturn.httpReturnFailure("资源释放失败");
}
userID = new NativeLong(-1);
if (!hCNetSDK.NET_DVR_Cleanup()){
int errorCode = hCNetSDK.NET_DVR_GetLastError();
logger.info("In disconnect NET_DVR_Cleanup errorCode: " + errorCode);
return CommonReturn.httpReturnFailure("SDK反初始化失败");
}
return CommonReturn.httpReturnSuccess("断开连接,抓拍结束");
}
private boolean isExistDir(String directory) {
File dir = new File(directory);
return dir.exists() && dir.isDirectory();
}
private boolean hasValidKey(JSONObject param) {
for (String key : validKeys) {
if (!param.containsKey(key)){
return false;
}
}
return true;
}
private void getLoginInfoAndDir(JSONObject param) {
loginInfo = new LoginInfo(param);
dirPath = param.getString("dirPath").trim();
}
}
Demo比较简单,connect作为Controller,可以接受REST请求,该请求的过程会进行初始化,用户注册,然后调用异步任务进行抓拍图像。其中抓图任务通过是通过注入的AsyncTask类实现的。
异步任务类源码如下:
package com.example.platform.service;
import com.sun.jna.NativeLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import com.example.platform.sdk.HCNetSDK;
/**
* 异步任务执行类
*
* @Owner: SongQuanHeng
* @Time: 2019/3/14-17:02
* @Version:
* @Change:
*/
@Service
public class AsyncTask {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private boolean connected = false;
static HCNetSDK hCNetSDK = HCNetSDK.INSTANCE;
private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
public boolean isConnected() {
return connected;
}
public void setConnected(boolean connected) {
this.connected = connected;
}
@Async
public void executeAsyncTask(NativeLong userID, String dirPath) throws Exception {
logger.info("Enter executeAsyncTask");
// 参数含义 参见chm文件
HCNetSDK.NET_DVR_JPEGPARA jpegpara = new HCNetSDK.NET_DVR_JPEGPARA();
jpegpara.wPicQuality = 3;
jpegpara.wPicSize = 0xFF;
logger.info(Thread.currentThread().getName());
logger.info(dirPath);
while (connected) {
String fileName = getImgName();
hCNetSDK.NET_DVR_CaptureJPEGPicture(userID, new NativeLong(1), jpegpara, dirPath+"\\"+fileName);
int errCode = hCNetSDK.NET_DVR_GetLastError();
if (errCode!=0) {
logger.info("errorCode: "+errCode);
}
Thread.sleep(1000/4);
logger.debug(dateFormat.format(new Date())+" produce Pic: "+dirPath+"\\"+fileName);
}
logger.info("Leave executeAsyncTask");
}
private String getImgName() {
return UUID.randomUUID().toString()+".jpg";
}
}
至此,即可实现Java调用C++功能(dll文件)的完整过程。其中最主要的工作即HCNetSDK.java文件的撰写,即Java的数据结构要映射到C/C++的数据结构的过程,在HCNetSDK.java中由海康威视的人做了很大一部分的工作,如果在实现某些特定的功能时,发现在HCNetSDK.java并未实现相应的数据结构,则需要开发人员自己去实现数据结构的映射,这是难点。但好在在摄像机抓拍图像是一个较为普通的工作,威视已经为我们提供了相应的数据结构映射。
boolean NET_DVR_CaptureJPEGPicture(NativeLong lUserID, NativeLong lChannel, NET_DVR_JPEGPARA lpJpegPara, String sPicFileName); //JNA
BOOL NET_DVR_CaptureJPEGPicture(LONG lUserID, LONG lChannel, LPNET_DVR_JPEGPARA lpJpegPara, char *sPicFileName); // C
表示图像质量的结构NET_DVR_JPEGPARA定义在HCNetSDK.java如下:
//图片质量
public static class NET_DVR_JPEGPARA extends Structure {
/*注意:当图像压缩分辨率为VGA时,支持0=CIF, 1=QCIF, 2=D1抓图,
当分辨率为3=UXGA(1600x1200), 4=SVGA(800x600), 5=HD720p(1280x720),6=VGA,7=XVGA, 8=HD900p
仅支持当前分辨率的抓图*/
public short wPicSize; /* 0=CIF, 1=QCIF, 2=D1 3=UXGA(1600x1200), 4=SVGA(800x600), 5=HD720p(1280x720),6=VGA*/
public short wPicQuality; /* 图片质量系数 0-最好 1-较好 2-一般 */
}
有过跨语言、跨平台开发的程序员都知道、跨平台、跨语言交互的难点,就是不同语言之间数据类型不一致的问题。因为C/C++基本数据类型与Java数据类型不一致,因此必须采用某种机制让它们保持一致。JNA提供了一组基本数据类型的映射,帮助程序员进行跨语言交互的问题
Java primitive types (and their object equivalents) map directly to the native C type of the same size.
Native Type | Size | Java Type | Common Windows Types |
char | 8-bit integer | byte | BYTE, TCHAR |
short | 16-bit integer | short | WORD |
wchar_t | 16/32-bit character | char | TCHAR |
int | 32-bit integer | int | DWORD |
int | boolean value | boolean | BOOL |
long | 32/64-bit integer | NativeLong | LONG |
long long | 64-bit integer | long | __int64 |
float | 32-bit FP | float | |
double | 64-bit FP | double | |
char* | C string | String | LPTCSTR |
void* | pointer | Pointer | LPVOID, HANDLE, LPXXX |
JNA实战笔记汇总<一> 简单认识JNA|成功调用JNA
JNA理解