本文最后更新于: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
# 1) 通不通
ping -c 2 192.168.2.2

# 2) 关键端口开没开:80=ISAPI/Web 554=RTSP 8000=SDK 5060=GB28181
nmap -p 80,443,554,8000,5060 192.168.2.2 # 无 nmap 用 python socket,见下文

# 3) ISAPI 抓图(开销最低的能力验证,HTTP Digest)
curl --digest -u admin:your_password -o probe.jpg \
"http://192.168.2.2/ISAPI/Streaming/channels/101/picture"
file probe.jpg # 期望:JPEG image data

# 4) RTSP 探流(密码含特殊字符如 @ 时,URL 中须编码为 %40)
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""验证海康相机 192.168.2.2 的 TCP 端口连通性与 ISAPI 认证。

对应文档《海康相机取图方式总览.md》「端口与认证」。
"""
import socket
import requests
from requests.auth import HTTPDigestAuth

HOST = "192.168.2.2"
USER = "admin"
PWD = "your_password"

# 端口表里的 TCP 端口(UDP/ONVIF 发现 3702 不在此列,需单独处理)
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)}")

# ---- 1) TCP 端口探测 ----
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}")

# ---- 2) ISAPI 认证探测 ----
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"

# 2a 无凭证 → 预期 401 + WWW-Authenticate: Digest ...
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)')}")

# 2b digest 凭证 → 预期 200
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")

# 2c digest 抓图 → 预期 200 image/jpeg
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.solibHCCore.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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""通过 HCNetSDK (libhcnetsdk.so) 抓取单张 JPEG(设备端编码)。

对应文档《海康相机取图方式总览.md》「SDK 层」。
运行前必须让动态链接器找得到 SDK 依赖库:
export LD_LIBRARY_PATH=<sdk>/lib
"""
import ctypes
import os
import sys
import 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 # 设备逻辑通道号(IP 相机一般从 1 起;注意与 RTSP 的 101 区分)
PIC_SIZE = 0xff # 0xff = 使用当前码流分辨率
PIC_QUALITY = 0 # 0 最好 / 1 较好 / 2 一般
OUT = "sdk_test.jpg"

# ---------- 加载 SDK ----------
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_bool


# ---------- 结构体 ----------
class 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)) # NET_SDK_INIT_CFG_SDK_PATH
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)) # LIBEAY_PATH
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)) # SSLEAY_PATH


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 # 0-Private(8000 私有协议)
info.byHttps = 0
devbuf = ctypes.create_string_buffer(4096) # NET_DVR_DEVICEINFO_V40,仅接收不解析
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:.0f} 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
# file: JPEG image data, baseline, 2560x1440

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 # 通道1
# 认证:HTTP Digest 返回:image/jpeg
能力 端点
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""通过 ISAPI HTTP 接口抓取单张 JPEG(设备端编码)。

对应文档《海康相机取图方式总览.md》「HTTP 层」。
无需 SDK,仅依赖 requests。认证用 HTTP Digest。
"""
import sys
import time
import requests
from 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 # 跳过 SOI(FF D8)
while i + 9 < len(data):
if data[i] != 0xFF:
return None, None
marker = data[i + 1]
if marker in (0xC0, 0xC1, 0xC2): # SOF0 / SOF1 / SOF2
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:.0f} 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__":
# 主码流(101) vs 子码流(102):标定只用主码流,子码流仅作对照
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
# 1) 探流:编码 / 分辨率 / 帧率
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"

# 2) 抓一帧(主机端解码 H.264 再编 JPEG,-q:v 2 高质量)
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=h264
profile=Main
width=2560
height=1440
r_frame_rate=25/1
codec_name=pcm_alaw # 音频轨
profile=unknown
r_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-zeepGetSnapshotUri() 返回的抓图 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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""用 OpenCV(封装层)经 RTSP 取一帧。

对应文档《海康相机取图方式总览.md》「封装层」。
OpenCV 的 VideoCapture 经 ffmpeg 后端拉 RTSP,返回 numpy 数组(BGR)。
注意:密码含 @,RTSP URL 中须编码为 %40。
"""
import cv2

# 主码流;密码替换为实际值(若含 @ 等特殊字符,URL 中须编码为 %40)
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/


“觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭”

微信二维码

微信支付

支付宝二维码

支付宝支付

海康相机取图方式整理
https://www.zywvvd.com/notes/study/camera-imaging/hk-image-acquisition/hk-image-acquisition/
作者
Yiwei Zhang
发布于
2026年7月9日
许可协议