Skip to content

Commit 8c80675

Browse files
committed
merged with latest master
2 parents 1de60a0 + ba3465f commit 8c80675

File tree

13 files changed

+207
-155
lines changed

13 files changed

+207
-155
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ opencamera-extended-firebase-adminsdk-yv5yz-e33a8ce5c1.json
4949

5050
#
5151
# Project specific
52+
venv
5253
PythonTools/uploads

CaptureSync.iml

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

PythonTools/CollateVideos.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import argparse
2+
from pathlib import Path
3+
import subprocess
4+
import math
5+
6+
from video import video_info
7+
8+
from typing import List, Tuple
9+
10+
# See https://ffmpeg.org/ffmpeg-filters.html#xstack-1 for documentation on the xstack filter
11+
# Example (assuming HD input videos):
12+
"""
13+
ffmpeg -y -i video1.mp4 \
14+
-i video2.mp4 \
15+
-i video3.mp4 \
16+
-filter_complex \
17+
"\
18+
[0:v]scale=w=960:h=540[v0]; \
19+
[1:v]scale=w=960:h=540[v1]; \
20+
[2:v]scale=w=960:h=540[v2]; \
21+
[v0][v1][v2]xstack=inputs=3:layout=0_0|960_0|0_540[v]\
22+
" \
23+
-map "[v]" \
24+
finalOutput.mp4
25+
"""
26+
27+
28+
def create_video_grid_collage(video_files: List[str], output_file: str, grid_size: Tuple[int, int]) -> None:
29+
# Determine the dimensions of each grid cell
30+
grid_width, grid_height = grid_size
31+
input_width, input_height, _ = video_info(video_files[0])
32+
cell_width = input_width // grid_width
33+
cell_height = input_height // grid_height
34+
35+
# Compose the ffmpeg command to create the collage
36+
cmd = [
37+
"ffmpeg",
38+
"-y", # Overwrite output file if it exists
39+
]
40+
41+
# Set the list of input videos
42+
for i, video_file in enumerate(video_files):
43+
cmd.extend(["-i", video_file])
44+
45+
#
46+
# Generate the ffmpeg filtergraph
47+
filtergraph = ""
48+
49+
# First, created the scaled video instances
50+
for i, video_file in enumerate(video_files):
51+
filtergraph += f"[{i}:v]scale=w={cell_width}:h={cell_height}[v{i}];"
52+
53+
# Now compose the layout for the xstack filter
54+
xscale_positions = []
55+
for i, video_file in enumerate(video_files):
56+
col = i % grid_width
57+
row = i // grid_width
58+
xscale_positions.append(f"{cell_width*col}_{cell_height*row}")
59+
60+
filtergraph += "".join([f"[v{i}]" for i in range(len(video_files))])
61+
filtergraph += f"xstack=inputs={len(xscale_positions)}:layout="
62+
filtergraph += "|".join(xscale_positions) # Compose the xstack layout (e.g.: "0_0|960_0|0_540")
63+
filtergraph += "[v]"
64+
65+
# Append the complex filter and the remaining parameters
66+
cmd.extend([
67+
"-filter_complex", filtergraph,
68+
"-map", "[v]",
69+
"-c:v", "libx264",
70+
"-crf", "18",
71+
"-preset", "fast",
72+
"-pix_fmt", "yuv420p",
73+
output_file
74+
])
75+
print("CMD:", cmd)
76+
subprocess.run(cmd, capture_output=False)
77+
78+
79+
#
80+
# MAIN
81+
if __name__ == "__main__":
82+
83+
parser = argparse.ArgumentParser(
84+
description="Fixes the videos produced by the RecSync recording sessions."
85+
"Converts the input recorder videos into videos with the same number of frames,"
86+
"with missing/dropped frames inserted as (black) artificial data."
87+
)
88+
parser.add_argument(
89+
"--infolder", "-i", type=str, help="The folder containing the fixed videos.",
90+
required=True
91+
)
92+
parser.add_argument(
93+
"--outvideo", "-o", type=str, help="The filename to save the generated video.",
94+
required=True
95+
)
96+
97+
args = parser.parse_args()
98+
99+
input_dir = Path(args.infolder)
100+
out_filename = args.outvideo
101+
102+
input_video_paths = input_dir.glob("*.mp4")
103+
video_list = [str(p) for p in input_video_paths]
104+
# print("Input videos: ", video_list)
105+
106+
# Compute the most squared grid to contain the given number of input videos (drops a row if needed)
107+
n_videos = len(video_list)
108+
n_cols = math.ceil(math.sqrt(n_videos))
109+
n_rows = math.ceil(n_videos / n_cols)
110+
# print(f"Composing grid of (cols X rows): {n_cols}X{n_rows}")
111+
112+
# Number of rows will be the same as number of columns, or one less
113+
assert n_rows * n_cols >= n_videos
114+
assert n_cols - 1 <= n_rows <= n_cols
115+
116+
grid_size = (n_cols, n_rows)
117+
118+
create_video_grid_collage(video_list, out_filename, grid_size)
File renamed without changes.

PythonTools/PostProcessVideos.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
#
2121
#
2222
#
23-
def main(input_dir: Path, output_dir: Path, threshold_ns: int):
23+
def post_process(input_dir: Path, output_dir: Path, threshold_ns: int = DEFAULT_THRESHOLD_NANOS):
2424

2525
print(f"Scanning dir {str(input_dir)}...")
2626
clientIDs, df_list, mp4_list = scan_session_dir(input_dir)
@@ -96,7 +96,7 @@ def main(input_dir: Path, output_dir: Path, threshold_ns: int):
9696

9797
parser = argparse.ArgumentParser(
9898
description="Fixes the videos produced by the RecSync recording sessions."
99-
"Output videos will have the same number of frames,"
99+
"Converts the input recorder videos into videos with the same number of frames,"
100100
"with missing/dropped frames inserted as (black) artificial data."
101101
)
102102
parser.add_argument(
@@ -107,6 +107,10 @@ def main(input_dir: Path, output_dir: Path, threshold_ns: int):
107107
"--outfolder", "-o", type=str, help="The folder where the repaired and aligned frames will be stored.",
108108
required=True
109109
)
110+
parser.add_argument(
111+
"--create-outfolder", "-co", action="store_true", help="If the output folder doesn't exist, tries to create it.",
112+
required=True
113+
)
110114
parser.add_argument(
111115
"--threshold", "-t", type=int, help="The allowed difference in ms between corresponding frames on different videos."
112116
" Increase this is post processing fails with trimmed tables of different sizes."
@@ -119,13 +123,17 @@ def main(input_dir: Path, output_dir: Path, threshold_ns: int):
119123

120124
infolder = Path(args.infolder)
121125
outfolder = Path(args.outfolder)
126+
create_outfolder = args.create_outfolder
122127
threshold_millis = args.threshold
123128
threshold_nanos = threshold_millis * 1000 * 1000
124129

125130
if not infolder.exists():
126131
raise Exception(f"Input folder '{infolder}' doesn't exist.")
127132

128133
if not outfolder.exists():
129-
raise Exception(f"Output folder '{outfolder}' doesn't exist.")
134+
if create_outfolder:
135+
outfolder.mkdir(parents=True, exist_ok=True)
136+
else:
137+
raise Exception(f"Output folder '{outfolder}' doesn't exist.")
130138

131-
main(infolder, outfolder, threshold_nanos)
139+
post_process(input_dir=infolder, output_dir=outfolder, threshold_ns=threshold_nanos)

PythonTools/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
This utility "repairs" the videos generated by the RecSyncNG app:
44
* Videos are unpacked
55
* Missing frames are injected as black frames
6-
* Frame sequences are "synchronized" to have the same start time (with tolerance) and number of frames
7-
* Videos are re-packed, and they will all hve the same starting frame and the same number of frames
6+
* Frame sequences are "synchronized" to have the same number of frames and the same start time (with tolerance)
7+
* Videos are re-packed, and they will all have the same starting frame and the same number of frames
88
* A frame counter is added
99

1010
## Installing

PythonTools/RemoteController.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,18 @@ def askStatus(self):
164164
def parseStatusInfo(self, status: str) -> None:
165165
"""Extract the leader ID from the status text."""
166166
# E.g.: "Leader b308: 0 clients."
167-
pattern = "^Leader ([0-9a-f][0-9a-f][0-9a-f][0-9a-f]): .*"
167+
pattern = "^Leader ([0-9a-f][0-9a-f][0-9a-f][0-9a-f]).*"
168+
# Alternative pattern, when there are no clients.
169+
pattern2 = "^Leader : ([0-9a-f][0-9a-f][0-9a-f][0-9a-f])"
168170
res = re.match(pattern=pattern, string=status)
171+
res2 = re.match(pattern=pattern2, string=status)
172+
169173
if res is not None:
170174
self.leaderID = res.group(1)
171175
print(f"Leader ID is {self.leaderID}")
176+
elif res2 is not None:
177+
self.leaderID = res2.group(1)
178+
print(f"Leader (alone) ID is {self.leaderID}")
172179

173180
def deleteRemoteContent(self):
174181
msgBox = QMessageBox()

PythonTools/uploads/README.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

README-orig.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
![Logo](https://imgur.com/YtJA0E2.png)
2+
3+
### Usage:
4+
5+
6+
#### Leader smartphone setup
7+
8+
1. Start a Wi-Fi hotspot.
9+
2. The app should display connected clients and buttons for recording control
10+
11+
#### Client smartphones setup
12+
13+
1. Enable WiFi and connect to the Wi-Fi hotspot.
14+
15+
#### Recording video
16+
17+
1. [Optional step] Press the ```calculate period``` button. The app will analyze frame stream and use the calculated frame period in further synchronization steps.
18+
2. Adjust exposure and ISO to your needs.
19+
3. Press the ```phase align``` button.
20+
4. Press the ```record video``` button to start synchronized video recording.
21+
5. Get videos from RecSync folder in smartphone root directory.
22+
23+
#### Extraction and matching of the frames
24+
25+
```
26+
Requirements:
27+
28+
- Python
29+
- ffmpeg
30+
```
31+
32+
1. Navigate to ```utils``` directory in the repository.
33+
2. Run ```./match.sh <VIDEO_1> <VIDEO_2>```.
34+
3. Frames will be extracted to directories ```output/1``` and ```output/2``` with timestamps in filenames, output directory will also contain ```match.csv``` file in the following format:
35+
```
36+
timestamp_1(ns) timestamp_2(ns)
37+
```
38+
39+
### Our contribution:
40+
41+
- Integrated **synchronized video recording**
42+
- Scripts for extraction, alignment and processing of video frames
43+
- Experiment with flash blinking to evaluate video frames synchronization accuracy
44+
- Panoramic video demo with automated Hugin stitching
45+
46+
### Panoramic video stitching demo
47+
48+
### [Link to youtube demo video](https://youtu.be/W6iANtCuQ-o)
49+
50+
- We provide scripts to **stitch 2 syncronized smatphone videos** with Hujin panorama CLI tools
51+
- Usage:
52+
- Run ```./make_demo.sh {VIDEO_LEFT} {VIDEO_RIGHT}```
53+
54+
### This work is based on "Wireless Software Synchronization of Multiple Distributed Cameras"
55+
56+
Reference code for the paper
57+
[Wireless Software Synchronization of Multiple Distributed Cameras](https://arxiv.org/abs/1812.09366).
58+
_Sameer Ansari, Neal Wadhwa, Rahul Garg, Jiawen Chen_, ICCP 2019.

0 commit comments

Comments
 (0)