diff --git a/entry.sh b/entry.sh index 330f640..1f3f3a8 100755 --- a/entry.sh +++ b/entry.sh @@ -3,6 +3,6 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" $DIR/bootstrap.sh $DIR $DIR/venv -$DIR/venv/bin/python $DIR/src/main.py +DISPLAY=:0 $DIR/venv/bin/python $DIR/src/main.py exit 0 diff --git a/manifest.json b/manifest.json index ee753a1..b42d1d7 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "camera-app-kivy": { "name": "camera-app-kivy", "type": "app", - "exec_cmd": "/mnt/managed_home/farm-ng-user-edgar/camera-streamer-kivy/entry.sh", + "exec_cmd": "/mnt/managed_home/farm-ng-user-/camera-streamer-kivy/entry.sh", "display_name": "Kivy Camera App", "autostart": false } diff --git a/service_config.json b/service_config.json new file mode 100644 index 0000000..3f7db6a --- /dev/null +++ b/service_config.json @@ -0,0 +1,19 @@ +{ + "configs": [ + { + "name": "oak0", + "port": 50010, + "host": "localhost", + "log_level": "INFO", + "subscriptions": [ + { + "uri": { + "path": "*", + "query": "service_name=oak0" + }, + "every_n": 1 + } + ] + } + ] +} diff --git a/setup.cfg b/setup.cfg index 4600c83..9ccdfef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,7 @@ install_requires = wheel kivy farm_ng_amiga - kornia-rs + PyTurboJPEG tests_require = pytest diff --git a/src/main.py b/src/main.py index 13dea02..41521b1 100644 --- a/src/main.py +++ b/src/main.py @@ -24,9 +24,10 @@ from farm_ng.core.event_service_pb2 import EventServiceConfig from farm_ng.core.event_service_pb2 import EventServiceConfigList from farm_ng.core.event_service_pb2 import SubscribeRequest +from farm_ng.core.events_file_reader import payload_to_protobuf from farm_ng.core.events_file_reader import proto_from_json_file from farm_ng.core.uri_pb2 import Uri -from kornia_rs import ImageDecoder +from turbojpeg import TurboJPEG os.environ["KIVY_NO_ARGS"] = "1" @@ -51,66 +52,93 @@ class CameraApp(App): STREAM_NAMES = ["rgb", "disparity", "left", "right"] - def __init__(self, service_config: EventServiceConfig, stream_every_n: int) -> None: + def __init__(self, service_config: EventServiceConfig) -> None: super().__init__() + self.service_config = service_config - self.stream_every_n = stream_every_n - self.image_decoder = ImageDecoder() - self.image_subscription_tasks: list[asyncio.Task] = [] + self.image_decoder = TurboJPEG() + + self.view_name = "rgb" + + self.async_tasks: list[asyncio.Task] = [] def build(self): return Builder.load_file("res/main.kv") def on_exit_btn(self) -> None: """Kills the running kivy application.""" + for task in self.tasks: + task.cancel() App.get_running_app().stop() + def update_view(self, view_name: str): + self.view_name = view_name + async def app_func(self): async def run_wrapper(): # we don't actually need to set asyncio as the lib because it is # the default, but it doesn't hurt to be explicit await self.async_run(async_lib="asyncio") - for task in self.image_subscription_tasks: + for task in self.async_tasks: task.cancel() + config_list = proto_from_json_file( + self.service_config, EventServiceConfigList() + ) + + oak0_client: EventClient | None = None + + for config in config_list.configs: + if config.name == "oak0": + oak0_client = EventClient(config) + + if oak0_client is None: + raise RuntimeError(f"No {config.name} service config provided in service_config.json") + # stream camera frames - self.image_subscription_tasks: list[asyncio.Task] = [ - asyncio.create_task(self.stream_camera(view_name)) + self.tasks: list[asyncio.Task] = [ + asyncio.create_task(self.stream_camera(oak0_client, view_name)) for view_name in self.STREAM_NAMES ] - return await asyncio.gather(run_wrapper(), *self.image_subscription_tasks) + return await asyncio.gather(run_wrapper(), *self.tasks) async def stream_camera( - self, view_name: Literal["rgb", "disparity", "left", "right"] = "rgb" + self, + oak_client: EventClient, + view_name: Literal["rgb", "disparity", "left", "right"] = "rgb", ) -> None: """Subscribes to the camera service and populates the tabbed panel with all 4 image streams.""" while self.root is None: await asyncio.sleep(0.01) - async for _, message in EventClient(self.service_config).subscribe( - SubscribeRequest( - uri=Uri(path=f"/{view_name}"), every_n=self.stream_every_n - ), - decode=True, + rate = oak_client.config.subscriptions[0].every_n + + async for event, payload in oak_client.subscribe( + SubscribeRequest(uri=Uri(path=f"/{view_name}"), every_n=rate), + decode=False, ): - try: - img = self.image_decoder.decode(message.image_data) - except Exception as e: - logger.exception(f"Error decoding image: {e}") - continue - - # create the opengl texture and set it to the image - texture = Texture.create(size=(img.shape[1], img.shape[0]), icolorfmt="rgb") - texture.flip_vertical() - texture.blit_buffer( - bytes(img.data), - colorfmt="rgb", - bufferfmt="ubyte", - mipmap_generation=False, - ) - self.root.ids[view_name].texture = texture + if view_name == self.view_name: + message = payload_to_protobuf(event, payload) + try: + img = self.image_decoder.decode(message.image_data) + except Exception as e: + logger.exception(f"Error decoding image: {e}") + continue + + # create the opengl texture and set it to the image + texture = Texture.create( + size=(img.shape[1], img.shape[0]), icolorfmt="bgr" + ) + texture.flip_vertical() + texture.blit_buffer( + bytes(img.data), + colorfmt="bgr", + bufferfmt="ubyte", + mipmap_generation=False, + ) + self.root.ids[view_name].texture = texture def find_config_by_name( @@ -129,32 +157,17 @@ def find_config_by_name( if __name__ == "__main__": - parser = argparse.ArgumentParser(prog="amiga-camera-app") - parser.add_argument( - "--service-config", type=Path, default="/opt/farmng/config.json" - ) - parser.add_argument("--camera-name", type=str, default="oak1") - parser.add_argument( - "--stream-every-n", type=int, default=1, help="Streaming frequency" - ) - args = parser.parse_args() + parser = argparse.ArgumentParser(prog="template-app") - # config with all the configs - service_config_list: EventServiceConfigList = proto_from_json_file( - args.service_config, EventServiceConfigList() - ) + # Add additional command line arguments here + parser.add_argument("--service-config", type=Path, default="service_config.json") - # filter out services to pass to the events client manager - oak_service_config = find_config_by_name(service_config_list, args.camera_name) - if oak_service_config is None: - raise RuntimeError(f"Could not find service config for {args.camera_name}") + args = parser.parse_args() loop = asyncio.get_event_loop() try: - loop.run_until_complete( - CameraApp(oak_service_config, args.stream_every_n).app_func() - ) + loop.run_until_complete(CameraApp(args.service_config).app_func()) except asyncio.CancelledError: pass loop.close() diff --git a/src/res/main.kv b/src/res/main.kv index 4b4c8f4..3b14249 100644 --- a/src/res/main.kv +++ b/src/res/main.kv @@ -4,18 +4,22 @@ RelativeLayout: do_default_tab: False TabbedPanelItem: text: "Rgb" + on_press: app.update_view('rgb') Image: id: rgb TabbedPanelItem: text: "Disparity" + on_press: app.update_view('disparity') Image: id: disparity TabbedPanelItem: text: "Left" + on_press: app.update_view('left') Image: id: left TabbedPanelItem: text: "Right" + on_press: app.update_view('right') Image: id: right Button: