koebelt-dash-scripts 1.3.0b1__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thomas Koebel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: koebelt-dash-scripts
3
+ Version: 1.3.0b1
4
+ Summary: Control drones, robots and test rigs connected in the koebelt/dash app from Python.
5
+ Author-email: Thomas Koebel <contact@koebelt.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://koebelt.com/work/koebelt-dash
8
+ Project-URL: Source, https://github.com/koebelt/koebelt_dash/tree/main/libs/python-dash-scripts
9
+ Project-URL: Protocol, https://github.com/koebelt/koebelt_dash/blob/main/protocol/SCRIPTS.md
10
+ Project-URL: Issues, https://github.com/koebelt/koebelt_dash/issues
11
+ Keywords: robotics,drone,rover,automation,computer-vision,koebelt
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Intended Audience :: Education
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Scientific/Engineering
19
+ Classifier: Topic :: System :: Hardware
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: websockets>=12
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # koebelt-dash-scripts
30
+
31
+ Control drones, robots and test rigs connected in
32
+ [koebelt/dash](https://koebelt.com/work/koebelt-dash) from Python: read their
33
+ telemetry, drive them, react to buttons and conditions, with any Python
34
+ package you like (OpenCV, PyTorch, YOLO…).
35
+
36
+ ```sh
37
+ pip install koebelt-dash-scripts
38
+ ```
39
+
40
+ ```python
41
+ from koebelt_dash_scripts import connect, every, pid
42
+
43
+ dev = connect() # the device selected in koebelt dash
44
+ speed = dev.setting("speed", 0.4, min=0, max=1) # a slider in dash
45
+ hold = pid(1.5, 0.0, 0.2)
46
+
47
+ @dev.trigger("Stop here", key="S") # a button in dash
48
+ def stop_here():
49
+ dev.mode("HOLD")
50
+
51
+ dev.mode("MANUAL")
52
+ for _ in every(0.05): # 20 Hz
53
+ error = 0.0 - dev.tlm.pos[0] # stay on the north-south line
54
+ dev.sticks(throttle=speed.value, steer=hold.update(error))
55
+ ```
56
+
57
+ Run it from koebelt dash (Scripts), or from a terminal while dash is open on
58
+ the same computer: it finds dash by itself.
59
+
60
+ With a model on the camera:
61
+
62
+ ```python
63
+ from ultralytics import YOLO
64
+ from koebelt_dash_scripts import connect, pid
65
+
66
+ model = YOLO("yolov8n.pt")
67
+ dev = connect()
68
+ steer = pid(1.2, 0, 0.1)
69
+
70
+ for frame in dev.camera.frames():
71
+ result = model(frame, verbose=False)[0]
72
+ dev.overlay(result) # see what the model sees, in dash
73
+ people = [b for b in result.boxes if int(b.cls) == 0]
74
+ if not people:
75
+ dev.sticks(throttle=0, steer=0)
76
+ continue
77
+ x = float(people[0].xywhn[0][0]) - 0.5 # −0.5 left … 0.5 right
78
+ dev.sticks(throttle=0.4, steer=steer.update(-x))
79
+ ```
80
+
81
+ ## Script hosts: scripts for phones and tablets
82
+
83
+ koebelt dash on an iPhone, iPad or Android device sends its scripts to a
84
+ computer on the same network, which runs them with its own Python (and its
85
+ GPU), while the phone keeps the device link, the sticks and STOP:
86
+
87
+ ```sh
88
+ pip install koebelt-dash-scripts
89
+ koebelt-dash-scripts host
90
+ ```
91
+
92
+ It prints a pairing code; in dash: Scripts > Runs on > Add a script host. A
93
+ laptop on the field, a Raspberry Pi 5 or a Jetson all work. A computer
94
+ running koebelt dash can also be a host (Settings > Scripts).
95
+
96
+ ## Safety
97
+
98
+ dash stays in charge, whatever the script does:
99
+
100
+ - STOP stops every device and ends the script.
101
+ - Moving the sticks in dash takes over; the script's sticks are ignored until
102
+ you resume it.
103
+ - Each script has limits set in dash: maximum speed, geofence, ceiling.
104
+ Arming, take-off and parameter writes are refused unless you allow them.
105
+ - If the script crashes, hangs or leaves the geofence, the device holds, and
106
+ the script cannot drive again until you resume it.
107
+
108
+ ## The API
109
+
110
+ | | |
111
+ |---|---|
112
+ | `connect(device=None)` | The device selected in dash, or the one with that name. |
113
+ | `dev.tlm` | Latest telemetry (20 Hz): `pos` (m from home, x east, y north), `alt`, `vel`, `speed`, `roll` `pitch` `yaw`, `heading`, `mode`, `armed`, `battery`, `gps`, `joints`… |
114
+ | `dev.next_tlm()` | Waits for the next telemetry. |
115
+ | `dev.sticks(...)` | −1…1, held until changed. Drones: `throttle yaw pitch roll`; rovers: `throttle steer`. |
116
+ | `dev.mode(m)`, `dev.arm()`, `dev.disarm()`, `dev.speed_limit(v)`, `dev.cmd(name, **args)`, `dev.stop()`, `dev.release()` | Commands. |
117
+ | `dev.takeoff(alt)`, `dev.goto(x, y, z)`, `dev.land()`, `dev.rth()`, `dev.pose(*deg)`, `dev.gripper(pct)`, `dev.home()`, `dev.throttle(pct, s)`, `dev.ramp(a, b, s)` | Program steps; they return once done (or at once with `wait=False`). |
118
+ | `dev.run_program(name)`, `dev.pause_program()`, `dev.resume_program()`, `dev.stop_program()` | Programs from dash's Program screen. |
119
+ | `dev.camera.frame()`, `for img in dev.camera.frames():` | The camera as numpy images (BGR, for OpenCV; needs `opencv-python` or `pillow`). The newest frame only: a slow model never falls behind. |
120
+ | `dev.overlay(boxes)` | Boxes over the camera in dash: `(x, y, w, h, label)` tuples or an ultralytics result. |
121
+ | `dev.params["KEY"]`, `dev.params.set(KEY=v)`, `dev.params.save()` | Parameters. |
122
+ | `dev.setting(name, default, min, max)` | A slider or switch in dash; read `.value`. |
123
+ | `@dev.trigger(name, kind, key, gamepad)` | A button (`button`), switch (`switch`) or number (`value`) in dash. |
124
+ | `@dev.on_trigger`, `@dev.on_stop`, `@dev.on_takeover` | Other events. |
125
+ | `dev.plot(name, v)`, `dev.mark(text)`, `dev.notify(text)` | Shown in dash and recorded. |
126
+ | `dev.store` | A dictionary kept between runs. |
127
+ | `dev.run()` | Waits, for scripts that only react to triggers. |
128
+ | `every(seconds)`, `wait(seconds)` | Steady loops; dash shows the real rate. |
129
+ | `pid`, `lowpass`, `rate_limit`, `clamp`, `distance`, `bearing`, `angle_diff`, `geo_distance`, `geo_bearing` | Helpers. |
130
+
131
+ The messages between scripts and dash are documented in
132
+ [protocol/SCRIPTS.md](https://github.com/koebelt/koebelt_dash/blob/main/protocol/SCRIPTS.md).
133
+
134
+ MIT licence.
@@ -0,0 +1,106 @@
1
+ # koebelt-dash-scripts
2
+
3
+ Control drones, robots and test rigs connected in
4
+ [koebelt/dash](https://koebelt.com/work/koebelt-dash) from Python: read their
5
+ telemetry, drive them, react to buttons and conditions, with any Python
6
+ package you like (OpenCV, PyTorch, YOLO…).
7
+
8
+ ```sh
9
+ pip install koebelt-dash-scripts
10
+ ```
11
+
12
+ ```python
13
+ from koebelt_dash_scripts import connect, every, pid
14
+
15
+ dev = connect() # the device selected in koebelt dash
16
+ speed = dev.setting("speed", 0.4, min=0, max=1) # a slider in dash
17
+ hold = pid(1.5, 0.0, 0.2)
18
+
19
+ @dev.trigger("Stop here", key="S") # a button in dash
20
+ def stop_here():
21
+ dev.mode("HOLD")
22
+
23
+ dev.mode("MANUAL")
24
+ for _ in every(0.05): # 20 Hz
25
+ error = 0.0 - dev.tlm.pos[0] # stay on the north-south line
26
+ dev.sticks(throttle=speed.value, steer=hold.update(error))
27
+ ```
28
+
29
+ Run it from koebelt dash (Scripts), or from a terminal while dash is open on
30
+ the same computer: it finds dash by itself.
31
+
32
+ With a model on the camera:
33
+
34
+ ```python
35
+ from ultralytics import YOLO
36
+ from koebelt_dash_scripts import connect, pid
37
+
38
+ model = YOLO("yolov8n.pt")
39
+ dev = connect()
40
+ steer = pid(1.2, 0, 0.1)
41
+
42
+ for frame in dev.camera.frames():
43
+ result = model(frame, verbose=False)[0]
44
+ dev.overlay(result) # see what the model sees, in dash
45
+ people = [b for b in result.boxes if int(b.cls) == 0]
46
+ if not people:
47
+ dev.sticks(throttle=0, steer=0)
48
+ continue
49
+ x = float(people[0].xywhn[0][0]) - 0.5 # −0.5 left … 0.5 right
50
+ dev.sticks(throttle=0.4, steer=steer.update(-x))
51
+ ```
52
+
53
+ ## Script hosts: scripts for phones and tablets
54
+
55
+ koebelt dash on an iPhone, iPad or Android device sends its scripts to a
56
+ computer on the same network, which runs them with its own Python (and its
57
+ GPU), while the phone keeps the device link, the sticks and STOP:
58
+
59
+ ```sh
60
+ pip install koebelt-dash-scripts
61
+ koebelt-dash-scripts host
62
+ ```
63
+
64
+ It prints a pairing code; in dash: Scripts > Runs on > Add a script host. A
65
+ laptop on the field, a Raspberry Pi 5 or a Jetson all work. A computer
66
+ running koebelt dash can also be a host (Settings > Scripts).
67
+
68
+ ## Safety
69
+
70
+ dash stays in charge, whatever the script does:
71
+
72
+ - STOP stops every device and ends the script.
73
+ - Moving the sticks in dash takes over; the script's sticks are ignored until
74
+ you resume it.
75
+ - Each script has limits set in dash: maximum speed, geofence, ceiling.
76
+ Arming, take-off and parameter writes are refused unless you allow them.
77
+ - If the script crashes, hangs or leaves the geofence, the device holds, and
78
+ the script cannot drive again until you resume it.
79
+
80
+ ## The API
81
+
82
+ | | |
83
+ |---|---|
84
+ | `connect(device=None)` | The device selected in dash, or the one with that name. |
85
+ | `dev.tlm` | Latest telemetry (20 Hz): `pos` (m from home, x east, y north), `alt`, `vel`, `speed`, `roll` `pitch` `yaw`, `heading`, `mode`, `armed`, `battery`, `gps`, `joints`… |
86
+ | `dev.next_tlm()` | Waits for the next telemetry. |
87
+ | `dev.sticks(...)` | −1…1, held until changed. Drones: `throttle yaw pitch roll`; rovers: `throttle steer`. |
88
+ | `dev.mode(m)`, `dev.arm()`, `dev.disarm()`, `dev.speed_limit(v)`, `dev.cmd(name, **args)`, `dev.stop()`, `dev.release()` | Commands. |
89
+ | `dev.takeoff(alt)`, `dev.goto(x, y, z)`, `dev.land()`, `dev.rth()`, `dev.pose(*deg)`, `dev.gripper(pct)`, `dev.home()`, `dev.throttle(pct, s)`, `dev.ramp(a, b, s)` | Program steps; they return once done (or at once with `wait=False`). |
90
+ | `dev.run_program(name)`, `dev.pause_program()`, `dev.resume_program()`, `dev.stop_program()` | Programs from dash's Program screen. |
91
+ | `dev.camera.frame()`, `for img in dev.camera.frames():` | The camera as numpy images (BGR, for OpenCV; needs `opencv-python` or `pillow`). The newest frame only: a slow model never falls behind. |
92
+ | `dev.overlay(boxes)` | Boxes over the camera in dash: `(x, y, w, h, label)` tuples or an ultralytics result. |
93
+ | `dev.params["KEY"]`, `dev.params.set(KEY=v)`, `dev.params.save()` | Parameters. |
94
+ | `dev.setting(name, default, min, max)` | A slider or switch in dash; read `.value`. |
95
+ | `@dev.trigger(name, kind, key, gamepad)` | A button (`button`), switch (`switch`) or number (`value`) in dash. |
96
+ | `@dev.on_trigger`, `@dev.on_stop`, `@dev.on_takeover` | Other events. |
97
+ | `dev.plot(name, v)`, `dev.mark(text)`, `dev.notify(text)` | Shown in dash and recorded. |
98
+ | `dev.store` | A dictionary kept between runs. |
99
+ | `dev.run()` | Waits, for scripts that only react to triggers. |
100
+ | `every(seconds)`, `wait(seconds)` | Steady loops; dash shows the real rate. |
101
+ | `pid`, `lowpass`, `rate_limit`, `clamp`, `distance`, `bearing`, `angle_diff`, `geo_distance`, `geo_bearing` | Helpers. |
102
+
103
+ The messages between scripts and dash are documented in
104
+ [protocol/SCRIPTS.md](https://github.com/koebelt/koebelt_dash/blob/main/protocol/SCRIPTS.md).
105
+
106
+ MIT licence.
@@ -0,0 +1,17 @@
1
+ """koebelt-dash-scripts: control devices connected in koebelt/dash from Python.
2
+
3
+ from koebelt_dash_scripts import connect, every
4
+ dev = connect()
5
+ for _ in every(0.05):
6
+ dev.sticks(throttle=0.3)
7
+ """
8
+
9
+ __version__ = "1.3.0b1"
10
+
11
+ from .camera import Camera, CameraError, decode_jpeg # noqa: E402
12
+ from .client import Device, Released, ScriptError, Setting, Telemetry, connect, every, wait # noqa: E402
13
+ from .tools import angle_diff, bearing, clamp, distance, geo_bearing, geo_distance, lowpass, pid, rate_limit # noqa: E402
14
+
15
+ __all__ = ["connect", "every", "wait", "Device", "Telemetry", "Setting", "ScriptError", "Released", "Camera", "CameraError", "decode_jpeg",
16
+ "pid", "lowpass", "rate_limit", "clamp", "distance", "bearing", "angle_diff", "geo_distance", "geo_bearing",
17
+ "__version__"]
@@ -0,0 +1,4 @@
1
+ """python -m koebelt_dash_scripts host"""
2
+ from .host import cli
3
+
4
+ cli()
@@ -0,0 +1,214 @@
1
+ """A device's camera, as numpy images for OpenCV, PyTorch, YOLO…
2
+
3
+ The stream is read straight from the device (MJPEG, or a still JPEG polled),
4
+ on a background thread that keeps only the newest picture: a slow model
5
+ always gets the latest frame, never a backlog.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import io
10
+ import threading
11
+ import time
12
+ import urllib.request
13
+ from typing import Any, Dict, Iterator, Optional
14
+
15
+
16
+ class CameraError(RuntimeError):
17
+ pass
18
+
19
+
20
+ def decode_jpeg(data: bytes):
21
+ """Image bytes (JPEG, PNG) → H×W×3 numpy array, BGR (OpenCV's order), with OpenCV or Pillow."""
22
+ try:
23
+ import cv2
24
+ import numpy as np
25
+ except ImportError:
26
+ cv2 = None
27
+ if cv2 is not None:
28
+ img = cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR)
29
+ if img is None:
30
+ raise CameraError("could not decode a camera frame")
31
+ return img
32
+ try:
33
+ import numpy as np
34
+ from PIL import Image
35
+ except ImportError:
36
+ raise CameraError("camera frames need numpy and OpenCV (pip install opencv-python) or Pillow (pip install pillow)") from None
37
+ return np.asarray(Image.open(io.BytesIO(data)).convert("RGB"))[:, :, ::-1].copy()
38
+
39
+
40
+ class Camera:
41
+ """One of the device's cameras. ``frame()`` is the newest picture;
42
+ ``for img in camera.frames():`` yields each new one."""
43
+
44
+ def __init__(self, info: Dict[str, Any]):
45
+ self.id: str = info.get("id", "cam")
46
+ self.label: str = info.get("label") or self.id
47
+ self.url: Optional[str] = info.get("url")
48
+ self.fmt: str = info.get("fmt", "mjpeg")
49
+ self._jpeg: Optional[bytes] = None
50
+ self._count = 0
51
+ self._cond = threading.Condition()
52
+ self._thread: Optional[threading.Thread] = None
53
+ self._stop = threading.Event()
54
+ self.error: Optional[str] = None
55
+ self.fps = 0.0
56
+
57
+ def __repr__(self):
58
+ return f"Camera({self.label!r}, {self.url!r})"
59
+
60
+ def _start(self) -> None:
61
+ if self._thread is not None:
62
+ return
63
+ if not self.url:
64
+ raise CameraError(f"camera {self.label} is not reachable from this computer")
65
+ self._thread = threading.Thread(target=self._read, daemon=True, name=f"koebelt-dash-camera-{self.id}")
66
+ self._thread.start()
67
+
68
+ def _publish(self, jpeg: bytes) -> None:
69
+ with self._cond:
70
+ self._jpeg = jpeg
71
+ self._count += 1
72
+ self._cond.notify_all()
73
+
74
+ def _read(self) -> None:
75
+ while not self._stop.is_set():
76
+ try:
77
+ with urllib.request.urlopen(self.url, timeout=5) as r:
78
+ ctype = r.headers.get("Content-Type", "")
79
+ if self.fmt == "jpeg" or not ctype.startswith("multipart/"):
80
+ self._publish(r.read())
81
+ self._stop.wait(0.1)
82
+ continue
83
+ boundary = ctype.split("boundary=", 1)[-1].strip().strip('"').encode()
84
+ self._parts(r, b"--" + boundary.lstrip(b"-") if boundary else b"")
85
+ self.error = None
86
+ except Exception as e:
87
+ self.error = str(e)
88
+ self._stop.wait(1.0)
89
+
90
+ def _parts(self, r, boundary: bytes) -> None:
91
+ """Reads multipart/x-mixed-replace parts: headers, then Content-Length
92
+ bytes (or, without a length, up to the next boundary)."""
93
+ rd = _Buffered(r)
94
+ last = time.monotonic()
95
+ while not self._stop.is_set():
96
+ line = rd.readline()
97
+ if line is None:
98
+ return
99
+ if not line.strip().startswith(b"--"):
100
+ continue
101
+ length = None
102
+ while True:
103
+ h = rd.readline()
104
+ if h is None:
105
+ return
106
+ if not h.strip():
107
+ break
108
+ k, _, v = h.decode("latin-1").partition(":")
109
+ if k.strip().lower() == "content-length":
110
+ length = int(v.strip())
111
+ data = rd.read(length) if length is not None else rd.until(b"\r\n" + boundary)
112
+ if data is None:
113
+ return
114
+ self._publish(data)
115
+ now = time.monotonic()
116
+ self.fps = self.fps * 0.9 + (1 / max(now - last, 1e-3)) * 0.1
117
+ last = now
118
+
119
+ def jpeg(self, timeout: float = 5.0) -> bytes:
120
+ """The newest frame as the camera sends it (JPEG, or PNG for simulated cameras)."""
121
+ self._start()
122
+ with self._cond:
123
+ if not self._cond.wait_for(lambda: self._jpeg is not None, timeout=timeout):
124
+ raise CameraError(f"no picture from camera {self.label} within {timeout} s" + (f": {self.error}" if self.error else ""))
125
+ return self._jpeg # type: ignore[return-value]
126
+
127
+ def frame(self, timeout: float = 5.0):
128
+ """The newest frame, H×W×3 BGR numpy array."""
129
+ return decode_jpeg(self.jpeg(timeout))
130
+
131
+ def frames(self, timeout: float = 5.0) -> Iterator[Any]:
132
+ """Each new frame, newest first: frames the loop was too slow for are skipped."""
133
+ self._start()
134
+ seen = -1
135
+ while True:
136
+ with self._cond:
137
+ if not self._cond.wait_for(lambda: self._count != seen and self._jpeg is not None, timeout=timeout):
138
+ raise CameraError(f"no picture from camera {self.label} within {timeout} s" + (f": {self.error}" if self.error else ""))
139
+ seen, data = self._count, self._jpeg
140
+ yield decode_jpeg(data) # type: ignore[arg-type]
141
+
142
+ def close(self) -> None:
143
+ self._stop.set()
144
+
145
+
146
+ class _Buffered:
147
+ """Line, length and marker reads over a live HTTP body, returning what has
148
+ arrived without waiting for more than it needs."""
149
+
150
+ def __init__(self, r):
151
+ self.r = r
152
+ self.buf = b""
153
+
154
+ def _more(self) -> bool:
155
+ chunk = self.r.read1(65536) if hasattr(self.r, "read1") else self.r.read(4096)
156
+ if not chunk:
157
+ return False
158
+ self.buf += chunk
159
+ if len(self.buf) > 32_000_000:
160
+ raise CameraError("camera stream without frame boundaries")
161
+ return True
162
+
163
+ def readline(self) -> Optional[bytes]:
164
+ while b"\n" not in self.buf:
165
+ if not self._more():
166
+ return None
167
+ line, _, self.buf = self.buf.partition(b"\n")
168
+ return line + b"\n"
169
+
170
+ def read(self, n: int) -> Optional[bytes]:
171
+ while len(self.buf) < n:
172
+ if not self._more():
173
+ return None
174
+ data, self.buf = self.buf[:n], self.buf[n:]
175
+ return data
176
+
177
+ def until(self, marker: bytes) -> Optional[bytes]:
178
+ """The bytes before marker; the marker stays for the next part."""
179
+ while marker not in self.buf:
180
+ if not self._more():
181
+ return None
182
+ i = self.buf.index(marker)
183
+ data, self.buf = self.buf[:i], self.buf[i + 2:]
184
+ return data
185
+
186
+
187
+ def boxes_of(items: Any, width: Optional[float] = None, height: Optional[float] = None) -> list:
188
+ """Boxes for dev.overlay(): from (x, y, w, h[, label]) tuples, normalised
189
+ or in pixels (with width and height), or straight from an ultralytics
190
+ result (``model(frame)[0]``) or its ``.boxes``."""
191
+ names = getattr(items, "names", None)
192
+ if hasattr(items, "boxes") and not hasattr(items, "xyxyn"):
193
+ items = items.boxes
194
+ if hasattr(items, "xyxyn"):
195
+ xyxy = items.xyxyn.tolist()
196
+ cls = items.cls.tolist() if hasattr(items, "cls") else [None] * len(xyxy)
197
+ conf = items.conf.tolist() if hasattr(items, "conf") else [None] * len(xyxy)
198
+ out = []
199
+ for (x1, y1, x2, y2), c, p in zip(xyxy, cls, conf):
200
+ label = "" if c is None else (names.get(int(c), str(int(c))) if isinstance(names, dict) else str(int(c)))
201
+ if p is not None:
202
+ label = f"{label} {p:.2f}".strip()
203
+ out.append([x1, y1, x2 - x1, y2 - y1, label])
204
+ return out
205
+ out = []
206
+ for b in items:
207
+ if hasattr(b, "xyxyn"):
208
+ out.extend(boxes_of(b))
209
+ continue
210
+ x, y, w, h = (float(v) for v in b[:4])
211
+ if width and height:
212
+ x, y, w, h = x / width, y / height, w / width, h / height
213
+ out.append([x, y, w, h, str(b[4]) if len(b) > 4 else ""])
214
+ return out