dji-telemetry 1.0.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.
@@ -0,0 +1,80 @@
1
+ """
2
+ DJI Telemetry Overlay Library
3
+
4
+ A Python library to parse DJI drone SRT telemetry files and overlay
5
+ flight data onto video footage.
6
+
7
+ Basic usage:
8
+ from dji_telemetry import parse_srt, process_video
9
+
10
+ # Parse telemetry
11
+ telemetry = parse_srt('video.SRT')
12
+
13
+ # Process video with overlay
14
+ process_video('video.MP4', telemetry, 'output.mp4')
15
+
16
+ Export telemetry data:
17
+ from dji_telemetry import parse_srt, export
18
+
19
+ telemetry = parse_srt('video.SRT')
20
+ export(telemetry, 'telemetry.csv')
21
+ export(telemetry, 'telemetry.json')
22
+ export(telemetry, 'telemetry.gpx')
23
+ """
24
+
25
+ __version__ = '1.0.0'
26
+
27
+ # Core parser
28
+ from .parser import (
29
+ parse_srt,
30
+ TelemetryFrame,
31
+ TelemetryData,
32
+ )
33
+
34
+ # Exporters
35
+ from .exporter import (
36
+ export,
37
+ to_csv,
38
+ to_json,
39
+ to_gpx,
40
+ )
41
+
42
+ # Overlay rendering
43
+ from .overlay import (
44
+ OverlayConfig,
45
+ OverlayRenderer,
46
+ create_transparent_frame,
47
+ )
48
+
49
+ # Video processing
50
+ from .video import (
51
+ process_video,
52
+ generate_overlay_video,
53
+ generate_overlay_frames,
54
+ add_audio,
55
+ get_video_info,
56
+ )
57
+
58
+ __all__ = [
59
+ # Version
60
+ '__version__',
61
+ # Parser
62
+ 'parse_srt',
63
+ 'TelemetryFrame',
64
+ 'TelemetryData',
65
+ # Exporters
66
+ 'export',
67
+ 'to_csv',
68
+ 'to_json',
69
+ 'to_gpx',
70
+ # Overlay
71
+ 'OverlayConfig',
72
+ 'OverlayRenderer',
73
+ 'create_transparent_frame',
74
+ # Video
75
+ 'process_video',
76
+ 'generate_overlay_video',
77
+ 'generate_overlay_frames',
78
+ 'add_audio',
79
+ 'get_video_info',
80
+ ]
@@ -0,0 +1,210 @@
1
+ """
2
+ Export telemetry data to various formats (CSV, JSON, GPX).
3
+ """
4
+
5
+ import csv
6
+ import json
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Optional
10
+ from xml.etree import ElementTree as ET
11
+ from xml.dom import minidom
12
+
13
+ from .parser import TelemetryData
14
+
15
+
16
+ def to_csv(data: TelemetryData, output_path: str | Path, include_all_fields: bool = True) -> Path:
17
+ """
18
+ Export telemetry data to CSV format.
19
+
20
+ Args:
21
+ data: TelemetryData object
22
+ output_path: Output file path
23
+ include_all_fields: Include all fields or just essential ones
24
+
25
+ Returns:
26
+ Path to the created file
27
+ """
28
+ output_path = Path(output_path)
29
+
30
+ if include_all_fields:
31
+ fieldnames = [
32
+ 'frame_num', 'timestamp', 'start_time_ms', 'end_time_ms',
33
+ 'latitude', 'longitude', 'rel_altitude_m', 'abs_altitude_m',
34
+ 'h_speed_ms', 'h_speed_kmh', 'v_speed_ms', 'distance_m',
35
+ 'iso', 'shutter', 'fnum', 'ev', 'color_temp'
36
+ ]
37
+ else:
38
+ fieldnames = [
39
+ 'timestamp', 'latitude', 'longitude', 'rel_altitude_m',
40
+ 'h_speed_kmh', 'v_speed_ms'
41
+ ]
42
+
43
+ with open(output_path, 'w', newline='', encoding='utf-8') as f:
44
+ writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction='ignore')
45
+ writer.writeheader()
46
+ for frame in data.frames:
47
+ writer.writerow(frame.to_dict())
48
+
49
+ return output_path
50
+
51
+
52
+ def to_json(data: TelemetryData, output_path: str | Path, indent: int = 2) -> Path:
53
+ """
54
+ Export telemetry data to JSON format.
55
+
56
+ Args:
57
+ data: TelemetryData object
58
+ output_path: Output file path
59
+ indent: JSON indentation level (None for compact)
60
+
61
+ Returns:
62
+ Path to the created file
63
+ """
64
+ output_path = Path(output_path)
65
+
66
+ output = {
67
+ 'metadata': {
68
+ 'source_file': data.source_file,
69
+ 'total_frames': len(data.frames),
70
+ 'duration_seconds': data.duration_seconds,
71
+ 'total_distance_m': data.total_distance,
72
+ 'max_altitude_m': data.max_altitude,
73
+ 'max_speed_kmh': data.max_speed * 3.6,
74
+ 'start_coordinates': {
75
+ 'latitude': data.start_coordinates[0],
76
+ 'longitude': data.start_coordinates[1]
77
+ },
78
+ 'end_coordinates': {
79
+ 'latitude': data.end_coordinates[0],
80
+ 'longitude': data.end_coordinates[1]
81
+ }
82
+ },
83
+ 'frames': data.to_list()
84
+ }
85
+
86
+ with open(output_path, 'w', encoding='utf-8') as f:
87
+ json.dump(output, f, indent=indent)
88
+
89
+ return output_path
90
+
91
+
92
+ def to_gpx(
93
+ data: TelemetryData,
94
+ output_path: str | Path,
95
+ name: Optional[str] = None,
96
+ description: Optional[str] = None
97
+ ) -> Path:
98
+ """
99
+ Export telemetry data to GPX format.
100
+
101
+ Args:
102
+ data: TelemetryData object
103
+ output_path: Output file path
104
+ name: Track name (defaults to source filename)
105
+ description: Track description
106
+
107
+ Returns:
108
+ Path to the created file
109
+ """
110
+ output_path = Path(output_path)
111
+
112
+ # Create GPX root element
113
+ gpx = ET.Element('gpx')
114
+ gpx.set('version', '1.1')
115
+ gpx.set('creator', 'dji-telemetry-overlay')
116
+ gpx.set('xmlns', 'http://www.topografix.com/GPX/1/1')
117
+ gpx.set('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')
118
+ gpx.set('xsi:schemaLocation', 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd')
119
+
120
+ # Metadata
121
+ metadata = ET.SubElement(gpx, 'metadata')
122
+ if name or data.source_file:
123
+ name_elem = ET.SubElement(metadata, 'name')
124
+ name_elem.text = name or Path(data.source_file).stem if data.source_file else 'DJI Flight'
125
+ if description:
126
+ desc_elem = ET.SubElement(metadata, 'desc')
127
+ desc_elem.text = description
128
+
129
+ # Create track
130
+ trk = ET.SubElement(gpx, 'trk')
131
+
132
+ trk_name = ET.SubElement(trk, 'name')
133
+ trk_name.text = name or (Path(data.source_file).stem if data.source_file else 'DJI Flight')
134
+
135
+ # Track segment
136
+ trkseg = ET.SubElement(trk, 'trkseg')
137
+
138
+ # Add track points
139
+ for frame in data.frames:
140
+ trkpt = ET.SubElement(trkseg, 'trkpt')
141
+ trkpt.set('lat', f'{frame.latitude:.6f}')
142
+ trkpt.set('lon', f'{frame.longitude:.6f}')
143
+
144
+ # Elevation (using absolute altitude for GPX)
145
+ ele = ET.SubElement(trkpt, 'ele')
146
+ ele.text = f'{frame.abs_alt:.1f}'
147
+
148
+ # Time
149
+ if frame.timestamp:
150
+ time_elem = ET.SubElement(trkpt, 'time')
151
+ try:
152
+ # Parse timestamp and convert to ISO format
153
+ dt = datetime.strptime(frame.timestamp, '%Y-%m-%d %H:%M:%S.%f')
154
+ time_elem.text = dt.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
155
+ except ValueError:
156
+ pass
157
+
158
+ # Extensions for speed data
159
+ extensions = ET.SubElement(trkpt, 'extensions')
160
+
161
+ speed = ET.SubElement(extensions, 'speed')
162
+ speed.text = f'{frame.h_speed:.2f}'
163
+
164
+ vspeed = ET.SubElement(extensions, 'vspeed')
165
+ vspeed.text = f'{frame.v_speed:.2f}'
166
+
167
+ # Pretty print
168
+ xml_str = ET.tostring(gpx, encoding='unicode')
169
+ dom = minidom.parseString(xml_str)
170
+ pretty_xml = dom.toprettyxml(indent=' ')
171
+
172
+ # Remove extra blank lines
173
+ lines = [line for line in pretty_xml.split('\n') if line.strip()]
174
+ pretty_xml = '\n'.join(lines)
175
+
176
+ with open(output_path, 'w', encoding='utf-8') as f:
177
+ f.write(pretty_xml)
178
+
179
+ return output_path
180
+
181
+
182
+ def export(
183
+ data: TelemetryData,
184
+ output_path: str | Path,
185
+ format: Optional[str] = None
186
+ ) -> Path:
187
+ """
188
+ Export telemetry data to specified format (auto-detected from extension if not specified).
189
+
190
+ Args:
191
+ data: TelemetryData object
192
+ output_path: Output file path
193
+ format: Output format ('csv', 'json', 'gpx') - auto-detected if None
194
+
195
+ Returns:
196
+ Path to the created file
197
+ """
198
+ output_path = Path(output_path)
199
+
200
+ if format is None:
201
+ format = output_path.suffix.lower().lstrip('.')
202
+
203
+ if format == 'csv':
204
+ return to_csv(data, output_path)
205
+ elif format == 'json':
206
+ return to_json(data, output_path)
207
+ elif format == 'gpx':
208
+ return to_gpx(data, output_path)
209
+ else:
210
+ raise ValueError(f"Unsupported format: {format}. Use 'csv', 'json', or 'gpx'.")
@@ -0,0 +1,248 @@
1
+ """
2
+ Telemetry overlay rendering.
3
+ """
4
+
5
+ import math
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+
9
+ import cv2
10
+ import numpy as np
11
+
12
+ from .parser import TelemetryFrame
13
+
14
+
15
+ @dataclass
16
+ class OverlayConfig:
17
+ """Configuration for telemetry overlay rendering."""
18
+ # What to display
19
+ show_altitude: bool = True
20
+ show_speed: bool = True
21
+ show_vertical_speed: bool = True
22
+ show_coordinates: bool = True
23
+ show_camera_settings: bool = True
24
+ show_timestamp: bool = True
25
+ show_speed_gauge: bool = True
26
+
27
+ # Speed gauge settings
28
+ gauge_max_speed_kmh: float = 50.0
29
+
30
+ # Font settings
31
+ font_scale_factor: float = 1.0
32
+
33
+ # Colors (BGR format)
34
+ text_color: tuple[int, int, int] = (255, 255, 255)
35
+ shadow_color: tuple[int, int, int] = (30, 30, 30)
36
+ gauge_color: tuple[int, int, int] = (255, 255, 255)
37
+ gauge_needle_color: tuple[int, int, int] = (255, 200, 0)
38
+
39
+ # Position adjustments (relative to video dimensions)
40
+ padding_factor: float = 0.015 # Padding as fraction of height
41
+
42
+
43
+ class OverlayRenderer:
44
+ """Renders telemetry overlay onto video frames."""
45
+
46
+ def __init__(self, width: int, height: int, config: Optional[OverlayConfig] = None):
47
+ """
48
+ Initialize the overlay renderer.
49
+
50
+ Args:
51
+ width: Video width in pixels
52
+ height: Video height in pixels
53
+ config: Overlay configuration (uses defaults if None)
54
+ """
55
+ self.width = width
56
+ self.height = height
57
+ self.config = config or OverlayConfig()
58
+
59
+ # Calculate scaled values
60
+ self.scale_factor = (height / 1080.0) * self.config.font_scale_factor
61
+ self.font = cv2.FONT_HERSHEY_SIMPLEX
62
+ self.font_scale_large = 0.7 * self.scale_factor
63
+ self.font_scale_small = 0.55 * self.scale_factor
64
+ self.thickness = max(1, int(2 * self.scale_factor))
65
+ self.padding = int(height * self.config.padding_factor)
66
+ self.line_height = int(30 * self.scale_factor)
67
+
68
+ def _draw_text_with_shadow(
69
+ self,
70
+ img: np.ndarray,
71
+ text: str,
72
+ pos: tuple[int, int],
73
+ font_scale: float,
74
+ color: Optional[tuple[int, int, int]] = None
75
+ ):
76
+ """Draw text with shadow for better visibility."""
77
+ x, y = pos
78
+ color = color or self.config.text_color
79
+ shadow_offset = max(1, int(2 * self.scale_factor))
80
+
81
+ cv2.putText(img, text, (x + shadow_offset, y + shadow_offset),
82
+ self.font, font_scale, self.config.shadow_color,
83
+ self.thickness + 1, cv2.LINE_AA)
84
+ cv2.putText(img, text, (x, y), self.font, font_scale, color,
85
+ self.thickness, cv2.LINE_AA)
86
+
87
+ def _get_text_size(self, text: str, font_scale: float) -> tuple[int, int]:
88
+ """Get the size of text."""
89
+ size = cv2.getTextSize(text, self.font, font_scale, self.thickness)[0]
90
+ return size[0], size[1]
91
+
92
+ def render(self, telemetry: TelemetryFrame, frame: Optional[np.ndarray] = None) -> np.ndarray:
93
+ """
94
+ Render telemetry overlay.
95
+
96
+ Args:
97
+ telemetry: Telemetry data for the current frame
98
+ frame: Video frame to draw on (creates transparent if None)
99
+
100
+ Returns:
101
+ Frame with telemetry overlay
102
+ """
103
+ if frame is None:
104
+ # Create transparent frame (BGRA)
105
+ overlay = np.zeros((self.height, self.width, 4), dtype=np.uint8)
106
+ is_transparent = True
107
+ else:
108
+ overlay = frame.copy()
109
+ is_transparent = False
110
+
111
+ # Get color for drawing (handle transparent vs opaque)
112
+ def get_color(bgr_color):
113
+ if is_transparent:
114
+ return (*bgr_color, 255) # Add alpha
115
+ return bgr_color
116
+
117
+ text_color = get_color(self.config.text_color)
118
+ shadow_color = get_color(self.config.shadow_color)
119
+
120
+ # === TOP LEFT: Flight Data ===
121
+ y_pos = self.padding + self.line_height
122
+
123
+ if self.config.show_altitude:
124
+ alt_text = f"ALT: {telemetry.rel_alt:.1f}m"
125
+ self._draw_text_with_shadow(overlay, alt_text, (self.padding, y_pos), self.font_scale_large)
126
+ y_pos += self.line_height
127
+
128
+ if self.config.show_speed:
129
+ h_speed_kmh = telemetry.h_speed * 3.6
130
+ speed_text = f"H.SPD: {h_speed_kmh:.1f} km/h"
131
+ self._draw_text_with_shadow(overlay, speed_text, (self.padding, y_pos), self.font_scale_large)
132
+ y_pos += self.line_height
133
+
134
+ if self.config.show_vertical_speed:
135
+ v_speed_text = f"V.SPD: {telemetry.v_speed:+.1f} m/s"
136
+ self._draw_text_with_shadow(overlay, v_speed_text, (self.padding, y_pos), self.font_scale_large)
137
+
138
+ # === TOP RIGHT: Camera Settings ===
139
+ if self.config.show_camera_settings:
140
+ y_pos = self.padding + self.line_height
141
+ right_margin = self.width - self.padding
142
+
143
+ # ISO
144
+ iso_text = f"ISO {telemetry.iso}"
145
+ text_w, _ = self._get_text_size(iso_text, self.font_scale_small)
146
+ self._draw_text_with_shadow(overlay, iso_text,
147
+ (right_margin - text_w, y_pos), self.font_scale_small)
148
+ y_pos += self.line_height
149
+
150
+ # Shutter
151
+ shutter_text = f"{telemetry.shutter}s"
152
+ text_w, _ = self._get_text_size(shutter_text, self.font_scale_small)
153
+ self._draw_text_with_shadow(overlay, shutter_text,
154
+ (right_margin - text_w, y_pos), self.font_scale_small)
155
+ y_pos += self.line_height
156
+
157
+ # Aperture
158
+ fnum_text = f"f/{telemetry.fnum}"
159
+ text_w, _ = self._get_text_size(fnum_text, self.font_scale_small)
160
+ self._draw_text_with_shadow(overlay, fnum_text,
161
+ (right_margin - text_w, y_pos), self.font_scale_small)
162
+ y_pos += self.line_height
163
+
164
+ # EV
165
+ ev_text = f"EV {telemetry.ev:+.1f}"
166
+ text_w, _ = self._get_text_size(ev_text, self.font_scale_small)
167
+ self._draw_text_with_shadow(overlay, ev_text,
168
+ (right_margin - text_w, y_pos), self.font_scale_small)
169
+
170
+ # === BOTTOM LEFT: GPS Coordinates ===
171
+ if self.config.show_coordinates:
172
+ y_pos = self.height - self.padding - self.line_height
173
+ lat_dir = "S" if telemetry.latitude < 0 else "N"
174
+ lon_dir = "W" if telemetry.longitude < 0 else "E"
175
+ coords_text = f"{abs(telemetry.latitude):.6f}{lat_dir} {abs(telemetry.longitude):.6f}{lon_dir}"
176
+ self._draw_text_with_shadow(overlay, coords_text, (self.padding, y_pos), self.font_scale_small)
177
+
178
+ # === BOTTOM RIGHT: Timestamp ===
179
+ if self.config.show_timestamp and telemetry.timestamp:
180
+ time_only = telemetry.timestamp.split(' ')[-1].split('.')[0] # Get HH:MM:SS
181
+ text_w, _ = self._get_text_size(time_only, self.font_scale_small)
182
+ self._draw_text_with_shadow(overlay, time_only,
183
+ (self.width - self.padding - text_w,
184
+ self.height - self.padding - self.line_height),
185
+ self.font_scale_small)
186
+
187
+ # === BOTTOM CENTER: Speed Gauge ===
188
+ if self.config.show_speed_gauge:
189
+ self._draw_speed_gauge(overlay, telemetry.h_speed * 3.6)
190
+
191
+ return overlay
192
+
193
+ def _draw_speed_gauge(self, img: np.ndarray, speed_kmh: float):
194
+ """Draw the speed gauge at bottom center."""
195
+ gauge_center_x = self.width // 2
196
+ gauge_center_y = self.height - int(80 * self.scale_factor)
197
+ gauge_radius = int(50 * self.scale_factor)
198
+
199
+ # Check if transparent (BGRA) or opaque (BGR)
200
+ is_transparent = img.shape[2] == 4
201
+
202
+ def get_color(bgr_color):
203
+ if is_transparent:
204
+ return (*bgr_color, 255)
205
+ return bgr_color
206
+
207
+ gauge_color = get_color(self.config.gauge_color)
208
+ shadow_color = get_color(self.config.shadow_color)
209
+ needle_color = get_color(self.config.gauge_needle_color)
210
+
211
+ # Draw gauge background arc
212
+ cv2.ellipse(img, (gauge_center_x, gauge_center_y), (gauge_radius, gauge_radius),
213
+ 0, 180, 360, shadow_color, max(2, int(4 * self.scale_factor)))
214
+ cv2.ellipse(img, (gauge_center_x, gauge_center_y), (gauge_radius, gauge_radius),
215
+ 0, 180, 360, gauge_color, max(1, int(2 * self.scale_factor)))
216
+
217
+ # Speed indicator needle
218
+ speed_ratio = min(speed_kmh / self.config.gauge_max_speed_kmh, 1.0)
219
+ angle_deg = 180 + speed_ratio * 180
220
+ angle_rad = math.radians(angle_deg)
221
+
222
+ needle_length = gauge_radius - int(10 * self.scale_factor)
223
+ needle_x = int(gauge_center_x + needle_length * math.cos(angle_rad))
224
+ needle_y = int(gauge_center_y + needle_length * math.sin(angle_rad))
225
+
226
+ cv2.line(img, (gauge_center_x, gauge_center_y), (needle_x, needle_y),
227
+ needle_color, max(2, int(3 * self.scale_factor)), cv2.LINE_AA)
228
+
229
+ # Speed value
230
+ speed_val_text = f"{speed_kmh:.0f}"
231
+ text_w, text_h = self._get_text_size(speed_val_text, self.font_scale_large)
232
+ self._draw_text_with_shadow(img, speed_val_text,
233
+ (gauge_center_x - text_w // 2,
234
+ gauge_center_y - int(10 * self.scale_factor)),
235
+ self.font_scale_large)
236
+
237
+ # km/h label
238
+ unit_text = "km/h"
239
+ text_w, _ = self._get_text_size(unit_text, self.font_scale_small * 0.8)
240
+ self._draw_text_with_shadow(img, unit_text,
241
+ (gauge_center_x - text_w // 2,
242
+ gauge_center_y + int(15 * self.scale_factor)),
243
+ self.font_scale_small * 0.8)
244
+
245
+
246
+ def create_transparent_frame(width: int, height: int) -> np.ndarray:
247
+ """Create a transparent BGRA frame."""
248
+ return np.zeros((height, width, 4), dtype=np.uint8)