本文最后更新于:2026年8月5日 中午
一台海康网络相机,把图像拿出来的通路不止一条。本文按协议栈把取图方式分成 SDK、HTTP(ISAPI)、流协议、封装层四级,记录各自的端口、认证与可操作接口,并附 Python 实测脚本与输出。
分层模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ┌─────────────────────────────────────────────────────────────┐ │ ④ 封装层(套壳,底层走 ①②③) │ │ OpenCV · ffmpeg · GStreamer · VLC · Python 包 │ │ iVMS-4200 · 浏览器 Web 页 · 萤石/Hik-Connect 云 │ ├─────────────────────────────────────────────────────────────┤ │ ③ 流协议层(跨厂商通用) │ │ RTSP/RTP (554) · ONVIF · GB28181 (SIP) │ ├─────────────────────────────────────────────────────────────┤ │ ② HTTP 层(海康私有但开放:ISAPI) │ │ 抓图 · MJPEG 预览 · 录像下载 · 事件订阅 端口 80 / 443 │ ├─────────────────────────────────────────────────────────────┤ │ ① SDK 层(海康私有协议,能力最全) │ │ HCNetSDK(libhcnetsdk.so) 端口 8000 │ └─────────────────────────────────────────────────────────────┘
①②③ 是彼此正交的数据出口;④ 不提供新出口,只把底下三层的接口封装成更便利的工具。判断一个工具属于哪一层,看它实际收发数据走的是 SDK 私有协议、HTTP、还是 RTSP。
连通性验证
任何方式取不到图,先确认链路,再谈代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 ping -c 2 192.168.2.2 nmap -p 80,443,554,8000,5060 192.168.2.2 curl --digest -u admin:your_password -o probe.jpg \ "http://192.168.2.2/ISAPI/Streaming/channels/101/picture" file probe.jpg ffprobe -rtsp_transport tcp "rtsp://admin:your_password@192.168.2.2:554/Streaming/Channels/101"
实测结果(192.168.2.2):ping 通(rtt ≈ 1.7 ms);nmap 80/443/554/8000 OPEN、5060 closed;curl 得 JPEG 2560×1440;ffprobe 得 h264 / Main / 2560×1440 / 25fps。四条主干(HTTP / RTSP / SDK)都通。
端口与认证
服务
端口
协议
认证
Web 配置页 / ISAPI
80 / 443
HTTP(S)
HTTP Digest
RTSP
554
TCP
Basic / Digest
HCNetSDK
8000
私有 TCP
SDK 登录 NET_DVR_Login_V40
ONVIF
80 + 3702(发现)
SOAP
WS-UsernameToken
GB28181
5060
SIP
SIP Digest
不依赖 nmap,纯 Python 探 TCP 端口、requests 探 ISAPI 认证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 """验证海康相机 192.168.2.2 的 TCP 端口连通性与 ISAPI 认证。 对应文档《海康相机取图方式总览.md》「端口与认证」。 """ import socketimport requestsfrom requests.auth import HTTPDigestAuth HOST = "192.168.2.2" USER = "admin" PWD = "your_password" TCP_PORTS = { 80 : "Web / ISAPI (HTTP)" , 443 : "Web / ISAPI (HTTPS)" , 554 : "RTSP" , 8000 : "HCNetSDK" , 5060 : "GB28181 / SIP" , }def tcp_probe (host, port, timeout=2.0 ): """TCP 三次握手探测:OPEN / closed / filtered / error。""" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(timeout) try : s.connect((host, port)) return "OPEN" except socket.timeout: return "filtered/timeout" except ConnectionRefusedError: return "closed" except OSError as e: return f"error({e.errno} )" finally : s.close()def banner (title ): print ("\n" + "=" * 60 ) print (title) print ("=" * 60 )def main (): print (f"target host : {HOST} " ) print (f"credentials : {USER} / {'*' * len (PWD)} " ) banner("1) TCP 端口探测 (socket.connect)" ) print (f" {'port' :<6 } {'state' :<20 } service" ) for port, name in TCP_PORTS.items(): print (f" {port:<6 } {tcp_probe(HOST, port):<20 } {name} " ) banner("2) ISAPI 认证探测 (HTTP Digest)" ) base = f"http://{HOST} " cap_url = f"{base} /ISAPI/System/capabilities" pic_url = f"{base} /ISAPI/Streaming/channels/101/picture" r = requests.get(cap_url, timeout=5 ) print (f" [no-auth] GET {cap_url} " ) print (f" status : {r.status_code} " ) print (f" WWW-Authenticate : {r.headers.get('WWW-Authenticate' , '(none)' )} " ) r = requests.get(cap_url, auth=HTTPDigestAuth(USER, PWD), timeout=10 ) print (f" [digest ] GET {cap_url} " ) print (f" status : {r.status_code} body: {len (r.content)} bytes" ) r = requests.get(pic_url, auth=HTTPDigestAuth(USER, PWD), timeout=10 ) ct = r.headers.get("Content-Type" , "(none)" ) print (f" [digest ] GET {pic_url} " ) print (f" status : {r.status_code} Content-Type: {ct} body: {len (r.content)} bytes" ) if r.status_code == 200 and ct.startswith("image/" ): with open ("probe.jpg" , "wb" ) as f: f.write(r.content) print (" saved -> probe.jpg" ) elif r.status_code == 401 : print (" 认证失败:账号/密码错误或无权限" ) elif r.status_code == 404 : print (" 端点不存在:主码流通道号可能不是 101" )if __name__ == "__main__" : main()
实测输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 target host : 192.168.2.2 credentials : admin / ***** ***** ** ============================================================ 1) TCP 端口探测 (socket.connect) ============================================================ port state service 80 OPEN Web / ISAPI (HTTP) 443 OPEN Web / ISAPI (HTTPS) 554 OPEN RTSP 8000 OPEN HCNetSDK 5060 closed GB28181 / SIP ============================================================ 2) ISAPI 认证探测 (HTTP Digest) ============================================================ [no-auth] GET http://192.168.2.2/ISAPI/System/capabilities status : 401 WWW-Authenticate : Digest qop="auth", realm="IP Camera(FW911)", nonce="...", stale="FALSE" [digest ] GET http://192.168.2.2/ISAPI/System/capabilities status : 200 body: 17589 bytes [digest ] GET http://192.168.2.2/ISAPI/Streaming/channels/101/picture status : 200 Content-Type: image/jpeg; charset="UTF-8" body: 193983 bytes saved -> probe.jpg
要点:80/443/554/8000 全 OPEN、5060 closed(设备未启用 GB28181 SIP);无凭证返回 401 且响应头带 WWW-Authenticate: Digest,证实采用 HTTP Digest(非 Basic),realm 还暴露了设备型号代号;digest 凭证把 401 翻成 200,即账号有效、端点正确。「无凭证 401 + 有凭证 200」这个状态翻转本身就是认证链路的最小证明,比单纯 ping 通端口可靠。
实测抓到的图:
SDK 层(HCNetSDK)
能力最全、延迟低,但耦合海康且依赖 .so 环境。Python 无官方绑定,须经 ctypes 手写结构体映射。按返回形态分四条路径:
SDK 函数
返回
形态
NET_DVR_CaptureJPEGPicture
设备端编码的全分辨率 JPEG
单张 JPEG
NET_DVR_CapturePicture
从已开预览句柄抓一帧
BMP
NET_DVR_RealPlay_V40 + NET_DVR_SaveRealData / 回调
实时码流
H.264 / 265 码流
NET_DVR_GetFileByName / GetFileByTime
设备 SD 卡 / NVR 历史录像
录像文件
环境配置(Linux)
HCNetSDK 在 Linux 下的失败几乎都来自库加载:
项
说明
LD_LIBRARY_PATH
libhcnetsdk.so 的 NEEDED 库(libhpr.so、libHCCore.so 等)无 RPATH,必须把 <sdk>/lib 加进搜索路径,否则 CDLL 直接报 cannot open shared object file。
组件库路径
lib/HCNetSDKCom/ 下组件库由主库运行时 dlopen,用 NET_DVR_SetSDKInitCfg(2, SDK_PATH) 告知 lib/ 目录。
OpenSSL 版本
SDK 自带 libcrypto.so.1.1 / libssl.so.1.1(OpenSSL 1.1)。系统库若是 OpenSSL 3.x,不能混用 ——用 SetSDKInitCfg(3/4) 显式指向 SDK 自带的那两个,避免符号冲突。
登录模式
byLoginMode=0(Private)走 8000 私有协议端口;1 走 ISAPI;2 自适应。
glibc
SDK 编译于较老 glibc,新发行版一般向下兼容;反向则可能 GLIBC_XXX not found。
验证依赖齐全:ldd lib/libhcnetsdk.so(全部 => 解析到、无 not found)。
取图(Python ctypes)
关键结构 NET_DVR_USER_LOGIN_INFO(匿名 struct,字段须与头文件顺序、类型一致,ctypes 按平台默认对齐,与 gcc 一致):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 """通过 HCNetSDK (libhcnetsdk.so) 抓取单张 JPEG(设备端编码)。 对应文档《海康相机取图方式总览.md》「SDK 层」。 运行前必须让动态链接器找得到 SDK 依赖库: export LD_LIBRARY_PATH=<sdk>/lib """ import ctypesimport osimport sysimport time SDK_LIB_DIR = "/path/to/EN-HCNetSDKV6.1.9.4_build20220412_linux64/lib" HOST = "192.168.2.2" PORT = 8000 USER = "admin" PWD = "your_password" CHANNEL = 1 PIC_SIZE = 0xff PIC_QUALITY = 0 OUT = "sdk_test.jpg" HCNetSDK = ctypes.CDLL(os.path.join(SDK_LIB_DIR, "libhcnetsdk.so" ))def _proto (): HCNetSDK.NET_DVR_Init.restype = ctypes.c_bool HCNetSDK.NET_DVR_SetConnectTime.restype = ctypes.c_bool HCNetSDK.NET_DVR_SetSDKInitCfg.restype = ctypes.c_bool HCNetSDK.NET_DVR_Login_V40.restype = ctypes.c_long HCNetSDK.NET_DVR_Login_V40.argtypes = [ctypes.c_void_p, ctypes.c_void_p] HCNetSDK.NET_DVR_Logout.restype = ctypes.c_bool HCNetSDK.NET_DVR_Logout.argtypes = [ctypes.c_long] HCNetSDK.NET_DVR_CaptureJPEGPicture.restype = ctypes.c_bool HCNetSDK.NET_DVR_CaptureJPEGPicture.argtypes = [ ctypes.c_long, ctypes.c_long, ctypes.c_void_p, ctypes.c_char_p] HCNetSDK.NET_DVR_GetLastError.restype = ctypes.c_uint32 HCNetSDK.NET_DVR_Cleanup.restype = ctypes.c_boolclass NET_DVR_LOCAL_SDK_PATH (ctypes.Structure): _fields_ = [("sPath" , ctypes.c_char * 256 ), ("byRes" , ctypes.c_ubyte * 128 )]class NET_DVR_USER_LOGIN_INFO (ctypes.Structure): _fields_ = [ ("sDeviceAddress" , ctypes.c_char * 129 ), ("byUseTransport" , ctypes.c_ubyte), ("wPort" , ctypes.c_uint16), ("sUserName" , ctypes.c_char * 64 ), ("sPassword" , ctypes.c_char * 64 ), ("cbLoginResult" , ctypes.c_void_p), ("pUser" , ctypes.c_void_p), ("bUseAsynLogin" , ctypes.c_uint32), ("byProxyType" , ctypes.c_ubyte), ("byUseUTCTime" , ctypes.c_ubyte), ("byLoginMode" , ctypes.c_ubyte), ("byHttps" , ctypes.c_ubyte), ("iProxyID" , ctypes.c_int32), ("byVerifyMode" , ctypes.c_ubyte), ("byRes3" , ctypes.c_ubyte * 119 ), ]class NET_DVR_JPEGPARA (ctypes.Structure): _fields_ = [("wPicSize" , ctypes.c_uint16), ("wPicQuality" , ctypes.c_uint16)]def err (msg ): print (f"{msg} (NET_DVR_GetLastError={HCNetSDK.NET_DVR_GetLastError()} )" )def set_sdk_paths (): """告诉 SDK 组件库(HCNetSDKCom)与加解密库(libcrypto/libssl)所在路径。""" p = NET_DVR_LOCAL_SDK_PATH(); p.sPath = SDK_LIB_DIR.encode() HCNetSDK.NET_DVR_SetSDKInitCfg(2 , ctypes.byref(p)) pe = NET_DVR_LOCAL_SDK_PATH(); pe.sPath = os.path.join(SDK_LIB_DIR, "libcrypto.so.1.1" ).encode() HCNetSDK.NET_DVR_SetSDKInitCfg(3 , ctypes.byref(pe)) ps = NET_DVR_LOCAL_SDK_PATH(); ps.sPath = os.path.join(SDK_LIB_DIR, "libssl.so.1.1" ).encode() HCNetSDK.NET_DVR_SetSDKInitCfg(4 , ctypes.byref(ps)) def main (): _proto() if not os.environ.get("LD_LIBRARY_PATH" ): print ("警告:未设置 LD_LIBRARY_PATH,依赖库可能加载失败" ) set_sdk_paths() if not HCNetSDK.NET_DVR_Init(): err("NET_DVR_Init 失败" ); sys.exit(1 ) HCNetSDK.NET_DVR_SetConnectTime(5000 , 3 ) info = NET_DVR_USER_LOGIN_INFO() info.sDeviceAddress = HOST.encode() info.wPort = PORT info.sUserName = USER.encode() info.sPassword = PWD.encode() info.byLoginMode = 0 info.byHttps = 0 devbuf = ctypes.create_string_buffer(4096 ) uid = HCNetSDK.NET_DVR_Login_V40(ctypes.byref(info), devbuf) if uid < 0 : err("登录失败" ); HCNetSDK.NET_DVR_Cleanup(); sys.exit(1 ) print (f"登录成功 userID={uid} " ) jpg = NET_DVR_JPEGPARA() jpg.wPicSize = PIC_SIZE jpg.wPicQuality = PIC_QUALITY t0 = time.monotonic() ok = HCNetSDK.NET_DVR_CaptureJPEGPicture(uid, CHANNEL, ctypes.byref(jpg), OUT.encode()) dt = (time.monotonic() - t0) * 1000 if not ok: err("抓图失败" ); HCNetSDK.NET_DVR_Logout(uid); HCNetSDK.NET_DVR_Cleanup(); sys.exit(1 ) print (f"抓图成功 -> {OUT} {os.path.getsize(OUT)} bytes {dt:.0 f} ms" ) HCNetSDK.NET_DVR_Logout(uid) HCNetSDK.NET_DVR_Cleanup()if __name__ == "__main__" : main()
实测输出:
1 2 3 4 loop[2] find 8 mac and 11 ip 登录成功 userID =0 抓图成功 -> sdk_test.jpg 389816 bytes 124 ms
loop[2] find 8 mac and 11 ip 是 SDK 启动时内置的局域网设备搜索日志,非报错,可忽略。CaptureJPEGPicture 走设备端 JPEG 编码器、不经预览链路,分辨率等于通道主分辨率,质量由 wPicQuality 设定(0 最高 / 1 较好 / 2 一般)。
JPEG 质量档与体积
wPicQuality 决定设备端 JPEG 的量化表,进而决定体积与质量。同场景一次登录连抓 q0/q1/q2 + ISAPI:
来源
wPicQuality
体积
亮度量化表均值
SDK
0(最高)
389 KB
23.1
SDK
1(较好)
231 KB
46.1
SDK
2(一般)
180 KB
72.1
ISAPI /picture
设备默认
180 KB
72.1
为何「亮度量化表均值」代表质量 :JPEG 有损只发生在一步——8×8 块 DCT 后,把 64 个系数除以量化表对应步长、再取整 。步长小→系数几乎不变→细节保留(质量高、文件大);步长大→系数多被除到 0→高频细节丢弃(质量低、文件小)。64 个步长的均值即整体量化激进程度,与质量反相关。它是代理量、非绝对 Q——真质量要 PSNR/SSIM 对未压缩原图,设备内拿不到,故量化表是能拿到的最好代理。
几个推论:ISAPI /picture 的质量与 SDK q2 逐位一致(均值均 72.1),即 ISAPI 走通道默认「一般」档;体积比随场景浮动(同为 q0、均值 23.1,细节多的场景 1.08 MB、平缓场景 389 KB,而 q2 恒 ~180 KB)——量化表只决定「质量天花板」,画面高频多则同质量下文件更大。判断质量别用 identify -format %q(海康自定义量化表,全报 16),直接读 PIL.Image.open(f).quantization[0] 取平均。
HTTP 层(ISAPI)
ISAPI 是基于 HTTP 的 REST 接口,开放、跨语言,无需 SDK。浏览器 Web 页与多数 Python 包抓图,底层即此处。抓图端点:
1 2 3 GET http:// 192.168 .2.2 /ISAPI/ Streaming/channels/ 101 /picture GET http:// 192.168 .2.2 /ISAPI/ Streaming/channels/ 1 /picture
能力
端点
JPEG 抓图
/ISAPI/Streaming/channels/N/picture
MJPEG 预览
/ISAPI/Streaming/channels/102/httpPreview
录像检索 / 下载
/ISAPI/ContentMgmt/search、/downloadURI
事件订阅
/ISAPI/Event/notification/alertStream
能力探测
/ISAPI/System/capabilities
requests + HTTPDigestAuth,单次 GET 拿 JPEG,附纯 Python 解析 SOF 段读分辨率(免装 PIL):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 """通过 ISAPI HTTP 接口抓取单张 JPEG(设备端编码)。 对应文档《海康相机取图方式总览.md》「HTTP 层」。 无需 SDK,仅依赖 requests。认证用 HTTP Digest。 """ import sysimport timeimport requestsfrom requests.auth import HTTPDigestAuth HOST = "192.168.2.2" USER = "admin" PWD = "your_password" def jpeg_size (data ): """纯 Python 解析 JPEG SOF 段,返回 (width, height),失败返回 (None, None)。""" i = 2 while i + 9 < len (data): if data[i] != 0xFF : return None , None marker = data[i + 1 ] if marker in (0xC0 , 0xC1 , 0xC2 ): h = int .from_bytes(data[i + 5 :i + 7 ], "big" ) w = int .from_bytes(data[i + 7 :i + 9 ], "big" ) return w, h seg = int .from_bytes(data[i + 2 :i + 4 ], "big" ) i += 2 + seg return None , None def grab (channel, out ): url = f"http://{HOST} /ISAPI/Streaming/channels/{channel} /picture" t0 = time.monotonic() r = requests.get(url, auth=HTTPDigestAuth(USER, PWD), timeout=10 ) dt = (time.monotonic() - t0) * 1000 print (f"GET {url} " ) print (f" status={r.status_code} Content-Type={r.headers.get('Content-Type' )} " ) print (f" size={len (r.content)} bytes latency={dt:.0 f} ms" ) if r.status_code == 200 : w, h = jpeg_size(r.content) print (f" 分辨率={w} x{h} " if w else " 分辨率=(解析失败)" ) with open (out, "wb" ) as f: f.write(r.content) print (f" saved -> {out} " ) return True print (f" 失败:body={r.content[:200 ]!r} " ) return False if __name__ == "__main__" : print ("== 主码流 channel 101 ==" ) grab(101 , "snap_main.jpg" ) print ("\n== 子码流 channel 102(对照,分辨率低)==" ) grab(102 , "snap_sub.jpg" )
实测输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 == 主码流 channel 101 == GET http://192.168.2.2/ISAPI/Streaming/channels/101/picture status=200 Content-Type=image/jpeg; charset="UTF-8" size=182667 bytes latency=114 ms 分辨率=2560x1440 saved -> snap_main.jpg == 子码流 channel 102(对照,分辨率低)== GET http://192.168.2.2/ISAPI/Streaming/channels/102/picture status=200 Content-Type=image/jpeg; charset="UTF-8" size=14522 bytes latency=645 ms 分辨率=640x360 saved -> snap_sub.jpg
一个反直觉点:子码流单次延迟(645 ms)反而高于主码流(114 ms)。/picture 要的是一帧完整 JPEG,主码流持续编码、当前帧立即可取;子码流常空闲,设备需重启编码器并等一个 I 帧才能产出。批量抓取时差异会被摊平。
ISAPI 的 JPEG 质量本层不可调:实测 /picture?quality=0、?JPEGQuality=0、?snapShotQuality=0、?resolution=0 均不改变量化表(均值恒 72.1);通道 capability 只暴露视频流质量(fixedQuality),无抓图 JPEG 质量档。即 ISAPI 抓图锁死在设备默认(= SDK q2),要拿高质量图只能走 SDK。
流协议层
RTSP / RTP
URL(新旧固件格式):
1 2 3 4 rtsp:// admin:pass@192.168 .2.2 :554 /Streaming/ Channels/101 rtsp:// admin:pass@192.168 .2.2 :554 /Streaming/ Channels/102 rtsp:// admin:pass@192.168 .2.2 :554 /h264/ ch1/main/ av_stream rtsp:// admin:pass@192.168 .2.2 :554 /h264/ ch1/sub/ av_stream
返回 H.264 / H.265 编码码流,需解码才得像素;解码引入 YUV→BGR 转换与 4:2:0 二次采样损失。主码流为全分辨率,子码流为低分辨率预览。用 ffmpeg 套件直接作为 RTSP 客户端验证流:
1 2 3 4 5 6 7 8 9 10 ffprobe -v error -rtsp_transport tcp \ -show_entries stream=codec_name,profile,width,height,r_frame_rate \ -of default=noprint_wrappers=1 \ "rtsp://admin:your_password@192.168.2.2:554/Streaming/Channels/101" ffmpeg -v error -rtsp_transport tcp -y -i \ "rtsp://admin:your_password@192.168.2.2:554/Streaming/Channels/101" \ -frames:v 1 -q:v 2 rtsp_frame.jpg
实测输出:
1 2 3 4 5 6 7 8 codec_name =h264profile =Mainwidth =2560 height =1440 r_frame_rate =25 /1 codec_name =pcm_alaw profile =unknownr_frame_rate =0 /0
主码流 H.264 Main @ 2560×1440 25fps。注意 RTSP 给的是编码码流,帧由主机端解码 得到——rtsp_frame.jpg 是 ffmpeg 在本机解码 H.264 后重编的 JPEG(质量由 -q:v 决定,非设备端),比设备端 JPEG 多一次解码损失。-rtsp_transport tcp 走 TCP 传 RTP,避免 UDP 丢包花屏。
ONVIF / GB28181
ONVIF 是厂商中立的 SOAP 标准,海康默认关闭,需在 Web 配置页开启并设账号;Python 用 onvif-zeep,GetSnapshotUri() 返回的抓图 URL 仍指向 ISAPI。GB28181 是公安部视频联网国标,基于 SIP+RTP,需配信令服务器。两者价值在跨厂商 / 平台联网,单机取图用不到。
封装层
不提供新数据出口,把 ①②③ 包装成工具:
工具
封装对象
OpenCV cv2.VideoCapture
RTSP(经 ffmpeg)
ffmpeg / ffprobe
RTSP / HTTP
GStreamer
RTSP 流水线
VLC / mpv
RTSP(GUI)
Python 包(hikvisionapi 等)
ISAPI
iVMS-4200
SDK + ISAPI
浏览器 Web 页
ISAPI
cv2.VideoCapture 经 ffmpeg 后端拉 RTSP,返回 numpy 数组(BGR),可直接喂标定 / 检测算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 """用 OpenCV(封装层)经 RTSP 取一帧。 对应文档《海康相机取图方式总览.md》「封装层」。 OpenCV 的 VideoCapture 经 ffmpeg 后端拉 RTSP,返回 numpy 数组(BGR)。 注意:密码含 @,RTSP URL 中须编码为 %40。 """ import cv2 URL = "rtsp://admin:your_password@192.168.2.2:554/Streaming/Channels/101" cap = cv2.VideoCapture(URL, cv2.CAP_FFMPEG) ok, frame = cap.read() cap.release()print (f"opened={ok} " )if ok: print (f"frame: shape={frame.shape} dtype={frame.dtype} (HxWxC, BGR)" ) cv2.imwrite("opencv_frame.jpg" , frame, [int (cv2.IMWRITE_JPEG_QUALITY), 95 ]) print ("saved -> opencv_frame.jpg (主机端 JPEG 质量 95)" )else : print ("取帧失败:检查 URL / 网络 / 后端" )
实测输出:
1 2 3 opened =True frame : shape=(1440 , 2560 , 3 ) dtype =uint8 (HxWxC , BGR )saved -> opencv_frame.jpg (主机端 JPEG 质量 95 )
返回 (1440, 2560, 3) uint8,H×W×C,BGR 顺序(OpenCV 惯例,喂 RGB 库前需 cv2.cvtColor)。封装层的价值是把 RTSP 包成 cap.read() 一行取帧、返回 numpy 直接对接下游管线;代价是经解码、且受视频码率限制——存盘体积 ≠ 源质量 :opencv_frame.jpg 的 665KB 是 JPEG_QUALITY=95 在主机端撑大的,并非源质量高。恰恰相反,RTSP 帧来自主码流 H.264,CBR 2Mbps / 25fps ≈ 10KB/帧 的压缩预算,解码后用 95 高质量重存只是忠实保留已被压掉细节的画面。故 OpenCV/RTSP 这张体积最大、保真反而最低;而 SDK q0 设备端 JPEG 体积更小、保真更高(相机为单张静帧独立编码,不受视频码率分摊)。
被动推送
相机主动输出图像,适合无人值守或触发式采集:SD 卡本地存储、NAS(NFS/SMB)、FTP 上传(定时或报警)、邮件附件、ISAPI alertStream 长连接事件订阅。
方法对比
方法
层 / 端口
返回
单张/连续
延迟
依赖
适用
ISAPI /picture
② 80
JPEG
单张
低
requests / curl
单张取图(质量不可调=q2)
SDK CaptureJPEGPicture
① 8000
JPEG
单张
低(质量↑耗时↑)
libhcnetsdk.so
单张取图(质量可调 q0/1/2)
SDK RealPlay 抓帧 / 存流
① 8000
BMP / 码流
连续
低
SDK + 解码
连续采集
RTSP + OpenCV / ffmpeg
③ 554
解码帧
连续
中
ffmpeg
视频 / 连续(主机解码)
ONVIF GetSnapshotUri
③ 80
指向 ISAPI
单张
中
onvif-zeep
跨厂商
SDK GetFileByName/Time
① 8000
录像
离线
高
SDK
回放取帧
SD / NAS / FTP / 邮件
被动
JPEG / 录像
触发
—
存储服务
批量 / 报警采集
标定取图
标定取图要求单张、全分辨率、像素级无损、参数冻结,与常规监控的连续流取向相反。取图前需关闭相机内部一切会改变像素几何或多次合成的处理:
项目
原因
镜头畸变矫正 / LDC
内部已去畸变,标不出真实畸变
数字防抖 / EIS
裁切、平移画面
宽动态 WDR
多帧合成
降噪 3DNR
时域空域滤波影响亚像素角点
自动曝光 AE
改手动固定快门 / 增益
自动白平衡 AWB
改固定
自动对焦 AF
改手动固定焦距
ROI 裁剪 / 镜像 / 翻转 / 强光抑制
改变像素映射
取主码流 / 主分辨率通道(101 / channel 1)。取图来源上,设备端 JPEG(ISAPI /picture、SDK CaptureJPEGPicture)不经解码,比 RTSP / OpenCV 解码帧更接近原始像素;其中 SDK 可选最高质量档(q0),是标定静态图的首选。内参是相机几何属性、与 JPEG 质量正交——q0 只是给出更精的估计,标定结果可用于任意质量的图像,前提是对焦、焦距、LDC 等几何相关设置在标定与使用时锁成同一状态。
文章链接:
https://www.zywvvd.com/notes/study/camera-imaging/hk-image-acquisition/hk-image-acquisition/