|
| 1 | +import argparse |
| 2 | +from pathlib import Path |
| 3 | +from typing import List, Tuple |
| 4 | +import tempfile |
| 5 | + |
| 6 | +import pandas as pd |
| 7 | +import re |
| 8 | + |
| 9 | +from dataframes import compute_time_range, trim_repaired_into_interval |
| 10 | +from dataframes import repair_dropped_frames, compute_time_step |
| 11 | + |
| 12 | +from video import extract_frames |
| 13 | +from video import rebuild_video |
| 14 | + |
| 15 | + |
| 16 | +THRESHOLD_NS = 10 * 1000 * 1000 # millis * micros * nanos |
| 17 | + |
| 18 | + |
| 19 | +def scan_session_dir(input_dir: Path) -> Tuple[List[str], List[pd.DataFrame], List[str]]: |
| 20 | + # |
| 21 | + # Find all CSV files in the directory and read it into a data frame |
| 22 | + # Use the following regular expression to check of the client ID is a 16-digit hexadecimal. |
| 23 | + clientIDpattern = "[\\da-f]" * 16 |
| 24 | + patt = re.compile("^" + clientIDpattern + "$") |
| 25 | + |
| 26 | + # Fill this list with the client IDs found n the directory |
| 27 | + clientIDs: List[str] = [] |
| 28 | + for p in input_dir.iterdir(): |
| 29 | + # Check if the ClientID complies to the numerical format (using regex). |
| 30 | + res = patt.match(p.stem) |
| 31 | + if res: |
| 32 | + print("Found client -->", p.stem) |
| 33 | + clientIDs.append(p.stem) |
| 34 | + else: |
| 35 | + print("Discarding ", p.stem) |
| 36 | + |
| 37 | + # |
| 38 | + # Accumulates the list of dataframes and mp4 files in the same order of the client IDs. |
| 39 | + df_list: List[pd.DataFrame] = [] |
| 40 | + mp4_list: List[str] = [] |
| 41 | + |
| 42 | + for cID in clientIDs: |
| 43 | + client_dir = input_dir / cID |
| 44 | + CSVs = list(client_dir.glob("*.csv")) |
| 45 | + MP4s = list(client_dir.glob("*.mp4")) |
| 46 | + # |
| 47 | + # Consistency check. Each clientID folder must have exactly 1 CSV and 1 mp4. |
| 48 | + if len(CSVs) != 1: |
| 49 | + raise Exception(f"Expecting 1 CSV file for client {cID}. Found {len(CSVs)}.") |
| 50 | + |
| 51 | + if len(MP4s) != 1: |
| 52 | + raise Exception(f"Expecting 1 MP4 file for client {cID}. Found {len(MP4s)}.") |
| 53 | + |
| 54 | + csv_file = CSVs[0] |
| 55 | + mp4_file = MP4s[0] |
| 56 | + |
| 57 | + df: pd.DataFrame = pd.read_csv(csv_file, header=None) |
| 58 | + |
| 59 | + df_list.append(df) |
| 60 | + mp4_list.append(str(mp4_file)) |
| 61 | + |
| 62 | + return clientIDs, df_list, mp4_list |
| 63 | + |
| 64 | + |
| 65 | +# |
| 66 | +# |
| 67 | +# |
| 68 | +def main(input_dir: Path, output_dir: Path): |
| 69 | + |
| 70 | + print(f"Scanning dir {str(input_dir)}...") |
| 71 | + clientIDs, df_list, mp4_list = scan_session_dir(input_dir) |
| 72 | + |
| 73 | + n_clients = len(clientIDs) |
| 74 | + |
| 75 | + |
| 76 | + # |
| 77 | + # Print collected info |
| 78 | + for i in range(n_clients): |
| 79 | + cID = clientIDs[i] |
| 80 | + df = df_list[i] |
| 81 | + mp4 = mp4_list[i] |
| 82 | + print(f"For client ID {cID}: {len(df)} frames for file {mp4}") |
| 83 | + |
| 84 | + # |
| 85 | + # Repair CSVs |
| 86 | + repaired_df_list: List[pd.DataFrame] = [] |
| 87 | + for cID, df in zip(clientIDs, df_list): |
| 88 | + time_step = compute_time_step(df) |
| 89 | + repaired_df = repair_dropped_frames(df=df, time_step=time_step) |
| 90 | + repaired_df_list.append(repaired_df) |
| 91 | + |
| 92 | + assert len(clientIDs) == len(df_list) == len(mp4_list) == len(repaired_df_list) |
| 93 | + |
| 94 | + # |
| 95 | + # Trim CSVs |
| 96 | + # Find time ranges |
| 97 | + min_common, max_common = compute_time_range(repaired_df_list) |
| 98 | + # Trim the data frames to the time range |
| 99 | + trimmed_dataframes = trim_repaired_into_interval(repaired_df_list, min_common, max_common, THRESHOLD_NS) |
| 100 | + |
| 101 | + assert len(clientIDs) == len(trimmed_dataframes), f"Expected {len(clientIDs)} trimmed dataframes. Found f{len(trimmed_dataframes)}" |
| 102 | + |
| 103 | + # Check that all the resulting dataframes have the same number of rows |
| 104 | + client0ID = clientIDs[0] |
| 105 | + client0size = len(trimmed_dataframes[0]) |
| 106 | + print(f"For client {client0ID}: {client0size} frames") |
| 107 | + for cID, df in zip(clientIDs[1:], trimmed_dataframes[1:]): |
| 108 | + dfsize = len(df) |
| 109 | + if client0size != dfsize: |
| 110 | + raise Exception(f"For client {cID}: expecting {client0size} frames, found {dfsize}") |
| 111 | + |
| 112 | + print("Good. All trimmed dataframes have the same number of entries.") |
| 113 | + |
| 114 | + # |
| 115 | + # Unpack the original videos, and repack them according to repaired and trimmed dataframes. |
| 116 | + for i, cID in enumerate(clientIDs): |
| 117 | + orig_df = df_list[i] |
| 118 | + trimmed_df = trimmed_dataframes[i] |
| 119 | + video_file = mp4_list[i] |
| 120 | + # Create a temporary directory for frames unpacking |
| 121 | + with tempfile.TemporaryDirectory(prefix="RecSyncNG", suffix=cID) as tmp_dir: |
| 122 | + # Extract the frames from the original videos |
| 123 | + # and rename the file names to the timestamps |
| 124 | + print(f"Extracting {len(orig_df)} frames from '{video_file}'...") |
| 125 | + extract_frames(video_file=video_file, timestamps_df=orig_df, output_dir=tmp_dir) |
| 126 | + |
| 127 | + # Reconstruct videos |
| 128 | + video_out_filepath = output_dir / (cID + ".mp4") |
| 129 | + rebuild_video(dir=Path(tmp_dir), frames=trimmed_df, outfile=video_out_filepath) |
| 130 | + # And save also the CSV |
| 131 | + csv_out_filepath = video_out_filepath.with_suffix(".csv") |
| 132 | + trimmed_df.to_csv(path_or_buf=csv_out_filepath, header=True, index=False) |
| 133 | + |
| 134 | + |
| 135 | +# |
| 136 | +# MAIN |
| 137 | +if __name__ == "__main__": |
| 138 | + |
| 139 | + parser = argparse.ArgumentParser( |
| 140 | + description="Fixes the videos produced by the RecSync recording sessions." |
| 141 | + "Output videos will have the same number of frames," |
| 142 | + "with missing/dropped frames inserted as (black) artificial data." |
| 143 | + ) |
| 144 | + parser.add_argument( |
| 145 | + "--infolder", "-i", type=str, help="The folder containing the collected videos and CSV files with the timestamps.", |
| 146 | + required=True |
| 147 | + ) |
| 148 | + parser.add_argument( |
| 149 | + "--outfolder", "-o", type=str, help="The folder where the repaired and aligned frames will be stored.", |
| 150 | + required=True |
| 151 | + ) |
| 152 | + |
| 153 | + args = parser.parse_args() |
| 154 | + |
| 155 | + infolder = Path(args.infolder) |
| 156 | + outfolder = Path(args.outfolder) |
| 157 | + |
| 158 | + if not infolder.exists(): |
| 159 | + raise Exception(f"Input folder '{infolder}' doesn't exist.") |
| 160 | + |
| 161 | + if not outfolder.exists(): |
| 162 | + raise Exception(f"Output folder '{outfolder}' doesn't exist.") |
| 163 | + |
| 164 | + main(infolder, outfolder) |
0 commit comments