From cf7f577cedf8180e473aaf0ab874a6295e6fa949 Mon Sep 17 00:00:00 2001 From: ohayoyogi <77435669+ohayoyogi@users.noreply.github.com> Date: Mon, 7 Oct 2024 23:19:58 +0900 Subject: [PATCH 1/4] add colmap script --- LICENSE | 35 +- .../__init__.py | 1 + blender-exporter-colmap/ext/__init__.py | 0 .../ext/read_write_model.py | 605 ++++++++++++++++++ 4 files changed, 640 insertions(+), 1 deletion(-) rename __init__.py => blender-exporter-colmap/__init__.py (96%) create mode 100644 blender-exporter-colmap/ext/__init__.py create mode 100644 blender-exporter-colmap/ext/read_write_model.py diff --git a/LICENSE b/LICENSE index 546efaf..a4b292c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -MIT License +# MIT License Copyright (c) 2024 ohayoyogi @@ -19,3 +19,36 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# New BSD License + +This project includes software developed by ETH Zurich and UNC Chapel Hill under the New BSD License. + +Copyright (c) 2023, ETH Zurich and UNC Chapel Hill. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/__init__.py b/blender-exporter-colmap/__init__.py similarity index 96% rename from __init__.py rename to blender-exporter-colmap/__init__.py index c3c5e38..a27185a 100644 --- a/__init__.py +++ b/blender-exporter-colmap/__init__.py @@ -14,6 +14,7 @@ import bpy from bpy.props import StringProperty from bpy_extras.io_utils import ExportHelper +from ext.read_write_model import write_model import mathutils from pathlib import Path diff --git a/blender-exporter-colmap/ext/__init__.py b/blender-exporter-colmap/ext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blender-exporter-colmap/ext/read_write_model.py b/blender-exporter-colmap/ext/read_write_model.py new file mode 100644 index 0000000..07aebe4 --- /dev/null +++ b/blender-exporter-colmap/ext/read_write_model.py @@ -0,0 +1,605 @@ +# Copyright (c) 2023, ETH Zurich and UNC Chapel Hill. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of +# its contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +import argparse +import collections +import os +import struct + +import numpy as np + +CameraModel = collections.namedtuple( + "CameraModel", ["model_id", "model_name", "num_params"] +) +Camera = collections.namedtuple( + "Camera", ["id", "model", "width", "height", "params"] +) +BaseImage = collections.namedtuple( + "Image", ["id", "qvec", "tvec", "camera_id", "name", "xys", "point3D_ids"] +) +Point3D = collections.namedtuple( + "Point3D", ["id", "xyz", "rgb", "error", "image_ids", "point2D_idxs"] +) + + +class Image(BaseImage): + def qvec2rotmat(self): + return qvec2rotmat(self.qvec) + + +CAMERA_MODELS = { + CameraModel(model_id=0, model_name="SIMPLE_PINHOLE", num_params=3), + CameraModel(model_id=1, model_name="PINHOLE", num_params=4), + CameraModel(model_id=2, model_name="SIMPLE_RADIAL", num_params=4), + CameraModel(model_id=3, model_name="RADIAL", num_params=5), + CameraModel(model_id=4, model_name="OPENCV", num_params=8), + CameraModel(model_id=5, model_name="OPENCV_FISHEYE", num_params=8), + CameraModel(model_id=6, model_name="FULL_OPENCV", num_params=12), + CameraModel(model_id=7, model_name="FOV", num_params=5), + CameraModel(model_id=8, model_name="SIMPLE_RADIAL_FISHEYE", num_params=4), + CameraModel(model_id=9, model_name="RADIAL_FISHEYE", num_params=5), + CameraModel(model_id=10, model_name="THIN_PRISM_FISHEYE", num_params=12), +} +CAMERA_MODEL_IDS = dict( + [(camera_model.model_id, camera_model) for camera_model in CAMERA_MODELS] +) +CAMERA_MODEL_NAMES = dict( + [(camera_model.model_name, camera_model) for camera_model in CAMERA_MODELS] +) + + +def read_next_bytes(fid, num_bytes, format_char_sequence, endian_character="<"): + """Read and unpack the next bytes from a binary file. + :param fid: + :param num_bytes: Sum of combination of {2, 4, 8}, e.g. 2, 6, 16, 30, etc. + :param format_char_sequence: List of {c, e, f, d, h, H, i, I, l, L, q, Q}. + :param endian_character: Any of {@, =, <, >, !} + :return: Tuple of read and unpacked values. + """ + data = fid.read(num_bytes) + return struct.unpack(endian_character + format_char_sequence, data) + + +def write_next_bytes(fid, data, format_char_sequence, endian_character="<"): + """pack and write to a binary file. + :param fid: + :param data: data to send, if multiple elements are sent at the same time, + they should be encapsuled either in a list or a tuple + :param format_char_sequence: List of {c, e, f, d, h, H, i, I, l, L, q, Q}. + should be the same length as the data list or tuple + :param endian_character: Any of {@, =, <, >, !} + """ + if isinstance(data, (list, tuple)): + bytes = struct.pack(endian_character + format_char_sequence, *data) + else: + bytes = struct.pack(endian_character + format_char_sequence, data) + fid.write(bytes) + + +def read_cameras_text(path): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::WriteCamerasText(const std::string& path) + void Reconstruction::ReadCamerasText(const std::string& path) + """ + cameras = {} + with open(path, "r") as fid: + while True: + line = fid.readline() + if not line: + break + line = line.strip() + if len(line) > 0 and line[0] != "#": + elems = line.split() + camera_id = int(elems[0]) + model = elems[1] + width = int(elems[2]) + height = int(elems[3]) + params = np.array(tuple(map(float, elems[4:]))) + cameras[camera_id] = Camera( + id=camera_id, + model=model, + width=width, + height=height, + params=params, + ) + return cameras + + +def read_cameras_binary(path_to_model_file): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::WriteCamerasBinary(const std::string& path) + void Reconstruction::ReadCamerasBinary(const std::string& path) + """ + cameras = {} + with open(path_to_model_file, "rb") as fid: + num_cameras = read_next_bytes(fid, 8, "Q")[0] + for _ in range(num_cameras): + camera_properties = read_next_bytes( + fid, num_bytes=24, format_char_sequence="iiQQ" + ) + camera_id = camera_properties[0] + model_id = camera_properties[1] + model_name = CAMERA_MODEL_IDS[camera_properties[1]].model_name + width = camera_properties[2] + height = camera_properties[3] + num_params = CAMERA_MODEL_IDS[model_id].num_params + params = read_next_bytes( + fid, + num_bytes=8 * num_params, + format_char_sequence="d" * num_params, + ) + cameras[camera_id] = Camera( + id=camera_id, + model=model_name, + width=width, + height=height, + params=np.array(params), + ) + assert len(cameras) == num_cameras + return cameras + + +def write_cameras_text(cameras, path): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::WriteCamerasText(const std::string& path) + void Reconstruction::ReadCamerasText(const std::string& path) + """ + HEADER = ( + "# Camera list with one line of data per camera:\n" + + "# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]\n" + + "# Number of cameras: {}\n".format(len(cameras)) + ) + with open(path, "w") as fid: + fid.write(HEADER) + for _, cam in cameras.items(): + to_write = [cam.id, cam.model, cam.width, cam.height, *cam.params] + line = " ".join([str(elem) for elem in to_write]) + fid.write(line + "\n") + + +def write_cameras_binary(cameras, path_to_model_file): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::WriteCamerasBinary(const std::string& path) + void Reconstruction::ReadCamerasBinary(const std::string& path) + """ + with open(path_to_model_file, "wb") as fid: + write_next_bytes(fid, len(cameras), "Q") + for _, cam in cameras.items(): + model_id = CAMERA_MODEL_NAMES[cam.model].model_id + camera_properties = [cam.id, model_id, cam.width, cam.height] + write_next_bytes(fid, camera_properties, "iiQQ") + for p in cam.params: + write_next_bytes(fid, float(p), "d") + return cameras + + +def read_images_text(path): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadImagesText(const std::string& path) + void Reconstruction::WriteImagesText(const std::string& path) + """ + images = {} + with open(path, "r") as fid: + while True: + line = fid.readline() + if not line: + break + line = line.strip() + if len(line) > 0 and line[0] != "#": + elems = line.split() + image_id = int(elems[0]) + qvec = np.array(tuple(map(float, elems[1:5]))) + tvec = np.array(tuple(map(float, elems[5:8]))) + camera_id = int(elems[8]) + image_name = elems[9] + elems = fid.readline().split() + xys = np.column_stack( + [ + tuple(map(float, elems[0::3])), + tuple(map(float, elems[1::3])), + ] + ) + point3D_ids = np.array(tuple(map(int, elems[2::3]))) + images[image_id] = Image( + id=image_id, + qvec=qvec, + tvec=tvec, + camera_id=camera_id, + name=image_name, + xys=xys, + point3D_ids=point3D_ids, + ) + return images + + +def read_images_binary(path_to_model_file): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadImagesBinary(const std::string& path) + void Reconstruction::WriteImagesBinary(const std::string& path) + """ + images = {} + with open(path_to_model_file, "rb") as fid: + num_reg_images = read_next_bytes(fid, 8, "Q")[0] + for _ in range(num_reg_images): + binary_image_properties = read_next_bytes( + fid, num_bytes=64, format_char_sequence="idddddddi" + ) + image_id = binary_image_properties[0] + qvec = np.array(binary_image_properties[1:5]) + tvec = np.array(binary_image_properties[5:8]) + camera_id = binary_image_properties[8] + binary_image_name = b"" + current_char = read_next_bytes(fid, 1, "c")[0] + while current_char != b"\x00": # look for the ASCII 0 entry + binary_image_name += current_char + current_char = read_next_bytes(fid, 1, "c")[0] + image_name = binary_image_name.decode("utf-8") + num_points2D = read_next_bytes( + fid, num_bytes=8, format_char_sequence="Q" + )[0] + x_y_id_s = read_next_bytes( + fid, + num_bytes=24 * num_points2D, + format_char_sequence="ddq" * num_points2D, + ) + xys = np.column_stack( + [ + tuple(map(float, x_y_id_s[0::3])), + tuple(map(float, x_y_id_s[1::3])), + ] + ) + point3D_ids = np.array(tuple(map(int, x_y_id_s[2::3]))) + images[image_id] = Image( + id=image_id, + qvec=qvec, + tvec=tvec, + camera_id=camera_id, + name=image_name, + xys=xys, + point3D_ids=point3D_ids, + ) + return images + + +def write_images_text(images, path): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadImagesText(const std::string& path) + void Reconstruction::WriteImagesText(const std::string& path) + """ + if len(images) == 0: + mean_observations = 0 + else: + mean_observations = sum( + (len(img.point3D_ids) for _, img in images.items()) + ) / len(images) + HEADER = ( + "# Image list with two lines of data per image:\n" + + "# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME\n" + + "# POINTS2D[] as (X, Y, POINT3D_ID)\n" + + "# Number of images: {}, mean observations per image: {}\n".format( + len(images), mean_observations + ) + ) + + with open(path, "w") as fid: + fid.write(HEADER) + for _, img in images.items(): + image_header = [ + img.id, + *img.qvec, + *img.tvec, + img.camera_id, + img.name, + ] + first_line = " ".join(map(str, image_header)) + fid.write(first_line + "\n") + + points_strings = [] + for xy, point3D_id in zip(img.xys, img.point3D_ids): + points_strings.append(" ".join(map(str, [*xy, point3D_id]))) + fid.write(" ".join(points_strings) + "\n") + + +def write_images_binary(images, path_to_model_file): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadImagesBinary(const std::string& path) + void Reconstruction::WriteImagesBinary(const std::string& path) + """ + with open(path_to_model_file, "wb") as fid: + write_next_bytes(fid, len(images), "Q") + for _, img in images.items(): + write_next_bytes(fid, img.id, "i") + write_next_bytes(fid, img.qvec.tolist(), "dddd") + write_next_bytes(fid, img.tvec.tolist(), "ddd") + write_next_bytes(fid, img.camera_id, "i") + for char in img.name: + write_next_bytes(fid, char.encode("utf-8"), "c") + write_next_bytes(fid, b"\x00", "c") + write_next_bytes(fid, len(img.point3D_ids), "Q") + for xy, p3d_id in zip(img.xys, img.point3D_ids): + write_next_bytes(fid, [*xy, p3d_id], "ddq") + + +def read_points3D_text(path): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadPoints3DText(const std::string& path) + void Reconstruction::WritePoints3DText(const std::string& path) + """ + points3D = {} + with open(path, "r") as fid: + while True: + line = fid.readline() + if not line: + break + line = line.strip() + if len(line) > 0 and line[0] != "#": + elems = line.split() + point3D_id = int(elems[0]) + xyz = np.array(tuple(map(float, elems[1:4]))) + rgb = np.array(tuple(map(int, elems[4:7]))) + error = float(elems[7]) + image_ids = np.array(tuple(map(int, elems[8::2]))) + point2D_idxs = np.array(tuple(map(int, elems[9::2]))) + points3D[point3D_id] = Point3D( + id=point3D_id, + xyz=xyz, + rgb=rgb, + error=error, + image_ids=image_ids, + point2D_idxs=point2D_idxs, + ) + return points3D + + +def read_points3D_binary(path_to_model_file): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadPoints3DBinary(const std::string& path) + void Reconstruction::WritePoints3DBinary(const std::string& path) + """ + points3D = {} + with open(path_to_model_file, "rb") as fid: + num_points = read_next_bytes(fid, 8, "Q")[0] + for _ in range(num_points): + binary_point_line_properties = read_next_bytes( + fid, num_bytes=43, format_char_sequence="QdddBBBd" + ) + point3D_id = binary_point_line_properties[0] + xyz = np.array(binary_point_line_properties[1:4]) + rgb = np.array(binary_point_line_properties[4:7]) + error = np.array(binary_point_line_properties[7]) + track_length = read_next_bytes( + fid, num_bytes=8, format_char_sequence="Q" + )[0] + track_elems = read_next_bytes( + fid, + num_bytes=8 * track_length, + format_char_sequence="ii" * track_length, + ) + image_ids = np.array(tuple(map(int, track_elems[0::2]))) + point2D_idxs = np.array(tuple(map(int, track_elems[1::2]))) + points3D[point3D_id] = Point3D( + id=point3D_id, + xyz=xyz, + rgb=rgb, + error=error, + image_ids=image_ids, + point2D_idxs=point2D_idxs, + ) + return points3D + + +def write_points3D_text(points3D, path): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadPoints3DText(const std::string& path) + void Reconstruction::WritePoints3DText(const std::string& path) + """ + if len(points3D) == 0: + mean_track_length = 0 + else: + mean_track_length = sum( + (len(pt.image_ids) for _, pt in points3D.items()) + ) / len(points3D) + HEADER = ( + "# 3D point list with one line of data per point:\n" + + "# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)\n" + + "# Number of points: {}, mean track length: {}\n".format( + len(points3D), mean_track_length + ) + ) + + with open(path, "w") as fid: + fid.write(HEADER) + for _, pt in points3D.items(): + point_header = [pt.id, *pt.xyz, *pt.rgb, pt.error] + fid.write(" ".join(map(str, point_header)) + " ") + track_strings = [] + for image_id, point2D in zip(pt.image_ids, pt.point2D_idxs): + track_strings.append(" ".join(map(str, [image_id, point2D]))) + fid.write(" ".join(track_strings) + "\n") + + +def write_points3D_binary(points3D, path_to_model_file): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadPoints3DBinary(const std::string& path) + void Reconstruction::WritePoints3DBinary(const std::string& path) + """ + with open(path_to_model_file, "wb") as fid: + write_next_bytes(fid, len(points3D), "Q") + for _, pt in points3D.items(): + write_next_bytes(fid, pt.id, "Q") + write_next_bytes(fid, pt.xyz.tolist(), "ddd") + write_next_bytes(fid, pt.rgb.tolist(), "BBB") + write_next_bytes(fid, pt.error, "d") + track_length = pt.image_ids.shape[0] + write_next_bytes(fid, track_length, "Q") + for image_id, point2D_id in zip(pt.image_ids, pt.point2D_idxs): + write_next_bytes(fid, [image_id, point2D_id], "ii") + + +def detect_model_format(path, ext): + if ( + os.path.isfile(os.path.join(path, "cameras" + ext)) + and os.path.isfile(os.path.join(path, "images" + ext)) + and os.path.isfile(os.path.join(path, "points3D" + ext)) + ): + print("Detected model format: '" + ext + "'") + return True + + return False + + +def read_model(path, ext=""): + # try to detect the extension automatically + if ext == "": + if detect_model_format(path, ".bin"): + ext = ".bin" + elif detect_model_format(path, ".txt"): + ext = ".txt" + else: + print("Provide model format: '.bin' or '.txt'") + return + + if ext == ".txt": + cameras = read_cameras_text(os.path.join(path, "cameras" + ext)) + images = read_images_text(os.path.join(path, "images" + ext)) + points3D = read_points3D_text(os.path.join(path, "points3D") + ext) + else: + cameras = read_cameras_binary(os.path.join(path, "cameras" + ext)) + images = read_images_binary(os.path.join(path, "images" + ext)) + points3D = read_points3D_binary(os.path.join(path, "points3D") + ext) + return cameras, images, points3D + + +def write_model(cameras, images, points3D, path, ext=".bin"): + if ext == ".txt": + write_cameras_text(cameras, os.path.join(path, "cameras" + ext)) + write_images_text(images, os.path.join(path, "images" + ext)) + write_points3D_text(points3D, os.path.join(path, "points3D") + ext) + else: + write_cameras_binary(cameras, os.path.join(path, "cameras" + ext)) + write_images_binary(images, os.path.join(path, "images" + ext)) + write_points3D_binary(points3D, os.path.join(path, "points3D") + ext) + return cameras, images, points3D + + +def qvec2rotmat(qvec): + return np.array( + [ + [ + 1 - 2 * qvec[2] ** 2 - 2 * qvec[3] ** 2, + 2 * qvec[1] * qvec[2] - 2 * qvec[0] * qvec[3], + 2 * qvec[3] * qvec[1] + 2 * qvec[0] * qvec[2], + ], + [ + 2 * qvec[1] * qvec[2] + 2 * qvec[0] * qvec[3], + 1 - 2 * qvec[1] ** 2 - 2 * qvec[3] ** 2, + 2 * qvec[2] * qvec[3] - 2 * qvec[0] * qvec[1], + ], + [ + 2 * qvec[3] * qvec[1] - 2 * qvec[0] * qvec[2], + 2 * qvec[2] * qvec[3] + 2 * qvec[0] * qvec[1], + 1 - 2 * qvec[1] ** 2 - 2 * qvec[2] ** 2, + ], + ] + ) + + +def rotmat2qvec(R): + Rxx, Ryx, Rzx, Rxy, Ryy, Rzy, Rxz, Ryz, Rzz = R.flat + K = ( + np.array( + [ + [Rxx - Ryy - Rzz, 0, 0, 0], + [Ryx + Rxy, Ryy - Rxx - Rzz, 0, 0], + [Rzx + Rxz, Rzy + Ryz, Rzz - Rxx - Ryy, 0], + [Ryz - Rzy, Rzx - Rxz, Rxy - Ryx, Rxx + Ryy + Rzz], + ] + ) + / 3.0 + ) + eigvals, eigvecs = np.linalg.eigh(K) + qvec = eigvecs[[3, 0, 1, 2], np.argmax(eigvals)] + if qvec[0] < 0: + qvec *= -1 + return qvec + + +def main(): + parser = argparse.ArgumentParser( + description="Read and write COLMAP binary and text models" + ) + parser.add_argument("--input_model", help="path to input model folder") + parser.add_argument( + "--input_format", + choices=[".bin", ".txt"], + help="input model format", + default="", + ) + parser.add_argument("--output_model", help="path to output model folder") + parser.add_argument( + "--output_format", + choices=[".bin", ".txt"], + help="output model format", + default=".txt", + ) + args = parser.parse_args() + + cameras, images, points3D = read_model( + path=args.input_model, ext=args.input_format + ) + + print("num_cameras:", len(cameras)) + print("num_images:", len(images)) + print("num_points3D:", len(points3D)) + + if args.output_model is not None: + write_model( + cameras, + images, + points3D, + path=args.output_model, + ext=args.output_format, + ) + + +if __name__ == "__main__": + main() \ No newline at end of file From a4f65f98afe6380b7ad8e1f7aa2f9fa026028a71 Mon Sep 17 00:00:00 2001 From: ohayoyogi <77435669+ohayoyogi@users.noreply.github.com> Date: Tue, 8 Oct 2024 00:21:12 +0900 Subject: [PATCH 2/4] replace logics with script from colmap --- blender-exporter-colmap/__init__.py | 121 +++++++++++++++------------- 1 file changed, 65 insertions(+), 56 deletions(-) diff --git a/blender-exporter-colmap/__init__.py b/blender-exporter-colmap/__init__.py index a27185a..391d1d3 100644 --- a/blender-exporter-colmap/__init__.py +++ b/blender-exporter-colmap/__init__.py @@ -14,11 +14,11 @@ import bpy from bpy.props import StringProperty from bpy_extras.io_utils import ExportHelper -from ext.read_write_model import write_model +from . ext.read_write_model import write_model, Camera, Image import mathutils from pathlib import Path - +import numpy as np class BlenderExporterForColmap(bpy.types.Operator, ExportHelper): bl_idname = "object.colmap_dataset_generator" @@ -33,68 +33,77 @@ class BlenderExporterForColmap(bpy.types.Operator, ExportHelper): def export_dataset(self, context, dirpath: Path): scene = context.scene - cameras = [ i for i in scene.objects if i.type == "CAMERA"] + scene_cameras = [ i for i in scene.objects if i.type == "CAMERA"] scale = scene.render.resolution_percentage / 100.0 output_dir = dirpath - cameras_file = output_dir / 'cameras.txt' - images_file = output_dir / 'images.txt' images_dir = output_dir / 'images' - points_file = output_dir / 'points3D.txt' output_dir.mkdir(parents=True, exist_ok=True) - with open(cameras_file, 'w') as f_cam, open(images_file, 'w') as f_img, open(points_file, 'w') as f_points: - f_cam.write(f'# Camera list generated by blender-exporter-colmap\n') - f_img.write(f'# Image list generated by blender-exporter-colmap\n') - f_points.write(f'# 3D point list generated by blender-exporter-colmap\n') - - for idx, cam in enumerate(sorted(cameras, key=lambda x: x.name_full + ".jpg")): - filename = cam.name_full - width = scene.render.resolution_x - height = scene.render.resolution_y - focal_length = cam.data.lens - sensor_width = cam.data.sensor_width - sensor_height = cam.data.sensor_height - fx = focal_length * width / sensor_width - fy = focal_length * height / sensor_height - # fx, fy, cx, cy, k1, k2, p1, p2 - params = [fx, fy, width/2 , height/2, 0, 0, 0, 0] - f_cam.write(f'{idx+1} OPENCV {width} {height} {" ".join(map(str,params))}\n') - - rotation_mode_bk = cam.rotation_mode - - cam.rotation_mode = "QUATERNION" - cam_rot_orig = mathutils.Quaternion(cam.rotation_quaternion) - cam_rot = mathutils.Quaternion(( - cam_rot_orig.x, - cam_rot_orig.w, - cam_rot_orig.z, - -cam_rot_orig.y)) - qw = cam_rot.w - qx = cam_rot.x - qy = cam_rot.y - qz = cam_rot.z - cam.rotation_mode = rotation_mode_bk - - T = mathutils.Vector(cam.location) - T1 = -(cam_rot.to_matrix() @ T) - - tx = T1[0] - ty = T1[1] - tz = T1[2] - - # IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME - f_img.write(f'{idx+1} {qw} {qx} {qy} {qz} {tx} {ty} {tz} {idx+1} {filename}.jpg\n') - # POINTS2D[] as (X, Y, POINT3D_ID) - f_img.write(f'\n') - - bpy.context.scene.camera = cam - bpy.ops.render.render() - bpy.data.images['Render Result'].save_render(str(images_dir / f'{filename}.jpg')) - yield 100.0 * idx / len(cameras) - + cameras = {} + images = {} + for idx, cam in enumerate(sorted(scene_cameras, key=lambda x: x.name_full + ".jpg")): + camera_id = f'{idx+1}' + filename = f'{cam.name_full}.jpg' + width = scene.render.resolution_x + height = scene.render.resolution_y + focal_length = cam.data.lens + sensor_width = cam.data.sensor_width + sensor_height = cam.data.sensor_height + fx = focal_length * width / sensor_width + fy = focal_length * height / sensor_height + # fx, fy, cx, cy, k1, k2, p1, p2 + params = [fx, fy, width/2, height/2, 0, 0, 0, 0] + cameras[camera_id] = Camera( + id=camera_id, + model='OPENCV', + width=width, + height=height, + params=params + ) + + image_id = camera_id + rotation_mode_bk = cam.rotation_mode + + cam.rotation_mode = "QUATERNION" + cam_rot_orig = mathutils.Quaternion(cam.rotation_quaternion) + cam_rot = mathutils.Quaternion(( + cam_rot_orig.x, + cam_rot_orig.w, + cam_rot_orig.z, + -cam_rot_orig.y)) + qw = cam_rot.w + qx = cam_rot.x + qy = cam_rot.y + qz = cam_rot.z + cam.rotation_mode = rotation_mode_bk + + T = mathutils.Vector(cam.location) + T1 = -(cam_rot.to_matrix() @ T) + + tx = T1[0] + ty = T1[1] + tz = T1[2] + images[image_id] = Image( + id=image_id, + qvec=np.array([qw, qx, qy, qz]), + tvec=np.array([tx, ty, tz]), + camera_id=camera_id, + name=filename, + xys=[], + point3D_ids=[] + ) + + # Render scene + bpy.context.scene.camera = cam + bpy.ops.render.render() + bpy.data.images['Render Result'].save_render( + str(images_dir / filename)) + yield 100.0 * idx / (len(scene_cameras) + 1) + + write_model(cameras, images, {}, str(output_dir), '.txt') yield 100.0 def execute(self, context): From 53f03867d1612ec362ace97192b2901d2f832014 Mon Sep 17 00:00:00 2001 From: ohayoyogi <77435669+ohayoyogi@users.noreply.github.com> Date: Tue, 8 Oct 2024 00:37:13 +0900 Subject: [PATCH 3/4] update addon definition --- blender-exporter-colmap/__init__.py | 44 +++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/blender-exporter-colmap/__init__.py b/blender-exporter-colmap/__init__.py index 391d1d3..cd15c41 100644 --- a/blender-exporter-colmap/__init__.py +++ b/blender-exporter-colmap/__init__.py @@ -21,20 +21,19 @@ import numpy as np class BlenderExporterForColmap(bpy.types.Operator, ExportHelper): - bl_idname = "object.colmap_dataset_generator" - bl_label = "Export as colmap dataset" - bl_options = {"PRESET"} - + filename_ext = "." directory: StringProperty() filter_folder = True - def export_dataset(self, context, dirpath: Path): + def export_dataset(self, context, dirpath: Path, format: str): scene = context.scene scene_cameras = [ i for i in scene.objects if i.type == "CAMERA"] + output_format = format if format in ['.txt', '.bin'] else '.txt' + scale = scene.render.resolution_percentage / 100.0 output_dir = dirpath @@ -45,7 +44,7 @@ def export_dataset(self, context, dirpath: Path): cameras = {} images = {} for idx, cam in enumerate(sorted(scene_cameras, key=lambda x: x.name_full + ".jpg")): - camera_id = f'{idx+1}' + camera_id = idx+1 filename = f'{cam.name_full}.jpg' width = scene.render.resolution_x height = scene.render.resolution_y @@ -103,32 +102,53 @@ def export_dataset(self, context, dirpath: Path): str(images_dir / filename)) yield 100.0 * idx / (len(scene_cameras) + 1) - write_model(cameras, images, {}, str(output_dir), '.txt') + write_model(cameras, images, {}, str(output_dir), output_format) yield 100.0 - def execute(self, context): + def execute_(self, context, format): dirpath = Path(self.directory) if not dirpath.is_dir(): return { "WARNING", "Illegal directory was passed: " + self.directory } context.window_manager.progress_begin(0, 100) - for progress in self.export_dataset(context, dirpath): + for progress in self.export_dataset(context, dirpath, format): context.window_manager.progress_update(progress) context.window_manager.progress_end() return {"FINISHED"} +class BlenderExporterForColmapBinary(BlenderExporterForColmap): + bl_idname = "object.colmap_dataset_generator_binary" + bl_label = "Export as colmap dataset with binary format" + bl_options = {"PRESET"} + + def execute(self, context): + return super().execute_(context, '.bin') + +class BlenderExporterForColmapText(BlenderExporterForColmap): + bl_idname = "object.colmap_dataset_generator_text" + bl_label = "Export as colmap dataset with text format" + bl_options = {"PRESET"} + + def execute(self, context): + return super().execute_(context, '.txt') + def _blender_export_operator_function(topbar_file_import, context): topbar_file_import.layout.operator( - BlenderExporterForColmap.bl_idname, text="Colmap dataset" + BlenderExporterForColmapText.bl_idname, text="Colmap dataset (.txt)" + ) + topbar_file_import.layout.operator( + BlenderExporterForColmapBinary.bl_idname, text="Colmap dataset (.bin)" ) def register(): - bpy.utils.register_class(BlenderExporterForColmap) + bpy.utils.register_class(BlenderExporterForColmapBinary) + bpy.utils.register_class(BlenderExporterForColmapText) bpy.types.TOPBAR_MT_file_export.append(_blender_export_operator_function) def unregister(): - bpy.utils.unregister_class(BlenderExporterForColmap) + bpy.utils.unregister_class(BlenderExporterForColmapBinary) + bpy.utils.unregister_class(BlenderExporterForColmapText) if __name__ == "__main__": register() \ No newline at end of file From 7f2e4640794cc49c1c592069ab9d417477d13751 Mon Sep 17 00:00:00 2001 From: ohayoyogi <77435669+ohayoyogi@users.noreply.github.com> Date: Tue, 8 Oct 2024 00:37:30 +0900 Subject: [PATCH 4/4] increment version --- blender-exporter-colmap/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender-exporter-colmap/__init__.py b/blender-exporter-colmap/__init__.py index cd15c41..d1eea7a 100644 --- a/blender-exporter-colmap/__init__.py +++ b/blender-exporter-colmap/__init__.py @@ -2,7 +2,7 @@ "name": "Scene exporter for colmap", "description": "Generates a dataset for colmap by exporting blender camera poses and rendering scene.", "author": "Ohayoyogi", - "version": (0,0,1), + "version": (0,1,0), "blender": (3,6,0), "location": "File/Export", "warning": "",