尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Unity接入海康热成像SDK测温:非托管回调、结构体封送与纹理刷新

发布时间:2026/9/29 21:26:24

资讯中心
01
ARTICLE

Unity接入海康热成像SDK测温:非托管回调、结构体封送与纹理刷新

Unity接入海康热成像SDK测温:非托管回调、结构体封送与纹理刷新
第一次拿到海康的热成像相机看着 SDK 压缩包里那一堆 DLL 和几千行的头文件很多人的第一反应是这玩意儿在 Unity 里难道要从解码器开始自己写其实不用。真正卡住人的从来不是测温算法而是把海康的数据搬进 Unity这条路上的几个硬骨头——非托管回调线程、结构体封送、64 位组件目录、打包后 DLL 消失。我见过不少团队在这上面耗掉一整周最后发现只是少拷了一个文件夹。这篇内容围绕Unity 接入海康 SDK 做热成像测温展开从取数路线的选择讲到 C# 层封装、温度标定、纹理刷新和现场排错尽量把每一步为什么这么做讲透。适合已经会写 Unity 脚本、但对原生 SDK 互操作不太熟的同学如果你手上正好有一台测温型热像仪点测温或全屏测温都行跟着走一遍基本能跑出可用的温度数据。文中涉及的宏名和字段名以海康官方 SDK 头文件为准不同版本会有差异我会在关键处标出以你手上的头文件为准避免你照抄踩空。1. 先搞清楚海康热成像的三条取数路线别一上来就啃 SDK热成像的温度数据不像普通摄像头那样拉流就完事它有一层额外的语义你拿到的是像素灰度还是物理温度这两者在设备端是两套完全不同的通路。所以在写第一行代码之前先花十分钟确认自己的需求属于哪一类比后面返工要划算得多。我把常见的做法归成三条路线。它们的差别不在难易而在帧率、精度、开发成本三个维度的取舍。下面这张表是我自己项目里总结的对照你可以直接拿来评估。路线数据来源典型帧率开发成本适用场景ISAPI 抓帧测温HTTP 接口返回温度矩阵0.5~2 Hz低巡检、定时抓拍、原型验证SDK 实时测温PlayM4 解码回调拿原始矩阵25 Hz中高实时监控、动态追踪、工业检测混合方案视频走 SDK数值走 ISAPI视频 25 Hz / 数值 1 Hz中多数互动展示类项目1.1 ISAPI 抓帧测温半小时能出图的笨办法很多人不知道海康的设备本身就带 HTTP 接口参数、抓图、测温都能走这条线。测温相关的资源一般在/ISAPI/Thermal/channels下面你可以先用GET /ISAPI/Thermal/channels枚举一下设备上有几个热成像通道再用GET /ISAPI/Thermal/channels/1/capabilities看这个通道支持哪些子能力。这一步很关键因为不同固件版本的路径差别不小有的机器是/ISAPI/Thermal/channels/1/thermometry/1/rulesTemperatureInfo有的直接是/ISAPI/Thermal/channels/1/thermometryCapture硬编码路径迟早出问题先探测再拼路径才是稳妥做法。抓一帧全屏温度矩阵的请求大概长这样字段名请以 capabilities 返回的结构为准这里只示意层级{ ThermalCapture: { captureType: fullFrameTemperature, temperatureUnit: celsius } }返回的是一个二维温度矩阵行数列数等于热成像分辨率常见 160x120、256x192、384x288、640x512。这套方案的优点非常明显不需要任何原生 DLLUnityWebRequest 发出去、JsonUtility 解回来、写进 Texture2D半天就能看到图。缺点同样明显一帧数据体积不小640x512 的浮点矩阵转成文本能到几 MB设备处理一帧要几百毫秒帧率根本上不去而且它是请求-响应模型没有实时性可言。这里有个绕不开的坑海康设备默认走Digest 认证而 UnityWebRequest 对 Digest 的支持一直不太利索。我的做法是绕开 HTTP直接用 SDK 里的NET_DVR_STDXMLConfig发 ISAPI 报文——这个函数复用已经登录的 SDK 会话认证那一步 SDK 内部已经处理掉了你只管拼 URL 和 XML 正文。代价是要多封送两个结构体但比折腾认证省事得多。如果你坚持走纯 HTTP那就得自己实现 Digest 摘要计算MD5 nonce 拼接代码量不小不是特别推荐。1.2 SDK 实时测温25fps 的代价是要处理非托管回调要走实时路线就必须面对海康的两套原生库HCNetSDK负责登录、预览、云台、配置和PlayM4负责把码流解码成 YUV 或位图。热成像的原始温度矩阵不是从 HCNetSDK 直接出来的而是在 PlayM4 解码回调里以16 位无符号整数矩阵的形式吐出来的。海康的 PlayM4 里有一个专门用来回调温度数据的类型头文件里通常以T_Y16开头命名不同版本命名有差别以你手上的 PlayM4.h 为准。拿到这个矩阵之后你的工作流大概是回调线程收到ushort[]→ 拷进线程安全队列 → 主线程取出来做标定换算 → 上传到 Texture2D → Shader 做伪彩映射。整条链路最需要小心的是回调线程不是 Unity 主线程任何 UnityEngine 的 API 都不能在里面调包括 Debug.Log 在部分平台也不安全。我一般只在回调里做一件事memcpy 到预分配的缓冲区然后置一个标志位。这条路线的性能上限取决于设备典型热成像的原始数据流是 25fps。听起来不多但你要算一下带宽384 × 288 × 2 字节 × 25 5.5 MB/s如果分辨率是 640x512那就是16 MB/s。这个量级在 PC 上不算什么但如果你每一帧都new byte[]GC 会立刻教你做人——几秒钟就能堆出几十 MB 的垃圾帧率肉眼可见地掉。1.3 混合方案视频走 SDK数值走 ISAPI实际项目里我用得最多的是混合方案尤其是展示类的数字孪生或者工业看板。逻辑很简单画面用 SDK 拉流渲染保证流畅温度数值用 ISAPI 定时轮询1 秒一次足够。人眼对温度数字刷新的敏感度远低于画面流畅度1 Hz 的数值更新在界面上完全看不出来卡顿但省下来的解码压力非常可观。具体做法是让 PlayM4 只解 YUV 出图不碰温度数据回调另外开一个协程每 500ms 到 1s 调一次 ISAPI 拿规则测温结果最高温、最低温、平均温、区域坐标。两条线互不干扰任何一条挂了都不至于整个界面黑掉。这个架构还有个额外好处ISAPI 返回的温度值是设备已经标定好的精度有保障你不需要自己去猜标定系数。注意混合方案里画面和温度在时间上不是严格对齐的画面可能比温度值晚 100~200ms。如果你做的是点击画面某个点读出温度这类交互这个延迟没问题但如果是做高速运动物体的温度追踪就必须走全实时路线。2. 把 HCNetSDK 和 PlayM4 请进 Unity 工程目录、位数与初始化坑选完路线接下来是最容易劝退人的环节让 Unity 找到并正确加载海康的原生库。这一步失败的表现往往很玄学——编辑器里跑得好好的打包出来就崩或者登录一直返回失败但错误码又指向一个跟登录无关的地方。绝大多数情况下问题都出在下面这三个点上。2.1 别只复制 DLLHCNetSDKCom 目录才是 64 位版的命门海康的 64 位 SDK 包里HCNetSDK.dll只是一个外壳真正干活的是同目录下的HCNetSDKCom文件夹里面有libcrypto、libssl、hpr、StreamTransClient、SystemTransform等一堆组件。这些组件的路径不是自动搜索的必须在调用NET_DVR_Init之前显式告诉 SDK否则初始化会失败或者初始化成功但登录时各种莫名其妙地报错。对应的是NET_DVR_SetSDKInitCfg这个函数需要把加密组件和 SSL 组件的完整路径传进去。C# 层大致是这样// 组件目录路径必须是绝对路径且以 \ 结尾 string comDir Path.Combine(Application.streamingAssetsPath, HCNetSDKCom) \\; IntPtr cryptoPath Marshal.StringToHGlobalAnsi(Path.Combine(comDir, libcrypto-1_1-x64.dll)); IntPtr sslPath Marshal.StringToHGlobalAnsi(Path.Combine(comDir, libssl-1_1-x64.dll)); NET_DVR_SetSDKInitCfg(SDK_INIT_CFG_TYPE.NET_SDK_INIT_CFG_LIBEAY_PATH, cryptoPath); NET_DVR_SetSDKInitCfg(SDK_INIT_CFG_TYPE.NET_SDK_INIT_CFG_SSLEAY_PATH, sslPath); // 用完记得释放否则每次初始化都漏一块内存 Marshal.FreeHGlobal(cryptoPath); Marshal.FreeHGlobal(sslPath);我一般会把整个HCNetSDKCom放在Assets/StreamingAssets/下面因为 StreamingAssets 在打包后会原样保留在可执行文件旁边路径好预测。放到Assets/Plugins/x86_64/也行但要注意 Unity 只会自动加载 Plugins 目录根层级的 DLL子目录里的不会被当作插件处理你得自己用绝对路径去 LoadLibrary。还有一个细节容易漏组件版本必须和主 DLL 版本一致。我踩过一次主 DLL 用的是 6.xHCNetSDKCom 是从另一个旧包里抠出来的结果预览能出图但一取温度就返回错误。这种问题排查起来极其痛苦因为错误码不指向版本冲突。养成习惯解压 SDK 包之后整个目录一起拷别挑着拷。2.2 Unity 的 Plugins 平台设置与 DllImport 命名HCNetSDK.dll和PlayM4.dll放进Assets/Plugins/x86_64/之后在 Inspector 里确认Platform Settings勾选了 x86_6464 位并且没有勾 Any Platform。C# 侧的声明直接写文件名不带扩展名[DllImport(HCNetSDK)] public static extern bool NET_DVR_Init(); [DllImport(HCNetSDK)] public static extern bool NET_DVR_Login_V40( ref NET_DVR_USER_LOGIN_INFO pLoginInfo, ref NET_DVR_DEVICEINFO_V40 lpDeviceInfo); [DllImport(PlayM4)] public static extern int PlayM4_GetPort(ref int nPort);如果你打算同时支持 32 位和 64 位DllImport 里可以写带占位符的名字HCNetSDK然后靠 Plugins 目录的 x86 / x86_64 分目录自动选择但前提是两个目录下的 DLL 都齐全包括各自的 HCNetSDKCom。说实话现在做 PC 端项目没必要再兼容 32 位直接用 64 位能省掉一大半麻烦。顺便提一个反直觉的点DllImport的CallingConvention不要乱加。海康的库在 Windows 上是__stdcall但默认的Winapi会解析成平台默认调用约定在 64 位下其实和StdCall等价所以不加也没事。但如果你为了保险手动写成Cdecl栈会在每次调用后被破坏表现是运行几秒后直接闪退而且崩的地点每次都不同非常难查。2.3 NET_DVR_Init 之前必须做的事日志、组件路径、异常回调初始化的顺序有讲究我按实际能跑通的顺序列一下设置组件路径NET_DVR_SetSDKInitCfg必须在 Init 之前。开启 SDK 日志NET_DVR_SetLogToFile指定一个可写目录。这一步在开发期千万别省体温一样的错误码翻日志往往一句话就能定位。调用NET_DVR_Init()。设置连接超时和重连策略NET_DVR_SetConnectTime、NET_DVR_SetReconnect。设置异常回调NET_DVR_SetExceptionCallBack_V30用来接管断网、IP 冲突、解码异常这些情况。日志目录选哪儿也有讲究。我曾经把它设到Application.dataPath下面编辑器里没问题打包之后那个目录是只读的SDK 写日志失败结果连带着某些内部状态也异常了。建议统一放到Application.persistentDataPath下的一个子目录并且提前 Directory.CreateDirectory 建好省得 SDK 自己创建失败。提示开发阶段每天开工前先清一次日志目录。海康 SDK 的日志是追加写的跑一天能到几百 MB而且里面的时间戳是设备时间不是本机时间混着看很费劲。3. 登录、预览、抓图C# 结构体封送的关键细节环境搞定之后进入正题把设备登录上、把流拉起来。这一段的核心难点全在结构体封送上。海康的头文件里那些结构体字段顺序、对齐方式、内嵌的定长字符数组只要有一处对不上轻则参数被截断重则直接内存越界崩溃而且崩溃位置和真正的错误点往往隔着好几层调用。3.1 NET_DVR_USER_LOGIN_INFO 的字段陷阱与字符集先看登录结构。NET_DVR_USER_LOGIN_INFO这个结构体有几个必须注意的地方设备地址和用户名密码都是定长字节数组不是 string结构体第一个字段必须是dwSize并且要赋成Marshal.SizeOf的结果SDK 靠这个值判断版本。[StructLayout(LayoutKind.Sequential, CharSet CharSet.Ansi)] public struct NET_DVR_USER_LOGIN_INFO { [MarshalAs(UnmanagedType.ByValArray, SizeConst 129)] public byte[] sDeviceAddress; // 设备 IP 或域名 public byte byUseTransport; // 是否走私有协议一般填 0 public ushort wPort; // 设备端口默认 8000 [MarshalAs(UnmanagedType.ByValArray, SizeConst 64)] public byte[] sUserName; [MarshalAs(UnmanagedType.ByValArray, SizeConst 64)] public byte[] sPassword; public IntPtr cbLoginResult; // 异步登录回调同步登录填 IntPtr.Zero public IntPtr pUser; public bool bUseAsynLogin; // false 同步登录推荐 public byte byProxyType; public byte byUseUTCTime; public byte byLoginMode; public byte byHttps; public int iProxyID; public byte byVerifyMode; public byte[] byRes3; // 保留字段要对齐到指定长度 public uint dwSize; }这里的坑我列几个自己的血泪教训SizeConst必须和头文件里的宏完全一致。129 写成 128登录可能成功但设备名读出来是乱码或者干脆在NET_DVR_Login_V40里直接被拒绝。字符数组统一用byte[]不要用string加ByValTStr。海康的字段很多是中英混填的用 ANSI 字符串封送遇到非 ASCII 会出问题。自己写一个Encoding.Default.GetBytes的辅助函数填进去长度不够补 0超长直接报错别指望 SDK 帮你截断。byUseAsynLogin建议填 false。异步登录虽然不阻塞主线程但你得处理回调回来时 Unity 对象可能已经被销毁的情况反而更麻烦。海康的登录在局域网里通常几十毫秒就返回了同步足够。密码里有特殊字符要小心。海康设备激活时设的密码如果包含!、、#这类符号某些版本的 SDK 在解析时会截断表现就是密码明明对却登不上。遇到这种先换成纯字母数字测一遍确认是不是这个原因。NET_DVR_DEVICEINFO_V40相对简单主要是接收设备返回的信息按头文件顺序抄下来就行注意里面内嵌的NET_DVR_DEVICEINFO_V30要展开成字段而不是嵌套结构体否则对齐可能出错。3.2 预览句柄与解码通道PlayM4 那套 API 到底在干什么登录成功拿到lUserID之后下一步是拉流。这里要把 HCNetSDK 和 PlayM4 的关系理清楚不然代码写出来自己都不知道在干什么。NET_DVR_RealPlay_V40的职责是建立码流通道告诉设备我要 101 通道的主码流设备开始往这边推数据SDK 返回一个预览句柄lRealPlayHandle。但这时候你手上还是压缩码流H.264/H.265不是能显示的图像。所以还需要PlayM4_GetPort拿一个解码通道号。PlayM4_SetStreamOpenMode设置流模式实时流用STREAME_REALTIME。PlayM4_OpenStream把解码通道和码流缓冲区关联起来。PlayM4_Play指定渲染窗口句柄Unity 里没有 HWND这里传IntPtr.Zero因为我们要自己接管渲染。NET_DVR_SetDecCallBackEx注册解码回调之后每解出一帧回调就会被触发一次。这套流程和 DirectShow 那种源滤镜-解码滤镜-渲染滤镜的思路是一样的只是海康把它拆成了两个库。理解了这个分工后面调参数就不会瞎试。回调里能拿到什么取决于你注册的回调类型。普通的 YV12 回调给你的是解码后的图像数据适合做视频显示而温度数据需要注册专门的类型拿到的是一个ushort矩阵。这里有个很关键的细节温度数据的宽高和视频分辨率可能不一样。有些型号的视频流是 1920x1080可见光叠加温度矩阵只有 384x288你在做坐标映射时必须分别处理不能想当然用一个分辨率。回调函数签名大致是public delegate void DecCBFun(int nPort, IntPtr pBuf, int nSize, ref NET_DVR_PICTURE_INFO pFrameInfo, int nUser, int nReserved2); DecCBFun _decCallback; // 必须存成字段防止被 GC 回收 void OnDecode(int nPort, IntPtr pBuf, int nSize, ref NET_DVR_PICTURE_INFO info, int nUser, int nReserved2) { // 这里运行在 SDK 的解码线程绝对不能碰 Unity API if (info.nType TEMP_DATA_TYPE) // 温度数据类型宏名以头文件为准 { lock (_tempLock) { if (_tempBuffer null || _tempBuffer.Length ! nSize / 2) return; // 尺寸变了丢弃这一帧别在这里 new Marshal.Copy(pBuf, _tempRawBytes, 0, nSize); _tempFrameReady true; } } }3.3 回调委托必须钉住否则十分钟内必崩这段代码里最要命的一行是DecCBFun _decCallback;。如果你写成局部变量传进NET_DVR_SetDecCallBackEx那么这个委托对象在方法返回后就没有托管引用了GC 随时可能把它回收掉。而 SDK 那边还记着这个函数指针下一次数据来的时候就会跳进一块已经被释放的内存——表现是随机崩溃有时候跑十分钟有时候一登录就崩调试器给出的堆栈毫无意义。我解决这个问题的方式是在类里用一个静态字段持有所有回调委托并且在OnDestroy里显式置为 null 之前先停流、再登出、最后清空委托。顺序错了照样崩因为回调可能在停流的过程中还在触发。还有一件事不要在回调里做任何耗时操作。我见过有人在回调里直接做温度换算和纹理上传单帧耗时 30ms 以上结果就是 SDK 内部缓冲区积压画面越来越延迟最后整个解码线程卡死。回调的唯一职责就是拷贝数据、置标志位其他全部交给主线程。4. 温度数据从哪来全屏矩阵的原始值与两点标定这是整篇文章的核心也是最容易出错的地方。拿到那个ushort矩阵之后很多人的第一反应是上网搜海康热成像温度计算公式然后直接抄一个T raw * 0.01 - 273.15上去。我劝你千万别这么干同一个型号不同固件版本标定系数都可能不一样抄来的公式算出来的温度能差好几度做工业检测直接就是事故。4.1 T_Y16 原始矩阵的正确解读方式先说结论海康大部分测温型热像仪的全屏原始数据是 uint16标定关系基本是线性的形式为T(℃) raw × a b。这里的a通常在 0.01 量级对应 0.01℃ 的分辨率b是一个偏移量可能是负数。但具体值取决于两个因素测温档位和设备型号。测温档位这件事值得单独说。热像仪一般有多个量程比如 -20~150℃ 和 0~550℃。切换量程之后同一个 raw 值对应的温度完全变了因为 16 位的动态范围要覆盖不同的温度区间精度分配自然不同。如果你在代码里硬编码了一套系数然后在设备上切换了量程读出来的温度就会荒谬地离谱——比如对着室温测出 300 度。所以正确的做法是每次开始测温前先从设备读一次当前量程和标定参数。ISAPI 里有个测温基础参数的资源一般在/ISAPI/Thermal/channels/id/thermometry下面返回的内容里会带温度单位、量程、标定系数之类的字段。把它解析出来存到内存换算时用这份参数而不是用硬编码。4.2 用设备自带规则测温值反解标定系数如果设备返回的参数不够明确或者你怀疑参数本身不准那就用两点标定法自己求。这个方法我用了很多次准确度完全够用而且不需要黑体炉有的话更好。具体步骤在设备 Web 页面上打开智能测温画一个点规则放在画面中心。让程序同时输出两个数据设备报出的该点温度 T_ref以及你在同一像素坐标取到的 raw 值 r。找两个温差尽量大的场景。比如第一个场景对着室温物体25℃ 左右第二个场景对着刚烧开的水杯附近的空气60~80℃或者把手贴在镜头前方几秒。温差越大拟合误差越小。得到两组数据 (r1, T1) 和 (r2, T2)解方程// 两点线性拟合求 T a * raw b float a (T2 - T1) / (float)(r2 - r1); float b T1 - a * r1; // 校验用第三个场景验证误差超过 1℃ 就重新采点 float checkTemp a * r3 b; Debug.Log($标定校验计算值 {checkTemp:F2}℃设备值 {T3:F2}℃误差 {checkTemp - T3:F2}℃);几个实操经验采样点不要选画面边缘热像仪的边缘往往有暗角和镜头衰减raw 值偏低避开强反射物体金属、玻璃表面的反射会干扰测温两点之间的温差最好大于 30℃否则拟合出的a会有明显偏差。求出来的系数建议写进配置文件而不是代码里这样现场换设备或者固件升级之后改配置就行不用重新出包。注意两点法求出来的是相对准确的系数绝对精度取决于设备本身的辐射标定。如果你的项目要做医疗级或工业级计量必须用标准黑体源做多点标定并且定期复校。这一点不要省。4.3 非测温机型的红线只能做相对温差这里必须划一条线。海康的热成像产品分两类测温型和观测型。观测型也叫非测温型的热像仪只输出灰度图像那个 uint16 值反映的是红外辐射强度的相对大小没有经过辐射标定不能换算成绝对温度。我遇到过一次客户拿了一台观测型机器希望我们用 SDK 读温度。折腾了半天读出来的数值范围倒是有但换算成温度完全不符合物理规律。后来查规格书才确认这台机器根本没有温度输出通道PlayM4 的温度回调压根不会触发我们读到的其实是图像数据。所以买设备之前一定确认型号后缀和规格书里有没有测温字样或者用GET /ISAPI/Thermal/channels看有没有 thermometry 相关的子资源。观测型可以做相对温差成像比如找出画面里最热的区域、做温度伪彩分布但报出来的数字不能标摄氏度这个在合同和验收标准里要提前说清楚不然后期扯皮很麻烦。5. 把温度画到 Unity 里纹理刷新、伪彩与框选交互数据拿到了接下来是把它变成用户能看懂的画面。这一段的性能和视觉效果直接决定项目品质也是很多人做得最粗糙的地方——直接SetPixels32一个像素一个像素地写帧率掉到 20 以下还以为是自己电脑不行。5.1 从 ushort[] 到 Texture2D 的三种刷新方式与性能对比同一件事有三种做法性能差距能到十倍以上我按推荐程度从低到高说。第一种CPU 逐像素转 Color32 再 SetPixels32。最直观也最慢。384x288 一共 11 万个像素每个像素要做一次浮点乘法、一次查色带、三次字节写入然后SetPixels32还要再拷贝一次到原生内存最后Apply()触发一次纹理上传。单帧耗时轻松超过 15ms25fps 直接不可能。第二种LoadRawTextureData RGBA32。你在一个NativeArrayColor32里先把伪彩算好然后texture.LoadRawTextureData(array)一次性上传。省掉了SetPixels32的那次拷贝快不少但 CPU 端的色带计算还在仍然是瓶颈。第三种单通道 R16 纹理 Shader 查色带。这是我最推荐的方案。做法是把 uint16 数据原样上传成一个 R16 的单通道纹理伪彩映射全部放到 Shader 里做。CPU 端的工作量只剩一次 memcpyGPU 那边一个全屏的片元着色器处理几百万像素毫无压力。// 创建单通道 16 位纹理只在尺寸变化时创建一次 _tempTex new Texture2D(texWidth, texHeight, GraphicsFormat.R16_UNorm, TextureCreationFlags.None); _tempTex.filterMode FilterMode.Bilinear; _tempTex.wrapMode TextureWrapMode.Clamp; // 每帧只需这一次调用数据已经在 NativeArray 里 _tempTex.LoadRawTextureData(_rawArray); _tempTex.Apply(false, false);注意这里用的是R16_UNorm也就是把 0~65535 归一化到 0~1 存储。折算到温度的时候Shader 里的公式是T (texValue * 65535) * a b把a、b通过 Material 属性传进去就行。这样切量程的时候只需要改 Material 的两个 float不用重建纹理切换是瞬间完成的。Shader 部分核心就是这么几行half raw SAMPLE_TEXTURE2D(_TempTex, sampler_TempTex, uv).r * 65535.0; half temp raw * _ScaleA _OffsetB; half t (temp - _TempMin) / max(_TempMax - _TempMin, 0.001); half3 col tex2D(_PaletteTex, float2(saturate(t), 0.5)).rgb;_PaletteTex是一条一维色带纹理用 2D 纹理的中间行你可以在 Photoshop 里画好各种色带——铁红、彩虹、灰度、白热换色带就是换一张图非常方便。5.2 伪彩映射色带怎么做才像专业热像仪色带这件事看着简单实际上很影响观感。我自己用的几条经验色带的两端要留余量。如果把温度范围刚好卡在最低温和最高温上画面会满屏都是饱和的红和黑看起来非常糊。专业热像仪一般会做自动量程Auto Range统计当前帧的温度分布取 2% 和 98% 分位数作为显示范围。这样画面里同时有红有蓝有绿层次感立马就出来了。色带的过渡要平滑。用 256 像素宽的一维纹理FilterMode.Bilinear采样出来的过渡非常顺。如果用 5~6 个色块硬接会出现明显的色带断层。温度数值和颜色要对应起来看。界面上最好放一条色带图例标上当前的最低温和最高温。用户可以直观地知道红色大概是 60 度左右减少来回问这个红的是多少度。上采样不要用最近邻。热成像分辨率普遍偏低256x192 拉到全屏会有明显马赛克。用双线性插值会平滑很多但会让数值读数看起来糊——所以一般做两层底层用插值后的图像做视觉效果上层单独把温度数值用精确坐标标注出来两者互不影响。5.3 点选、框选与最高温追踪的坐标换算交互部分最容易出错的是坐标换算。这里涉及三套坐标系屏幕坐标鼠标位置、视频纹理 UV、温度矩阵像素坐标。三者之间隔着一次缩放和一次可能的偏移。温度矩阵到视频纹理的映射取决于你的视频流是怎么配置的。如果是纯热成像通道两者一般是等比例缩放tempUV videoUV如果是可见光和热成像融合的通道就要看设备端有没有做对齐标定。我的建议是自己做一次手动标定在设备画面上放一个十字标记然后在 Unity 里调整偏移和缩放参数让标记和温度矩阵的对应位置重合。虽然土但比相信设备文档靠谱。最高温追踪的实现很简单但要注意性能。在 11 万个 uint16 里找最大值纯 C# 循环大概 0.3ms完全可以接受。如果你想更省用IJobParallelFor分块求局部最大值再合并能压到 0.05ms 以内// 单线程版本够用且好调试 int maxIdx 0; ushort maxRaw 0; for (int i 0; i _rawArray.Length; i) { if (_rawArray[i] maxRaw) { maxRaw _rawArray[i]; maxIdx i; } } int mx maxIdx % texWidth; int my maxIdx / texWidth; // 注意纹理 y 轴方向可能需要翻转这里有个坑纹理的 y 轴和屏幕坐标的 y 轴方向相反。Unity 的纹理坐标原点在左下屏幕坐标原点在左上或者在 UI 里又是另一套。我第一次做的时候最高温标记总是上下颠倒查了半天才发现是这里。建议在代码里明确写一个FlipY的开关调试的时候一眼就能看出来对不对。6. 联调现场最常遇到的六类故障与排查顺序前面讲的都是顺利情况实际项目里大部分时间花在排错上。我把这些年遇到的高频问题整理成一张表按现象分类你可以对照着从最可能的开始查。现象最可能的原因排查入口登录返回失败错误码 1组件路径未设置 / HCNetSDKCom 缺失开启 SDK 日志看加载记录登录成功但预览黑屏PlayM4 端口未正确关联 / 解码回调类型不对检查 PlayM4_OpenStream 返回值编辑器正常打包后崩溃DLL 未随包输出 / 组件路径用了 Editor 专用路径检查导出目录下是否有 HCNetSDKCom温度回调不触发设备非测温型 / 回调类型注册错误用 ISAPI 探测 thermometry 资源温度值明显偏大或偏小量程切换后未重读标定系数对比设备 Web 页面显示值运行十几分钟后卡死回调线程内存分配过多 / 委托被 GCProfiler 看 GC Alloc6.1 从 NET_DVR_GetLastError 开始而不是瞎猜每次 HCNetSDK 的调用返回 false 之后第一件事是调NET_DVR_GetLastError()而不是去改参数试运气。错误码能覆盖绝大部分情况常见的几个我列一下1 是用户名密码错误或用户不存在2 是权限不足7 是连接设备失败网络不通或端口错47 是用户不存在还有一种比较隐蔽的是IP 通道达到上限一般出现在短时间内反复登录登出、句柄没释放的情况下。PlayM4 那套 API 的错误码是分开的用PlayM4_GetLastError取。这个很容易忘因为两个 GetLastError 名字太像我见过有人在 PlayM4 失败之后去调 HCNetSDK 的取错误码拿到一个跟当前问题完全无关的数字然后查文档查到怀疑人生。另外强烈建议开发期把NET_DVR_SetLogToFile的日志等级开到 3最高日志里会打印每次 API 调用的入参和返回很多问题看一眼日志就明白了。上线前记得关掉或者降级否则日志文件增长很快。6.2 打包后 DLL 找不到 / 回调线程崩溃这两个问题我放在一起说因为它们经常同时出现而且都和打包环境与编辑器环境的差异有关。DLL 找不到的典型表现是编辑器里跑得好好的打包出来启动就报DllNotFoundException。原因是 Unity 在编辑器里会从 Plugins 目录加载插件但打包之后插件是按平台设置的目录结构输出的如果你的 x86_64 目录没勾对DLL 就不会被输出。另外如果你用了绝对路径去 LoadLibrary 加载 HCNetSDKCom 里的组件那个路径在打包后必须重新计算——我一般统一用Application.streamingAssetsPath因为它在所有平台上都是可读的。回调线程崩溃的表现更隐蔽通常是运行一段时间之后随机崩溃堆栈指向 SDK 内部或者一个无效地址。除了前面说的委托被 GC 之外还有两个原因一是回调里调用了 UnityEngine 的 API包括Time.time、Debug.Log、GameObject.SetActive二是 SDK 的清理顺序不对。第二个问题的正确顺序是停止预览 → 注销所有回调 → 登出 → 清理 SDK → 销毁纹理和缓冲区。中间任何一步跳过都可能让回调在下一次触发时访问到已经释放的托管对象。提示在编辑器里调试回调崩溃时Unity 的堆栈信息经常被优化掉很难看。这时候用EditorApplication.isPlaying之外的方式定位——在回调入口和出口各写一行文件日志不是 Debug.Log崩溃后看日志最后停在哪一行往往能直接锁定问题。6.3 温度对不上的三种可能如果程序跑起来了温度也能读出来但和设备 Web 页面显示的对不上按这个顺序排查第一量程不一致。这是最常见的原因占了我遇到过的一半以上。设备切换了测温档位你的程序还是按旧系数换算。解决办法是在测温开始前主动读一次量程参数或者干脆在 UI 上让用户手动选。第二坐标没对齐。你取的像素点和设备规则点不在同一个物理位置测的压根不是同一个地方。验证方法很简单在 Unity 里把温度矩阵的某个固定像素的值打印出来和在设备 Web 上对着同一个点手动测量对比如果两个位置的实际温度差很多那数值对不上很正常。第三标定系数本身有问题。用前面讲的两点法重新标一次如果重新标完之后误差还是超过 1℃那就要怀疑设备本身有没有做过辐射标定或者镜头前面是不是装了不该装的滤光片、防护罩。红外窗口材料对透过率影响很大普通玻璃基本不透红外装了玻璃罩子测出来的温度完全是错的。7. 工程化落地帧率、内存与线程模型的实际取舍把功能跑通只是第一步真正上线之前还有几个决定项目能不能稳定运行的工程问题。这部分我不讲理论只说我在实际项目里最终采用的方案和理由。关于帧率热成像原始数据 25fps但我会把它降到 10fps 甚至 5fps 处理只把视频画面的帧率保持在高位。为什么因为人眼对温度数字的变化并不敏感一个温度数值每秒变 5 次已经显得很灵敏了而处理 25fps 的温度矩阵多出来的 CPU 开销和内存带宽完全没必要。做法是在回调里做一个计数器每 5 帧处理一次其余的直接丢弃。这个取舍看似粗暴但实际体验几乎无差别。关于内存所有缓冲区都在初始化时一次性分配好回调里只用Marshal.Copy往已有缓冲区里拷绝不 new。温度矩阵用NativeArrayushort分配一来避免 GC二来可以直接传给 Job 系统和LoadRawTextureData。这里有个细节NativeArray 必须用Allocator.Persistent因为它的生命周期跨帧用Temp或TempJob会在几帧之后报释放警告。// 初始化时一次性分配 _rawArray new NativeArrayushort(texWidth * texHeight, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); // 回调里只用阿里云拷贝到托管缓冲主线程再转到 NativeArray // 或者直接在回调里往 NativeArray 的指针写需要加锁注意线程安全 // 销毁时机确认所有回调都已停止之后再 Dispose void OnDestroy() { StopPreviewAndWait(); // 内部等待回调计数归零 if (_rawArray.IsCreated) _rawArray.Dispose(); }关于线程模型我用的是最朴素也最稳的一种——双缓冲 标志位。回调线程往缓冲 A 里写写完把标志位置 1主线程在 Update 里看到标志位是 1就把缓冲 A 的内容拷到缓冲 B或者直接交换指针然后把标志位清 0。这样回调永远不会阻塞在主线程的锁上主线程也不会读到写了一半的数据。代价是有一帧的延迟对测温来说完全无所谓。关于退出清理这一段代码必须严谨否则编辑器会时不时崩一次让人以为程序有问题。我的做法是维护一个回调计数器进回调加一、出回调减一OnDestroy里先停流再等计数器归零加超时保护然后才注销回调、登出、清理 SDK。加超时是因为极端情况下回调可能卡住不能无限等下去——超时之后就放弃清理并打日志总比整个进程挂掉好。关于跨平台如果你要做 Android 或 iOSWindows 版的 HCNetSDK 是用不了的。海康在移动端有自己的 SDKAndroid 那边是 Java 层的库需要通过AndroidJavaObject从 Unity 侧调用中间再串一层 JNI。这条路我走过一次工作量大概是 PC 端的两到三倍而且调试体验很差。如果不是硬性要求移动端强烈建议在 PC 上跑或者让设备端出一个 HTTP 接口Unity 这边只做展示。用 ISAPI 方案的话Android 和 iOS 反倒变得非常简单因为只需要发 HTTP 请求这也是我在移动端项目里优先选混合方案的原因。再补充一个实际用下来的小技巧调试阶段把温度矩阵同时输出成一张 BMP 或者 PNG 存到本地出问题的时候可以直接用图片查看器看比在 Unity 里盯着一个 Texture2D 效率高得多。PlayM4 有现成的转 BMP 接口PlayM4_ConvertToBmpFile一行调用就能存图排错的时候非常省时间。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。