From c9cd243c44b39eda735b89de09b3e4536ddeaea2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <tmueller@nvidia.com>
Date: Tue, 3 Jan 2023 13:12:09 +0100
Subject: [PATCH] `colmap2nerf.py`: download COLMAP and FFmpeg automatically on
 Windows

---
 .gitignore                  |  1 +
 docs/nerf_dataset_tips.md   |  8 ++++++--
 scripts/colmap2nerf.py      | 41 +++++++++++++++++++++++++++++++------
 scripts/download_colmap.bat | 14 +++++++++++++
 scripts/download_ffmpeg.bat | 14 +++++++++++++
 5 files changed, 70 insertions(+), 8 deletions(-)
 create mode 100644 scripts/download_colmap.bat
 create mode 100644 scripts/download_ffmpeg.bat

diff --git a/.gitignore b/.gitignore
index 0b222e8..ad9c8b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 /.vscode
 /.vs
 /build*
+/external
 /figures
 /out
 /results
diff --git a/docs/nerf_dataset_tips.md b/docs/nerf_dataset_tips.md
index 7952082..f3d501a 100644
--- a/docs/nerf_dataset_tips.md
+++ b/docs/nerf_dataset_tips.md
@@ -3,6 +3,8 @@
 Our NeRF implementation expects initial camera parameters to be provided in a `transforms.json` file in a format compatible with [the original NeRF codebase](https://www.matthewtancik.com/nerf).
 We provide a script as a convenience, [scripts/colmap2nerf.py](/scripts/colmap2nerf.py), that can be used to process a video file or sequence of images, using the open source [COLMAP](https://colmap.github.io/) structure from motion software to extract the necessary camera data.
 
+If you are using Windows, you do not need to install COLMAP yourself; running [scripts/colmap2nerf.py](/scripts/colmap2nerf.py) will automatically download COLMAP for you.
+
 The training process can be quite picky about the dataset.
 For example, it is important for the dataset to have good coverage, to not contain mislabelled camera data, and to not contain blurry frames (motion blur and defocus blur are both problematic).
 This document attempts to give a few tips.
@@ -57,9 +59,11 @@ To train on self-captured data, one has to process the data into an existing for
 
 ### COLMAP
 
-Make sure that you have installed [COLMAP](https://colmap.github.io/) and that it is available in your PATH. If you are using a video file as input, also be sure to install [FFmpeg](https://www.ffmpeg.org/) and make sure that it is available in your PATH.
+If you use Linux, make sure that you have installed [COLMAP](https://colmap.github.io/) and that it is available in your PATH. If you are using a video file as input, also be sure to install [FFmpeg](https://www.ffmpeg.org/) and make sure that it is available in your PATH.
 To check that this is the case, from a terminal window, you should be able to run `colmap` and `ffmpeg -?` and see some help text from each.
 
+If you use Windows, you do not need to install anything. COLMAP and FFmpeg will be downloaded automatically when running the following scripts.
+
 If you are training from a video file, run the [scripts/colmap2nerf.py](/scripts/colmap2nerf.py) script from the folder containing the video, with the following recommended parameters:
 
 ```sh
@@ -74,7 +78,7 @@ For training from images, place them in a subfolder called `images` and then use
 data-folder$ python [path-to-instant-ngp]/scripts/colmap2nerf.py --colmap_matcher exhaustive --run_colmap --aabb_scale 16
 ```
 
-The script will run FFmpeg and/or COLMAP as needed, followed by a conversion step to the required `transforms.json` format, which will be written in the current directory. 
+The script will run (and install, if you use Windows) FFmpeg and COLMAP as needed, followed by a conversion step to the required `transforms.json` format, which will be written in the current directory. 
 
 By default, the script invokes colmap with the "sequential matcher", which is suitable for images taken from a smoothly changing camera path, as in a video. The exhaustive matcher is more appropriate if the images are in no particular order, as shown in the image example above.
 For more options, you can run the script with `--help`. For more advanced uses of COLMAP or for challenging scenes, please see the [COLMAP documentation](https://colmap.github.io/cli.html); you may need to modify the [scripts/colmap2nerf.py](/scripts/colmap2nerf.py) script itself.
diff --git a/scripts/colmap2nerf.py b/scripts/colmap2nerf.py
index 943373c..4dd0b9c 100755
--- a/scripts/colmap2nerf.py
+++ b/scripts/colmap2nerf.py
@@ -9,6 +9,7 @@
 # license agreement from NVIDIA CORPORATION is strictly prohibited.
 
 import argparse
+from glob import glob
 import os
 from pathlib import Path, PurePosixPath
 
@@ -20,6 +21,9 @@ import cv2
 import os
 import shutil
 
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+SCRIPTS_FOLDER = os.path.join(ROOT_DIR, "scripts")
+
 def parse_args():
 	parser = argparse.ArgumentParser(description="Convert a text colmap export to nerf format transforms.json; optionally convert video to images, and optionally run colmap in the first place.")
 
@@ -49,8 +53,21 @@ def do_system(arg):
 		sys.exit(err)
 
 def run_ffmpeg(args):
+	ffmpeg_binary = "ffmpeg"
+	if os.name == 'nt':
+		ffmpeg_glob = os.path.join(ROOT_DIR, "external", "ffmpeg", "*", "bin", "ffmpeg.exe")
+		candidates = glob(ffmpeg_glob)
+		if not candidates:
+			print("FFmpeg not found. Attempting to download FFmpeg from the internet.")
+			do_system(os.path.join(SCRIPTS_FOLDER, "download_ffmpeg.bat"))
+			candidates = glob(ffmpeg_glob)
+
+		if candidates:
+			ffmpeg_binary = candidates[0]
+
 	if not os.path.isabs(args.images):
 		args.images = os.path.join(os.path.dirname(args.video_in), args.images)
+
 	images = "\"" + args.images + "\""
 	video =  "\"" + args.video_in + "\""
 	fps = float(args.video_fps) or 1.0
@@ -69,9 +86,21 @@ def run_ffmpeg(args):
 	if time_slice:
 		start, end = time_slice.split(",")
 		time_slice_value = f",select='between(t\,{start}\,{end})'"
-	do_system(f"ffmpeg -i {video} -qscale:v 1 -qmin 1 -vf \"fps={fps}{time_slice_value}\" {images}/%04d.jpg")
+	do_system(f"{ffmpeg_binary} -i {video} -qscale:v 1 -qmin 1 -vf \"fps={fps}{time_slice_value}\" {images}/%04d.jpg")
 
 def run_colmap(args):
+	colmap_binary = "colmap"
+	if os.name == 'nt':
+		colmap_glob = os.path.join(ROOT_DIR, "external", "colmap", "*", "COLMAP.bat")
+		candidates = glob(colmap_glob)
+		if not candidates:
+			print("COLMAP not found. Attempting to download COLMAP from the internet.")
+			do_system(os.path.join(SCRIPTS_FOLDER, "download_colmap.bat"))
+			candidates = glob(colmap_glob)
+
+		if candidates:
+			colmap_binary = candidates[0]
+
 	db = args.colmap_db
 	images = "\"" + args.images + "\""
 	db_noext=str(Path(db).with_suffix(""))
@@ -85,8 +114,8 @@ def run_colmap(args):
 		sys.exit(1)
 	if os.path.exists(db):
 		os.remove(db)
-	do_system(f"colmap feature_extractor --ImageReader.camera_model {args.colmap_camera_model} --ImageReader.camera_params \"{args.colmap_camera_params}\" --SiftExtraction.estimate_affine_shape=true --SiftExtraction.domain_size_pooling=true --ImageReader.single_camera 1 --database_path {db} --image_path {images}")
-	match_cmd = f"colmap {args.colmap_matcher}_matcher --SiftMatching.guided_matching=true --database_path {db}"
+	do_system(f"{colmap_binary} feature_extractor --ImageReader.camera_model {args.colmap_camera_model} --ImageReader.camera_params \"{args.colmap_camera_params}\" --SiftExtraction.estimate_affine_shape=true --SiftExtraction.domain_size_pooling=true --ImageReader.single_camera 1 --database_path {db} --image_path {images}")
+	match_cmd = f"{colmap_binary} {args.colmap_matcher}_matcher --SiftMatching.guided_matching=true --database_path {db}"
 	if args.vocab_path:
 		match_cmd += f" --VocabTreeMatching.vocab_tree_path {args.vocab_path}"
 	do_system(match_cmd)
@@ -95,14 +124,14 @@ def run_colmap(args):
 	except:
 		pass
 	do_system(f"mkdir {sparse}")
-	do_system(f"colmap mapper --database_path {db} --image_path {images} --output_path {sparse}")
-	do_system(f"colmap bundle_adjuster --input_path {sparse}/0 --output_path {sparse}/0 --BundleAdjustment.refine_principal_point 1")
+	do_system(f"{colmap_binary} mapper --database_path {db} --image_path {images} --output_path {sparse}")
+	do_system(f"{colmap_binary} bundle_adjuster --input_path {sparse}/0 --output_path {sparse}/0 --BundleAdjustment.refine_principal_point 1")
 	try:
 		shutil.rmtree(text)
 	except:
 		pass
 	do_system(f"mkdir {text}")
-	do_system(f"colmap model_converter --input_path {sparse}/0 --output_path {text} --output_type TXT")
+	do_system(f"{colmap_binary} model_converter --input_path {sparse}/0 --output_path {text} --output_type TXT")
 
 def variance_of_laplacian(image):
 	return cv2.Laplacian(image, cv2.CV_64F).var()
diff --git a/scripts/download_colmap.bat b/scripts/download_colmap.bat
new file mode 100644
index 0000000..cac83db
--- /dev/null
+++ b/scripts/download_colmap.bat
@@ -0,0 +1,14 @@
+@echo off
+
+set cwd=%cd%
+cd /D %~dp0
+
+echo Downloading COLMAP...
+powershell -Command "(New-Object Net.WebClient).DownloadFile('https://github.com/colmap/colmap/releases/download/3.7/COLMAP-3.7-windows-no-cuda.zip', 'colmap.zip')"
+
+echo Unzipping...
+powershell Expand-Archive colmap.zip -DestinationPath ..\external\colmap -Force
+
+echo Cleaning up...
+if exist colmap.zip del /f /q colmap.zip
+exit /b
diff --git a/scripts/download_ffmpeg.bat b/scripts/download_ffmpeg.bat
new file mode 100644
index 0000000..62b4828
--- /dev/null
+++ b/scripts/download_ffmpeg.bat
@@ -0,0 +1,14 @@
+@echo off
+
+set cwd=%cd%
+cd /D %~dp0
+
+echo Downloading FFmpeg...
+powershell -Command "(New-Object Net.WebClient).DownloadFile('https://github.com/GyanD/codexffmpeg/releases/download/5.1.2/ffmpeg-5.1.2-full_build.zip', 'ffmpeg.zip')"
+
+echo Unzipping...
+powershell Expand-Archive ffmpeg.zip -DestinationPath ..\external\ffmpeg -Force
+
+echo Cleaning up...
+if exist ffmpeg.zip del /f /q ffmpeg.zip
+exit /b
-- 
GitLab