Skip to content

Commit 6c6f07c

Browse files
committed
video diff
1 parent 8ffe3f2 commit 6c6f07c

File tree

5 files changed

+331
-2
lines changed

5 files changed

+331
-2
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ docs = [
8585
]
8686

8787
testing = [
88+
"coverage",
8889
"hypothesis ==6.47.*",
8990
"mypy",
9091
"pytest",

selfdrive/ui/tests/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@ test
22
test_translations
33
test_ui/report_1
44
test_ui/raylib_report
5+
6+
diff/*.mp4
7+
diff/*.html
8+
diff/.coverage
9+
diff/htmlcov/

selfdrive/ui/tests/diff/diff.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import sys
4+
import subprocess
5+
import tempfile
6+
import base64
7+
import webbrowser
8+
import argparse
9+
from pathlib import Path
10+
11+
12+
def extract_frames(video_path, output_dir):
13+
output_pattern = str(output_dir / "frame_%04d.png")
14+
cmd = ['ffmpeg', '-i', video_path, '-vsync', '0', output_pattern, '-y']
15+
subprocess.run(cmd, capture_output=True, check=True)
16+
frames = sorted(output_dir.glob("frame_*.png"))
17+
return frames
18+
19+
20+
def compare_frames(frame1_path, frame2_path):
21+
result = subprocess.run(['cmp', '-s', frame1_path, frame2_path])
22+
return result.returncode == 0
23+
24+
25+
def frame_to_data_url(frame_path):
26+
with open(frame_path, 'rb') as f:
27+
data = f.read()
28+
return f"data:image/png;base64,{base64.b64encode(data).decode()}"
29+
30+
31+
def create_diff_video(video1, video2, output_path):
32+
"""Create a diff video using ffmpeg blend filter with difference mode."""
33+
print("Creating diff video...")
34+
cmd = ['ffmpeg', '-i', video1, '-i', video2, '-filter_complex', '[0:v]blend=all_mode=difference', '-vsync', '0', '-y', output_path]
35+
subprocess.run(cmd, capture_output=True, check=True)
36+
37+
38+
def find_differences(video1, video2):
39+
with tempfile.TemporaryDirectory() as tmpdir:
40+
tmpdir = Path(tmpdir)
41+
42+
print(f"Extracting frames from {video1}...")
43+
frames1_dir = tmpdir / "frames1"
44+
frames1_dir.mkdir()
45+
frames1 = extract_frames(video1, frames1_dir)
46+
47+
print(f"Extracting frames from {video2}...")
48+
frames2_dir = tmpdir / "frames2"
49+
frames2_dir.mkdir()
50+
frames2 = extract_frames(video2, frames2_dir)
51+
52+
if len(frames1) != len(frames2):
53+
print(f"WARNING: Frame count mismatch: {len(frames1)} vs {len(frames2)}")
54+
min_frames = min(len(frames1), len(frames2))
55+
frames1 = frames1[:min_frames]
56+
frames2 = frames2[:min_frames]
57+
58+
print(f"Comparing {len(frames1)} frames...")
59+
different_frames = []
60+
frame_data = []
61+
62+
for i, (f1, f2) in enumerate(zip(frames1, frames2, strict=False)):
63+
is_different = not compare_frames(f1, f2)
64+
if is_different:
65+
different_frames.append(i)
66+
67+
if i < 10 or i >= len(frames1) - 10 or is_different:
68+
frame_data.append({'index': i, 'different': is_different, 'frame1_url': frame_to_data_url(f1), 'frame2_url': frame_to_data_url(f2)})
69+
70+
return different_frames, frame_data, len(frames1)
71+
72+
73+
def generate_html_report(video1, video2, different_frames, frame_data, total_frames):
74+
chunks = []
75+
if different_frames:
76+
current_chunk = [different_frames[0]]
77+
for i in range(1, len(different_frames)):
78+
if different_frames[i] == different_frames[i - 1] + 1:
79+
current_chunk.append(different_frames[i])
80+
else:
81+
chunks.append(current_chunk)
82+
current_chunk = [different_frames[i]]
83+
chunks.append(current_chunk)
84+
85+
result_text = (
86+
f"✅ Videos are identical! ({total_frames} frames)"
87+
if len(different_frames) == 0
88+
else f"❌ Found {len(different_frames)} different frames out of {total_frames} total ({(len(different_frames) / total_frames * 100):.1f}%)"
89+
)
90+
91+
html = f"""<h2>UI Diff</h2>
92+
<table>
93+
<tr>
94+
<td width='33%'>
95+
<p><strong>Video 1</strong></p>
96+
<video id='video1' width='100%' autoplay muted loop onplay='syncVideos()'>
97+
<source src='{os.path.basename(video1)}' type='video/mp4'>
98+
Your browser does not support the video tag.
99+
</video>
100+
</td>
101+
<td width='33%'>
102+
<p><strong>Video 2</strong></p>
103+
<video id='video2' width='100%' autoplay muted loop onplay='syncVideos()'>
104+
<source src='{os.path.basename(video2)}' type='video/mp4'>
105+
Your browser does not support the video tag.
106+
</video>
107+
</td>
108+
<td width='33%'>
109+
<p><strong>Pixel Diff</strong></p>
110+
<video id='diffVideo' width='100%' autoplay muted loop>
111+
<source src='diff.mp4' type='video/mp4'>
112+
Your browser does not support the video tag.
113+
</video>
114+
</td>
115+
</tr>
116+
</table>
117+
<script>
118+
function syncVideos() {{
119+
const video1 = document.getElementById('video1');
120+
const video2 = document.getElementById('video2');
121+
const diffVideo = document.getElementById('diffVideo');
122+
video1.currentTime = video2.currentTime = diffVideo.currentTime;
123+
}}
124+
video1.addEventListener('timeupdate', () => {{
125+
if (Math.abs(video1.currentTime - video2.currentTime) > 0.1) {{
126+
video2.currentTime = video1.currentTime;
127+
}}
128+
if (Math.abs(video1.currentTime - diffVideo.currentTime) > 0.1) {{
129+
diffVideo.currentTime = video1.currentTime;
130+
}}
131+
}});
132+
video2.addEventListener('timeupdate', () => {{
133+
if (Math.abs(video2.currentTime - video1.currentTime) > 0.1) {{
134+
video1.currentTime = video2.currentTime;
135+
}}
136+
if (Math.abs(video2.currentTime - diffVideo.currentTime) > 0.1) {{
137+
diffVideo.currentTime = video2.currentTime;
138+
}}
139+
}});
140+
diffVideo.addEventListener('timeupdate', () => {{
141+
if (Math.abs(diffVideo.currentTime - video1.currentTime) > 0.1) {{
142+
video1.currentTime = diffVideo.currentTime;
143+
video2.currentTime = diffVideo.currentTime;
144+
}}
145+
}});
146+
</script>
147+
<hr>
148+
<p><strong>Results:</strong> {result_text}</p>
149+
"""
150+
return html
151+
152+
153+
def main():
154+
parser = argparse.ArgumentParser(description='Compare two videos and generate HTML diff report')
155+
parser.add_argument('video1', help='First video file')
156+
parser.add_argument('video2', help='Second video file')
157+
parser.add_argument('output', nargs='?', default='diff.html', help='Output HTML file (default: diff.html)')
158+
parser.add_argument('--no-open', action='store_true', help='Do not open HTML report in browser')
159+
160+
args = parser.parse_args()
161+
162+
print("=" * 60)
163+
print("VIDEO DIFF - HTML REPORT")
164+
print("=" * 60)
165+
print(f"Video 1: {args.video1}")
166+
print(f"Video 2: {args.video2}")
167+
print(f"Output: {args.output}")
168+
print()
169+
170+
# Create diff video
171+
diff_video_path = os.path.join(os.path.dirname(args.output), "diff.mp4")
172+
create_diff_video(args.video1, args.video2, diff_video_path)
173+
174+
different_frames, frame_data, total_frames = find_differences(args.video1, args.video2)
175+
176+
if different_frames is None:
177+
sys.exit(1)
178+
179+
print()
180+
print("Generating HTML report...")
181+
html = generate_html_report(args.video1, args.video2, different_frames, frame_data, total_frames)
182+
183+
with open(args.output, 'w') as f:
184+
f.write(html)
185+
186+
# Open in browser by default
187+
if not args.no_open:
188+
print(f"Opening {args.output} in browser...")
189+
webbrowser.open(f'file://{os.path.abspath(args.output)}')
190+
191+
192+
if __name__ == "__main__":
193+
main()

selfdrive/ui/tests/diff/replay.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import time
4+
import coverage
5+
import pyray as rl
6+
7+
os.environ["RECORD"] = "1"
8+
if "RECORD_OUTPUT" not in os.environ:
9+
os.environ["RECORD_OUTPUT"] = "mici_ui_replay.mp4"
10+
11+
from openpilot.common.params import Params
12+
from openpilot.system.version import terms_version, training_version
13+
from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent
14+
from openpilot.selfdrive.ui.ui_state import ui_state
15+
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
16+
17+
18+
FPS = 60
19+
HEADLESS = os.getenv("WINDOWED", "0") == "1"
20+
21+
SCRIPT = [
22+
(0, None),
23+
(FPS * 1, (100, 100)),
24+
(FPS * 2, None),
25+
]
26+
27+
28+
def setup_state():
29+
params = Params()
30+
params.put("HasAcceptedTerms", terms_version)
31+
params.put("CompletedTrainingVersion", training_version)
32+
params.put("DongleId", "test123456789")
33+
params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30")
34+
return None
35+
36+
37+
def inject_click(x, y):
38+
press_event = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=True, t=time.monotonic())
39+
40+
release_event = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=time.monotonic())
41+
42+
with gui_app._mouse._lock:
43+
gui_app._mouse._events.append(press_event)
44+
gui_app._mouse._events.append(release_event)
45+
46+
47+
def run_replay():
48+
setup_state()
49+
50+
if not HEADLESS:
51+
rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)
52+
gui_app.init_window("ui diff test", fps=FPS)
53+
main_layout = MiciMainLayout()
54+
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
55+
56+
frame = 0
57+
script_index = 0
58+
59+
for should_render in gui_app.render():
60+
while script_index < len(SCRIPT) and SCRIPT[script_index][0] == frame:
61+
_, coords = SCRIPT[script_index]
62+
if coords is not None:
63+
inject_click(*coords)
64+
script_index += 1
65+
66+
ui_state.update()
67+
68+
if should_render:
69+
main_layout.render()
70+
71+
frame += 1
72+
73+
if script_index >= len(SCRIPT):
74+
break
75+
76+
gui_app.close()
77+
78+
print(f"Total frames: {frame}")
79+
print(f"Video saved to: {os.environ['RECORD_OUTPUT']}")
80+
81+
82+
def main():
83+
cov = coverage.coverage(source=['openpilot.selfdrive.ui.mici'])
84+
with cov.collect():
85+
run_replay()
86+
cov.stop()
87+
cov.save()
88+
cov.report()
89+
cov.html_report(directory='htmlcov')
90+
print("HTML report: htmlcov/index.html")
91+
92+
if __name__ == "__main__":
93+
main()

uv.lock

Lines changed: 39 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)