hiktemp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hiktemp/__init__.py ADDED
@@ -0,0 +1,76 @@
1
+ """
2
+ hiktemp
3
+ ~~~~~~~
4
+ Minimal radiometric temperature extraction from Hikvision thermal cameras.
5
+
6
+ >>> from hiktemp import hiktemp
7
+ >>> from hiktemp import hiktemp
8
+ >>> frame = hiktemp("http://192.168.1.1", "admin", "password")
9
+ >>> frame.matrix # np.ndarray (H,W) float32 °C
10
+ >>> frame.hotspot() # (row, col) global
11
+ >>> frame.hotspot(lo=28, hi=29) # (row, col) within band
12
+ >>> frame.masked(lo=28, hi=29) # NaN outside band
13
+
14
+ Only requires: requests, numpy.
15
+ Visualization (colormap, bgr, rgba) is left to the caller via matplotlib/cv2.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import requests
21
+
22
+ from ._fetch import fetch
23
+ from ._frame import ThermalFrame
24
+
25
+ __version__ = "0.1.0"
26
+ __all__ = ["hiktemp", "ThermalFrame"]
27
+
28
+
29
+ def hiktemp(
30
+ url: str,
31
+ username: str,
32
+ password: str,
33
+ *,
34
+ channel: int = 1,
35
+ timeout: float = 10.0,
36
+ session: requests.Session | None = None,
37
+ ) -> ThermalFrame:
38
+ """
39
+ Pull one radiometric frame from a Hikvision thermal camera.
40
+
41
+ Parameters
42
+ ----------
43
+ url : str
44
+ Base URL of the camera, e.g. ``"http://192.168.1.1"``.
45
+ username : str
46
+ Digest-auth username.
47
+ password : str
48
+ Digest-auth password.
49
+ channel : int
50
+ ISAPI thermal channel, default 1.
51
+ timeout : float
52
+ HTTP request timeout in seconds.
53
+ session : requests.Session, optional
54
+ Reuse an existing session (avoids repeated digest handshakes).
55
+
56
+ Returns
57
+ -------
58
+ ThermalFrame
59
+ ``.matrix`` — raw float32 °C array (H, W)
60
+ ``.jpeg`` — raw JPEG bytes (AGC, display only)
61
+ ``.meta`` — camera descriptor dict
62
+ ``.min/max/mean/std`` — scalar stats over full matrix
63
+ ``.hotspot(lo, hi)`` — (row, col) of peak temp, optional band
64
+ ``.coldspot(lo, hi)`` — (row, col) of min temp, optional band
65
+ ``.masked(lo, hi)`` — matrix with out-of-band pixels = NaN
66
+ ``.alpha(lo, hi)`` — float32 (H,W) quintic smoothstep mask
67
+ """
68
+ meta, jpeg, matrix = fetch(
69
+ url,
70
+ username,
71
+ password,
72
+ channel=channel,
73
+ timeout=timeout,
74
+ session=session,
75
+ )
76
+ return ThermalFrame(matrix, jpeg, meta)
hiktemp/_fetch.py ADDED
@@ -0,0 +1,81 @@
1
+ """
2
+ hiktemp._fetch
3
+ ~~~~~~~~~~~~~~
4
+ HTTP layer: digest auth, multipart parse, float32 blob decode.
5
+ Only depends on `requests` and `numpy`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import requests
12
+ from requests.auth import HTTPDigestAuth
13
+
14
+ import numpy as np
15
+
16
+ _ENDPOINT = "/ISAPI/Thermal/channels/{ch}/thermometry/jpegPicWithAppendData?format=json"
17
+ _BOUNDARY = b"--boundary"
18
+
19
+
20
+ def _body(part: bytes) -> bytes:
21
+ """Strip multipart headers, return body bytes."""
22
+ i = part.find(b"\r\n\r\n")
23
+ return part[i + 4 :]
24
+
25
+
26
+ def fetch(
27
+ url: str,
28
+ username: str,
29
+ password: str,
30
+ channel: int = 1,
31
+ timeout: float = 10.0,
32
+ session: requests.Session | None = None,
33
+ ) -> tuple[dict, bytes, np.ndarray]:
34
+ """
35
+ Pull one thermal frame from a Hikvision ISAPI endpoint.
36
+
37
+ Returns
38
+ -------
39
+ meta : dict
40
+ Parsed JSON descriptor from Part 1.
41
+ jpeg : bytes
42
+ Raw JPEG bytes from Part 2.
43
+ matrix : np.ndarray shape (H, W) dtype float32
44
+ Per-pixel temperature in °C from Part 3.
45
+ """
46
+ own_session = session is None
47
+ if own_session:
48
+ session = requests.Session()
49
+ session.auth = HTTPDigestAuth(username, password)
50
+
51
+ try:
52
+ resp = session.get(
53
+ url.rstrip("/") + _ENDPOINT.format(ch=channel),
54
+ stream=True,
55
+ timeout=timeout,
56
+ )
57
+ resp.raise_for_status()
58
+ finally:
59
+ if own_session:
60
+ session.close()
61
+
62
+ raw = resp.content
63
+ parts = raw.split(_BOUNDARY)
64
+
65
+ # Part 1 — JSON metadata
66
+ meta_body = _body(parts[1])
67
+ meta_body = meta_body[: meta_body.find(b"--")]
68
+ meta = json.loads(meta_body)["JpegPictureWithAppendData"]
69
+
70
+ W: int = meta["jpegPicWidth"]
71
+ H: int = meta["jpegPicHeight"]
72
+
73
+ # Part 2 — thermal JPEG
74
+ jpeg = _body(parts[2])
75
+ jpeg = jpeg[: jpeg.find(_BOUNDARY)]
76
+
77
+ # Part 3 — float32 temperature blob
78
+ blob = _body(parts[3])[: W * H * 4]
79
+ matrix = np.frombuffer(blob, dtype="<f4").reshape(H, W).copy()
80
+
81
+ return meta, jpeg, matrix
hiktemp/_frame.py ADDED
@@ -0,0 +1,120 @@
1
+ """
2
+ hiktemp._frame
3
+ ~~~~~~~~~~~~~~
4
+ ThermalFrame — thin wrapper around the float32 temperature matrix.
5
+ No dependencies beyond numpy.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import numpy as np
11
+
12
+
13
+ def _band_alpha(matrix: np.ndarray, lo: float, hi: float) -> np.ndarray:
14
+ """
15
+ Quintic smoothstep alpha mask for band [lo, hi].
16
+ 1.0 inside band, fades to 0.0 outside over a 5% margin.
17
+ Pure polynomial — no exp, no overflow.
18
+ """
19
+ fade = max((hi - lo) * 0.05, 0.1)
20
+
21
+ def _quintic(t: np.ndarray) -> np.ndarray:
22
+ t = np.clip(t, 0.0, 1.0)
23
+ return t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
24
+
25
+ rise = _quintic((matrix - (lo - fade)) / fade)
26
+ fall = _quintic(((hi + fade) - matrix) / fade)
27
+ return (rise * fall).astype(np.float32)
28
+
29
+
30
+ class ThermalFrame:
31
+ """
32
+ A single radiometric thermal frame. Stores only raw sensor data.
33
+ Band filtering is applied at query time via method arguments.
34
+
35
+ Parameters
36
+ ----------
37
+ matrix : np.ndarray (H, W) float32
38
+ Per-pixel temperature in °C.
39
+ jpeg : bytes
40
+ Raw JPEG from the camera (AGC, display only).
41
+ meta : dict
42
+ JSON descriptor returned by the camera.
43
+ """
44
+
45
+ __slots__ = ("matrix", "jpeg", "meta")
46
+
47
+ def __init__(self, matrix: np.ndarray, jpeg: bytes, meta: dict) -> None:
48
+ self.matrix = matrix
49
+ self.jpeg = jpeg
50
+ self.meta = meta
51
+
52
+ # ── stats ──────────────────────────────────────────────────────────────────
53
+
54
+ @property
55
+ def min(self) -> float:
56
+ return float(self.matrix.min())
57
+
58
+ @property
59
+ def max(self) -> float:
60
+ return float(self.matrix.max())
61
+
62
+ @property
63
+ def mean(self) -> float:
64
+ return float(self.matrix.mean())
65
+
66
+ @property
67
+ def std(self) -> float:
68
+ return float(self.matrix.std())
69
+
70
+ def hotspot(
71
+ self,
72
+ lo: float | None = None,
73
+ hi: float | None = None,
74
+ ) -> tuple[int, int]:
75
+ """
76
+ (row, col) of maximum temperature pixel.
77
+ With lo/hi → restricted to pixels inside band.
78
+ """
79
+ if lo is not None and hi is not None:
80
+ m = self.matrix.copy()
81
+ m[self.alpha(lo, hi) < 0.01] = -np.inf
82
+ else:
83
+ m = self.matrix
84
+ r, c = np.unravel_index(int(np.argmax(m)), m.shape)
85
+ return int(r), int(c)
86
+
87
+ def coldspot(
88
+ self,
89
+ lo: float | None = None,
90
+ hi: float | None = None,
91
+ ) -> tuple[int, int]:
92
+ """
93
+ (row, col) of minimum temperature pixel.
94
+ With lo/hi → restricted to pixels inside band.
95
+ """
96
+ if lo is not None and hi is not None:
97
+ m = self.matrix.copy()
98
+ m[self.alpha(lo, hi) < 0.01] = np.inf
99
+ else:
100
+ m = self.matrix
101
+ r, c = np.unravel_index(int(np.argmin(m)), m.shape)
102
+ return int(r), int(c)
103
+
104
+ # ── band ───────────────────────────────────────────────────────────────────
105
+
106
+ def alpha(self, lo: float, hi: float) -> np.ndarray:
107
+ """Float32 (H, W) quintic smoothstep mask — 1.0 inside [lo, hi], 0.0 outside."""
108
+ return _band_alpha(self.matrix, lo, hi)
109
+
110
+ def masked(self, lo: float, hi: float) -> np.ndarray:
111
+ """Float32 (H, W) matrix with out-of-band pixels set to NaN."""
112
+ out = self.matrix.copy()
113
+ out[self.alpha(lo, hi) < 0.01] = np.nan
114
+ return out
115
+
116
+ # ── dunder ─────────────────────────────────────────────────────────────────
117
+
118
+ def __repr__(self) -> str:
119
+ H, W = self.matrix.shape
120
+ return f"<ThermalFrame {W}×{H} min={self.min:.2f} max={self.max:.2f} °C>"
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: hiktemp
3
+ Version: 0.1.0
4
+ Summary: Minimal radiometric temperature extraction from Hikvision thermal cameras
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests>=2.28
9
+ Requires-Dist: numpy>=1.23
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest; extra == "dev"
12
+ Requires-Dist: ruff; extra == "dev"
13
+
14
+ # hiktemp
15
+
16
+ Minimal radiometric temperature extraction from Hikvision thermal cameras via ISAPI.
17
+ No SDK required. Only depends on `requests` and `numpy`.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install hiktemp
23
+ ```
24
+
25
+ Or from source:
26
+
27
+ ```bash
28
+ git clone https://github.com/yourname/hiktemp
29
+ cd hiktemp
30
+ pip install -e .
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```python
36
+ from hiktemp import hiktemp
37
+
38
+ # pull one frame
39
+ frame = hiktemp("http://192.168.1.1", "admin", "password")
40
+
41
+ frame.matrix # np.ndarray (H, W) float32 — per-pixel °C
42
+ frame.jpeg # bytes — raw JPEG from camera (AGC, display only)
43
+ frame.meta # dict — camera descriptor
44
+ frame.min # float — global min °C
45
+ frame.max # float — global max °C
46
+ frame.mean # float — global mean °C
47
+ frame.std # float — global std °C
48
+ frame.hotspot() # (row, col) — global hotspot
49
+ frame.coldspot() # (row, col) — global coldspot
50
+ frame.to_bgr() # np.ndarray (H,W,3) uint8 — cv2.imshow() ready
51
+
52
+ # band filter — lo/hi passed at call time
53
+ frame.hotspot(lo=28, hi=29) # hotspot within band
54
+ frame.coldspot(lo=28, hi=29) # coldspot within band
55
+ frame.masked(lo=28, hi=29) # matrix, out-of-band pixels = NaN
56
+ frame.alpha(lo=28, hi=29) # float32 (H,W) quintic smoothstep mask
57
+ frame.to_rgba(lo=28, hi=29) # (H,W,4) uint8 with alpha mask applied
58
+
59
+ # reuse session for continuous polling
60
+ import requests
61
+ from requests.auth import HTTPDigestAuth
62
+
63
+ session = requests.Session()
64
+ session.auth = HTTPDigestAuth("admin", "password")
65
+
66
+ while True:
67
+ frame = hiktemp("http://192.168.1.1", "admin", "password", session=session)
68
+ bgr = frame.to_bgr()
69
+ # cv2.imshow("thermal", bgr)
70
+ ```
71
+
72
+ ## Requirements
73
+
74
+ - Python >= 3.10
75
+ - `requests >= 2.28`
76
+ - `numpy >= 1.23`
77
+
78
+ opencv-python is **optional** — `to_bgr()` and `to_rgba()` return plain numpy arrays.
79
+
80
+ ## Tested on
81
+
82
+ - DS-2TD2138-10/QY (384×288, float32 °C, channel 1)
@@ -0,0 +1,7 @@
1
+ hiktemp/__init__.py,sha256=8K3loa7DGTDE1CbbpOWRQrQbNHvWuW4rmFsRZdgbnqA,2256
2
+ hiktemp/_fetch.py,sha256=nsOg1pVnstzoDvL1_df4TTaTEgHalBLvwqn_ZxrY5-s,2032
3
+ hiktemp/_frame.py,sha256=Itl1zPBkO5D126WJQ4AtdqpY27e8GWnWy7TzU2nNES8,3987
4
+ hiktemp-0.1.0.dist-info/METADATA,sha256=my-9_-eNT9LwoKE7FrCnIfBeJbl2kFUYwBcMh7RFFw4,2309
5
+ hiktemp-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ hiktemp-0.1.0.dist-info/top_level.txt,sha256=0qqqqDgHhms1RsQjvCHnsnU2pIoFuI9bDJYNHiOapu4,8
7
+ hiktemp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ hiktemp