From 8daba3cfc7e18f12975b4621358982af2e6ff498 Mon Sep 17 00:00:00 2001 From: dotag1 Date: Wed, 30 Apr 2025 19:38:09 -0400 Subject: [PATCH] metrics automation --- README-pandora.md | 76 +++++++-- lantern_scripts/align_fast_to_well.py | 18 +- lantern_scripts/calculate_exposures.py | 21 +++ lantern_scripts/calculate_metrics.py | 160 ++++++++++++++++++ lantern_scripts/create_GT_folder.py | 45 +++++ lantern_scripts/create_data_folder.py | 31 +--- lantern_scripts/exr2hdr.py | 44 +++++ lantern_scripts/ldr_res.py | 15 +- lantern_scripts/process_videos_for_sfm.py | 16 +- lantern_scripts/render_mask.png | Bin 0 -> 2424 bytes lantern_scripts/res_table_1.py | 154 +++++++---------- .../process_data/GT_HDR_to_nerfstudio.py | 1 + stick_masks/GT_center_mask.png | Bin 0 -> 12355 bytes stick_masks/GT_side_mask.png | Bin 0 -> 16553 bytes 14 files changed, 442 insertions(+), 139 deletions(-) create mode 100644 lantern_scripts/calculate_exposures.py create mode 100644 lantern_scripts/calculate_metrics.py create mode 100644 lantern_scripts/create_GT_folder.py create mode 100644 lantern_scripts/exr2hdr.py create mode 100755 lantern_scripts/render_mask.png create mode 100755 stick_masks/GT_center_mask.png create mode 100755 stick_masks/GT_side_mask.png diff --git a/README-pandora.md b/README-pandora.md index 4e75ccf23..6d205efa9 100644 --- a/README-pandora.md +++ b/README-pandora.md @@ -4,15 +4,23 @@ ``` tmux new -s nerf_studio + docker run -it --rm -v /gel/usr/{USER_NAME}:/mnt/workspace/ -v /home-local2/{USER_NAME}.extra.nobkp/:/mnt/data/ --gpus '"device=0"' -p 8008:8008 -p 6006:6006 lantern_docker:latest /bin/bash + cd /mnt/workspace/lantern/nerfstudio-HDR git clone https://github.com/MrKarimiD/nerfstudio-HDR.git conda activate nerfstudio-HDR export PYTHONPATH=$pwd:$PYTHONPATH pip install -e . -pip install skylibs equilib scikit-surgerycore pydub +pip install scikit-surgerycore pydub skylibs piq OpenEXR Imath equilib + pip install -U git+https://github.com/luca-medeiros/lang-segment-anything.git +If lang-sam not installed, do the following... +``` +cd ../lang-segment-anything +pip install . +cd ../nerfstudio-HDR ``` If there is an issue with equilib, do the following... @@ -46,7 +54,8 @@ cd ../nerfstudio-HDR - Shutter speed: determined below - ISO: 800 - WB: 3500 - - Make sure the cameras point up to avoid having lights in the seam. + - Make sure the cameras point up to avoid having lights in the seam. + - Make sure to film up and down to get good coverage 2. First capture with well-exposed cameras. - Change shutter speed to get a well-exposed image. **Note the shutter speed.** @@ -54,7 +63,19 @@ cd ../nerfstudio-HDR - Clap at the beginning and at the end to use for synchronization. - Go around the scene with the two cameras well-exposed. -3. Second capture with Camera 1 (left) well-exposed and Camera 2 (right) fast-exposed. Change shutter speed of Camera 2 to 1/25000 (fastest exposure) and repeat the steps above +3. Second capture with Camera 1 (left) well-exposed and Camera 2 (right) fast-exposed. Change shutter speed of Camera 2 to 1/25000 (fastest exposure) and repeat the steps above + + If calculating **METRICS**: Capture GT HDR brakets + - Go to settings and change the camera mode shooting method to multiple brackets + - Set the camera parameters: + - Apreture: 2.1 + - Shutter speed: Calculate the 11 shutter speeds and select the closest ones on the app in order from smallest to hightes + ``` + python lantern_scripts/calculate_exposures.py --min 0.00004 --max 0.004 + ``` + - ISO: 800 + - WB: 3500 + - Take 10 different locations 4. Import the files on computer. If on a mac, use Ricoh Theta File Transfer for Mac @@ -65,11 +86,21 @@ cd ../nerfstudio-HDR - Camera 1: - First capture: left_sfm - Second capture: left_e1 - - Camera 2: + - Camera 2 (fast exposed): - First capture: right_sfm - Second capture: right_e2 -6. Transfer videos on lab machine. + If calculating **METRICS**: Group GT brackets under GT folder like so + GT/ + GT1/ + 001.jpg + 002.jpg + ... + GT2/ + ... + ... + +6. Transfer files on lab machine. ``` scp -r /path/to/source/file /path/to/destination/ @@ -77,10 +108,20 @@ cd ../nerfstudio-HDR 7. Process the videos for OpenSFM. Input the right shutter speed noted above. + If not calculating metrics: ``` python lantern_scripts/process_videos_for_sfm.py --input_dir /mnt/data/scene/ --shutter_speed 0.004 ``` + If calculating **METRICS**: + ``` + python lantern_scripts/process_videos_for_sfm.py --input_dir /mnt/data/scene/ --shutter_speed 0.004 --gt + ``` + - Resize GT_exr and GT_jpg images to 3840x1920 with GNU Image Manipulation Program + - Go to Image > Scale Image + - Transfer those back on lab machine in th GT folder of the scene naming them GT_jpg_small and GT_exr_small + - Add GT_jpg images into sfm/images/ with their appropriate mask (checkout stick_masks folder) + ## 2. Process data using OpenSFM ``` @@ -167,34 +208,45 @@ ns-eval --load-config=outputs/scene_ns/lantern-nerfacto/2024-06-17_152246/config ## 4. Calculate metrics +``` +python lantern_scripts/calculate_metrics.py --input_dir /mnt/data/coffee_room2/ --checkpoint /mnt/workspace/lantern/nerfstudio-HDR/outputs/coffee_room2_ns/lantern-nerfacto/2025-01-22_194346/ --metric_name unaligned +``` + +and point #8 + +or + 1. Get ground truth transformations: ``` - ns-process-data lantern-GT-HDR --data /mnt/data/lab_downstairs_ns/ --output-dir /mnt/data/metrics/lab_downstairs --metadata /mnt/data/lab_downstairs_ns/reconstruction.json --checkpoint /mnt/workspace/lantern/nerfstudio-HDR/outputs/lab_downstairs_ns/lantern-nerfacto/2024-10-06_232947/ + ns-process-data lantern-GT-HDR --data /mnt/data/coffee_room2/coffee_room2_ns/ --output-dir /mnt/data/coffee_room2/metrics --metadata /mnt/data/coffee_room2/sfm/reconstruction.json --checkpoint /mnt/workspace/lantern/nerfstudio-HDR/outputs/coffee_room2_ns/lantern-nerfacto/2025-01-22_194346/ ``` 2. Render panos at ground truth positions for well and fast exposed: ``` - ns-render camera-path --load-config /mnt/workspace/lantern/nerfstudio-HDR/outputs/lab_downstairs_ns/lantern-nerfacto/2024-10-06_232947/config.yml --camera-path-filename /mnt/data/metrics/lab_downstairs/GT_transforms.json --output-path /mnt/data/metrics/renders/lab_downstairs_aligned --output-format images --rendered_output_names + ns-render camera-path --load-config /mnt/workspace/lantern/nerfstudio-HDR/outputs/coffee_room2_ns/lantern-nerfacto/2025-01-22_194346/config.yml --camera-path-filename /mnt/data/coffee_room2/metrics/GT_transforms.json --output-path /mnt/data/coffee_room2/metrics/renders/unaligned --output-format images ``` ``` - ns-render camera-path --load-config /mnt/workspace/lantern/nerfstudio-HDR/outputs/lab_downstairs_ns/lantern-nerfacto/2024-10-06_232947/config.yml --camera-path-filename /mnt/data/metrics/lab_downstairs/GT_transforms.json --output-path /mnt/data/metrics/renders/lab_downstairs_aligned_fast --output-format images --rendered_output_names rgb_fast + ns-render camera-path --load-config /mnt/workspace/lantern/nerfstudio-HDR/outputs/coffee_room2_ns/lantern-nerfacto/2025-01-22_194346/config.yml --camera-path-filename /mnt/data/coffee_room2/metrics/GT_transforms.json --output-path /mnt/data/coffee_room2/metrics/renders/unaligned_fast --output-format images --rendered_output_names rgb_fast ``` 3. Combine both well and fast exposed panos: ``` - python lantern_scripts/combine.py --well_dir /mnt/data/metrics/renders/lab_downstairs_aligned/ --fast_dir /mnt/data/metrics/renders/lab_downstairs_aligned_fast/ --out_dir /mnt/data/metrics/renders/lab_downstairs_lin/ --experiment_location /mnt/data/lab_downstairs_ns/ --do_linearization + python lantern_scripts/combine.py --well_dir /mnt/data/coffee_room2/metrics/renders/unaligned/ --fast_dir /mnt/data/coffee_room2/metrics/renders/unaligned_fast/ --out_dir /mnt/data/coffee_room2/metrics/renders/unaligned_lin/ --experiment_location /mnt/data/coffee_room2/data/ --do_linearization ``` 4. Make sure the panos from renders have the same names as the GT panos. 5. Calculate PSNR, SSIM and LPIPS: ``` - python lantern_scripts/ldr_res.py --gt_dir /mnt/data/metrics/GT_lab_downstairs/pano/ --data_dir /mnt/data/metrics/renders/lab_downstairs_lin/ + python lantern_scripts/ldr_res.py --gt_dir /mnt/data/coffee_room2/GT/GT_exr/ --data_dir /mnt/data/coffee_room2/metrics/renders/unaligned_lin/ ``` -5. Generate HDR renders with blender: +5. Generate GT and results renders with blender: ``` - blender --background /mnt/data/metrics/scene_new.blend -P lantern_scripts/hdr_blender.py -- /mnt/data/metrics/renders/lab_downstairs_lin_less/ /mnt/data/metrics/renders/lab_downstairs_lin_renders/ + blender --background lantern_scripts/scene_new.blend -P lantern_scripts/hdr_blender.py -- /mnt/data/coffee_room2/GT/GT_exr/ /mnt/data/coffee_room2/GT/GT_exr_renders/ + ``` + ``` + blender --background lantern_scripts/scene_new.blend -P lantern_scripts/hdr_blender.py -- /mnt/data/coffee_room2/metrics/renders/unaligned_lin/ /mnt/data/coffee_room2/metrics/renders/unaligned_lin_renders/ ``` 6. Calculate si-RMSE, RMSE, RGB ang. and PSNR (LDR r.). Need to change values in code: diff --git a/lantern_scripts/align_fast_to_well.py b/lantern_scripts/align_fast_to_well.py index 5cfd48d00..1966ca958 100644 --- a/lantern_scripts/align_fast_to_well.py +++ b/lantern_scripts/align_fast_to_well.py @@ -125,6 +125,20 @@ class AlignImages: iterations += 1 return list(zip(self.matched_well_contours, self.fast_contours)) + def match_contours_simplified(self): #something to try out + for contour_index, fast_contour in enumerate(self.fast_contours): + for threshold_index, _ in enumerate(self.well_thresholds): + for well_contour in self.well_images[threshold_index].contours: + try: + well_contour = Contour(well_contour, threshold_index) + score = fast_contour.get_matching_score(well_contour) + if score < fast_contour.score and score <= 200: + fast_contour.match_contour(well_contour, score) + self.matched_well_contours[contour_index] = well_contour + except ContourTooSmallException as e: + continue + return list(zip(self.matched_well_contours, self.fast_contours)) + def get_point_descriptors(self, points, image): sift = cv2.SIFT_create() keypoints = [cv2.KeyPoint(x=float(p[0]), y=float(p[1]), size=20) for p in points] @@ -233,8 +247,8 @@ if __name__ == '__main__': argparser = argparse.ArgumentParser() argparser.add_argument("--input_dir", type=str, default="/mnt/data/scene/scene_ns/", help="Nerfstudio directory of the scene") argparser.add_argument("--config", type=str, default="outputs/scene_ns/lantern-nerfacto/2024-06-20_142413/config.yml", help="Nerfstudio config file") - argparser.add_argument("--no_render", action='store_true', default="False", help="Doesn't render images for alignment if flag is present") - argparser.add_argument("--no_copy", action='store_true', default="Flase", help="Doesn't copy the original data if flag is present. Use if already copied.") + argparser.add_argument("--no_render", action='store_true', default=False, help="Doesn't render images for alignment if flag is present") + argparser.add_argument("--no_copy", action='store_true', default=False, help="Doesn't copy the original data if flag is present. Use if already copied.") args = argparser.parse_args() json_path = os.path.join(args.input_dir, 'alignment_matrices.json') diff --git a/lantern_scripts/calculate_exposures.py b/lantern_scripts/calculate_exposures.py new file mode 100644 index 000000000..d024a89e7 --- /dev/null +++ b/lantern_scripts/calculate_exposures.py @@ -0,0 +1,21 @@ +import numpy as np +import argparse + +def calculate_exposures(min_exposure, max_exposure, nb_exposures): + return 2 ** np.linspace(np.log2(min_exposure), np.log2(max_exposure), nb_exposures) + +if __name__ == "__main__": + argparser = argparse.ArgumentParser() + argparser.add_argument("--min", type=float, required=True, default=0.00004) + argparser.add_argument("--max", type=float, required=True, default=0.004) + args = argparser.parse_args() + + min_exposure = args.min + max_exposure = args.max + nb_exposures = 11 # min and max exposures included + + exposures = calculate_exposures(min_exposure, max_exposure, nb_exposures) + print("Calculated exposures:") + for exposure in reversed(exposures): + denominator = round(1 / exposure) + print(f"1/{denominator}") \ No newline at end of file diff --git a/lantern_scripts/calculate_metrics.py b/lantern_scripts/calculate_metrics.py new file mode 100644 index 000000000..8f65b89e1 --- /dev/null +++ b/lantern_scripts/calculate_metrics.py @@ -0,0 +1,160 @@ +import subprocess +import argparse +import os +import shutil + +if __name__ == '__main__': + argparser = argparse.ArgumentParser() + argparser.add_argument("--input_dir", type=str, required=True, default="/mnt/data/scene/scene_ns/", + help="Directory containing input data (e.g. videos, GT, etc.)") + argparser.add_argument("--checkpoint", type=str, required=True, + default="/mnt/workspace/lantern/nerfstudio-HDR/outputs/coffee_room2_ns/lantern-nerfacto/2025-01-22_194346/", + help="Directory containing training data (e.g. config.yml and checkpoint info)") + argparser.add_argument("--metric_name", type=str, required=True, default="unaligned", + help="Name of the method, used for naming output directories") + + args = argparser.parse_args() + + if not args.input_dir.endswith('/'): + args.input_dir += '/' + + metrics_dir = os.path.join(args.input_dir, "metrics") + renders_dir = os.path.join(metrics_dir, "images") + renders_well = os.path.join(renders_dir, args.metric_name) + renders_fast = os.path.join(renders_dir, args.metric_name + "_fast") + renders_lin = os.path.join(renders_dir, args.metric_name + "_lin") + renders_lin_HDR = os.path.join(renders_dir, args.metric_name + "_lin_HDR") + lin_renders = os.path.join(renders_dir, args.metric_name + "_lin_renders") + + gt_dir = os.path.join(args.input_dir, "GT") + gt_hdr = os.path.join(gt_dir, "GT_hdr") + gt_exr = os.path.join(gt_dir, "GT_exr") + gt_exr_renders = os.path.join(gt_dir, "GT_exr_renders") + + # 1. Get ground truth transformations: + print("Running ns-process-data to get GT transformations...") + subprocess.run([ + "ns-process-data", "lantern-GT-HDR", + "--data", args.input_dir, + "--output-dir", metrics_dir, + "--metadata", os.path.join(args.input_dir, "sfm/reconstruction.json"), + "--checkpoint", args.checkpoint + ], check=True) + + # 2. Render panos at GT positions for well and fast exposures: + print("Rendering well-exposed panos...") + subprocess.run([ + "ns-render", "camera-path", + "--load-config", os.path.join(args.checkpoint, "config.yml"), + "--camera-path-filename", os.path.join(metrics_dir, "GT_transforms.json"), + "--output-path", renders_well, + "--output-format", "images" + ], check=True) + + print("Rendering fast-exposed panos...") + subprocess.run([ + "ns-render", "camera-path", + "--load-config", os.path.join(args.checkpoint, "config.yml"), + "--camera-path-filename", os.path.join(metrics_dir, "GT_transforms.json"), + "--output-path", renders_fast, + "--output-format", "images", + "--rendered_output_names", "rgb_fast" + ], check=True) + + # 3. Combine both well and fast exposed panos: + print("Combining well and fast exposure renders...") + subprocess.run([ + "python", "lantern_scripts/combine.py", + "--well_dir", renders_well + "/", + "--fast_dir", renders_fast + "/", + "--out_dir", renders_lin + "/", + "--experiment_location", os.path.join(args.input_dir, "data") + "/", + "--do_linearization" + ], check=True) + + # 4. Rename the render files: + print("Step 4a: Renaming render filenames for consistency between GT and renders...") + render_files = sorted([f for f in os.listdir(renders_lin) if f.endswith('.exr')]) + print("Render files found:", render_files) + total = len(render_files) + number_strings = [str(i) for i in range(1, total + 1)] + lex_order = sorted(number_strings) + for old_path, num in zip(render_files, lex_order): + new_filename = f"GT{num}.exr" + new_path = os.path.join(renders_lin, new_filename) + old_path = os.path.join(renders_lin, old_path) + shutil.move(old_path, new_path) + print(f"Renamed {os.path.basename(old_path)} to {new_filename}") + + + print("Step 4b: Verifying filename consistency between GT and renders...") + try: + gt_files = set(f for f in os.listdir(gt_exr) if f.endswith('.exr')) + render_files = set(f for f in os.listdir(renders_lin) if f.endswith('.exr')) + missing_in_renders = gt_files - render_files + extra_in_renders = render_files - gt_files + if missing_in_renders or extra_in_renders: + if missing_in_renders: + print("Error: The following GT files are missing in the renders directory:", missing_in_renders) + if extra_in_renders: + print("Error: There are extra files in the renders directory not found in GT:", extra_in_renders) + sys.exit(1) + else: + print("All GT filenames are present in the renders directory.") + except Exception as e: + print("Error during filename verification:", e) + sys.exit(1) + + # 5. Calculate PSNR, SSIM and LPIPS: + print("Calculating LDR metrics (PSNR, SSIM, LPIPS)...") + subprocess.run([ + "python", "lantern_scripts/ldr_res.py", + "--gt_dir", gt_exr + "/", + "--data_dir", renders_lin + "/" + ], check=True) + + # 6. Generate GT and result renders with Blender: + expected_render_dir = gt_exr_renders + rendered_files_exist = any( + f.endswith('.exr') for f in os.listdir(expected_render_dir) + ) if os.path.exists(expected_render_dir) else False + + if not rendered_files_exist: + print("Rendering GT images with Blender...") + subprocess.run([ + "blender", "--background", "lantern_scripts/scene_new.blend", + "-P", "lantern_scripts/hdr_blender.py", "--", + os.path.join(gt_exr, ""), os.path.join(gt_exr_renders, "") + ], check=True) + else: + print("Skipping Blender render — output files already exist.") + + print("Rendering result images with Blender...") + subprocess.run([ + "blender", "--background", "lantern_scripts/scene_new.blend", + "-P", "lantern_scripts/hdr_blender.py", "--", + renders_lin + "/", lin_renders + "/" + ], check=True) + + # 7. Calculate si-RMSE, RMSE, RGB angular error and PSNR (LDR rendered): + print("Calculating additional metrics (si-RMSE, RMSE, angular error, PSNR)...") + subprocess.run([ + "python", "lantern_scripts/res_table_1.py", + "--results_dataset_roots", lin_renders + "/", + "--gt_dataset_root", gt_exr_renders + "/", + "--output_folder", metrics_dir + "/results_table/", + "--dataset_names", args.metric_name + ], check=True) + + # 8. Convert EXR panos to HDR format: + print("Converting EXR panos to HDR...") + subprocess.run([ + "python", "lantern_scripts/exr2hdr.py", + "--hdr_dir", renders_lin, + "--output_dir", renders_lin_HDR + ], check=True) + subprocess.run([ + "python", "lantern_scripts/exr2hdr.py", + "--hdr_dir", gt_exr, + "--output_dir", gt_hdr + ], check=True) diff --git a/lantern_scripts/create_GT_folder.py b/lantern_scripts/create_GT_folder.py new file mode 100644 index 000000000..d961c1397 --- /dev/null +++ b/lantern_scripts/create_GT_folder.py @@ -0,0 +1,45 @@ +import os +import shutil +import re +import argparse + +def copy_files(input_dir): + brackets = os.listdir(input_dir) + + target_exr = os.path.join(input_dir, "GT_exr") + target_jpg = os.path.join(input_dir, "GT_jpg") + os.makedirs(target_exr, exist_ok=True) + os.makedirs(target_jpg, exist_ok=True) + + for folder in brackets: + if not folder.startswith("GT_"): + folder_path = os.path.join(input_dir, folder) + if os.path.isdir(folder_path): + smallest_value = None + smallest_file = None + for file in os.listdir(folder_path): + if file.startswith("GT") and file.lower().endswith(".exr"): + source_file = os.path.join(folder_path, file) + destination_file = os.path.join(target_exr, file) + shutil.copy2(source_file, destination_file) + print(f"Copied EXR: {source_file} -> {destination_file}") + + if file.lower().endswith(".jpg"): + numbers = re.findall(r'\d+', file) + num = int(numbers[0]) + if smallest_value is None or num < smallest_value: + smallest_value = num + smallest_file = file + + source_file = os.path.join(folder_path, smallest_file) + destination_file = os.path.join(target_jpg, f"{folder}.jpg") + shutil.copy2(source_file, destination_file) + print(f"Copied JPG: {source_file} -> {destination_file}") + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--input_dir', type=str, required=True, + help='GT directory containing all the subdirectories (e.g., GT1, GT2, ...) with frames') + args = parser.parse_args() + + copy_files(args.input_dir) diff --git a/lantern_scripts/create_data_folder.py b/lantern_scripts/create_data_folder.py index 6372df618..f1a147b67 100644 --- a/lantern_scripts/create_data_folder.py +++ b/lantern_scripts/create_data_folder.py @@ -3,22 +3,6 @@ import shutil import json import argparse -def move_subfolder(source_dir, destination_dir): - if not os.path.exists(source_dir): - print(f"Source directory '{source_dir}' does not exist.") - return - - os.makedirs(destination_dir, exist_ok=True) - - try: - for filename in os.listdir(source_dir): - if filename.lower().count('png') < 2: - shutil.move(os.path.join(source_dir, filename), os.path.join(destination_dir, filename)) - except Exception as e: - print(f"Error moving subfolder: {e}") - - delete_empty_folder(source_dir) - def copy_subfolder(source_dir, destination_dir): if not os.path.exists(source_dir): print(f"Source directory '{source_dir}' does not exist.") @@ -33,17 +17,6 @@ def copy_subfolder(source_dir, destination_dir): except Exception as e: print(f"Error copying subfolder: {e}") -def delete_empty_folder(folder_path): - try: - if os.path.exists(folder_path): - if not os.listdir(folder_path): - os.rmdir(folder_path) - print(f"Folder '{folder_path}' deleted successfully.") - else: - print(f"Folder '{folder_path}' does not exist.") - except Exception as e: - print(f"Error deleting folder: {e}") - def create_capture_settings_file(file_path, shutter_speed): capture_settings = { "left": { @@ -66,7 +39,7 @@ def create_capture_settings_file(file_path, shutter_speed): if __name__ == "__main__": argparser = argparse.ArgumentParser() - argparser.add_argument("--input_dir", type=str, default="/mnt/data/scene/") + argparser.add_argument("--input_dir", type=str, default="/mnt/data/scene/data/") argparser.add_argument("--shutter_speed", type=float, default=0.003125) args = argparser.parse_args() @@ -76,8 +49,6 @@ if __name__ == "__main__": for item in os.listdir(masks_folder): copy_subfolder(masks_folder + item, dest_folder + item + "/mask") - delete_empty_folder(args.input_dir + "masks") - create_capture_settings_file(dest_folder + "capture_settings.json", args.shutter_speed) diff --git a/lantern_scripts/exr2hdr.py b/lantern_scripts/exr2hdr.py new file mode 100644 index 000000000..9e42048f5 --- /dev/null +++ b/lantern_scripts/exr2hdr.py @@ -0,0 +1,44 @@ +import os +import cv2 +import argparse +import numpy as np +from tqdm import tqdm +import numpy as np +from pathlib import Path +from hdrio import imwrite +import imageio + + +os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1" +imageio.plugins.freeimage.download() + +def convert(src_addr, trg_addr): + img = imageio.imread(src_addr) + imwrite( img[:, :, 0:3], trg_addr) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--hdr_dir', type=str) + parser.add_argument('--output_dir', type=str) + parser.add_argument('--change_name', action='store_true') + args = parser.parse_args() + + hdrs = [] + for path in Path(args.hdr_dir).glob('*.exr'): + if path.is_file(): + hdrs.append(str(path)) + if len(hdrs) == 0: + raise Exception("No data available!!!") + hdrs = sorted(hdrs) + + os.makedirs(args.output_dir, exist_ok=True) + + for index, src_addr in enumerate(tqdm(hdrs)): + src_addr = str(src_addr) + if args.change_name: + trg_addr = str(Path(args.output_dir) / f"{index:05d}.hdr") + else: + trg_addr = src_addr.replace(args.hdr_dir, args.output_dir) + trg_addr = trg_addr.replace('.exr', '.hdr') + convert(src_addr, trg_addr) diff --git a/lantern_scripts/ldr_res.py b/lantern_scripts/ldr_res.py index 7e614f3a2..84e64d999 100755 --- a/lantern_scripts/ldr_res.py +++ b/lantern_scripts/ldr_res.py @@ -80,4 +80,17 @@ if __name__ == "__main__": print("SSIM: ", ssim_index) print("PSNR: ", psnr_index) lpips_index = piq.LPIPS(reduction='none')(torch.stack(gts), torch.stack(preds)) - print("LPIPS: ", torch.mean(lpips_index)) \ No newline at end of file + print("LPIPS: ", torch.mean(lpips_index)) + + results = { + "SSIM": str(ssim_index), + "PSNR": str(psnr_index), + "LPIPS": str(torch.mean(lpips_index)) + } + + output_path = os.path.join(args.data_dir, "ldr_results.json") + + with open(output_path, 'w') as f: + json.dump(results, f, indent=4) + + print(f"Saved metrics to: {output_path}") \ No newline at end of file diff --git a/lantern_scripts/process_videos_for_sfm.py b/lantern_scripts/process_videos_for_sfm.py index f4bda5575..8a6941249 100644 --- a/lantern_scripts/process_videos_for_sfm.py +++ b/lantern_scripts/process_videos_for_sfm.py @@ -5,10 +5,16 @@ if __name__ == '__main__': argparser = argparse.ArgumentParser() argparser.add_argument("--input_dir", type=str, required=True, default="/mnt/data/scene/", help="Directory containing all the 4 videos") argparser.add_argument("--shutter_speed", type=float, required=True, default=0.003125) + argparser.add_argument("--gt", action='store_true', default=False, help="Process GT images for metrics if flag is present") args = argparser.parse_args() - subprocess.run(["python", "lantern_scripts/sync_and_trim.py", "--input_dir", args.input_dir]) - subprocess.run(["python", "lantern_scripts/mp4_videos_to_png_frames.py", "--input_dir", args.input_dir + "data/"]) - subprocess.run(["python", "lantern_scripts/mask_humans2.py", "--input_dir", args.input_dir + "data/", "--output_dir", args.input_dir + "data/masks"]) - subprocess.run(["python", "lantern_scripts/create_data_folder.py", "--input_dir", args.input_dir + "data/", "--shutter_speed", str(args.shutter_speed)]) - subprocess.run(["python", "lantern_scripts/create_sfm_folder.py", "--input_dir", args.input_dir]) + subprocess.run(["python", "lantern_scripts/sync_and_trim.py", "--input_dir", args.input_dir], check=True) + subprocess.run(["python", "lantern_scripts/mp4_videos_to_png_frames.py", "--input_dir", args.input_dir + "data/"], check=True) + subprocess.run(["python", "lantern_scripts/mask_humans2.py", "--input_dir", args.input_dir + "data/", "--output_dir", args.input_dir + "data/masks"], check=True) + subprocess.run(["python", "lantern_scripts/create_data_folder.py", "--input_dir", args.input_dir + "data/", "--shutter_speed", str(args.shutter_speed)], check=True) + subprocess.run(["python", "lantern_scripts/create_sfm_folder.py", "--input_dir", args.input_dir], check=True) + + # For GT + if args.gt: + subprocess.run(["python", "lantern_scripts/debevec_bracket_merge.py", "--data_dir", args.input_dir + "GT/", "--experiment_location", args.input_dir + "data/"], check=True) + subprocess.run(["python", "lantern_scripts/create_GT_folder.py", "--input_dir", args.input_dir + "GT/"], check=True) diff --git a/lantern_scripts/render_mask.png b/lantern_scripts/render_mask.png new file mode 100755 index 0000000000000000000000000000000000000000..22b70fe1bd252268587980417611837bd51d32e9 GIT binary patch literal 2424 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