immdebug 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.
- immdebug/__init__.py +16 -0
- immdebug/__main__.py +5 -0
- immdebug/client.py +127 -0
- immdebug/viewer.py +269 -0
- immdebug-0.1.0.dist-info/METADATA +115 -0
- immdebug-0.1.0.dist-info/RECORD +8 -0
- immdebug-0.1.0.dist-info/WHEEL +4 -0
- immdebug-0.1.0.dist-info/entry_points.txt +2 -0
immdebug/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""immdebug: send images to the immdebug viewer for visual debugging.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
import numpy as np
|
|
5
|
+
from immdebug import immdebug
|
|
6
|
+
|
|
7
|
+
image = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8)
|
|
8
|
+
immdebug(image, "my image")
|
|
9
|
+
|
|
10
|
+
The viewer can be launched with:
|
|
11
|
+
python -m immdebug.viewer
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from immdebug.client import immdebug
|
|
15
|
+
|
|
16
|
+
__all__ = ["immdebug"]
|
immdebug/__main__.py
ADDED
immdebug/client.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Pure Python client for immdebug: send images to the immdebug_viewer app.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
import numpy as np
|
|
5
|
+
from immdebug import immdebug
|
|
6
|
+
|
|
7
|
+
image = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8)
|
|
8
|
+
immdebug(image, "my image")
|
|
9
|
+
|
|
10
|
+
The immdebug_viewer must be running separately. It polls a temp directory
|
|
11
|
+
for incoming image payloads.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import struct
|
|
16
|
+
import tempfile
|
|
17
|
+
import time
|
|
18
|
+
import random
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Tuple
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# OpenCV depth constants: cv::Mat::depth()
|
|
26
|
+
_CV_DEPTH = {
|
|
27
|
+
np.dtype("uint8"): 0,
|
|
28
|
+
np.dtype("int8"): 1,
|
|
29
|
+
np.dtype("uint16"): 2,
|
|
30
|
+
np.dtype("int16"): 3,
|
|
31
|
+
np.dtype("int32"): 4,
|
|
32
|
+
np.dtype("float32"): 5,
|
|
33
|
+
np.dtype("float64"): 6,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Random base ID, incremented per call (mirrors C++ static id)
|
|
37
|
+
_next_id = random.randint(100_000_000_000, 1_000_000_000_000)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _immdebug_folder(while_writing: bool) -> Path:
|
|
41
|
+
base = Path(tempfile.gettempdir()) / "ImmDebug"
|
|
42
|
+
folder = base / "Writing" if while_writing else base
|
|
43
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
return folder
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _make_unique_filename(while_writing: bool) -> Path:
|
|
48
|
+
global _next_id
|
|
49
|
+
_next_id += 1
|
|
50
|
+
return _immdebug_folder(while_writing) / f"{_next_id}.bindata"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _remove_old_images(max_age_seconds: float = 3600.0) -> None:
|
|
54
|
+
folder = _immdebug_folder(False)
|
|
55
|
+
now = time.time()
|
|
56
|
+
for entry in folder.rglob("*"):
|
|
57
|
+
if not entry.is_file():
|
|
58
|
+
continue
|
|
59
|
+
try:
|
|
60
|
+
if now - entry.stat().st_mtime > max_age_seconds:
|
|
61
|
+
entry.unlink()
|
|
62
|
+
except OSError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _write_string(buf: bytearray, s: str) -> None:
|
|
67
|
+
encoded = s.encode("utf-8")
|
|
68
|
+
buf += struct.pack("=Q", len(encoded)) # size_t (8 bytes on 64-bit)
|
|
69
|
+
buf += encoded
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _write_mat(buf: bytearray, image: np.ndarray) -> None:
|
|
73
|
+
image = np.ascontiguousarray(image)
|
|
74
|
+
rows, cols = image.shape[0], image.shape[1]
|
|
75
|
+
channels = 1 if image.ndim == 2 else image.shape[2]
|
|
76
|
+
|
|
77
|
+
depth = _CV_DEPTH.get(image.dtype)
|
|
78
|
+
if depth is None:
|
|
79
|
+
raise ValueError(f"Unsupported dtype: {image.dtype}. Supported: {list(_CV_DEPTH.keys())}")
|
|
80
|
+
|
|
81
|
+
elem_type = depth + (channels - 1) * 8
|
|
82
|
+
elem_size = image.dtype.itemsize * channels
|
|
83
|
+
|
|
84
|
+
# Match C++ WriteValue<int>, WriteValue<size_t>, WriteValue<int>
|
|
85
|
+
# "=" = native byte order, no padding; "Q" = uint64 for size_t
|
|
86
|
+
buf += struct.pack("=iiQi", cols, rows, elem_size, elem_type)
|
|
87
|
+
buf += image.tobytes()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def immdebug(
|
|
91
|
+
image: np.ndarray,
|
|
92
|
+
legend: str = "",
|
|
93
|
+
zoom_center: Tuple[float, float] = (0.0, 0.0),
|
|
94
|
+
zoom_ratio: float = -1.0,
|
|
95
|
+
zoom_key: str = "",
|
|
96
|
+
color_adjustments_key: str = "",
|
|
97
|
+
is_color_order_bgr: bool = True,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Send an image to the immdebug_viewer application.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
image: numpy array (HxW for grayscale, HxWxC for multi-channel)
|
|
103
|
+
legend: display name in the viewer
|
|
104
|
+
zoom_center: initial zoom center (x, y)
|
|
105
|
+
zoom_ratio: initial zoom ratio (-1 for auto-fit)
|
|
106
|
+
zoom_key: link zoom across images sharing this key
|
|
107
|
+
color_adjustments_key: link color adjustments across images sharing this key
|
|
108
|
+
is_color_order_bgr: True if channels are BGR (OpenCV default), False for RGB
|
|
109
|
+
"""
|
|
110
|
+
_remove_old_images()
|
|
111
|
+
|
|
112
|
+
buf = bytearray()
|
|
113
|
+
_write_string(buf, legend)
|
|
114
|
+
buf += struct.pack("=dd", zoom_center[0], zoom_center[1])
|
|
115
|
+
buf += struct.pack("=d", zoom_ratio)
|
|
116
|
+
_write_string(buf, zoom_key)
|
|
117
|
+
_write_string(buf, color_adjustments_key)
|
|
118
|
+
buf += struct.pack("=?", is_color_order_bgr)
|
|
119
|
+
_write_mat(buf, image)
|
|
120
|
+
|
|
121
|
+
writing_path = _make_unique_filename(while_writing=True)
|
|
122
|
+
done_path = _make_unique_filename(while_writing=False)
|
|
123
|
+
|
|
124
|
+
with open(writing_path, "wb") as f:
|
|
125
|
+
f.write(buf)
|
|
126
|
+
|
|
127
|
+
os.rename(str(writing_path), str(done_path))
|
immdebug/viewer.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Pure Python immdebug viewer using imgui_bundle.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m immdebug.viewer
|
|
5
|
+
|
|
6
|
+
Polls the temp directory for images sent by immdebug (C++ or Python client)
|
|
7
|
+
and displays them using immvision's inspector.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import struct
|
|
11
|
+
import tempfile
|
|
12
|
+
import time
|
|
13
|
+
import threading
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional, Tuple
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# OpenCV depth constants -> numpy dtype
|
|
21
|
+
_CV_DEPTH_TO_DTYPE = {
|
|
22
|
+
0: np.dtype("uint8"),
|
|
23
|
+
1: np.dtype("int8"),
|
|
24
|
+
2: np.dtype("uint16"),
|
|
25
|
+
3: np.dtype("int16"),
|
|
26
|
+
4: np.dtype("int32"),
|
|
27
|
+
5: np.dtype("float32"),
|
|
28
|
+
6: np.dtype("float64"),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _ImagePayload:
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self.image: np.ndarray = np.zeros((0, 0), dtype=np.uint8)
|
|
35
|
+
self.legend: str = ""
|
|
36
|
+
self.zoom_center: Tuple[float, float] = (0.0, 0.0)
|
|
37
|
+
self.zoom_ratio: float = -1.0
|
|
38
|
+
self.zoom_key: str = ""
|
|
39
|
+
self.color_adjustments_key: str = ""
|
|
40
|
+
self.is_color_order_bgr: bool = True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read_value(data: bytes, offset: int, fmt: str) -> Tuple:
|
|
44
|
+
size = struct.calcsize(fmt)
|
|
45
|
+
values = struct.unpack_from(fmt, data, offset)
|
|
46
|
+
return values, offset + size
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _read_string(data: bytes, offset: int) -> Tuple[str, int]:
|
|
50
|
+
(length,), offset = _read_value(data, offset, "=Q")
|
|
51
|
+
s = data[offset : offset + length].decode("utf-8")
|
|
52
|
+
return s, offset + length
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _read_mat(data: bytes, offset: int) -> Tuple[np.ndarray, int]:
|
|
56
|
+
(cols, rows, elem_size, elem_type), offset = _read_value(data, offset, "=iiQi")
|
|
57
|
+
|
|
58
|
+
depth = elem_type % 8
|
|
59
|
+
channels = (elem_type // 8) + 1
|
|
60
|
+
dtype = _CV_DEPTH_TO_DTYPE[depth]
|
|
61
|
+
|
|
62
|
+
data_size = cols * rows * elem_size
|
|
63
|
+
pixel_data = data[offset : offset + data_size]
|
|
64
|
+
|
|
65
|
+
if channels == 1:
|
|
66
|
+
image = np.frombuffer(pixel_data, dtype=dtype).reshape((rows, cols))
|
|
67
|
+
else:
|
|
68
|
+
image = np.frombuffer(pixel_data, dtype=dtype).reshape((rows, cols, channels))
|
|
69
|
+
|
|
70
|
+
return image.copy(), offset + data_size
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _read_image_payload(data: bytes) -> _ImagePayload:
|
|
74
|
+
payload = _ImagePayload()
|
|
75
|
+
offset = 0
|
|
76
|
+
payload.legend, offset = _read_string(data, offset)
|
|
77
|
+
(cx, cy), offset = _read_value(data, offset, "=dd")
|
|
78
|
+
payload.zoom_center = (cx, cy)
|
|
79
|
+
(payload.zoom_ratio,), offset = _read_value(data, offset, "=d")
|
|
80
|
+
payload.zoom_key, offset = _read_string(data, offset)
|
|
81
|
+
payload.color_adjustments_key, offset = _read_string(data, offset)
|
|
82
|
+
(payload.is_color_order_bgr,), offset = _read_value(data, offset, "=?")
|
|
83
|
+
payload.image, offset = _read_mat(data, offset)
|
|
84
|
+
return payload
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _immdebug_folder() -> Path:
|
|
88
|
+
return Path(tempfile.gettempdir()) / "ImmDebug"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _remove_old_images(max_age_seconds: float = 3600.0) -> None:
|
|
92
|
+
folder = _immdebug_folder()
|
|
93
|
+
if not folder.exists():
|
|
94
|
+
return
|
|
95
|
+
now = time.time()
|
|
96
|
+
for entry in folder.rglob("*"):
|
|
97
|
+
if not entry.is_file():
|
|
98
|
+
continue
|
|
99
|
+
try:
|
|
100
|
+
if now - entry.stat().st_mtime > max_age_seconds:
|
|
101
|
+
entry.unlink()
|
|
102
|
+
except OSError:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _read_one_payload() -> Optional[_ImagePayload]:
|
|
107
|
+
from imgui_bundle import hello_imgui
|
|
108
|
+
|
|
109
|
+
folder = _immdebug_folder()
|
|
110
|
+
if not folder.exists():
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
bindata_files = sorted(
|
|
114
|
+
[f for f in folder.iterdir() if f.is_file() and f.suffix == ".bindata"],
|
|
115
|
+
key=lambda f: f.stat().st_mtime,
|
|
116
|
+
)
|
|
117
|
+
if not bindata_files:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
oldest = bindata_files[0]
|
|
121
|
+
try:
|
|
122
|
+
data = oldest.read_bytes()
|
|
123
|
+
payload = _read_image_payload(data)
|
|
124
|
+
oldest.unlink()
|
|
125
|
+
return payload
|
|
126
|
+
except Exception as e:
|
|
127
|
+
hello_imgui.log(hello_imgui.LogLevel.error, f"Error reading {oldest.name}: {e}")
|
|
128
|
+
try:
|
|
129
|
+
oldest.unlink()
|
|
130
|
+
except OSError:
|
|
131
|
+
pass
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# --- Single instance app (port of single_instance_app.cpp) ---
|
|
136
|
+
|
|
137
|
+
class _SingleInstanceApp:
|
|
138
|
+
def __init__(self, lock_name: str) -> None:
|
|
139
|
+
self._lock_name = lock_name
|
|
140
|
+
self._exit = threading.Event()
|
|
141
|
+
self._ping_received = threading.Event()
|
|
142
|
+
self._thread: Optional[threading.Thread] = None
|
|
143
|
+
|
|
144
|
+
def _ping_filename(self) -> Path:
|
|
145
|
+
return Path(tempfile.gettempdir()) / f"{self._lock_name}.ping"
|
|
146
|
+
|
|
147
|
+
def run_single_instance(self) -> bool:
|
|
148
|
+
ping_file = self._ping_filename()
|
|
149
|
+
|
|
150
|
+
# Check for stale ping file
|
|
151
|
+
if ping_file.is_file():
|
|
152
|
+
print("Ooops : stale ping file!")
|
|
153
|
+
ping_file.unlink(missing_ok=True)
|
|
154
|
+
time.sleep(0.1)
|
|
155
|
+
|
|
156
|
+
# Create ping file and wait for master to remove it
|
|
157
|
+
ping_file.write_text("Lock")
|
|
158
|
+
time.sleep(0.12)
|
|
159
|
+
|
|
160
|
+
if not ping_file.is_file():
|
|
161
|
+
print("Other instance already launched!")
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
# We are the first instance
|
|
165
|
+
print("First instance!")
|
|
166
|
+
ping_file.unlink(missing_ok=True)
|
|
167
|
+
|
|
168
|
+
self._thread = threading.Thread(target=self._ping_loop, daemon=True)
|
|
169
|
+
self._thread.start()
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
def was_pinged(self) -> bool:
|
|
173
|
+
if self._ping_received.is_set():
|
|
174
|
+
self._ping_received.clear()
|
|
175
|
+
return True
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
def _ping_loop(self) -> None:
|
|
179
|
+
while not self._exit.is_set():
|
|
180
|
+
ping_file = self._ping_filename()
|
|
181
|
+
if ping_file.is_file():
|
|
182
|
+
print("Answering ping!")
|
|
183
|
+
self._ping_received.set()
|
|
184
|
+
ping_file.unlink(missing_ok=True)
|
|
185
|
+
self._exit.wait(timeout=0.06)
|
|
186
|
+
|
|
187
|
+
def stop(self) -> None:
|
|
188
|
+
self._exit.set()
|
|
189
|
+
if self._thread:
|
|
190
|
+
self._thread.join()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# --- Main viewer ---
|
|
194
|
+
|
|
195
|
+
def _focus_window() -> None:
|
|
196
|
+
import glfw
|
|
197
|
+
from imgui_bundle import glfw_utils
|
|
198
|
+
glfw.focus_window(glfw_utils.glfw_window_hello_imgui())
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def main() -> None:
|
|
202
|
+
import glfw
|
|
203
|
+
from imgui_bundle import imgui, hello_imgui, immvision
|
|
204
|
+
|
|
205
|
+
single_instance = _SingleInstanceApp("immdebug_viewer")
|
|
206
|
+
|
|
207
|
+
if not single_instance.run_single_instance():
|
|
208
|
+
print("Exit...")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
frame_count = 0
|
|
212
|
+
|
|
213
|
+
def add_incoming_images() -> bool:
|
|
214
|
+
_remove_old_images()
|
|
215
|
+
found_new = False
|
|
216
|
+
while True:
|
|
217
|
+
payload = _read_one_payload()
|
|
218
|
+
if payload is None:
|
|
219
|
+
break
|
|
220
|
+
hello_imgui.log(hello_imgui.LogLevel.info, f"Received image: {payload.legend}")
|
|
221
|
+
if payload.is_color_order_bgr:
|
|
222
|
+
immvision.use_bgr_color_order()
|
|
223
|
+
else:
|
|
224
|
+
immvision.use_rgb_color_order()
|
|
225
|
+
immvision.inspector_add_image(
|
|
226
|
+
payload.image,
|
|
227
|
+
payload.legend,
|
|
228
|
+
payload.zoom_key,
|
|
229
|
+
payload.color_adjustments_key,
|
|
230
|
+
(payload.zoom_center[0], payload.zoom_center[1]),
|
|
231
|
+
payload.zoom_ratio,
|
|
232
|
+
)
|
|
233
|
+
found_new = True
|
|
234
|
+
if found_new:
|
|
235
|
+
_focus_window()
|
|
236
|
+
return found_new
|
|
237
|
+
|
|
238
|
+
def gui() -> None:
|
|
239
|
+
nonlocal frame_count
|
|
240
|
+
immvision.inspector_show()
|
|
241
|
+
|
|
242
|
+
# Poll for new images every 10 frames
|
|
243
|
+
if frame_count % 10 == 0:
|
|
244
|
+
add_incoming_images()
|
|
245
|
+
frame_count += 1
|
|
246
|
+
|
|
247
|
+
if single_instance.was_pinged():
|
|
248
|
+
_focus_window()
|
|
249
|
+
hello_imgui.log(hello_imgui.LogLevel.warning, "Pong")
|
|
250
|
+
|
|
251
|
+
params = hello_imgui.RunnerParams()
|
|
252
|
+
params.app_window_params.window_geometry.full_screen_mode = (
|
|
253
|
+
hello_imgui.FullScreenMode.full_monitor_work_area
|
|
254
|
+
)
|
|
255
|
+
params.app_window_params.restore_previous_geometry = True
|
|
256
|
+
params.app_window_params.window_title = "ImmVision - immdebug viewer"
|
|
257
|
+
params.callbacks.show_gui = gui
|
|
258
|
+
params.ini_folder_type = hello_imgui.IniFolderType.app_user_config_folder
|
|
259
|
+
params.ini_filename = "immdebug_viewer.ini"
|
|
260
|
+
|
|
261
|
+
glfw.init()
|
|
262
|
+
try:
|
|
263
|
+
hello_imgui.run(params)
|
|
264
|
+
finally:
|
|
265
|
+
single_instance.stop()
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
main()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: immdebug
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Send images to the immdebug viewer for visual debugging — from any Python process
|
|
5
|
+
Project-URL: Homepage, https://github.com/pthom/immvision
|
|
6
|
+
Project-URL: Repository, https://github.com/pthom/immvision
|
|
7
|
+
Project-URL: Issues, https://github.com/pthom/immvision/issues
|
|
8
|
+
Author-email: Pascal Thomet <pthomet@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: computer-vision,debugging,image,imgui,immvision
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
16
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Requires-Dist: glfw
|
|
19
|
+
Requires-Dist: imgui-bundle
|
|
20
|
+
Requires-Dist: numpy
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# immdebug
|
|
24
|
+
|
|
25
|
+
Visual image debugger for Python — inspect images from any running process with zoom, pan, pixel values, and colormaps.
|
|
26
|
+
|
|
27
|
+
Part of the [immvision](https://github.com/pthom/immvision) / [Dear ImGui Bundle](https://github.com/pthom/imgui_bundle) ecosystem.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install immdebug
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This installs both the client (for sending images) and the viewer (for displaying them).
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
**1. Start the viewer** (in a terminal):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
immdebug-viewer
|
|
43
|
+
# or: python -m immdebug
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**2. Send images** (from your code, a script, a notebook, ...):
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import numpy as np
|
|
50
|
+
from immdebug import immdebug
|
|
51
|
+
|
|
52
|
+
image = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8)
|
|
53
|
+
immdebug(image, "random noise")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The image appears in the viewer immediately.
|
|
57
|
+
|
|
58
|
+
## How it works
|
|
59
|
+
|
|
60
|
+
`immdebug()` serializes the image to a temp directory (`<tempdir>/ImmDebug/`). The viewer polls this directory and displays incoming images using [immvision](https://github.com/pthom/immvision)'s inspector.
|
|
61
|
+
|
|
62
|
+
This is the same protocol used by the C++ `ImmDebug()` function, so the Python client works with both the Python and C++ viewers.
|
|
63
|
+
|
|
64
|
+
## Use with OpenCV
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import cv2
|
|
68
|
+
from immdebug import immdebug
|
|
69
|
+
|
|
70
|
+
image = cv2.imread("photo.jpg")
|
|
71
|
+
immdebug(image, "original")
|
|
72
|
+
|
|
73
|
+
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
74
|
+
immdebug(gray, "grayscale")
|
|
75
|
+
|
|
76
|
+
edges = cv2.Canny(gray, 100, 200)
|
|
77
|
+
immdebug(edges, "edges")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Images default to BGR channel order (OpenCV convention). For RGB images (e.g. from PIL or matplotlib), pass `is_color_order_bgr=False`.
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
def immdebug(
|
|
86
|
+
image: np.ndarray,
|
|
87
|
+
legend: str = "",
|
|
88
|
+
zoom_center: tuple[float, float] = (0.0, 0.0),
|
|
89
|
+
zoom_ratio: float = -1.0,
|
|
90
|
+
zoom_key: str = "",
|
|
91
|
+
color_adjustments_key: str = "",
|
|
92
|
+
is_color_order_bgr: bool = True,
|
|
93
|
+
) -> None:
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| Parameter | Description |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `image` | numpy array — HxW (grayscale) or HxWxC (color). Supports uint8, int8, uint16, int16, int32, float32, float64. |
|
|
99
|
+
| `legend` | Display name in the viewer |
|
|
100
|
+
| `zoom_center` | Initial zoom center (x, y) |
|
|
101
|
+
| `zoom_ratio` | Initial zoom ratio (-1 for auto-fit) |
|
|
102
|
+
| `zoom_key` | Link zoom/pan across images sharing this key |
|
|
103
|
+
| `color_adjustments_key` | Link color adjustments across images sharing this key |
|
|
104
|
+
| `is_color_order_bgr` | True for BGR (OpenCV), False for RGB (PIL, matplotlib) |
|
|
105
|
+
|
|
106
|
+
## Features
|
|
107
|
+
|
|
108
|
+
- **Non-blocking**: `immdebug()` just writes a file and returns immediately
|
|
109
|
+
- **Post-mortem**: images persist in the temp directory for 1 hour — start the viewer after your script finishes
|
|
110
|
+
- **Single instance**: launching a second viewer brings the existing one to the front
|
|
111
|
+
- **Cross-language**: works with both the Python and C++ immdebug viewers
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
immdebug/__init__.py,sha256=iJ2Fsmhiso4JorTO2OgguclqKmKe6BH8O3zqwtC3_ws,367
|
|
2
|
+
immdebug/__main__.py,sha256=dh6StRaztiCptl96YPoA7GS1PmUAR7gX_acjHI7qIsI,98
|
|
3
|
+
immdebug/client.py,sha256=PALTTzek1ByRQdh0YuvXIyTa4ax7r4YnCzBBifFg9ZM,3823
|
|
4
|
+
immdebug/viewer.py,sha256=vEV-aL2tU1HP2sO53V1v2AAHCMlydLQgAVWCfAbUClg,7864
|
|
5
|
+
immdebug-0.1.0.dist-info/METADATA,sha256=LHg-fx9dFIjagoalRAwpQ2y2ciPnTxYlW13gwzMt0Kg,3639
|
|
6
|
+
immdebug-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
immdebug-0.1.0.dist-info/entry_points.txt,sha256=X4TrKG9xwtnvcOluIOrHsI8saPdR0QlLXNQDqJBgtB0,57
|
|
8
|
+
immdebug-0.1.0.dist-info/RECORD,,
|