rocket-welder-sdk 1.1.43__py3-none-any.whl → 1.1.45__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.
- rocket_welder_sdk/__init__.py +44 -22
- rocket_welder_sdk/binary_frame_reader.py +222 -0
- rocket_welder_sdk/binary_frame_writer.py +213 -0
- rocket_welder_sdk/confidence.py +206 -0
- rocket_welder_sdk/delta_frame.py +150 -0
- rocket_welder_sdk/graphics/__init__.py +42 -0
- rocket_welder_sdk/graphics/layer_canvas.py +157 -0
- rocket_welder_sdk/graphics/protocol.py +72 -0
- rocket_welder_sdk/graphics/rgb_color.py +109 -0
- rocket_welder_sdk/graphics/stage.py +494 -0
- rocket_welder_sdk/graphics/vector_graphics_encoder.py +575 -0
- rocket_welder_sdk/high_level/__init__.py +8 -1
- rocket_welder_sdk/high_level/client.py +114 -3
- rocket_welder_sdk/high_level/connection_strings.py +88 -15
- rocket_welder_sdk/high_level/frame_sink_factory.py +2 -15
- rocket_welder_sdk/high_level/transport_protocol.py +4 -130
- rocket_welder_sdk/keypoints_protocol.py +520 -55
- rocket_welder_sdk/rocket_welder_client.py +210 -89
- rocket_welder_sdk/segmentation_result.py +387 -2
- rocket_welder_sdk/session_id.py +7 -182
- rocket_welder_sdk/transport/__init__.py +10 -3
- rocket_welder_sdk/transport/frame_sink.py +3 -3
- rocket_welder_sdk/transport/frame_source.py +2 -2
- rocket_welder_sdk/transport/websocket_transport.py +316 -0
- rocket_welder_sdk/varint.py +213 -0
- {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/METADATA +1 -4
- rocket_welder_sdk-1.1.45.dist-info/RECORD +51 -0
- {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/WHEEL +1 -1
- rocket_welder_sdk/transport/nng_transport.py +0 -197
- rocket_welder_sdk-1.1.43.dist-info/RECORD +0 -40
- {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/top_level.txt +0 -0
|
@@ -15,7 +15,12 @@ import numpy as np
|
|
|
15
15
|
from .connection_string import ConnectionMode, ConnectionString, Protocol
|
|
16
16
|
from .controllers import DuplexShmController, IController, OneWayShmController
|
|
17
17
|
from .frame_metadata import FrameMetadata # noqa: TC001 - used at runtime in callbacks
|
|
18
|
-
from .
|
|
18
|
+
from .graphics import ILayerCanvas, IStageSink, IStageWriter, RgbColor, StageSink
|
|
19
|
+
from .high_level.connection_strings import (
|
|
20
|
+
GraphicsConnectionString,
|
|
21
|
+
KeyPointsConnectionString,
|
|
22
|
+
SegmentationConnectionString,
|
|
23
|
+
)
|
|
19
24
|
from .high_level.frame_sink_factory import FrameSinkFactory
|
|
20
25
|
from .keypoints_protocol import IKeyPointsSink, IKeyPointsWriter, KeyPointsSink
|
|
21
26
|
from .opencv_controller import OpenCvController
|
|
@@ -24,12 +29,6 @@ from .segmentation_result import (
|
|
|
24
29
|
ISegmentationResultWriter,
|
|
25
30
|
SegmentationResultSink,
|
|
26
31
|
)
|
|
27
|
-
from .session_id import (
|
|
28
|
-
get_configured_nng_urls,
|
|
29
|
-
get_nng_urls_from_env,
|
|
30
|
-
has_explicit_nng_urls,
|
|
31
|
-
)
|
|
32
|
-
from .transport.nng_transport import NngFrameSink
|
|
33
32
|
|
|
34
33
|
if TYPE_CHECKING:
|
|
35
34
|
import numpy.typing as npt
|
|
@@ -67,9 +66,6 @@ class RocketWelderClient:
|
|
|
67
66
|
self._controller: Optional[IController] = None
|
|
68
67
|
self._lock = threading.Lock()
|
|
69
68
|
|
|
70
|
-
# NNG publishers for streaming results (auto-created if SessionId env var is set)
|
|
71
|
-
self._nng_publishers: dict[str, NngFrameSink] = {}
|
|
72
|
-
|
|
73
69
|
# Preview support
|
|
74
70
|
self._preview_enabled = (
|
|
75
71
|
self._connection.parameters.get("preview", "false").lower() == "true"
|
|
@@ -89,50 +85,6 @@ class RocketWelderClient:
|
|
|
89
85
|
with self._lock:
|
|
90
86
|
return self._controller is not None and self._controller.is_running
|
|
91
87
|
|
|
92
|
-
@property
|
|
93
|
-
def nng_publishers(self) -> dict[str, NngFrameSink]:
|
|
94
|
-
"""Get NNG publishers for streaming results.
|
|
95
|
-
|
|
96
|
-
Returns:
|
|
97
|
-
Dictionary with 'segmentation', 'keypoints', 'actions' keys.
|
|
98
|
-
Empty if SessionId env var was not set at startup.
|
|
99
|
-
|
|
100
|
-
Example:
|
|
101
|
-
client.nng_publishers["segmentation"].write_frame(seg_data)
|
|
102
|
-
"""
|
|
103
|
-
return self._nng_publishers
|
|
104
|
-
|
|
105
|
-
def _create_nng_publishers(self) -> None:
|
|
106
|
-
"""Create NNG publishers for result streaming.
|
|
107
|
-
|
|
108
|
-
URLs are read from environment variables (preferred) or derived from SessionId (fallback).
|
|
109
|
-
|
|
110
|
-
Priority:
|
|
111
|
-
1. Explicit URLs: SEGMENTATION_SINK_URL, KEYPOINTS_SINK_URL, ACTIONS_SINK_URL
|
|
112
|
-
2. Derived from SessionId environment variable (backwards compatibility)
|
|
113
|
-
"""
|
|
114
|
-
try:
|
|
115
|
-
urls = get_configured_nng_urls()
|
|
116
|
-
|
|
117
|
-
for name, url in urls.items():
|
|
118
|
-
sink = NngFrameSink.create_publisher(url)
|
|
119
|
-
self._nng_publishers[name] = sink
|
|
120
|
-
logger.info("NNG publisher ready: %s at %s", name, url)
|
|
121
|
-
|
|
122
|
-
# Log configuration summary
|
|
123
|
-
logger.info(
|
|
124
|
-
"NNG publishers configured: seg=%s, kp=%s, actions=%s",
|
|
125
|
-
urls.get("segmentation", "(not configured)"),
|
|
126
|
-
urls.get("keypoints", "(not configured)"),
|
|
127
|
-
urls.get("actions", "(not configured)"),
|
|
128
|
-
)
|
|
129
|
-
except ValueError as ex:
|
|
130
|
-
# No URLs configured - this is expected for containers that don't publish results
|
|
131
|
-
logger.debug("NNG publishers not configured: %s", ex)
|
|
132
|
-
except Exception as ex:
|
|
133
|
-
logger.warning("Failed to create NNG publishers: %s", ex)
|
|
134
|
-
# Don't fail start() - NNG is optional for backwards compatibility
|
|
135
|
-
|
|
136
88
|
def get_metadata(self) -> Optional[GstMetadata]:
|
|
137
89
|
"""
|
|
138
90
|
Get the current GStreamer metadata.
|
|
@@ -180,21 +132,6 @@ class RocketWelderClient:
|
|
|
180
132
|
else:
|
|
181
133
|
raise ValueError(f"Unsupported protocol: {self._connection.protocol}")
|
|
182
134
|
|
|
183
|
-
# Auto-create NNG publishers if URLs are configured
|
|
184
|
-
# (explicit URLs via SEGMENTATION_SINK_URL etc., or derived from SessionId)
|
|
185
|
-
if has_explicit_nng_urls():
|
|
186
|
-
self._create_nng_publishers()
|
|
187
|
-
else:
|
|
188
|
-
# Log that NNG is not configured (informational)
|
|
189
|
-
urls = get_nng_urls_from_env()
|
|
190
|
-
logger.info(
|
|
191
|
-
"NNG sink URLs not configured (this is normal if not publishing AI results). "
|
|
192
|
-
"seg=%s, kp=%s, actions=%s",
|
|
193
|
-
urls.get("segmentation") or "(not set)",
|
|
194
|
-
urls.get("keypoints") or "(not set)",
|
|
195
|
-
urls.get("actions") or "(not set)",
|
|
196
|
-
)
|
|
197
|
-
|
|
198
135
|
# If preview is enabled, wrap the callback to capture frames
|
|
199
136
|
if self._preview_enabled:
|
|
200
137
|
self._original_callback = on_frame
|
|
@@ -256,25 +193,26 @@ class RocketWelderClient:
|
|
|
256
193
|
|
|
257
194
|
def start_with_writers(
|
|
258
195
|
self,
|
|
259
|
-
on_frame: Callable[[Mat, ISegmentationResultWriter, IKeyPointsWriter, Mat], None], # type: ignore[valid-type]
|
|
196
|
+
on_frame: Callable[[Mat, ISegmentationResultWriter, IKeyPointsWriter, IStageWriter, Mat], None], # type: ignore[valid-type]
|
|
260
197
|
cancellation_token: Optional[threading.Event] = None,
|
|
261
198
|
) -> None:
|
|
262
199
|
"""
|
|
263
|
-
Start receiving frames with segmentation and
|
|
200
|
+
Start receiving frames with segmentation, keypoints, and graphics output support.
|
|
264
201
|
|
|
265
202
|
Creates sinks for streaming AI results to rocket-welder2.
|
|
266
203
|
|
|
267
204
|
Configuration via environment variables:
|
|
268
205
|
- SEGMENTATION_SINK_URL: URL for segmentation output (e.g., socket:///tmp/seg.sock)
|
|
269
206
|
- KEYPOINTS_SINK_URL: URL for keypoints output (e.g., socket:///tmp/kp.sock)
|
|
207
|
+
- STAGE_SINK_URL: URL for graphics/stage output (e.g., socket:///tmp/stage.sock)
|
|
270
208
|
|
|
271
209
|
Args:
|
|
272
|
-
on_frame: Callback receiving (input_mat, seg_writer, kp_writer, output_mat).
|
|
210
|
+
on_frame: Callback receiving (input_mat, seg_writer, kp_writer, stage_writer, output_mat).
|
|
273
211
|
The writers are created per-frame and auto-flush on context exit.
|
|
274
212
|
cancellation_token: Optional cancellation token
|
|
275
213
|
|
|
276
214
|
Example:
|
|
277
|
-
def process_frame(input_mat, seg_writer, kp_writer, output_mat):
|
|
215
|
+
def process_frame(input_mat, seg_writer, kp_writer, stage_writer, output_mat):
|
|
278
216
|
# Run AI inference
|
|
279
217
|
result = ai_model.infer(input_mat)
|
|
280
218
|
|
|
@@ -286,6 +224,11 @@ class RocketWelderClient:
|
|
|
286
224
|
for kp in result.keypoints:
|
|
287
225
|
kp_writer.append(kp.id, kp.x, kp.y, kp.confidence)
|
|
288
226
|
|
|
227
|
+
# Draw graphics overlay
|
|
228
|
+
layer = stage_writer[0]
|
|
229
|
+
layer.set_font_size(24)
|
|
230
|
+
layer.draw_text("Detection count: 5", 10, 30)
|
|
231
|
+
|
|
289
232
|
# Copy/draw to output
|
|
290
233
|
output_mat[:] = input_mat
|
|
291
234
|
|
|
@@ -316,11 +259,13 @@ class RocketWelderClient:
|
|
|
316
259
|
# Create sinks from environment
|
|
317
260
|
seg_sink = self._get_or_create_segmentation_sink()
|
|
318
261
|
kp_sink = self._get_or_create_keypoints_sink()
|
|
262
|
+
stage_sink = self._get_or_create_stage_sink()
|
|
319
263
|
|
|
320
264
|
logger.info(
|
|
321
|
-
"Starting RocketWelder client with AI output support: seg=%s, kp=%s",
|
|
265
|
+
"Starting RocketWelder client with AI output support: seg=%s, kp=%s, stage=%s",
|
|
322
266
|
"configured" if seg_sink else "null",
|
|
323
267
|
"configured" if kp_sink else "null",
|
|
268
|
+
"configured" if stage_sink else "null",
|
|
324
269
|
)
|
|
325
270
|
|
|
326
271
|
# Wrapper callback that creates per-frame writers
|
|
@@ -337,17 +282,26 @@ class RocketWelderClient:
|
|
|
337
282
|
frame_metadata.frame_number,
|
|
338
283
|
)
|
|
339
284
|
# Use no-op writers
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
285
|
+
with stage_sink.create_writer(frame_metadata.frame_number) as stage_writer:
|
|
286
|
+
on_frame(
|
|
287
|
+
input_mat,
|
|
288
|
+
_NoOpSegmentationWriter(),
|
|
289
|
+
_NoOpKeyPointsWriter(),
|
|
290
|
+
stage_writer,
|
|
291
|
+
output_mat,
|
|
292
|
+
)
|
|
343
293
|
return
|
|
344
294
|
|
|
345
|
-
# Create per-frame writers from sinks
|
|
295
|
+
# Create per-frame writers from sinks (all auto-flush on context exit)
|
|
346
296
|
with seg_sink.create_writer(
|
|
347
297
|
frame_metadata.frame_number, caps.width, caps.height
|
|
348
|
-
) as seg_writer, kp_sink.create_writer(
|
|
298
|
+
) as seg_writer, kp_sink.create_writer(
|
|
299
|
+
frame_metadata.frame_number
|
|
300
|
+
) as kp_writer, stage_sink.create_writer(
|
|
301
|
+
frame_metadata.frame_number
|
|
302
|
+
) as stage_writer:
|
|
349
303
|
# Call user callback with writers
|
|
350
|
-
on_frame(input_mat, seg_writer, kp_writer, output_mat)
|
|
304
|
+
on_frame(input_mat, seg_writer, kp_writer, stage_writer, output_mat)
|
|
351
305
|
# Writers auto-flush on context exit
|
|
352
306
|
|
|
353
307
|
# Start the controller with our wrapper
|
|
@@ -392,6 +346,23 @@ class RocketWelderClient:
|
|
|
392
346
|
logger.warning("Failed to create keypoints sink from %s: %s", url, ex)
|
|
393
347
|
return _NullKeyPointsSink()
|
|
394
348
|
|
|
349
|
+
def _get_or_create_stage_sink(self) -> IStageSink:
|
|
350
|
+
"""Get or create graphics stage sink from environment."""
|
|
351
|
+
import os
|
|
352
|
+
|
|
353
|
+
url = os.environ.get("GRAPHICS_SINK_URL")
|
|
354
|
+
if not url:
|
|
355
|
+
logger.debug("GRAPHICS_SINK_URL not set, using null sink")
|
|
356
|
+
return _NullStageSink()
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
cs = GraphicsConnectionString.parse(url)
|
|
360
|
+
frame_sink = FrameSinkFactory.create(cs.protocol, cs.address)
|
|
361
|
+
return StageSink(frame_sink=frame_sink, owns_sink=True)
|
|
362
|
+
except Exception as ex:
|
|
363
|
+
logger.warning("Failed to create graphics stage sink from %s: %s", url, ex)
|
|
364
|
+
return _NullStageSink()
|
|
365
|
+
|
|
395
366
|
def stop(self) -> None:
|
|
396
367
|
"""Stop the client and clean up resources."""
|
|
397
368
|
with self._lock:
|
|
@@ -403,15 +374,6 @@ class RocketWelderClient:
|
|
|
403
374
|
if self._preview_enabled:
|
|
404
375
|
self._preview_queue.put(None) # Sentinel value
|
|
405
376
|
|
|
406
|
-
# Clean up NNG publishers
|
|
407
|
-
for name, sink in self._nng_publishers.items():
|
|
408
|
-
try:
|
|
409
|
-
sink.close()
|
|
410
|
-
logger.debug("Closed NNG publisher: %s", name)
|
|
411
|
-
except Exception as ex:
|
|
412
|
-
logger.warning("Failed to close NNG publisher %s: %s", name, ex)
|
|
413
|
-
self._nng_publishers.clear()
|
|
414
|
-
|
|
415
377
|
logger.info("RocketWelder client stopped")
|
|
416
378
|
|
|
417
379
|
def show(self, cancellation_token: Optional[threading.Event] = None) -> None:
|
|
@@ -697,3 +659,162 @@ class _NullSegmentationSink(ISegmentationResultSink):
|
|
|
697
659
|
def close(self) -> None:
|
|
698
660
|
"""No-op close."""
|
|
699
661
|
pass
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
class _NoOpLayerCanvas(ILayerCanvas):
|
|
665
|
+
"""No-op layer canvas that discards all drawing operations."""
|
|
666
|
+
|
|
667
|
+
@property
|
|
668
|
+
def layer_id(self) -> int:
|
|
669
|
+
"""The layer ID."""
|
|
670
|
+
return 0
|
|
671
|
+
|
|
672
|
+
# Frame type
|
|
673
|
+
def master(self) -> None:
|
|
674
|
+
"""No-op."""
|
|
675
|
+
pass
|
|
676
|
+
|
|
677
|
+
def remain(self) -> None:
|
|
678
|
+
"""No-op."""
|
|
679
|
+
pass
|
|
680
|
+
|
|
681
|
+
def clear(self) -> None:
|
|
682
|
+
"""No-op."""
|
|
683
|
+
pass
|
|
684
|
+
|
|
685
|
+
# Context state - Styling
|
|
686
|
+
def set_stroke(self, color: RgbColor) -> None:
|
|
687
|
+
"""No-op."""
|
|
688
|
+
pass
|
|
689
|
+
|
|
690
|
+
def set_fill(self, color: RgbColor) -> None:
|
|
691
|
+
"""No-op."""
|
|
692
|
+
pass
|
|
693
|
+
|
|
694
|
+
def set_thickness(self, width: int) -> None:
|
|
695
|
+
"""No-op."""
|
|
696
|
+
pass
|
|
697
|
+
|
|
698
|
+
def set_font_size(self, size: int) -> None:
|
|
699
|
+
"""No-op."""
|
|
700
|
+
pass
|
|
701
|
+
|
|
702
|
+
def set_font_color(self, color: RgbColor) -> None:
|
|
703
|
+
"""No-op."""
|
|
704
|
+
pass
|
|
705
|
+
|
|
706
|
+
# Context state - Transforms
|
|
707
|
+
def translate(self, dx: float, dy: float) -> None:
|
|
708
|
+
"""No-op."""
|
|
709
|
+
pass
|
|
710
|
+
|
|
711
|
+
def rotate(self, degrees: float) -> None:
|
|
712
|
+
"""No-op."""
|
|
713
|
+
pass
|
|
714
|
+
|
|
715
|
+
def scale(self, sx: float, sy: float) -> None:
|
|
716
|
+
"""No-op."""
|
|
717
|
+
pass
|
|
718
|
+
|
|
719
|
+
def skew(self, kx: float, ky: float) -> None:
|
|
720
|
+
"""No-op."""
|
|
721
|
+
pass
|
|
722
|
+
|
|
723
|
+
def set_matrix(
|
|
724
|
+
self,
|
|
725
|
+
scale_x: float,
|
|
726
|
+
skew_x: float,
|
|
727
|
+
trans_x: float,
|
|
728
|
+
skew_y: float,
|
|
729
|
+
scale_y: float,
|
|
730
|
+
trans_y: float,
|
|
731
|
+
) -> None:
|
|
732
|
+
"""No-op."""
|
|
733
|
+
pass
|
|
734
|
+
|
|
735
|
+
# Context stack
|
|
736
|
+
def save(self) -> None:
|
|
737
|
+
"""No-op."""
|
|
738
|
+
pass
|
|
739
|
+
|
|
740
|
+
def restore(self) -> None:
|
|
741
|
+
"""No-op."""
|
|
742
|
+
pass
|
|
743
|
+
|
|
744
|
+
def reset_context(self) -> None:
|
|
745
|
+
"""No-op."""
|
|
746
|
+
pass
|
|
747
|
+
|
|
748
|
+
# Draw operations
|
|
749
|
+
def draw_polygon(self, points: Any) -> None:
|
|
750
|
+
"""No-op."""
|
|
751
|
+
pass
|
|
752
|
+
|
|
753
|
+
def draw_text(self, text: str, x: int, y: int) -> None:
|
|
754
|
+
"""No-op."""
|
|
755
|
+
pass
|
|
756
|
+
|
|
757
|
+
def draw_circle(self, center_x: int, center_y: int, radius: int) -> None:
|
|
758
|
+
"""No-op."""
|
|
759
|
+
pass
|
|
760
|
+
|
|
761
|
+
def draw_rectangle(self, x: int, y: int, width: int, height: int) -> None:
|
|
762
|
+
"""No-op."""
|
|
763
|
+
pass
|
|
764
|
+
|
|
765
|
+
def draw_line(self, x1: int, y1: int, x2: int, y2: int) -> None:
|
|
766
|
+
"""No-op."""
|
|
767
|
+
pass
|
|
768
|
+
|
|
769
|
+
def draw_jpeg(self, jpeg_data: bytes, x: int, y: int, width: int, height: int) -> None:
|
|
770
|
+
"""No-op."""
|
|
771
|
+
pass
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
# Singleton instance
|
|
775
|
+
_NO_OP_LAYER_CANVAS = _NoOpLayerCanvas()
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
class _NoOpStageWriter(IStageWriter):
|
|
779
|
+
"""No-op stage writer that discards all graphics operations."""
|
|
780
|
+
|
|
781
|
+
@property
|
|
782
|
+
def frame_id(self) -> int:
|
|
783
|
+
"""The frame ID."""
|
|
784
|
+
return 0
|
|
785
|
+
|
|
786
|
+
def __getitem__(self, layer_id: int) -> ILayerCanvas:
|
|
787
|
+
"""Returns no-op layer canvas."""
|
|
788
|
+
return _NO_OP_LAYER_CANVAS
|
|
789
|
+
|
|
790
|
+
def layer(self, layer_id: int) -> ILayerCanvas:
|
|
791
|
+
"""Returns no-op layer canvas."""
|
|
792
|
+
return _NO_OP_LAYER_CANVAS
|
|
793
|
+
|
|
794
|
+
def close(self) -> None:
|
|
795
|
+
"""No-op close."""
|
|
796
|
+
pass
|
|
797
|
+
|
|
798
|
+
def __enter__(self) -> _NoOpStageWriter:
|
|
799
|
+
"""Context manager entry."""
|
|
800
|
+
return self
|
|
801
|
+
|
|
802
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
|
|
803
|
+
"""Context manager exit."""
|
|
804
|
+
pass
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
# Singleton instance
|
|
808
|
+
_NO_OP_STAGE_WRITER = _NoOpStageWriter()
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
class _NullStageSink(IStageSink):
|
|
812
|
+
"""Null stage sink that creates no-op writers."""
|
|
813
|
+
|
|
814
|
+
def create_writer(self, frame_id: int) -> IStageWriter:
|
|
815
|
+
"""Create a no-op writer."""
|
|
816
|
+
return _NO_OP_STAGE_WRITER
|
|
817
|
+
|
|
818
|
+
def close(self) -> None:
|
|
819
|
+
"""No-op close."""
|
|
820
|
+
pass
|