Skip to content

Commit 9b4dc47

Browse files
authored
Merge pull request #1 from SpaceTime-Vision-Robotics-Laboratory/sebnae/streaming_mods
[PR] Streaming mode
2 parents fdd252d + a272427 commit 9b4dc47

File tree

8 files changed

+100
-26
lines changed

8 files changed

+100
-26
lines changed

README.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,39 @@
1-
# Drone Base
1+
# Drone Base Framework
2+
3+
A modular base framework for building drone control systems using Anafi Parrot drones.
4+
5+
It is designed to be used as a git submodule in drone software projects.
6+
7+
### Features
8+
- Abstraction over drone states, control and streaming.
9+
- Extensible for integrating custom modules
10+
- Lightweight and focused on real-time.
11+
12+
### Submodule Installation
13+
To include this framework in your project:
14+
```bash
15+
git submodule add [email protected]:SpaceTime-Vision-Robotics-Laboratory/drone-base.git
16+
git submodule update --init --recursive
17+
```
18+
19+
To clone a project that already includes this as a submodule:
20+
```bash
21+
git clone --recurse-submodules <LINK TO YOUR PROJECT>
22+
```
23+
24+
### Setup as standalone
25+
Install the packages from the [requirements.txt](requirements.txt).
26+
27+
28+
### Examples
29+
Run the example [controller](examples/display_only_controller.py) that only displays the video stream from the drone and has keyboard control.
30+
```bash
31+
python -m examples.display_only_controller
32+
```
33+
34+
Run the [video to frame](main/stream/saving/frames_to_video.py) to create a video from the frames generated
35+
from the video stream of a mission:
36+
```bash
37+
python -m main.stream.saving.frames_to_video
38+
```
39+

main/basic_file.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

main/config/video.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
class VideoConfig:
22
"""Configuration class for the drone video stream with validation"""
33

4-
def __init__(self, width: int = 640, height: int = 360):
4+
def __init__(self, width: int = 640, height: int = 360, cam_mode: str | None = None):
55
"""
66
Sets the width and height of the video stream. Computes the center of the frame.
77
8-
@param width: Width of the video stream in pixels, by default 640 (half of the PDRAW stream)
9-
@param height: Height of the video stream in pixels, by default 360 (half of the PDRAW stream)
8+
:param width: Width of the video stream in pixels, by default 640 (half of the PDRAW stream)
9+
:param height: Height of the video stream in pixels, by default 360 (half of the PDRAW stream)
10+
:param cam_mode: Camera mode to use, either "photo" or "recording" or default None.
11+
Photo mode has a stream for 1024x768. Recording mode is the default and it's 1280x720.
1012
"""
1113
if width <= 0 or height <= 0:
1214
raise ValueError("Video width and height must be greater than zero.")
@@ -17,6 +19,14 @@ def __init__(self, width: int = 640, height: int = 360):
1719
self.frame_area = self.__compute_frame_area()
1820
self.max_queue_size = 30
1921

22+
if cam_mode not in ("photo", "recording", None):
23+
raise ValueError(
24+
f"Invalid camera mode for: \"{cam_mode}\". "
25+
f"Possible values are \"photo\", \"recording\" or default None."
26+
)
27+
28+
self.cam_mode = cam_mode
29+
2030
def __compute_frame_center(self) -> tuple[int, int]:
2131
return self.width // 2, self.height // 2
2232

@@ -41,3 +51,4 @@ def update_dimensions(self, width: int, height: int):
4151
print(f"Width: {v.width}, Height: {v.height}")
4252
print(f"Frame center: {v.frame_center_x}, Frame center: {v.frame_center_y}")
4353
print(f"Frame area: {v.frame_area}")
54+
print(f"Camera mode: {v.cam_mode}")

main/control/manual/keyboard_controller.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from pathlib import Path
22
from typing import Final
33

4-
from main.control.manual.operations import DroneCommand, MovementVector
54
from pynput.keyboard import KeyCode, Listener
5+
66
from main.config.drone import DroneSpeed, GimbalType
77
from main.config.logger import LoggerSetup
88
from main.control.drone_commander import DroneCommander
9+
from main.control.manual.operations import DroneCommand, MovementVector
910

1011

1112
class KeyboardController:

main/stream/base_streaming_controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def __init__(
3535
log_path = Path(log_path) / self.start_time
3636
if results_path is not None:
3737
results_path = Path(results_path) / self.start_time
38-
self.video_config = VideoConfig(width=640, height=360)
38+
self.video_config = VideoConfig(width=640, height=360, cam_mode="photo")
3939

4040
self.drone = olympe.Drone(ip)
4141
self.drone_commander = DroneCommander(self.drone, logger_dir=log_path)

main/stream/base_video_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def _run_processing_loop(self):
8181
with self._frame_display_context():
8282
while self._running.is_set() and threading.main_thread().is_alive():
8383
try:
84-
frame = self.frame_queue.get_nowait()
84+
frame = self.frame_queue.get(timeout=0.1)
8585
with self._lock:
8686
self._frame_count += 1
8787
if self.is_frame_saved:

main/stream/saving/frames_to_video.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,20 +70,32 @@ def __analyze_frame_timing(self):
7070
self.logger.info("Average time between frames: %.3f s (%.2f fps)", avg_diff, 1 / avg_diff)
7171
self.logger.info("Median time between frames: %.3f s (%.2f fps)", median_diff, 1 / median_diff)
7272
self.logger.info("Standard deviation: %.3f s", std_diff)
73-
self.logger.info("Min time between frames: %.3f s (%.2f fps)", min_diff, 1 / min_diff)
74-
self.logger.info("Max time between frames: %.3f s (%.2f fps)", max_diff, 1 / max_diff)
73+
try:
74+
self.logger.info("Min time between frames: %.3f s (%.2f fps)", min_diff, 1 / min_diff)
75+
self.logger.info("Max time between frames: %.3f s (%.2f fps)", max_diff, 1 / max_diff)
76+
except ZeroDivisionError:
77+
self.logger.info("Division by zero, skipping min and max fps calculation.")
7578

7679
self.logger.info("Frame timing distribution:")
7780
for range_key, count in sorted(ranges.items()):
7881
percentage = (count / len(time_diffs)) * 100
79-
self.logger.info("%s ms: %s frames (%s.1f %)", range_key, count, percentage)
80-
81-
return {
82-
'avg_fps': 1 / avg_diff,
83-
'min_fps': 1 / max_diff,
84-
'max_fps': 1 / min_diff,
85-
'std_dev': std_diff
86-
}
82+
self.logger.info("%s ms: %s frames (%.1f %%)", range_key, count, percentage)
83+
84+
try:
85+
return {
86+
'avg_fps': 1 / avg_diff,
87+
'min_fps': 1 / max_diff,
88+
'max_fps': 1 / min_diff,
89+
'std_dev': std_diff
90+
}
91+
except ZeroDivisionError:
92+
self.logger.info("Division by zero, skipping fps calculation.")
93+
return {
94+
'avg_fps': 0,
95+
'min_fps': 0,
96+
'max_fps': 0,
97+
'std_dev': 0
98+
}
8799

88100
def create_video(self):
89101
self.__populate_frame_paths()
@@ -125,8 +137,21 @@ def create_video(input_dir: str | Path, output_path: str | Path = "output.mp4",
125137

126138

127139
if __name__ == "__main__":
140+
import argparse
141+
142+
parser = argparse.ArgumentParser(description="Create a video from a frame folder.")
143+
parser.add_argument(
144+
"--input_dir",
145+
default="./examples/results/2025-04-01_12-54-59/frames",
146+
type=str,
147+
help="Path to the input directory containing frames"
148+
)
149+
parser.add_argument("--output_path", default="./output.mp4", type=str, help="Path to save the output video")
150+
parser.add_argument("--frame_type", default="png", type=str, help="Frame type (e.g., png, jpg)")
151+
args = parser.parse_args()
152+
128153
create_video(
129-
input_dir="example/path/to/input/dir",
130-
output_path="output.mp4",
131-
frame_type="jpg"
154+
input_dir=args.input_dir,
155+
output_path=args.output_path,
156+
frame_type=args.frame_type
132157
)

main/stream/stream_handler.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import numpy as np
88
import olympe
99
from olympe import PdrawRenderer
10+
from olympe.messages import camera
1011

1112
from main.config.logger import LoggerSetup
1213
from main.config.video import VideoConfig
@@ -61,6 +62,10 @@ def start_streaming(self):
6162
)
6263
self.logger.info("Starting streaming...")
6364
self.drone.streaming.start()
65+
66+
if self.config.cam_mode in ("photo", "recording"):
67+
self.drone(camera.set_camera_mode(cam_id=0, value=self.config.cam_mode))
68+
6469
self.is_streaming = True
6570
if self.is_renderer_started:
6671
self.renderer = PdrawRenderer(pdraw=self.drone.streaming)

0 commit comments

Comments
 (0)