nerfstudio-project__nerfstudio METASEP

tests/nerf/graph/configs/test_repeat_graph.yaml METASEP
defaults:
  - test_basic_graph
  - _self_
  
network:
  mlp_0:
    inputs: ['encoder_0', 'encoder_0']
    
tests/nerf/graph/configs/test_complex_graph.yaml METASEP
defaults:
  - test_basic_graph
  - _self_
  
network:
  mlp_1:
    inputs: ['encoder_0']
  mlp_2:
    class_name: MLP
    inputs: ['encoder_0', 'mlp_0']
    meta_data:
      in_dim: 4
      out_dim: 5
      num_layers: 6
      layer_width: 7
  mlp_3:
    class_name: MLP
    inputs: ['mlp_0']
    meta_data:
      in_dim: 4
      out_dim: 5
      num_layers: 6
      layer_width: 7
tests/nerf/graph/configs/test_basic_graph.yaml METASEP
machine_config:
  num_gpus: 8
  num_machines: 1
  machine_rank: 0
  dist_url: auto
network:
  mlp_0:
    class_name: MLP
    inputs: ['encoder_0']
    meta_data: 
      in_dim: 0
      out_dim: 1
      num_layers: 2
      layer_width: 3
  encoder_0:
    class_name: ScalingAndOffset
    inputs: ['x']
    meta_data:
      in_dim: 16
  mlp_1:
    class_name: MLP
    inputs: ['mlp_0']
    meta_data:
      in_dim: 4
      out_dim: 5
      num_layers: 6
      layer_width: 7
tests/nerf/dataloader/configs/blender_lego.yaml METASEP
# dataset options
dataset:
  data_directory: data/lego
  dataset_type: blender
  downscale_factor: 1.0

# dataloader options
dataloader:
  num_workers: 0
  shuffle: true
  num_images_to_sample_from: 8
  num_rays_per_batch: 1024

# ray generation module and its option
network:
  ray_generator:
    class_name: RayGenerator
    inputs: ['cxy'] # camera, x, and y indices
    meta_data:
      in_dim: 0
      learning_rate: 0.0
data/blender/lego_test/val/r_0.png METASEP

data/blender/lego_test/train/r_0.png METASEP

tests/nerf/graph/configs/__init__.py METASEP

data/blender/lego_test/transforms_val.json METASEP
{
    "camera_angle_x": 0.6911112070083618,
    "frames": [
        {
            "file_path": "./val/r_0",
            "rotation": 0.012566370614359171,
            "transform_matrix": [
                [
                    -0.963964581489563,
                    -0.2611401677131653,
                    0.0507759265601635,
                    0.2046843022108078
                ],
                [
                    0.26603081822395325,
                    -0.9462433457374573,
                    0.18398693203926086,
                    0.7416750192642212
                ],
                [
                    7.450580596923828e-09,
                    0.1908649355173111,
                    0.9816163182258606,
                    3.957021951675415
                ],
                [
                    0.0,
                    0.0,
                    0.0,
                    1.0
                ]
            ]
        }
    ]
}
data/blender/lego_test/transforms_train.json METASEP
{
    "camera_angle_x": 0.6911112070083618,
    "frames": [
        {
            "file_path": "./train/r_0",
            "rotation": 0.012566370614359171,
            "transform_matrix": [
                [
                    -0.9999021887779236,
                    0.004192245192825794,
                    -0.013345719315111637,
                    -0.05379832163453102
                ],
                [
                    -0.013988681137561798,
                    -0.2996590733528137,
                    0.95394366979599,
                    3.845470428466797
                ],
                [
                    -4.656612873077393e-10,
                    0.9540371894836426,
                    0.29968830943107605,
                    1.2080823183059692
                ],
                [
                    0.0,
                    0.0,
                    0.0,
                    1.0
                ]
            ]
        }
    ]
}
tests/nerf/field_modules/test_mlp.py METASEP
"""
MLP Test
"""
import torch
from torch import nn

from mattport.nerf.field_modules import MLP


def test_mlp():
    """Test mlp"""
    in_dim = 6
    out_dim = 10
    num_layers = 2
    layer_width = 32
    out_activation = nn.ReLU()
    mlp = MLP(
        in_dim=in_dim, out_dim=out_dim, num_layers=num_layers, layer_width=layer_width, out_activation=out_activation
    )
    assert mlp.get_out_dim() == out_dim

    x = torch.ones((9, in_dim))

    mlp.build_nn_modules()
    y = mlp(x)

    assert y.shape[-1] == out_dim


if __name__ == "__main__":
    test_mlp()

tests/nerf/field_modules/test_field_outputs.py METASEP
"""
Field output tests
"""
import torch
from torch import nn

from mattport.nerf.field_modules.field_heads import DensityFieldHead, FieldHead, FieldHeadNames, RGBFieldHead


def test_field_output():
    """Test render output"""
    in_dim = 6
    out_dim = 4
    field_head_name = FieldHeadNames.DENSITY
    activation = nn.ReLU()
    render_head = FieldHead(in_dim=in_dim, out_dim=out_dim, field_head_name=field_head_name, activation=activation)
    assert render_head.get_out_dim() == out_dim

    x = torch.ones((9, in_dim))
    render_head(x)


def test_density_output():
    """Test rgb output"""
    in_dim = 6
    density_head = DensityFieldHead(in_dim)
    assert density_head.get_out_dim() == 1

    x = torch.ones((9, in_dim))
    density_head(x)


def test_rgb_output():
    """Test rgb output"""
    in_dim = 6
    rgb_head = RGBFieldHead(in_dim)
    assert rgb_head.get_out_dim() == 3

    x = torch.ones((9, in_dim))
    rgb_head(x)


if __name__ == "__main__":
    test_field_output()
    test_density_output()
    test_rgb_output()

tests/nerf/field_modules/test_encodings.py METASEP
"""
Encoding Tests
"""
import pytest
import torch
from mattport.nerf.field_modules import encoding


def test_scaling_and_offset():
    """Test scaling and offset encoder"""
    in_dim = 4
    in_tensor = torch.ones((2, 3, in_dim))

    scaling = 2.0
    offset = 4.5
    encoder = encoding.ScalingAndOffset(in_dim=in_dim, scaling=scaling, offset=offset)

    assert encoder.get_out_dim() == in_dim
    encoded = encoder.encode(in_tensor)
    assert encoded.shape[-1] == in_dim
    assert in_tensor * 6.5 == pytest.approx(encoded)

    with pytest.raises(ValueError):
        encoding.ScalingAndOffset(in_dim=-1)


def test_nerf_encoder():
    """Test NeRF encoder"""
    in_dim = 4
    out_dim = 24
    in_tensor = torch.ones((2, 3, in_dim))

    num_frequencies = 3
    min_freq_exp = 0
    max_freq_exp = 3
    encoder = encoding.NeRFEncoding(
        in_dim=in_dim, num_frequencies=num_frequencies, min_freq_exp=min_freq_exp, max_freq_exp=max_freq_exp
    )
    assert encoder.get_out_dim() == out_dim

    in_tensor = torch.ones((2, 3, in_dim))
    encoded = encoder.encode(in_tensor)
    assert encoded.shape[-1] == out_dim
    assert torch.max(encoded) == 1

    in_tensor = torch.zeros((2, 3, in_dim))
    encoded = encoder.encode(in_tensor)
    assert encoded.shape[-1] == out_dim
    assert torch.min(encoded) == 0


def test_rff_encoder():
    """Test RFF encoder"""
    in_dim = 3
    out_dim = 24
    in_tensor = torch.ones((2, 3, in_dim))

    num_frequencies = 12
    scale = 5
    encoder = encoding.RFFEncoding(in_dim=in_dim, num_frequencies=num_frequencies, scale=scale)
    assert encoder.get_out_dim() == out_dim

    in_tensor = torch.ones((2, 3, in_dim))
    encoded = encoder.encode(in_tensor)
    assert encoded.shape[-1] == out_dim

tests/nerf/dataloader/test_ray_generator.py METASEP
"""
Code to test ray generation.
"""

import imageio
import torch
from torch.utils.data import DataLoader
from tqdm import tqdm

from mattport.nerf.dataset.collate import CollateIterDataset, collate_batch_size_one
from mattport.nerf.dataset.image_dataset import ImageDataset, collate_batch
from mattport.nerf.dataset.utils import get_dataset_inputs_dict
from mattport.nerf.field_modules.ray_generator import RayGenerator
from mattport.utils.io import get_absolute_path
from mattport.viewer.plotly import visualize_dataset


def visualize_batch(batch):
    """Returns an image from a batch."""
    assert "image" in batch, "You must call collate_batch with keep_full_image=True for this to work."
    # set the color of the sampled rays
    c, y, x = [i.flatten() for i in torch.split(batch["local_indices"], 1, dim=-1)]
    batch["image"][c, y, x] = 1.0

    # batch["image"] is num_images, h, w, 3
    images = torch.split(batch["image"], 1, dim=0)
    image_list = [image[0] for image in images]
    image = torch.cat(image_list, dim=1)  # cat along the width dimension
    image = (image * 255.0).to(torch.uint8)
    return image


def test_dataloader(visualize=False):
    """Testing for the dataloader from input dataset parameters to rays."""

    data_directory = "data/blender/lego_test"
    dataset_type = "blender"
    downscale_factor = 1
    num_images_to_sample_from = 1
    num_times_to_repeat_images = 40
    num_rays_per_batch = 1024
    num_workers = 0

    data_directory = get_absolute_path(data_directory)
    dataset_inputs = get_dataset_inputs_dict(
        data_directory=data_directory, dataset_type=dataset_type, downscale_factor=downscale_factor
    )["train"]
    image_dataset = ImageDataset(
        image_filenames=dataset_inputs.image_filenames, downscale_factor=dataset_inputs.downscale_factor
    )
    iter_dataset = CollateIterDataset(
        image_dataset,
        collate_fn=lambda batch_list: collate_batch(batch_list, num_rays_per_batch, keep_full_image=True),
        num_samples_to_collate=num_images_to_sample_from,
        num_times_to_repeat=num_times_to_repeat_images,
    )
    dataloader = DataLoader(
        iter_dataset,
        batch_size=1,
        num_workers=num_workers,
        collate_fn=collate_batch_size_one,
        pin_memory=True,
    )
    dataloader_iter = iter(dataloader)

    ray_generator = RayGenerator(dataset_inputs.intrinsics, dataset_inputs.camera_to_world)

    num_batches = 10
    for _ in tqdm(range(num_batches)):
        batch = next(dataloader_iter)
        ray_bundle = ray_generator.forward(batch["indices"])

    # visualize the batch
    image = visualize_batch(batch)
    if visualize:
        imageio.imwrite("temp0.png", image)

    # visualize the RayBundle
    small_ray_bundle = ray_bundle.sample(100)
    fig = visualize_dataset(camera_origins=dataset_inputs.camera_to_world[:, :3, 3], ray_bundle=small_ray_bundle)
    if visualize:
        fig.write_image("temp1.png")
        fig.write_html("temp1.html")
    assert True


if __name__ == "__main__":
    test_dataloader(visualize=True)

configs/logging/default_logging.yaml METASEP
steps_per_log: 10
# stats tracker logs configurable training stats per iteration to terminal (e.g. rays/s, PSNR)
enable_stats: True
stats_tracker:
  max_history: 10
  stats_to_track: [ITER_LOAD_TIME, ITER_TRAIN_TIME, RAYS_PER_SEC, CURR_TEST_PSNR] # see mattport/utils/stats_tracker.py for options
# writer logs losses and images per iteration
writer: 
  type: TensorboardWriter
  save_dir: './'
# profiler logs run times of functions and prints at end of training
enable_profiler: True
configs/graph/instant_ngp_graph.yaml METASEP
# saving model specifications
model_dir: /tmp/mattport_models/ 
steps_per_save: null
steps_per_test: 100
max_num_iterations: 1000000

# pretrained loading options
resume_train:
  load_dir: # pretrained model directory
  load_step: # pretrained model's step number

# NOTE(ethan): currently setting to nothing b/c we're still designing the config format...
network:
  _target_: mattport.nerf.graph.instant_ngp.NGPGraph
  near_plane: 1.0
  far_plane: 6.0
  num_coarse_samples: 64
  num_importance_samples: 64

# optimizer options for network
param_groups:
  fields:
    optimizer:
      type: Adam
      lr: 1e-2
    scheduler:
      type: StepLR
      step_size: 30 #  Period of learning rate decay.
      gamma: 0.1 # Multiplicative factor of learning rate decay. Default: 0.1.
configs/graph/default_graph.yaml METASEP
# saving model specifications
model_dir: /tmp/mattport_models/ 
steps_per_save: null
steps_per_test: 100
max_num_iterations: 1000000

# pretrained loading options
resume_train:
  load_dir: # pretrained model directory
  load_step: # pretrained model's step number

# NOTE(ethan): currently setting to nothing b/c we're still designing the config format...
network:
  _target_: mattport.nerf.graph.vanilla_nerf.NeRFGraph
  near_plane: 1.0
  far_plane: 6.0
  num_coarse_samples: 64
  num_importance_samples: 64

# optimizer options for network
param_groups:
  fields:
    optimizer:
      type: Adam
      lr: 1e-4
    scheduler:
      type: StepLR
      step_size: 30 #  Period of learning rate decay.
      gamma: 0.1 # Multiplicative factor of learning rate decay. Default: 0.1.
  cameras:
    optimizer:
      type: SGD
      lr: 1e-4
    scheduler:
      type: ExponentialLR
      gamma: 0.1
configs/data/default_data.yaml METASEP
# dataset options
dataset:
  data_directory: data/blender/lego
  dataset_type: blender
  downscale_factor: 4
  alpha_color: white

# validation options
validation_image_indices: [0, 10] # Which images to evaluate on.

# dataloader options
dataloader:
  num_workers: 4
  shuffle: true
  num_images_to_sample_from: 8
  num_rays_per_batch: 1024
  num_times_to_repeat_images: 40

tests/structures/test_cameras.py METASEP
"""
Test the camera classes.
"""

import torch

from mattport.structures.cameras import PinholeCamera
from mattport.structures.rays import CameraRayBundle


def test_pinhole_camera():
    """Test that the pinhole camera model works."""
    pinhole_camera = PinholeCamera(cx=400.0, cy=400.0, fx=10.0, fy=10.0)
    camera_ray_bundle = pinhole_camera.generate_camera_rays()
    assert isinstance(camera_ray_bundle, CameraRayBundle)
    assert torch.allclose(camera_ray_bundle.origins[0], torch.tensor([0.0, 0.0, 0.0]))

tests/nerf/test_sampler.py METASEP
"""
Test samplers
"""
import torch
from mattport.nerf.sampler import PDFSampler, UniformSampler

from mattport.structures.rays import RayBundle


def test_uniform_sampler():
    """Test uniform sampler"""
    near_plane = 2
    far_plane = 4
    num_samples = 15
    sampler = UniformSampler(near_plane=near_plane, far_plane=far_plane, num_samples=num_samples)

    origins = torch.zeros((10, 3))
    directions = torch.ones_like(origins)
    ray_bundle = RayBundle(origins=origins, directions=directions)

    ray_samples = sampler(ray_bundle)

    assert ray_samples.positions.shape[-2] == num_samples

    # TODO Tancik: Add more precise tests


def test_pdf_sampler():
    """Test pdf sampler"""
    near_plane = 2
    far_plane = 4
    num_samples = 15

    origins = torch.zeros((10, 3))
    directions = torch.ones_like(origins)
    ray_bundle = RayBundle(origins=origins, directions=directions)

    uniform_sampler = UniformSampler(near_plane=near_plane, far_plane=far_plane, num_samples=num_samples)
    coarse_ray_samples = uniform_sampler(ray_bundle)

    weights = torch.ones((10, num_samples))

    # Just check that it doesn't crash. Need to add some actual tests.
    pdf_sampler = PDFSampler(num_samples)
    pdf_sampler(coarse_ray_samples, weights, num_samples)

    # TODO Tancik: Add more precise tests


if __name__ == "__main__":
    test_uniform_sampler()
    test_pdf_sampler()

tests/nerf/test_renderers.py METASEP
"""
Test renderers
"""
import pytest
import torch

from mattport.nerf import renderers


def test_rgb_renderer():
    """Test RGB volumetric rendering"""
    num_samples = 10

    rgb = torch.ones((3, num_samples, 3))
    weights = torch.ones((3, num_samples))
    weights /= torch.sum(weights, axis=-1, keepdim=True)

    rgb_renderer = renderers.RGBRenderer()

    out = rgb_renderer(rgb=rgb, weights=weights)
    assert torch.max(out.rgb) > 0.9

    out = rgb_renderer(rgb=rgb * 0, weights=weights)
    assert torch.max(out.rgb) == pytest.approx(0)

    # TODO Tancik: Add more precise tests


if __name__ == "__main__":
    test_rgb_renderer()

tests/nerf/__init__.py METASEP

viewer/README.md METASEP
# Viewer

The frontend ReactJS viewer.
tests/README.md METASEP

scripts/README.md METASEP

notebooks/visualize_encoders.ipynb METASEP
{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import mediapy as media\n",
    "from mattport.nerf.field_modules import encoding\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# NeRF Positional Encoding\n",
    "First introduced in the original NeRF paper. This encoding assumes the inputs are between zero and one and can opperate on any dimensional input."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Input Values:\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<table class=\"show_images\" style=\"border-spacing:0px;\"><tr><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAABuUlEQVR4nO3aQa7gNgxEQdvqGcwm586lk+85Ri386gQEiCYl2fc/f/694uz3++gaPm2/36Nr+LTtKgHSftUAantvXcOnNYKwGoDt1ACqHYCVAGy7SoC0UwOorbcgqgRgNQDb6RhKdQrCGkFYIwgrAVgNwDZdwce1A7D1T4TVDsBKAFYDsJ0mEFUCsBqANYKwGoDt3K+u4dNKALbzlACpBmA7/RZE7SkBVCMI23l+dA2ftnNKgNQOwBpBWEsY23NKgFQCsD3tAKoljO3pHkA1grBGEFYCsO4BWAnA2gFYr6FYOwBrB2A1AKsBWDsAKwFYx1Bsz/7XNXxaIwirAVgNwPokie0uAVQjCKsB2O52AFUCsBqA7e41lOoxDusegPVBBtvdCKJKANYOwGoA1nM0VgKwbsJYN2GsBGDtAKwEYH0RwxpBWCMIqwFYz9FYCcBqALarYyjVMRRrBGG7agDVUwRWArB2ANYxFGsEYSUAawdgJQBrB2AlANtbA6gSgO1aDZAaQdiuo0v4thKA7Zou4dv2NoKovefWNXza3kYQ1QjC9q4RJLUDsP00gqiWMLa3DzLU3v2na/i0v30YbAheKpYKAAAAAElFTkSuQmCC\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAB00lEQVR4nO3RMY7dQAwEUZI9XjjxuX1pe+hAK/jfoJJ6gdDgKKv+9fP3187X5tT8qDnbp+bUpOZsp+ZUp/rspDrV2T7veC6nKtupSj3fStf/0ZXedGU2s5ma73GTnWfMTm5mZ27mTt4xd3Lfvc+ej/vMzvn7cdx+d2ffcWe2505uz05uz+335+/X55jb77Fz+7nn1nus3M7We6zn6WNvtrJ1nlGbrVOb2vQ+4/Smb2pPbe6eP1NCGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmANg/vMBrChrbL84AAAAASUVORK5CYII=\"/></td></tr></table>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Encoded Values:\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<table class=\"show_images\" style=\"border-spacing:0px;\"><tr><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAACMUlEQVR4nO3d25GjQBAFUVpKAWPZurRGrj8DSII1Iz/IY0GFblS/aMH49+fvFA+v5bBruDVey9uu4dYKQMa8NgSZoA5QQZOwCtY6wNQQJONZB6h4Lh+7hlvj0TJUxaMOUDEKQMVYC8DEVAeo6gAZ13zaNdwa01IAJq75smu4Na5XAZg4F7uEe+N6DbuGW+MsABXX0y7h3rioA0x1gIyTlqEmplEAJq5HAZiYHl+7hlt72AXcXQHICkBWADKms52YiXF2FGFifBuFTIwuRahoH+ZifDuKMDGOAjDx6P8ZKsa7DjAx6gAV42gjZmLsbcRMjKMATExbh3EmrqMATEw7dg23xrW97BpujbMAVJwNQSq+dYCKcy8AE58CUPFpCFLVAbICkPEuABXv39mu4dZ4HwVg4mgIUnFsdYCJYy8AUwHI2ApAxV4AKvZWQSq2AlCxHT0RM7HtXUsxsb+7GWfi91MAJvbe26rit/9nqNimEjCx9cYsFcfUJGBiGwVgYi8AVUOQjH30sggT7zpAxdEcoOI9NQSZOApAxaedsKpJWManAFQc7QNUfDuOVvFtElb1QFhWALICkBWAjOfVS/tMPKcCMBWAjFffMlTxah5WQQGooFWQinlqDjA1B8hYWgWp6gBZHSBjvuoAE0tDkIq1DlAVgIyl01AVa7+/qgBk/Dy7F2RiobuhJta5AEysS5/UNrEufc3TxDoXgImlDlCxrLtdw63VATLm5bBruDWWAlD9Bzbwo3ncxRhVAAAAAElFTkSuQmCC\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAC8klEQVR4nO3deXLaQBDFYSR+EOO48IZZAhLcJlfKIXOHmC1mMZiycWyzSDnG+6Pfd4JXNUjUzHS3kt8/f1VMh4dspc4QGjfZWp0hNC7zjTpDaFTznTpDaJy7B3WG0Di0UnWG0DhdleoMoVFyUmcIjeSEOkNo1N4SdYbQqK0LdYbQqM4v1BlC4zS9VWcIjffpvTpDaLxMW+oMobGadNQZQmM+66ozhMZ47j9hJR733gkrMUl9HK3EU/qhzhAaay+AFNvkU50hNM6JL2SUaJRVdYbQuCt8GqpEp7xUZwiNwbmhzhAaw8T/AUqMWt4HKJEPXB2tRG+wVGcIjZbL06W4zr0ASlxkW3WG0EiyV3WG0Dj1XJyrxPHWGzElTpdndYbQqKReACXSr7o6Q2jUdi5PV6LmfZgUydwXMkocJ3fqDKHxNnF5uhLbyYM6Q2gsZ+4PUGLmBZBi/NRUZwiNP19u1FZiWv1SZwiNhcvTpXhOvABK7FK/gpQ8LkuMZuH7ACXuS/cHKNEtfBytRHb2E6DEqKaOEBvD7ps6Q2hk7g+QouP+ACnu/f0AKZoeXy9FPXd/gBKVwV6dITSOHQ/rUOJ47QNRJc4NjyxT8s9fjOq77wOUqL+4P0AJln4LKVGZX6kzhMbn2OPrlXj1+HopNtO2OkNoLKYuT1diMvMToMTj6rs6Q2g8Ft6IKTFPPbxbiWX6T50hNDYeXy/FPj2qM4RGtXSTnhI35Td1htDcHyBG7+z+ACXywq8gJUYND+1TYvRjp84QGv3M5elKtN2gIcVd7gVQ4ip/VmcIDbKdOkNolH1PS1Hi0PZhnBJHj4yToqz5PkCJSuEvaChR23sBlKhtfBinBAuPS1GimF6rM4TGx8T9AUrs3B8gxdr9AVL8nXbVGUJjMvcrSInx1lURSowr/pqqErOq7wOUWCXuD1Bi4w4ZKQ6Je8SUqJce1qHErcvTpWgXDXWG0Oh5AaQYlr6QUWLUdFWEEsO+Z0cr0c8W6gyh/QeNsLqFbQuV4gAAAABJRU5ErkJggg==\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAADf0lEQVR4nO3d32vNcRzH8b33fe0cY2c1tGaJKUwuRmorLNTMj2JJ+ZH8KEkiu1hqRilXYqzFWjKGpXHhR8kulKVQJCsubBeMrW2YNp1x9uPs7Oz4M14Xr/fjL3hefb+9+/yyD5tPpzkem25fzm6QZtH4InaDNGS/nMVukGbvys6yG6TZjvQWdoM0tIcG2A3SUBFfwG6QZp8qqtgN0iz5ehm7QZrPAWSIPM9mN0izV6Xn2Q3SbGtwh90gDW8yfrIbpGH3RAG7QZp17jrJbpBmyfdL2Q3SfA4gQ+TpbHaDNHtRfIHdIM3KgmZ2gzR04De7QRr2xQvYDdLsy/5j7AZpNvVxCbtBGmKFKXaDNGQ9nMtukGbPiq6wG6RZKZrYDdLQFfxhN0jDocmF7AZp1nvkMLtBmiU+L2Y3SMNYgbEbpCGzNY/dIM0eFV5jN0jDpe4Qu0Eavgcj7AZpOJLw8wFMNnDiALtBmsW/+hzAhHhuwG6QhlBLPrtBmrUWXGc3SEPtgC9JMuG3jbEbpOFYyv8BTDZYtYfdIM0men1fEBMSkXR2gzSkN/uaMJM1z7vFbpCGuqFxdoM0xCzBbpCG40Euu0GaDZ/ZyW6QZmM//JwwE1IZvi+ICdM3fEWMyRrn3GU3SEP93yi7QRqm/RdAhcpMvyuCyUbOb2c3SLPRQZ8DmGC+Jk+FeKO/oMFkdVn32A3S0DA5xG6QhlDK14SZUJUTYTdIs9jFLewGaRYbLmQ3SEMwOs1ukIZYw0p2gzS7EG5lN0hDY+oXu0EaclJhdoM0nMrLYDdIs/H6jewGafYv6nMAE0LDPgcw4c/VYnaDNDsXPGA3SENT0M9ukIb8lD9pzoTqBf4TZrL49XXsBmk2EvN9QUzI7PdPEBMG69eyG6RZtfkcwISboV52gzQUJnPYDdJQvdQvbGKyxO017AZpNjLq9wUxYeY3PyDAhL66DewGaVaZ5nMAE1rCPewGaViR9KcMmVBTNMxukGZTrSXsBmkWHffbUpiQ1emXRTChu7ac3SDNjqbdZzdIw/0ZPewGaVid8KcMmVBTMsBukGbJx6vYDdIsGl/EbpCGSIc/5smErstb2Q3S7KD5OWEmPAn7viAmrJ/0Z6yYULOum90gzZJtRewGaT4HkCH77Qx2gzR8rN3GbpBme83vDWVCW7iP3SANmybnsxukoab8M7tB2n93eMPeWdlx3AAAAABJRU5ErkJggg==\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAADWUlEQVR4nO3dS0uUYRjGca+ZZ2YSKwNLGEPKMLXQwgQ17UB4qKhAjCAI2gQtWrRoUeFCiAhTzEzspKWlZUHQIhLaRJbQQiw6CRUFTQspPOR4Gkdnhj7Gtbju3yf4b254n8PLg+G955MMj4vFvOwGaS45JcJukObCk6nsBmludXCc3SDNjf7KYDdIc+tyQ+wGae7bp43sBmmuoHiE3SDNDQ8WshukudLKIXaDNPe6v5zdIM1V1L5iN0hDV/Auu0EaJupq2Q3ScCPtPrtBGsIXDrEbpKFl+QN2gzTMNu5jN0hDQ6CP3SANkdZKdoM01HsfsxukIXprF7tBGs7BJoAJS91l7AZpOJ1kE8CEWF8xu0EaTiY9YjdIQ/zpNnaDNByHrYSZEO/fwm6QhqOw3VAmJF5uZjdIQ42nh90gDfHBPHaDNOz33mM3SEN8KIfdIA0V3i52gzTEPtj1dCbscJ3sBmlYGslmN0hDibvNbpCG6A+bACYU+m6yG6RhIWRfQUzI97WzG6RhftRWwkzI8V1jN0jD3F+bACZs8LeyG6RhdiKX3SANmf4WdoM0zEzZBDAh6G9mN0hDeNa+gpiQFmhkN0hDeM72gpiwKnCZ3SANUxE7D2DCimUN7AZpmIpmsRuk2QSQ2QSQ2QSQ2QSQ2QSQ2QSQ2QSQ2QSQ2V4Qme0Fkdl5AJmdB5AhPdDEbpCG6Wm7FcGEtf4r7AZpmJm0CWDCev9VdoM0zI3ZBDAh22//BzBh/o/djGNCnq+N3SANkd+2EmbCFt91doM0RH/aBDChyP6Up8LiVzsPYEKZ62A3SMPSZ5sAJux2d9gN0hB7Z19BTKj2drMbpCH+1nZDmXDQY6+pMiExsIndIA2HPb3sBmlIvMhnN0jDMTxkN0hD/NlWdoM0nLB3xKgQf1LEbpCGU/aWJBVivaXsBmk4Y+8JU2Gxcye7QRrqPDYBTFho38NukIaLPlsHMGGuuZrdIA1NybYXxISZSwfYDdLQlmrnAUz4V1/DbpCGjnS7FcGEsbNH2A3S0JNpd0OZXFXtALtBmht8Xs5ukOa2Vw2xG6S5928K2Q3SXEHJF3aDNPf9o11PZ3JZOSF2gzQ3GspgN0hza4Lj7AZpLjy5kt0gzSWnRNgN0lwi5mU3SPsPpOG4A6i64hUAAAAASUVORK5CYII=\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAACMElEQVR4nO3RsZXgNhAE0RmQIHiRKSUFqXyWAEjgHBmbQTn1jbb7vcr//vm3tlHbrG1e9zjbPNs473m2edzzaG+5R2lvtjfvN9qb97uvFW3ta++6V4tdc9XcR+wz9xHr3JF7lx3li9/WkSvzK/lG+SK/nWOXGTl3zsiR2UuOEs+xxxH93E9dT139/J66en17fZ/69vr2OnudP9cc1+h1PNfo1+jX06/er97r0+szzqcffZaft/QVPzue2E/uEevJ1XONWD2/GWvkmvGN+N7cM9Yba+T3xf5y//5+7Dwij8i6jxrljHLuvOKoUdr+f69dWpR7l3uXFnln3Bl/jt3OdV/rbu/d5n3N1ma7e2vzaqO1UUIoA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMAPsLqcSii6LB02YAAAAASUVORK5CYII=\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAACh0lEQVR4nO3R61XaAACA0TwIAvWgCAJWEtmmK3XI7lAhUCUIcnyBQJKO8f357go3/PPr921aXKfrTraJs105Ph4H0fmyrhvn8NxI3sNkXcXL1jnvfeb913xQzEfLxXi27D1+NObR8Snar6P9NjyUYd2u45uqNao7k7L9EMbTwT6bFHeT1SBdX2XrVroN07fz3fnUi8+dMojK6LuZ7KpkHYTLzml+8z4fbOe3q8VosRjNnrp/v8M8/n6O9i/hfhd9B0HQrZr9ujWuOmnZmibBw/g9nRSjyaqfFt1s08y2weTjNKpPV1HZPgZBEH82m69VYxUFy8vDrP+WDzb58DkfzRfDx+LHY1Uto8Mq+tqEh4/oFNfhdX3Rr1t3ZSerLqbtcvpzd5+uhpPiJisus5dGuqvv98dheOoGdXIKqjj5iJNN2XhOqvxqP+/v8sE6H/7Lx/PlYLa9mAXnRbwvwq9NdDiGVbOOenVrWLXvqvZDnUy7p4f7zX36HAVCGQAzAGYAzACYATADYAbADIAZADMAZgDMAJgBMANgBsAMgBkAMwBmAMwAmAEwA2AGwAyAGQAzAGYAzACYATADYAbADIAZADMAZgDMAJgBMANgBsAMgBkAMwBmAMwAmAEwA2AGwAyAGQAzAGYAzACYATADYAbADIAZADMAZgDMAJgBMANgBsAMgBkAMwBmAMwAmAEwA2AGwAyAGQAzAGYAzACYATADYAbADIAZADMAZgDMAJgBMANgBsAMgBkAMwBmAMwAmAEwA2AGwAyAGQAzAGYAzACYATADYAbADIAZADMAZgDMAJgBMANgBsAMgBkAMwBmAMwAmAEwA2AGwAyAGQAzAGYAzACYATADYP8B6+65i0vtKKUAAAAASUVORK5CYII=\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAC9klEQVR4nO3RW0vTAQDG4c3p1HSGIiYj1CASIjKCpKisCy3soBg6QQQDJewoU6f5CZRNxUKMShIMKiUPGEoGYkQlREoXpXRhKiqZqZUzdS7372P8bt7nKzzmj2dvH67qWz2xETUc8cGTXTecPGSdz/IlVF0cPeAa8Kb6bINR79yXakcS34Z8d2wmVeS9T3YNelP8tr6YYXdu7diu0eDFAl+Ss/D1nsqhtWQj8nnsS09e3XjUhGWlaCuxrPiV3flmPckc/iT+Rb3DPWmdsvwp8Sdcv9of6xzxxVms7fbu+nzPvLFoXi817Fdu9u4sG/PbgoIeJXY05DcubayZ/dcsccXOrvAbn40Qc+DB3seNjqbV3wGz6VZ4TFF5Z3DpV7Nh8rXsb2vMbd5ashpB5dG2gooOU8mU5W9grflQ652cFmMh2gitjA/Jq+j8d3nWuhxYuXvkXnPWQ8uc3YioTghkuzo2CxfC5wI/mo43389stc4kb0dX71s/5+pcd/zc8c2YbTzd1JbeHjqdsh1bc3A5vbJrLWclctw86cloeHbyadj0MX98Tep8mqvbe/6XbdQ6UZ/p7kntCZ05tWWvSZs86ur1pnujRsI+eS64B1L6Q2fPbO2uyfgSZBKUAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgCkApgCYAmAKgP0HFJDAi8s+uCQAAAAASUVORK5CYII=\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAC6ElEQVR4nO3RW2gNAACH8XPOds4oopSSlMu5IE3INdfIyCWRyDWXJHJp5RK1Epotl5nNZi7D2E5EEeWBiEhC5BK5FHJL2Gwzs53lYc+ev5fv9/B/+r99gftZm++O3/p4evbNkduez15zdXDuqwUrL2Xuebd86bnEgY+rF1Z2L/2aPedYl6Pft8w82OlEzbZpe9udqsublJtR2VAwISct2Vg6elMw2VQ+Ym0g2Vw5ZEWgKnV+4KJgZepy5tzgqZZrfWeETqZu9Z6cdjx1Lz4+7Vjzo9jI9MNNz6JD0w81vo4OCJf8eRfrFy76/SkeD++v/xrvGSmo+57oFtlbW53oEtldUxfvlJFXUx/rmLGruiHavk1udWOP/23rp/XfOSP/169E18ie2h+J7pF99d8S0cj+31/ivcOFDe9jmeHixjexQeGSvy+iI9LLmp5Ex6QfaX4Qm5hWnrqTmBo60XKjz6xQRcuVfvODp1MX+y8LVKXODloVSDZXDMsOJv8eHrUllPxTNG57uKp+98T8tqdrd04p7FDxM2dGWefybxtnn+xW9nndvDO9ij+sXHyhb8HbJcuvDMx7OXfV9eE7ns5cf3tszsMpG+5nbQ4FhDIAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzACwf5/+uOWhzMtUAAAAAElFTkSuQmCC\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAACK0lEQVR4nO3d0bGbQBAFUZBawHMWzj85f9mAJHAY/bF9IpiqWzMLCyzzn+P3FM/DLmB0BSBjup52DUNjvma7hqEx3QVg4vEpABPz1y5hbMyf265haDzeBWBiftsljK0OkNUBMuajy1AT89l2kImpAFTMe5txJu6zAExMO3YNQ+M+CsDEVQAqrn8vu4ahcZ11gInvvtg1DI3v3ggy8SkAFZ+jAEwFIONdACreR1dBpjpAxtl9gKoRJONsBKk4jtWuYWgcdYCKszVAxV4HqBpBMvYeyKjYeyas4l/vBanYP72aaGLvGzEVR99nqPhrVzA4zqkWMLHPl13D0ApAxlkAKo6p61BTHSDjnArAxNFxKSrerQEqPo0gVYuwjHcdoGoEyfg2glTdB8j4zm1Hm3ogLCsAWQHICkBWADKenZ6uYqkJVDzvAjBBHaDiVQAqlkaQqhEk4zX1gYaJ9S4AU/cBshZhGWtrgKoOkLEVgKoAZCxT29EmftkVDI61BlCxdRWqYqM340z8LH2gYWJbP3YNQ2NbCsDEuvY7VRNbAahY1tOuYWiNIBnretg1DI2lDlDxag1QsWwFYOLVCFI1gmR1gAwKQFUAMtgKwMSzAFQ8uw9Q8eiBjIrHTyPIxKNnwirmAlAxbQVgYu69IBX3VgAmpqVXE03cBaDiXjs10cTdz1RVXK++0DDVAbI6QMZNAZjosBQXF12Gmpg6PV3F/SgAE9OjzThTJ3XICkD2HwykofZQoJZIAAAAAElFTkSuQmCC\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAC6UlEQVR4nO3dWVIbQRBFUbV0hYHAmBkMkgX7XxTIEvNoTIBBQ3sZ9yPfWcGLKKk7uiozq3n5POuEh2aOnaE0eGvsDKWx8rC0M5RG73LVzlAa8+kPO0NpvI/37Ayl8TzJApi4nxzaGUrjapoFMDG+3LEzlMbFa9/OUBoX3ZmdoTSuux92htK4677bGUrjqflnZyiNRdPaGUpjre3ZGUpjZ5ndUBMH7bqdoTROFmt2htIYdfIOMHG2lw8xE8OTBztDafwc3NoZSmN/eGdnKI0fv/IIMrH269HOUBrN8NXOUBqLoxzImPjazYeYifn6ws5QGp1uFsBEM0tVhImVl5yImejnQ1hFc5UDGROz8badoTTefqc83cTTdN/OUBp3kwM7Q2lM0x+g4uJqy85QGuf/unaG0pj2Pu0MpXGV8nQVD03qgky8dPMIMuUNLGNjmfMAE7tt+gNMHC2zHW1imP4AFSMysszE2eFfO0NpDAb3dobSOBre2BlKYzf/ABWboyyAiW/DZztDaXQGb3aG0pgdZnKuidl2NkRNLNa+7Ayl5ecvo/exYmcojf5zXsIm+rd5CpnoTDfsDKXxOUl/gInXccrTTTymP0DFzeTIzlAa0/wDVJzffrczlMZ4nkZtE5NeqqNN3KQ/QMVjxtereMsVJqrsxMnYWn6zM5TGXpvqaBPH6Q9QMVjkEWTibDVHkiZOj1/sDKUxyPh6FQcZX69iJ/0BKjZyf4CK/ij9ASba42xHm5hlZp+Kr600apto+zkPMNFZ5gYNE7xnAUysPOYGDRO9m0zMMtFONu0MpfHxe9fOUBp/cn+AivuMr1dxnfH1KiaXeQSZOH9IXZCJcScfYiYu0x+g4q7JgYyJp276A0x8NHkHmOi1OREzsZPx9SoOUp6u4ucyH2ImTnOFiYrTzVRFmBidPNkZSuM4/QEq9odZABPbw5Snm1gfZQFMMPhjZyiNxUl2Q0187WVeh4n5RkaWmWiZ2xlK+w8g3roCyX82/gAAAABJRU5ErkJggg==\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAADeElEQVR4nO3dy0tUcRjGcd+Zx3GsxuwulqF2Q5AKCowECzKCEiJwU0FQLUSCLlCkbWzVPWSIAi0UicogIihadFkULapFgRBGC5UawqLoiE5za2b6M57F837+gu/u8BzO73csyNSVOB5UvJrNbpBm77efYTdIsz2h2+wGaXgRSbAbpKEts5zdIM0+tp1kN0iz/OsGdoM08x3AhdjzCnaDNHvT0sNukGa7wkPsBml4Xfqd3SAN7eladoM0+9R+lN0gzfJv17AbpPkOIEPs8Xx2gzR72XSO3SDNWsMD7AZpeFf6g90gDfvS/hBmsi/7O9kN0uzfh1XsBmmYaSiyG6RhzsMF7AZp9nT9ZXaDNGvBLXaDNIyEf7EbpOFgppbdIM3GDx1mN0iz3KeV7AZp+Ftr7AZpKL+/mN0gzR41xNkN0mwT+tkN0jAWDtgN0nAo6+cDmCzReYDdIM2yX3wHMCFVHWI3SEPZUDW7QZoN199gN0jDxW/+KoIJk5ZkN0hDR2EZu0GaTR7fy26QZulx/y6ICbl5vgOYEB6sYTdIs6GlN9kN0nD5Z4bdIA1TlmU3SMORUBW7QZr9Ot3ObpBmqYTvACbky30HMKHYv4LdIM36Fg2yG6ShN5hmN0hD1grsBmk4FvUzYkwW9OxmN0iz5I/V7AZpPgLIkLvhN+cyWbzCr69nQjz9m90gDZGiPwaYcKIyxm6QZtPnd7IbpNnMb783lAnhlL8LYkLy2jp2gzS7EL3LbpCG64VJdoM0zC1G2A3ScGpxGbtBmv3tbWU3SLPpwHcAE0r/+A5gQhDfyG6QZmcxzG6Qhr6Q/86WCVVF/6U5E07X+OXdTJa+vpXdIM2mZvy7ICaUf/cdwISfvZvZDdKsy3wHMGEg8pXdIA31+Up2gzR0rUqxG6RZdqCZ3SDNppJ+TpgJsyb8VQQTEle3sBuk2YkS3wFMGCybYDdIw9r8QnaDNHQ1/mE3SLN/d5rYDdIsSPn/A5gwZ9Qv72bC2FX/NJHJOnwHUOFudJzdIA1NuSXsBmno3ugnZJgs/2ADu0GaBZk6doM0xD76GTEmfL6yg90gzQ6W3GM3SMOD6AS7QRpacv4rQyZ0N4+xG6RZ/olf1sHkO4AMsXdRdoM0jFzaxW6QZnvtDrtBGp6U+fkAJrRm/Xe2TOjeNspukGaFZ43sBmm+A8j+A7Kfwqe2DtNkAAAAAElFTkSuQmCC\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAADD0lEQVR4nO3dS0uUYRjGca98ZkYQRReJErNQzIwksYKMDkgIQkER1KJlq9atalc7RcRMDOmAEdEBahVtbBFSQS5KKJAiIhe60Cgc02k8NNHH+C+u+/cJ/psb3pn3fe5HhY3misBRTVU/3WBNMQGsmABYTAAsJgCmQqmVbrCmutwA3WBNK8WddIM1NeQG6QZrWllroxusqSk7RDdY02phF91gTfnsMN1gTWu/YgJIasmO0A3WVFyKpyCS2jOjdIM1lRbilzBJHZkxusGa1udiAkjqyozTDda0+TXeB5DUnW7TDda0NRsTQNKxdJdusKa/M/EURFJv5QTdYE3l6fg3lKSTlffpBmsqv95NN1jTmW0P6AZr+veyg26wpvN6SDdYU/lFJ91gTRcqHtMN1lR+tp9usKaLFU/oBmv6+/Ag3WBNl2ICUNqcOEw3WNMVxQSQtH6zh26wpmspJoCkP9d76QZrGqh6RDdY02r/CbrBmm7UxvsAkgpXT9MN1nRr+z26wZp+Xj5LN1jT/R136AZrqe/cK7rBWpp6fpRusJaO9E3TDdbS+6l4JUlKnYc+0Q3W0ueZdrrBWmrd851usJbmv+XpBmupMb9IN1hLyz/q6QZrqbq2SDdYS1sbWbrB2jY6wF3K5DbpBmup+LuabrCW6huW6QZraXG+kW6wlvKt83SDtfRttoVusJZ27/tCN1hLH9/tpRuspQM9H+gGa+ntZDfdYC31nHpDN1hLk0+P0w3W4tNEWHycC4vP02FxQAMWR5RgcUgPFsdUYXFQGxarCmCxrAMW62pgsbAJFivLYLG0DxZrK2GxuBUWq4thsbwbFuvrYXGBAyyuMIHFJT6wuMYKFhe5weIqQ1hc5gmL62xhcaEzLK40h6krM043WNP6XDwFkdSRGaMbrKm0EBNAUntmlG6wpuJSG91gTS3ZEbrBmtZ+xb+hJOWzw3SDNa0WYgJIasoO0Q3WtLIWT0EkNeQG6QZrWinGL2GS6nIDdIM1FUrxPoCkmqp+usGaChvNdIO1mABYTAAsJgAWEwD7D9njttGUHOdYAAAAAElFTkSuQmCC\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAACK0lEQVR4nO3RMY7dMAwAUUq2JG9ukftfLlXWkmwpxd8mJ5hmHgiCJYFJf/rv+FhHWil2yk9Kb6Rn57nTjM9OPaWRY+R0H3sccZ+7n6uf67uscb53fe/y3OXp5ell9jJ7nb2Mu85eRy+9t97L6PXupfdyj/Pu5/c47iffb/QdfyNG7DutO62RVo93pDVi9fTOeJ9YI60Z64n1pjVivWnH/46dauRj5zNyiVx/jqPto0auO7c46s7XztfONdKviJbiOuI611d9r/Zc9WltXm3WNlqbrfXaZmmjXqO0WdoobZ6fueZxzeMauT75a+b2pPbE9aT67uuNunZdu+1dYpX0s8+0j1jnjrR33pHfz9s5hDIAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzACwf8pEoQr48yW8AAAAAElFTkSuQmCC\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAChElEQVR4nO3RWVLiUABA0ZeEKFC2LcrgkIjuf1GKCSiDY1uAhiS9jPtzzxZO9PFzHx06ne/oaNMk8+6h/LudDd+L4bqYLMrJbH7++JU+xtVzvFvF27doX0dtr03Om+647d/UvWlI7oe7/GZzlS1H+erv7aZ3+xrlX/Vl9XuRHPp1iOuoSo8+2nQVokW/mg2+n4Zv5WhVjMty8rg4e9jHZfKziLebaPcR/4QQTpr0ou1eNv287k070f3kX5atL/OXi2x9Ol0f5+8h+64mTTWI695vCCHZHaXvTbqMQ3nyUwy+ZqPXcvRSXJbl6GH5Z3Zoi2T3Em9fo/13XIUQzprjYdu7bvpZfXzfbe6uP7JsOc5X59n65HaTTt/b6201Dr9nUZtWoUk62+TotU5e0rY43T1dfD4N18X4uZwU8+HDpjcL9TzZraLtW7zfRXXSRudtd9z0r5reXZPenVbTm7frbDnKl4N8059uOtlnfbP/HcaHk7btHOIglAEwA2AGwAyAGQAzAGYAzACYATADYAbADIAZADMAZgDMAJgBMANgBsAMgBkAMwBmAMwAmAEwA2AGwAyAGQAzAGYAzACYATADYAbADIAZADMAZgDMAJgBMANgBsAMgBkAMwBmAMwAmAEwA2AGwAyAGQAzAGYAzACYATADYAbADIAZADMAZgDMAJgBMANgBsAMgBkAMwBmAMwAmAEwA2AGwAyAGQAzAGYAzACYATADYAbADIAZADMAZgDMAJgBMANgBsAMgBkAMwBmAMwAmAEwA2AGwAyAGQAzAGYAzACYATADYAbADIAZADMAZgDMAJgBMANgBsAMgBkAMwBmAMwAmAEwA2AGwAyAGQAzAPYf40W5CjJYBR8AAAAASUVORK5CYII=\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAC9klEQVR4nO3R20oTAADHYbfh3NKZZpiIihpBQhiCIBZkkBZoBzEPYBeiRElZYjrTJ9CcJpaFRSheJKhL7KBQGmRQIgiSBAaRiYqYZC6n6Tbceozfzf97hc/g8iSFT4ROOy42vU8ZNy+f9yTU582k1o+4M3ZtY+GTLflNn5I/Bq8U7ibWFU4erXvrTvPaXh/44LjcNB0zFfyrdDep9spEcu34VkogbCjqnaOw+WvkrOl3uSfxdsVYXO3Ev0SDtT961FHc/N06b3JVeBOqKkejaz7vxBpDemOH24rvLRlWDdvX/XGVVS8jqqd9kUZTT/xga4ljzfPX4L1pjLlaPRR668ue1Rh4erjvflG7y+01+KstUWU1zuAbc0FBQb7HKb3thR276+aAsSbCVnpn0HDth2nHv/3w+LOOgkf+1f0Bsz06pKiuf698KXjD7+pI7+q89MS4HBMIvRsfyK8d8JStWFf8a+0nOrtyu82LyXsRDUd2cu0DOyVr+xYCy21ZD7pzekIWUvcONhzbOGt3bhWsh80Z5tuy255n9Vl+ZvgONaavnra/cF/4Y5sxf2s91+rMdFoWTvliG0/OZ9YPu3M2bVOW2Za8lpG0NyGL2d64xjNz6fZXm1nbxiBBKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmAJgCYAqAKQCmANh/3MnACp5N56UAAAAASUVORK5CYII=\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAC2klEQVR4nO3RvSv8AQDH8Tv3ZPnJQkkG1z24ukKGU0qGs6CkLFIKg24RpcjCxIU85SEPna7r+A4GgwzKIoMMTLfccssNFHVy587x1W/+/f6B9/J5DZ8/4PO25L4a/1Qu/7NFT7Ur+l7w1rpW3vO+OufaR87f4FzPv/ndzs3Ci6/JsV3MeoOOnVLG2+rYL6c97fbD75Sn03788+gN22Lmvb/XFjdvAwMVid/r4JA1aV42j1rOzPO2CYvxkwxNW4xyrGPOapR2uxbtxudGOFp5+rHcs1WVyC30H9ScvM4OxuuPnqeGDPdeNjJyEdjKjI1ftaymhyM3oaXU4ORd58JT38xD9/xD9/xT38xd50JqcPImtJQejly1rGbGxi8CW9nIiOHee54aitcfvc4OHtSc5Bb6t6oSH8s90crTz43wot0o7XbNWY1yrGPaYvwkQxMWwzxvG7WcmZfNQ9bk73VwoCJh3gZ6bXHz3h+2xX4evZ324++Up91+WE57Wh37pYw36NgpZr1Nju3Ci8/t3My/+Ruc6x85f51z7T3vq3WtvBe81a5oruj5/+qvxgqLoBQApgAwBYApAEwBYAoAUwCYAsAUAKYAMAWAKQBMAWAKAFMAmALAFACmADAFgCkATAFgCgBTAJgCwBQApgAwBYApAEwBYAoAUwCYAsAUAKYAMAWAKQBMAWAKAFMAmALAFACmADAFgCkATAFgCgBTAJgCwBQApgAwBYApAEwBYAoAUwCYAsAUAKYAMAWAKQBMAWAKAFMAmALAFACmADAFgCkATAFgCgBTAJgCwBQApgAwBYApAEwBYAoAUwCYAsAUAKYAMAWAKQBMAWAKAFMAmALAFACmADAFgCkATAFgCgBTAJgCwBQApgAwBYApAEwBYAoAUwCYAsAUAKYAMAWAKQBMAWAKAFMAmALAFACmADAFgCkATAFgCgBTAJgCwBQApgAwBYApAEwBYAoA+wsIi7ljmSQvewAAAABJRU5ErkJggg==\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAABuUlEQVR4nO3aQa7gNgxEQdvqGcwm586lk+85Ri386gQEiCYl2fc/f/694uz3++gaPm2/36Nr+LTtKgHSftUAantvXcOnNYKwGoDt1ACqHYCVAGy7SoC0UwOorbcgqgRgNQDb6RhKdQrCGkFYIwgrAVgNwDZdwce1A7D1T4TVDsBKAFYDsJ0mEFUCsBqANYKwGoDt3K+u4dNKALbzlACpBmA7/RZE7SkBVCMI23l+dA2ftnNKgNQOwBpBWEsY23NKgFQCsD3tAKoljO3pHkA1grBGEFYCsO4BWAnA2gFYr6FYOwBrB2A1AKsBWDsAKwFYx1Bsz/7XNXxaIwirAVgNwPokie0uAVQjCKsB2O52AFUCsBqA7e41lOoxDusegPVBBtvdCKJKANYOwGoA1nM0VgKwbsJYN2GsBGDtAKwEYH0RwxpBWCMIqwFYz9FYCcBqALarYyjVMRRrBGG7agDVUwRWArB2ANYxFGsEYSUAawdgJQBrB2AlANtbA6gSgO1aDZAaQdiuo0v4thKA7Zou4dv2NoKovefWNXza3kYQ1QjC9q4RJLUDsP00gqiWMLa3DzLU3v2na/i0v30YbAheKpYKAAAAAElFTkSuQmCC\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAB00lEQVR4nO3RMY7dQAwEUZI9XjjxuX1pe+hAK/jfoJJ6gdDgKKv+9fP3187X5tT8qDnbp+bUpOZsp+ZUp/rspDrV2T7veC6nKtupSj3fStf/0ZXedGU2s5ma73GTnWfMTm5mZ27mTt4xd3Lfvc+ej/vMzvn7cdx+d2ffcWe2505uz05uz+335+/X55jb77Fz+7nn1nus3M7We6zn6WNvtrJ1nlGbrVOb2vQ+4/Smb2pPbe6eP1NCGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmANg/vMBrChrbL84AAAAASUVORK5CYII=\"/></td></tr></table>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "num_frequencies = 4\n",
    "min_freq_exp = 0\n",
    "max_freq_exp = 6\n",
    "include_input = True\n",
    "resolution = 128\n",
    "\n",
    "encoder = encoding.NeRFEncoding(\n",
    "    in_dim=2,\n",
    "    num_frequencies=num_frequencies,\n",
    "    min_freq_exp=min_freq_exp,\n",
    "    max_freq_exp=max_freq_exp,\n",
    "    include_input=include_input,\n",
    ")\n",
    "\n",
    "x_samples = torch.linspace(0, 1, resolution)\n",
    "grid = torch.stack(torch.meshgrid([x_samples, x_samples], indexing=\"ij\"), dim=-1)\n",
    "\n",
    "encoded_values = encoder(grid)\n",
    "\n",
    "print(\"Input Values:\")\n",
    "media.show_images(torch.moveaxis(grid, 2, 0), cmap=\"plasma\", border=True)\n",
    "print(\"Encoded Values:\")\n",
    "media.show_images(torch.moveaxis(encoded_values, 2, 0), cmap=\"plasma\", border=True)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Random Fourier Feature (RFF) Encoding\n",
    "This encoding assumes the inputs are between zero and one and can opperate on any dimensional input."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Input Values:\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<table class=\"show_images\" style=\"border-spacing:0px;\"><tr><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAABuUlEQVR4nO3aQa7gNgxEQdvqGcwm586lk+85Ri386gQEiCYl2fc/f/694uz3++gaPm2/36Nr+LTtKgHSftUAantvXcOnNYKwGoDt1ACqHYCVAGy7SoC0UwOorbcgqgRgNQDb6RhKdQrCGkFYIwgrAVgNwDZdwce1A7D1T4TVDsBKAFYDsJ0mEFUCsBqANYKwGoDt3K+u4dNKALbzlACpBmA7/RZE7SkBVCMI23l+dA2ftnNKgNQOwBpBWEsY23NKgFQCsD3tAKoljO3pHkA1grBGEFYCsO4BWAnA2gFYr6FYOwBrB2A1AKsBWDsAKwFYx1Bsz/7XNXxaIwirAVgNwPokie0uAVQjCKsB2O52AFUCsBqA7e41lOoxDusegPVBBtvdCKJKANYOwGoA1nM0VgKwbsJYN2GsBGDtAKwEYH0RwxpBWCMIqwFYz9FYCcBqALarYyjVMRRrBGG7agDVUwRWArB2ANYxFGsEYSUAawdgJQBrB2AlANtbA6gSgO1aDZAaQdiuo0v4thKA7Zou4dv2NoKovefWNXza3kYQ1QjC9q4RJLUDsP00gqiWMLa3DzLU3v2na/i0v30YbAheKpYKAAAAAElFTkSuQmCC\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAB00lEQVR4nO3RMY7dQAwEUZI9XjjxuX1pe+hAK/jfoJJ6gdDgKKv+9fP3187X5tT8qDnbp+bUpOZsp+ZUp/rspDrV2T7veC6nKtupSj3fStf/0ZXedGU2s5ma73GTnWfMTm5mZ27mTt4xd3Lfvc+ej/vMzvn7cdx+d2ffcWe2505uz05uz+335+/X55jb77Fz+7nn1nus3M7We6zn6WNvtrJ1nlGbrVOb2vQ+4/Smb2pPbe6eP1NCGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmANg/vMBrChrbL84AAAAASUVORK5CYII=\"/></td></tr></table>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Encoded Values:\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<table class=\"show_images\" style=\"border-spacing:0px;\"><tr><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td></tr></table>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "num_frequencies = 8\n",
    "scale = 10\n",
    "resolution = 128\n",
    "\n",
    "encoder = encoding.RFFEncoding(in_dim=2, num_frequencies=num_frequencies, scale=scale)\n",
    "\n",
    "x_samples = torch.linspace(0, 1, resolution)\n",
    "grid = torch.stack(torch.meshgrid([x_samples, x_samples], indexing=\"ij\"), dim=-1)\n",
    "\n",
    "encoded_values = encoder(grid)\n",
    "\n",
    "print(\"Input Values:\")\n",
    "media.show_images(torch.moveaxis(grid, 2, 0), cmap=\"plasma\", border=True)\n",
    "print(\"Encoded Values:\")\n",
    "media.show_images(torch.moveaxis(encoded_values, 2, 0), cmap=\"plasma\", border=True)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Hash Encoding\n",
    "The hash incoding was originally introduced in Instant-NGP. The encoding is optimized during training. This is a visualization of the initialization."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Input Values:\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<table class=\"show_images\" style=\"border-spacing:0px;\"><tr><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAABL0lEQVR4nO3RMQ0AIADAMODiQTe2kdGDVcGSzbPviLN0wO8agDUAawDWAKwBWAOwBmANwBqANQBrANYArAFYA7AGYA3AGoA1AGsA1gCsAVgDsAZgDcAagDUAawDWAKwBWAOwBmANwBqANQBrANYArAFYA7AGYA3AGoA1AGsA1gCsAVgDsAZgDcAagDUAawDWAKwBWAOwBmANwBqANQBrANYArAFYA7AGYA3AGoA1AGsA1gCsAVgDsAZgDcAagDUAawDWAKwBWAOwBmANwBqANQBrANYArAFYA7AGYA3AGoA1AGsA1gCsAVgDsAZgDcAagDUAawDWAKwBWAOwBmANwBqANQBrANYArAFYA7AGYA3AGoA1AGsA1gCsAVgDsAZgDcAagDUAawDWAKwBWAOwB1lYAZykwLVxAAAAAElFTkSuQmCC\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAABuUlEQVR4nO3aQa7gNgxEQdvqGcwm586lk+85Ri386gQEiCYl2fc/f/694uz3++gaPm2/36Nr+LTtKgHSftUAantvXcOnNYKwGoDt1ACqHYCVAGy7SoC0UwOorbcgqgRgNQDb6RhKdQrCGkFYIwgrAVgNwDZdwce1A7D1T4TVDsBKAFYDsJ0mEFUCsBqANYKwGoDt3K+u4dNKALbzlACpBmA7/RZE7SkBVCMI23l+dA2ftnNKgNQOwBpBWEsY23NKgFQCsD3tAKoljO3pHkA1grBGEFYCsO4BWAnA2gFYr6FYOwBrB2A1AKsBWDsAKwFYx1Bsz/7XNXxaIwirAVgNwPokie0uAVQjCKsB2O52AFUCsBqA7e41lOoxDusegPVBBtvdCKJKANYOwGoA1nM0VgKwbsJYN2GsBGDtAKwEYH0RwxpBWCMIqwFYz9FYCcBqALarYyjVMRRrBGG7agDVUwRWArB2ANYxFGsEYSUAawdgJQBrB2AlANtbA6gSgO1aDZAaQdiuo0v4thKA7Zou4dv2NoKovefWNXza3kYQ1QjC9q4RJLUDsP00gqiWMLa3DzLU3v2na/i0v30YbAheKpYKAAAAAElFTkSuQmCC\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAB00lEQVR4nO3RMY7dQAwEUZI9XjjxuX1pe+hAK/jfoJJ6gdDgKKv+9fP3187X5tT8qDnbp+bUpOZsp+ZUp/rspDrV2T7veC6nKtupSj3fStf/0ZXedGU2s5ma73GTnWfMTm5mZ27mTt4xd3Lfvc+ej/vMzvn7cdx+d2ffcWe2505uz05uz+335+/X55jb77Fz+7nn1nus3M7We6zn6WNvtrJ1nlGbrVOb2vQ+4/Smb2pPbe6eP1NCGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmAJgBYAaAGQBmANg/vMBrChrbL84AAAAASUVORK5CYII=\"/></td></tr></table>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Encoded Values:\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<table class=\"show_images\" style=\"border-spacing:0px;\"><tr><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td><td style=\"padding:1px;\"><img width=\"128\" height=\"128\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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\"/></td></tr></table>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "num_levels = 8\n",
    "min_res = 2\n",
    "max_res = 128\n",
    "hash_table_size = 2**4  # Typically much larger tables are used\n",
    "\n",
    "resolution = 128\n",
    "slice = 0\n",
    "\n",
    "# Fixing features_per_level to 3 for easy RGB visualization. Typical value is 2 in networks\n",
    "features_per_level = 3\n",
    "\n",
    "encoder = encoding.HashEncoding(\n",
    "    num_levels=num_levels,\n",
    "    min_res=min_res,\n",
    "    max_res=max_res,\n",
    "    hash_table_size=hash_table_size,\n",
    "    features_per_level=features_per_level,\n",
    "    hash_init_scale=0.001,\n",
    ")\n",
    "\n",
    "x_samples = torch.linspace(0, 1, resolution)\n",
    "grid = torch.stack(torch.meshgrid([x_samples, x_samples, x_samples], indexing=\"ij\"), dim=-1)\n",
    "\n",
    "encoded_values = encoder(grid)\n",
    "\n",
    "grid_slice = grid[slice, ...]\n",
    "encoded_values_slice = encoded_values[slice, ...]\n",
    "\n",
    "print(\"Input Values:\")\n",
    "media.show_images(torch.moveaxis(grid_slice, 2, 0), cmap=\"plasma\", border=True)\n",
    "\n",
    "print(\"Encoded Values:\")\n",
    "encoded_images = encoded_values_slice.view(resolution, resolution, num_levels, 3)\n",
    "encoded_images = torch.moveaxis(encoded_images, 2, 0)\n",
    "encoded_images -= torch.min(encoded_images)\n",
    "encoded_images /= torch.max(encoded_images)\n",
    "media.show_images(encoded_images.detach().numpy(), cmap=\"plasma\", border=True)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "interpreter": {
   "hash": "024fe9dea615e4e34961116ff517c7e9f5910440e3d2c8eb4560299f11aaf2e7"
  },
  "kernelspec": {
   "display_name": "Python 3.9.11 ('mattport')",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.11"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}

notebooks/visualize_dataset.ipynb METASEP
{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "3c00934d-0f14-4c1a-a563-94d627b377e5",
   "metadata": {},
   "source": [
    "## Notebook to visualize a dataset\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b15dfdc1-5054-40bc-9631-4c7426eecfd9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from notebook_imports import *\n",
    "\n",
    "import torch\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "from mattport.nerf.dataset.image_dataset import ImageDataset, collate_batch\n",
    "from mattport.nerf.dataset.collate import CollateIterDataset, collate_batch_size_one\n",
    "from mattport.nerf.dataset.utils import get_dataset_inputs_dict\n",
    "from mattport.nerf.field_modules.ray_generator import RayGenerator\n",
    "from mattport.structures.rays import RayBundle\n",
    "from mattport.utils.io import get_absolute_path\n",
    "from mattport.viewer.plotly import get_line_segments_from_lines\n",
    "\n",
    "from hydra import compose, initialize\n",
    "from omegaconf import open_dict\n",
    "import pprint\n",
    "from tqdm import tqdm\n",
    "\n",
    "import random\n",
    "from tqdm import tqdm"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "001bdf3b-60a0-4bb2-9f88-ae36f2036c80",
   "metadata": {},
   "outputs": [],
   "source": [
    "with initialize(config_path=\"../configs\"):\n",
    "    cfg = compose(config_name=\"default.yaml\")\n",
    "    cfg.dataset.downscale_factor = 4\n",
    "    \n",
    "cfg.dataset.data_directory = get_absolute_path(cfg.dataset.data_directory)\n",
    "dataset_inputs = get_dataset_inputs(**cfg.dataset)[\"train\"]\n",
    "image_dataset = ImageDataset(\n",
    "            image_filenames=dataset_inputs.image_filenames, downscale_factor=dataset_inputs.downscale_factor\n",
    "        )\n",
    "iter_dataset = CollateIterDataset(\n",
    "    image_dataset,\n",
    "    collate_fn=lambda batch_list: collate_batch(\n",
    "        batch_list, cfg.dataloader.num_rays_per_batch, keep_full_image=True\n",
    "    ),\n",
    "    num_samples_to_collate=cfg.dataloader.num_images_to_sample_from,\n",
    "    num_times_to_repeat=cfg.dataloader.num_times_to_repeat_images,\n",
    ")\n",
    "dataloader = DataLoader(\n",
    "    iter_dataset,\n",
    "    batch_size=1,\n",
    "    num_workers=cfg.dataloader.num_workers,\n",
    "    collate_fn=collate_batch_size_one,\n",
    "    pin_memory=True,\n",
    ")\n",
    "dataloader_iter = iter(dataloader)\n",
    "\n",
    "ray_generator = RayGenerator(dataset_inputs.intrinsics, dataset_inputs.camera_to_world)\n",
    "\n",
    "num_batches = 10\n",
    "for _ in tqdm(range(num_batches)):\n",
    "    batch = next(dataloader_iter)\n",
    "    ray_bundle = ray_generator.forward(batch[\"indices\"])\n",
    "    break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19b85061-a1b0-458b-9d00-96cde6f37dca",
   "metadata": {},
   "outputs": [],
   "source": [
    "media.show_image(image_dataset.get_image(10))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b90f4a78-4b8b-4615-a127-7d7579874765",
   "metadata": {},
   "outputs": [],
   "source": [
    "def show_batch(batch):\n",
    "    # set the color of the sampled rays\n",
    "    c, y, x = [i.flatten() for i in torch.split(batch[\"local_indices\"], 1, dim=-1)]\n",
    "    batch[\"image\"][c,y,x] = 1.0\n",
    "\n",
    "    # batch[\"image\"] is num_images, h, w, 3\n",
    "    images = torch.split(batch[\"image\"], 1, dim=0)\n",
    "    image_list = [image[0] for image in images]\n",
    "    image = torch.cat(image_list, dim=1) # cat along the width dimension\n",
    "\n",
    "    # the white pixels are rays\n",
    "    media.show_image((image*255).to(torch.uint8))\n",
    "\n",
    "def sample_and_show_batch():\n",
    "    batch = next(iter(dataloader))\n",
    "    show_batch(batch)\n",
    "    return batch"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "09e625f7-fa1f-4b3f-acd4-bddfcdf7cd9f",
   "metadata": {},
   "outputs": [],
   "source": [
    "batch = sample_and_show_batch()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fd8cf48a-823e-4395-99ca-eda4c2026440",
   "metadata": {},
   "outputs": [],
   "source": [
    "ray_indices = batch[\"indices\"]\n",
    "ray_bundle = ray_generator(ray_indices)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "646107c0-f979-408a-941f-98b2ad3e07f7",
   "metadata": {},
   "outputs": [],
   "source": [
    "skip = 1\n",
    "size = 8\n",
    "data = []\n",
    "data += [go.Scatter3d(\n",
    "    x=ray_generator.camera_to_world[::skip,0,3],\n",
    "    y=ray_generator.camera_to_world[::skip,1,3],\n",
    "    z=ray_generator.camera_to_world[::skip,2,3],\n",
    "    mode='markers',\n",
    "    name=\"origins\",\n",
    "    marker=dict(color='rgba(0, 0, 0, 1)', size=size)\n",
    ")]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c73ba9bd-2902-4c46-b592-bc87ac8c71f6",
   "metadata": {},
   "outputs": [],
   "source": [
    "near_plane = 1.0\n",
    "far_plane = 6.0\n",
    "lines = torch.stack([ray_bundle.origins + ray_bundle.directions * near_plane,\n",
    "                     ray_bundle.origins + ray_bundle.directions * far_plane\n",
    "                    ], dim=1).tolist() # (num_rays, 2, 3)\n",
    "lines = torch.tensor(random.sample(lines, k=100))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "991df6a2-efe3-4412-ba41-9a244e2e90cc",
   "metadata": {},
   "outputs": [],
   "source": [
    "data += get_line_segments_from_lines(lines)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0b2404d8-ca9e-4630-9f67-7c68d19458cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "layout = go.Layout(\n",
    "    autosize=False,\n",
    "    width=1000,\n",
    "    height=1000,\n",
    "    margin=go.layout.Margin(\n",
    "        l=50,\n",
    "        r=50,\n",
    "        b=100,\n",
    "        t=100,\n",
    "        pad=4\n",
    "    ),\n",
    "    scene=go.layout.Scene(\n",
    "        aspectmode=\"data\",\n",
    "        camera=dict(\n",
    "            up=dict(x=0, y=0, z=1),\n",
    "            center=dict(x=0, y=0, z=0),\n",
    "            eye=dict(x=1.25, y=1.25, z=1.25)\n",
    "        )\n",
    "    )\n",
    ")\n",
    "fig = go.Figure(data=data, layout=layout)\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7aa506fe-66a1-4284-ade4-8c90fde5476a",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f259edb0-4126-4930-99f4-b5574dbb2eea",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig.write_image(\"temp.png\")\n",
    "plotly_image = imageio.imread(\"temp.png\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d1a11772-4692-4800-b446-cfdbe4ab92fa",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "722f5370-3b0f-4ad1-87a1-df479ae85b60",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "mattport",
   "language": "python",
   "name": "mattport"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}

notebooks/README.md METASEP

external/README.md METASEP

environment/run_jupyter.sh METASEP
python -m jupyter lab --ip 0.0.0.0 --port 9999 --no-browser --config environments/jupyter.py
environment/requirements.txt METASEP
black
hydra-colorlog
hydra-core
imageio==2.16.1
ipywidgets>=7.6
jupyterlab
kaleido
mediapy
plotly==5.7.0
pylint==2.13.4
pytest
tensorboard==2.8.0
torch==1.11.0
torchtyping
torchvision==0.12.0
tqdm
configs/instant_ngp.yaml METASEP
defaults:
  # This makes the logging module logs colorful
  - data: default_data.yaml
  - graph: instant_ngp_graph.yaml
  - logging: default_logging.yaml
  - override hydra/job_logging: colorlog
  - override hydra/hydra_logging: colorlog

hydra:
  run:
    # Configure output dir of each experiment programmatically from the arguments
    # Example "outputs/mnist/classifier/baseline/2021-03-10-141516"
    dir: outputs/${experiment_name}/${method_name}/${now:%Y-%m-%d_%H%M%S}

experiment_name: blender_lego
method_name: instant_ngp

# machine options
machine_config:
  num_gpus: 1
  num_machines: 1
  machine_rank: 0
  dist_url: auto

configs/default.yaml METASEP
defaults:
  # This makes the logging module logs colorful
  - data: default_data.yaml
  - graph: default_graph.yaml
  - logging: default_logging.yaml
  - override hydra/job_logging: colorlog
  - override hydra/hydra_logging: colorlog

hydra:
  run:
    # Configure output dir of each experiment programmatically from the arguments
    # Example "outputs/mnist/classifier/baseline/2021-03-10-141516"
    dir: outputs/${experiment_name}/${method_name}/${now:%Y-%m-%d_%H%M%S}

experiment_name: blender_lego
method_name: vanilla_nerf

# machine options
machine_config:
  num_gpus: 1
  num_machines: 1
  machine_rank: 0
  dist_url: auto

configs/README.md METASEP

tests/test_run_train.py METASEP
"""
Default test to make sure train runs
"""
import pytest
from hydra import compose, initialize

from scripts.run_train import main


@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_run_train():
    """test run train script works properly"""
    initialize(config_path="../configs/test/")
    conf = compose("test_dryrun.yaml")
    main(conf)

scripts/run_train.py METASEP
"""
run_train_nerf.py
"""
import datetime
import logging
import random
import socket
from datetime import timedelta
from typing import Any, Callable

import hydra
import numpy as np
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
from omegaconf import DictConfig

from mattport.nerf.trainer import Trainer
from mattport.utils import profiler

logging.basicConfig(format="[%(filename)s:%(lineno)d] %(message)s", level=logging.DEBUG)

DEFAULT_TIMEOUT = timedelta(minutes=30)

# speedup for when input size to model doesn't change (much)
torch.backends.cudnn.benchmark = True


def _find_free_port() -> str:
    """Find free port on open socket"""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(("", 0))
    port = sock.getsockname()[1]
    sock.close()
    return port


def _set_random_seed(seed) -> None:
    """Set randomness seed in torch and numpy"""
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)


def _synchronize(world_size, local_rank):
    """Wait for all processes on node finish"""
    if world_size > 1:
        dist.barrier(device_ids=[local_rank])


def _distributed_worker(
    local_rank: int,
    main_func: Callable,
    world_size: int,
    num_gpus_per_machine: int,
    machine_rank: int,
    dist_url: str,
    config: DictConfig,
    timeout: datetime = DEFAULT_TIMEOUT,
) -> Any:
    """Spawned distributed worker that handles the initialization of process group and handles the
       training process on multiple processes

    Args:
        local_rank (int): current rank of process
        main_func (Callable): function that will be called by the distributed workers
        world_size (int): total number of gpus available
        num_gpus_per_machine (int): number of GPUs per machine
        machine_rank (int): rank of this machine
        dist_url (str): url to connect to for distributed jobs, including protocol
                        e.g. "tcp://127.0.0.1:8686".
                        Can be set to "auto" to automatically select a free port on localhost
        config (DictConfig): config file specifying trainng regimen
        timeout (datetime, optional): timeout of the distributed workers Defaults to DEFAULT_TIMEOUT.

    Raises:
        e: Exception in initializing the process group

    Returns:
        Any: TODO(): determine the return type
    """
    assert torch.cuda.is_available(), "cuda is not available. Please check your installation."
    global_rank = machine_rank * num_gpus_per_machine + local_rank
    try:
        dist.init_process_group(
            backend="NCCL",
            init_method=dist_url,
            world_size=world_size,
            rank=global_rank,
            timeout=timeout,
        )
    except Exception as e:
        logger = logging.getLogger(__name__)
        logger.error("Process group URL: %s", dist_url)
        raise e

    assert num_gpus_per_machine <= torch.cuda.device_count()
    torch.cuda.set_device(local_rank)
    _set_random_seed(1234 + local_rank)  # NOTE(ethan): will this work for multiple machines? should we use global_rank?
    _synchronize(world_size, local_rank)

    output = main_func(local_rank, world_size, config)
    _synchronize(world_size, local_rank)
    dist.destroy_process_group()
    return output


def _train(local_rank: int, world_size: int, config: DictConfig, cpu: bool = False) -> Any:
    """Main training function that sets up and runs the trainer per process

    Args:
        local_rank (int): _description_
        world_size (int): _description_
        config (DictConfig): _description_

    Returns:
        Any: TODO(): determine the return type
    """
    trainer = Trainer(config, local_rank, world_size, cpu=cpu)
    trainer.setup()
    trainer.train()
    return 0


def launch(
    main_func: Callable,
    num_gpus_per_machine: int,
    num_machines: int = 1,
    machine_rank: int = 0,
    dist_url: str = "auto",
    config: DictConfig = None,
    timeout: datetime = DEFAULT_TIMEOUT,
) -> None:
    """Function that spawns muliple processes to call on main_func

    Args:
        main_func (Callable): function that will be called by the distributed workers
        num_gpus_per_machine (int): number of GPUs per machine
        num_machines (int, optional): total number of machines
        machine_rank (int, optional): rank of this machine. Defaults to 0.
        dist_url (str, optional): url to connect to for distributed jobs. Defaults to "auto".
        config (DictConfig, optional): config file specifying trainng regimen Defaults to None.
        timeout (datetime, optional): timeout of the distributed workers Defaults to DEFAULT_TIMEOUT.
    """
    world_size = num_machines * num_gpus_per_machine
    if world_size == 0:
        # Using only CPU and one process.
        main_func(local_rank=0, world_size=0, config=config, cpu=True)
    elif world_size == 1:
        # Using one gpu and one process.
        try:
            main_func(local_rank=0, world_size=1, config=config)
        except KeyboardInterrupt:
            pass
        finally:
            if config.logging.enable_profiler:
                profiler.PROFILER.print_profile()
    elif world_size > 1:
        # Using multiple gpus with multiple processes.
        if dist_url == "auto":
            assert num_machines == 1, "dist_url=auto not supported in multi-machine jobs."
            port = _find_free_port()
            dist_url = f"tcp://127.0.0.1:{port}"
        if num_machines > 1 and dist_url.startswith("file://"):
            logger = logging.getLogger(__name__)
            logger.warning("file:// is not a reliable init_method in multi-machine jobs. Prefer tcp://")

        process_context = mp.spawn(
            _distributed_worker,
            nprocs=num_gpus_per_machine,
            args=(
                main_func,
                world_size,
                num_gpus_per_machine,
                machine_rank,
                dist_url,
                config,
                timeout,
            ),
            join=False,
        )
        try:
            process_context.join()
        except KeyboardInterrupt:
            logger = logging.getLogger(__name__)
            for i, process in enumerate(process_context.processes):
                if process.is_alive():
                    logger.info("Terminating process %s", str(i))
                    process.terminate()
                process.join()
                logger.info("Process %s finished", str(i))
        finally:
            if config.logging.enable_profiler:
                profiler.PROFILER.print_profile()


@hydra.main(config_path="../configs", config_name="default.yaml")
def main(config: DictConfig):
    """Main function."""
    launch(
        _train,
        config.machine_config.num_gpus,
        num_machines=config.machine_config.num_machines,
        machine_rank=config.machine_config.machine_rank,
        dist_url=config.machine_config.dist_url,
        config=config,
    )


if __name__ == "__main__":
    main()

scripts/run_dataset_processor.py METASEP
"""
Script to process different dataset types into our format.
"""

# TODO(ethan): we should write a script here to convert from other datasets to our formats
# e.g., Blender, LLFF, ShapeNet, DTU

notebooks/notebook_imports.py METASEP
# image stuff
import cv2
import imageio
import mediapy as media

# ipython stuff
from IPython import get_ipython

# plotly stuff
import plotly.graph_objects as go


def setup_ipynb():
    """
    https://stackoverflow.com/questions/15411967/how-can-i-check-if-code-is-executed-in-the-ipython-notebook
    https://stackoverflow.com/questions/35595766/matplotlib-line-magic-causes-syntaxerror-in-python-script
    This gets reference to the InteractiveShell instance
    """
    try:
        from IPython import get_ipython

        get_ipython().run_line_magic("load_ext", "autoreload")
        get_ipython().run_line_magic("autoreload", "2")
        get_ipython().run_line_magic("matplotlib", "inline")
        return True
    except:
        return False


setup_ipynb()

environment/jupyter.py METASEP
## https://github.com/jupyterlab/jupyterlab/issues/2049
## Allow access to hidden files
c.ContentsManager.allow_hidden = True

mattport/viewer/README.md METASEP
# Viewer

The backend viewer code (python, websockets, ...).

# References

```
https://github.com/lp-research/openvslam/tree/main/viewer
https://github.com/rdeits/meshcat
```
pyproject.toml METASEP
[tool.black]
line-length = 120
README.md METASEP
# MattPort


# Quickstart
## Installation: Setup the environment

```
# Clone the repo
git clone --recurse-submodules git@github.com:ethanweber/mattport.git

# Create the python environment
conda create --name mattport python=3.8.13
conda activate mattport
pip install -r environment/requirements.txt

# Install mattport as a library
python setup.py develop

# Running the test cases
pytest tests
```

## Getting the data

Download the original NeRF dataset and put it in the following format.

```
- data/
    - blender/
        - fern/
        - lego/
```

## Training a model

```
# Run with the default config
python scripts/run_train.py
# Run with config changes
python scripts/run_train.py machine_config.num_gpus=1
```

# Logging and profiling features
We provide many logging functionalities for timing and/or tracking losses during training. All of these loggers are configurable via `configs/logging.yml`

1. **Writer**: Logs losses and generated images during training to a specified output stream. Specify the type of writer (Tensorboard, local directory, Weights and Biases), and how often to log in the config.

2. **StatsTracker**: Computes select statistics and prints to the terminal for local debugging. Specify the kinds of statistics to be logged, how often to log, and the maximum history buffer size in the config.

3. **Profiler**: Computes the average total time of execution for any function with the `@profiler.time_function` decorator. Prints out the full profile at the termination of training or the program.


# Setting up Jupyter

```
python -m jupyter lab build
bash environments/run_jupyter.sh
```

# Tooling

We use [autoenv](https://github.com/hyperupcall/autoenv) to make setting up the environment and environment variables easier. This will run the `.envrc` file upon entering the `/path/to/mattport` folder. It will also remove the set environment parameters upon leaving.

```
# Install direnv.
sudo apt install direnv

# Add the following line to the bottom of your ~/.bashrc file.
eval "$(direnv hook bash)"

# Populate your .envrc with commands you want to run. Then, run the following to allow updates.
cd /path/to/mattport
direnv allow .
```

<!-- Our PyTorch version uses cuda-11.3. -->
<!-- export PATH=/usr/local/cuda-11.3/bin:$PATH -->
mattport/viewer/plotly.py METASEP
"""
Visualization code for plotly.
"""

import numpy as np
import plotly.graph_objects as go
import torch

from mattport.structures.rays import RayBundle


def color_str(color):
    """Plotly color string.

    Args:
        color: list [r, g, b] in [0, 1] range

    Returns:
        str: plotly-formatted color string
    """
    color = list((np.array(color) * 255.0).astype("int"))
    return f"""rgb({color[0]}, {color[1]}, {color[2]})"""


def get_line_segments_from_lines(
    lines,
    color=color_str((1, 0, 0)),
    marker_color=color_str((1, 0, 0)),
    colors=None,
    draw_marker=True,
    draw_line=True,
    marker_size=4,
    line_width=10,
):
    """Returns a list of Scatter3D objects for creating lines with plotly.
    # TODO(ethan): make this function more efficient instead of having a list of objects.

    Args:
        lines (_type_): list of [np.array(2, 3), ...], line is defined by a (2, 3) array
        color (_type_, optional): _description_. Defaults to color_str((1, 0, 0)).
        marker_color (_type_, optional): _description_. Defaults to color_str((1, 0, 0)).
        colors (_type_, optional): _description_. Defaults to None.
        draw_marker (bool, optional): _description_. Defaults to True.
        draw_line (bool, optional): _description_. Defaults to True.
        marker_size (int, optional): _description_. Defaults to 4.
        line_width (int, optional): _description_. Defaults to 10.

    Returns:
        _type_: _description_
    """
    data = []
    for idx, line in enumerate(lines):
        thiscolor = color if draw_line else "rgba(0, 0, 0, 0)"
        if colors is not None:
            marker_color = colors[idx]
            thiscolor = colors[idx]
        data.append(
            go.Scatter3d(
                x=line[:, 0],
                y=line[:, 1],
                z=line[:, 2],
                showlegend=False,
                marker=dict(
                    size=marker_size,
                    color=marker_color,
                    colorscale="Viridis",
                )
                if draw_marker
                else dict(color="rgba(0, 0, 0, 0)"),
                line=dict(color=thiscolor, width=line_width),
            )
        )
    return data


def visualize_dataset(camera_origins, ray_bundle: RayBundle):
    """Visualize a dataset with plotly using our cameras and generated rays."""

    skip = 1
    size = 8
    assert len(ray_bundle) < 500, "Let's not break plotly by plotting too many rays!"

    data = []
    data += [
        go.Scatter3d(
            x=camera_origins[::skip, 0],
            y=camera_origins[::skip, 1],
            z=camera_origins[::skip, 2],
            mode="markers",
            name="camera origins",
            marker=dict(color="rgba(0, 0, 0, 1)", size=size),
        )
    ]

    length = 2.0
    lines = torch.stack(
        [ray_bundle.origins, ray_bundle.origins + ray_bundle.directions * length], dim=1
    )  # (num_rays, 2, 3)

    data += get_line_segments_from_lines(lines)

    layout = go.Layout(
        autosize=False,
        width=1000,
        height=1000,
        margin=go.layout.Margin(l=50, r=50, b=100, t=100, pad=4),
        scene=go.layout.Scene(
            aspectmode="data",
            camera=dict(up=dict(x=0, y=0, z=1), center=dict(x=0, y=0, z=0), eye=dict(x=1.25, y=1.25, z=1.25)),
        ),
    )
    fig = go.Figure(data=data, layout=layout)
    return fig

mattport/viewer/__init__.py METASEP

mattport/utils/writer.py METASEP
"""
Generic Writer class
"""


import os
from abc import abstractmethod
from typing import Dict

import imageio
import numpy as np
import torch
from torch.utils.tensorboard import SummaryWriter
from torchtyping import TensorType

from mattport.utils.decorators import check_main_thread, decorate_all

to8b = lambda x: (255 * torch.clamp(x, min=0, max=1)).to(torch.uint8)


def get_tensorboard_name(name: str, group: str = None, prefix: str = None):
    """Returns a string for tensorboard with an optional group and prefix.
    Where tensorboard_name has the form `group/prefix-name`.
    """
    group_string = f"{group}/" if group else ""
    prefix_string = f"{prefix}" if prefix else ""
    tensorboard_name = f"{group_string}{prefix_string}{name}"
    return tensorboard_name


class Writer:
    """Writer class"""

    def __init__(self, is_main_thread: bool, save_dir: str):
        self.is_main_thread = is_main_thread
        self.save_dir = save_dir

    @abstractmethod
    def write_image(
        self, name: str, x: TensorType["H", "W", 3], step: int, group: str = None, prefix: str = None
    ) -> None:
        """_summary_

        Args:
            name (str): data identifier
            x (TensorType["H", "W", 3]): rendered image to write
            step (int): the time step to log
            group (str): the group e.g., "Loss", "Accuracy", "Time"
            prefix (str): the prefix e.g., "train-", "test-"
        """
        raise NotImplementedError

    @abstractmethod
    def write_scalar(self, name: str, scalar: float, step: int, group: str = None, prefix: str = None) -> None:
        """Required method to write a single scalar value to the logger

        Args:
            name (str): data identifier
            step (int): the time step to log
            group (str): the group e.g., "Loss", "Accuracy", "Time"
            prefix (str): the prefix e.g., "train-", "test-"
        """
        raise NotImplementedError

    @check_main_thread
    def write_scalar_dict(
        self, scalar_dict: Dict[str, float], step: int, group: str = None, prefix: str = None
    ) -> None:
        """Function that writes out all scalars from a given dictionary to the logger

        Args:
            scalar_dict (dict): dictionary containing all scalar values with key names and quantities
            step (int): the time step to log
            group (str): the group e.g., "Loss", "Accuracy", "Time"
            prefix (str): the prefix e.g., "train-", "test-"
        """
        if self.is_main_thread:
            for name, scalar in scalar_dict.items():
                self.write_scalar(name, scalar, step, group=group, prefix=prefix)


@decorate_all([check_main_thread])
class TensorboardWriter(Writer):
    """Tensorboard Writer Class"""

    def __init__(self, is_main_thread: bool, save_dir: str):
        super().__init__(is_main_thread, save_dir)
        if self.is_main_thread:
            self.tb_writer = SummaryWriter(log_dir=self.save_dir)

    def write_image(
        self, name: str, x: TensorType["H", "W", 3], step: int, group: str = None, prefix: str = None
    ) -> None:
        """_summary_

        Args:
            name (str): data identifier
            x (TensorType["H", "W", 3]): rendered image to write
        """
        x = to8b(x)
        tensorboard_name = get_tensorboard_name(name, group=group, prefix=prefix)
        self.tb_writer.add_image(tensorboard_name, x, step, dataformats="HWC")

    def write_scalar(self, name: str, scalar: float, step: int, group: str = None, prefix: str = None) -> None:
        """Tensorboard method to write a single scalar value to the logger

        Args:
            name (str): data identifier
            x (float): x value to write
            y (float): y value to write
            group (str)): a prefix to group tensorboard scalars
        """
        tensorboard_name = get_tensorboard_name(name, group=group, prefix=prefix)
        self.tb_writer.add_scalar(tensorboard_name, scalar, step)


@decorate_all([check_main_thread])
class LocalWriter(Writer):
    """Local Writer Class"""

    def write_image(
        self, name: str, x: TensorType["H", "W", 3], step: int, group: str = None, prefix: str = None
    ) -> None:
        x = to8b(x)
        image_path = os.path.join(self.save_dir, f"{name}.jpg")
        imageio.imwrite(image_path, np.uint8(x.cpu().numpy() * 255.0))

    def write_scalar(self, name: str, scalar: float, step: int, group: str = None, prefix: str = None) -> None:
        raise NotImplementedError

mattport/utils/stats_tracker.py METASEP
"""
Stats tracker base class and functionality
"""
import datetime
import enum
import logging

from omegaconf import DictConfig
from mattport.utils.decorators import check_main_thread, check_print_stats_step, check_stats_enabled, decorate_all


class Stats(enum.Enum):
    """Possible Stats values for StatsTracker

    Args:
        enum (_type_): _description_
    """

    ITER_LOAD_TIME = "Data Load (time)"
    ITER_TRAIN_TIME = "Train Iter (time)"
    TOTAL_TRAIN_TIME = "Train Total (time)"
    RAYS_PER_SEC = "Rays Per Sec (1/s)"
    CURR_TEST_PSNR = "Test PSNR"
    ETA = "ETA (time)"


@decorate_all([check_stats_enabled, check_main_thread])
class StatsTracker:
    """Stats Tracker class"""

    def __init__(self, config: DictConfig, is_main_thread: bool):
        self.config = config
        if self.config.logging.enable_stats:
            self.is_main_thread = is_main_thread
            self.max_history = self.config.logging.stats_tracker.max_history
            self.step = 0
            self.stats_dict = {}
            self.past_stats = []
            self.new_key = False
            self.stats_to_track = set()
            for name in self.config.logging.stats_tracker.stats_to_track:
                self.stats_to_track.add(Stats[name])
            logging.info("Successfully set up StatsTracker")
        else:
            logging.info("StatsTracker disabled; enable in config.")

    def update_value(self, name: enum.Enum, value: float, step: int):
        """update stats dictionary with key value pair

        Args:
            name (enum.Enum): Enum name of statistic we are logging
            value (float): value to update.
            step (int): number of total iteration steps.
        """
        if name in self.stats_to_track:
            self.step = step
            self.new_key = not name in self.stats_dict or self.new_key
            self.stats_dict[name] = value

    def update_time(self, name: enum.Enum, start_time: float, end_time: float, step: int, batch_size: int = None):
        """update the stats dictionary with running averages/cumulative durations

        Args:
            name (enum.Enum): Enum name of statistic we are logging
            start_time (float): start time for the call in seconds
            end_time (float): end time when the call finished executing in seconds
            step (int): number of total iteration steps.
            batch_size (int, optional): total number of rays in a batch;
                if None, reports duration instead of batch per second. Defaults to None.
        """
        if name in self.stats_to_track:
            self.step = step
            self.new_key = not name in self.stats_dict or self.new_key
            val = end_time - start_time
            if batch_size:
                # calculate the batch per second stat
                val = batch_size / val

            if step == -1:
                # logging total time instead of average
                self.stats_dict[name] = val
            else:
                # calculate updated average
                self.stats_dict[name] = (self.stats_dict.get(name, 0) * step + val) / (step + 1)

            if name == Stats.ITER_TRAIN_TIME and Stats.ETA in self.stats_to_track:
                # update ETA if logging iteration train time
                remain_iter = self.config.graph.max_num_iterations - step
                self.stats_dict[Stats.ETA] = remain_iter * self.stats_dict[name]

    @check_print_stats_step
    def print_stats(self, fraction_done: float):
        """helper to print out the stats dictionary.

        Args:
            fraction_done (float): fraction of steps executed in training iterations
        """
        # print a new header line if there is a new key added
        if self.step == 0 or self.new_key:
            mssg = f"{'Step (% Done)':<20}"
            for k in self.stats_dict:
                mssg += f"{k.value:<20} "
            if self.step > 0:
                cursorup(len(self.past_stats) + 2)
            print(mssg)
            print("-" * len(mssg))
            if self.step > 0:
                for mssg in self.past_stats:
                    print(mssg)

        # generate a new stats reporting message
        if self.step > 0:
            curr_mssg = f"{self.step} ({fraction_done*100:.02f}%)"
            curr_mssg = f"{curr_mssg:<20}"
            for k, v in self.stats_dict.items():
                if "(time)" in k.value:
                    v = str(datetime.timedelta(seconds=v))
                else:
                    v = f"{v:0.4f}"
                curr_mssg += f"{v:<20} "
            # update the history buffer
            if len(self.past_stats) >= self.max_history:
                self.past_stats.pop(0)
                cursorup(len(self.past_stats) + 1)
                for mssg in self.past_stats:
                    print(mssg)
            print(curr_mssg)
            self.past_stats.append(curr_mssg)

    def dump_stats(self):
        """Dump stats locally to a json file"""
        raise NotImplementedError


def cursorup(x: int):
    """utility tool to move the cursor up on the terminal

    Args:
        x (int): amount of lines to move cursor upward
    """
    print(f"\r\033[{x}A", end="")

mattport/utils/profiler.py METASEP
"""
Profiler base class and functionality
"""
import logging
import sys
import time
from typing import Callable

from omegaconf import DictConfig

from mattport.utils.decorators import check_main_thread, check_profiler_enabled, decorate_all

PROFILER = None


def time_function(func: Callable) -> Callable:
    """Decorator: time a function call"""

    def wrapper(*args, **kwargs):
        start = time.time()
        ret = func(*args, **kwargs)
        if PROFILER:
            vals = vars(sys.modules[func.__module__])
            class_str = ""
            for attr in func.__qualname__.split(".")[:-1]:
                class_str += f"{vals[attr].__qualname__}_"
            class_str += func.__name__
            PROFILER.update_time(class_str, start, time.time())
        return ret

    return wrapper


@decorate_all([check_profiler_enabled, check_main_thread])
class Profiler:
    """Profiler class"""

    def __init__(self, config: DictConfig, is_main_thread: bool):
        self.config = config
        if self.config.logging.enable_profiler:
            self.is_main_thread = is_main_thread
        self.profiler_dict = {}

    def update_time(self, func_name: str, start_time: float, end_time: float):
        """update the profiler dictionary with running averages of durations

        Args:
            func_name (str): the function name that is being profiled
            start_time (float): the start time when function is called
            end_time (float): the end time when function terminated
        """
        val = end_time - start_time
        func_dict = self.profiler_dict.get(func_name, {"val": 0, "step": 0})
        prev_val = func_dict["val"]
        prev_step = func_dict["step"]
        self.profiler_dict[func_name] = {"val": (prev_val * prev_step + val) / (prev_step + 1), "step": prev_step + 1}

    def print_profile(self):
        """helper to print out the profiler stats"""
        logging.info("Printing profiling stats, from longest to shortest duration in seconds")
        sorted_keys = [k for k, _ in sorted(self.profiler_dict.items(), key=lambda item: item[1]["val"], reverse=True)]
        for k in sorted_keys:
            val = f"{self.profiler_dict[k]['val']:0.4f}"
            print(f"{k:<20}: {val:<20}")

mattport/utils/misc.py METASEP
"""
Miscellaneous helper code.
"""


class DotDict(dict):
    """
    dot.notation access to dictionary attributes
    """

    def __getattr__(self, attr):
        return self[attr]

    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

mattport/utils/io.py METASEP
"""
Input/output utils.
"""
import json
import os


def load_from_json(filename: str):
    """_summary_

    Args:
        filename (str): _description_

    Returns:
        _type_: _description_
    """
    assert filename.endswith(".json")
    with open(filename, "r", encoding="UTF-8") as file:
        return json.load(file)


def write_to_json(filename: str, content: dict):
    """_summary_

    Args:
        filename (str): _description_
        content (dict): _description_
    """
    assert filename.endswith(".json")
    with open(filename, "w", encoding="UTF-8") as file:
        json.dump(content, file)


def get_git_root(path, dirs=(".git",), default=None):
    """_summary_
    "https://stackoverflow.com/questions/22081209/find-the-root-of-the-git-repository-where-the-file-lives

    Args:
        path (_type_): _description_
        dirs (tuple, optional): _description_. Defaults to (".git",).
        default (_type_, optional): _description_. Defaults to None.

    Returns:
        _type_: _description_
    """
    prev, test = None, os.path.abspath(path)
    while prev != test:
        if any(os.path.isdir(os.path.join(test, directory)) for directory in dirs):
            return test
        prev, test = test, os.path.abspath(os.path.join(test, os.pardir))
    return default


def get_absolute_path(path, proj_root_func=get_git_root):
    """
    Returns the full, absolute path.
    Relative paths are assumed to start at the repo directory.
    """
    if path == "":
        return ""
    absolute_path = path
    if absolute_path[0] != "/":
        absolute_path = os.path.join(proj_root_func(path), absolute_path)
    return absolute_path


def make_dir(filename_or_folder):
    """Make the directory for either the filename or folder.
    Note that filename_or_folder currently needs to end in / for it to be recognized as a folder.
    """
    if filename_or_folder[-1] != "/" and filename_or_folder.find(".") < 0:
        folder = os.path.dirname(filename_or_folder + "/")
    else:
        folder = os.path.dirname(filename_or_folder)
    if not os.path.exists(folder):
        try:
            os.makedirs(folder)
        except Exception as e:  # pylint: disable=broad-except
            print(f"Couldn't create folder: {folder}. Maybe due to a parallel process?")
            print(e)
    return filename_or_folder

mattport/utils/decorators.py METASEP
"""
Decorator definitions
"""
from typing import Callable, List


def decorate_all(decorators: List[Callable]) -> Callable:
    """A decorator to decorate all member functions of a class"""

    def decorate(cls):
        for attr in cls.__dict__:
            if callable(getattr(cls, attr)) and attr != "__init__":
                for decorator in decorators:
                    setattr(cls, attr, decorator(getattr(cls, attr)))
        return cls

    return decorate


def check_stats_enabled(func: Callable) -> Callable:
    """Decorator: check if stats tracker is enabled"""

    def wrapper(self, *args, **kwargs):
        ret = None
        if self.config.logging.enable_stats:
            ret = func(self, *args, **kwargs)
        return ret

    return wrapper


def check_profiler_enabled(func: Callable) -> Callable:
    """Decorator: check if profiler is enabled"""

    def wrapper(self, *args, **kwargs):
        ret = None
        if self.config.logging.enable_stats:
            ret = func(self, *args, **kwargs)
        return ret

    return wrapper


def check_main_thread(func: Callable) -> Callable:
    """Decorator: check if you are on main thread"""

    def wrapper(self, *args, **kwargs):
        ret = None
        if self.is_main_thread:
            ret = func(self, *args, **kwargs)
        return ret

    return wrapper


def check_print_stats_step(func: Callable) -> Callable:
    """Decorator: check if it is time to print stats update"""

    def wrapper(self, *args, **kwargs):
        ret = None
        if (
            self.step % self.config.logging.steps_per_log == 0
            or (self.config.graph.steps_per_save and self.step % self.config.graph.steps_per_save == 0)
            or self.step % self.config.graph.steps_per_test == 0
        ):
            ret = func(self, *args, **kwargs)
        return ret

    return wrapper

mattport/utils/__init__.py METASEP

mattport/structures/rays.py METASEP
"""
Some ray datastructures.
"""
import random
from dataclasses import dataclass
from typing import Optional
import torch

from torchtyping import TensorType


@dataclass
class RayBundle:
    """_summary_

    Returns:
        _type_: _description_
    """

    origins: TensorType["num_rays", 3]
    directions: TensorType["num_rays", 3]
    camera_indices: Optional[TensorType["num_rays"]] = None

    def to_camera_ray_bundle(self, image_height, image_width) -> "CameraRayBundle":
        """_summary_

        Args:
            image_height (_type_): _description_
            image_width (_type_): _description_

        Returns:
            CameraRayBundle: _description_
        """
        camera_ray_bundle = CameraRayBundle(
            origins=self.origins.view(image_height, image_width, 3),
            directions=self.directions.view(image_height, image_width, 3),
        )
        return camera_ray_bundle

    def __len__(self):
        num_rays = self.origins.shape[0]
        return num_rays

    def sample(self, num_rays: int):
        """Returns a RayBundle as a subset of rays.

        Args:
            num_rays (int):

        Returns:
            RayBundle: _description_
        """
        assert num_rays <= len(self)
        indices = random.sample(range(len(self)), k=num_rays)
        return RayBundle(
            origins=self.origins[indices],
            directions=self.directions[indices],
            camera_indices=self.camera_indices[indices],
        )


@dataclass
class CameraRayBundle:
    """_summary_"""

    origins: TensorType["image_height", "image_width", 3]
    directions: TensorType["image_height", "image_width", 3]
    camera_index: int = None


class RaySamples:
    """_summary_"""

    def __init__(
        self,
        ts: TensorType["num_rays", "num_samples"],
        ray_bundle: RayBundle,
    ) -> None:
        self.ray_bundle = ray_bundle
        self.ts = ts
        self.positions = self.get_positions(ray_bundle)
        self.directions = ray_bundle.directions.unsqueeze(1).repeat(1, self.positions.shape[1], 1)
        self.deltas = self.get_deltas()

    def get_positions(self, ray_bundle: RayBundle) -> TensorType["num_rays", "num_samples", 3]:
        """Returns positions."""
        return ray_bundle.origins[:, None] + self.ts[:, :, None] * ray_bundle.directions[:, None]

    def get_deltas(self) -> TensorType[..., "num_samples"]:
        """Returns deltas."""
        dists = self.ts[..., 1:] - self.ts[..., :-1]
        dists = torch.cat([dists, dists[..., -1:]], -1)  # [N_rays, N_samples]
        deltas = dists * torch.norm(self.ray_bundle.directions[..., None, :], dim=-1)
        return deltas

    def get_weights(self, densities: TensorType[..., "num_samples", 1]) -> TensorType[..., "num_samples"]:
        """Return weights based on predicted densities

        Args:
            densities (TensorType[..., "num_samples", 1]): Predicted densities for samples along ray

        Returns:
            TensorType[..., "num_samples"]: Weights for each sample
        """

        delta_density = self.deltas * densities[..., 0]
        alphas = 1 - torch.exp(-delta_density)

        transmittance = torch.cumsum(delta_density[..., :-1], dim=-1)
        transmittance = torch.cat(
            [torch.zeros((*transmittance.shape[:1], 1)).to(densities.device), transmittance], axis=-1
        )
        transmittance = torch.exp(-transmittance)  # [..., "num_samples"]

        weights = alphas * transmittance  # [..., "num_samples"]

        return weights

mattport/structures/colors.py METASEP
"""Common Colors"""
from typing import Union
from omegaconf import ListConfig
import torch
from torchtyping import TensorType

WHITE = torch.tensor([1.0, 1.0, 1.0])
BLACK = torch.tensor([0.0, 0.0, 0.0])
RED = torch.tensor([1.0, 0.0, 0.0])
GREEN = torch.tensor([0.0, 1.0, 0.0])
BLUE = torch.tensor([0.0, 0.0, 1.0])

COLORS_DICT = {
    "white": WHITE,
    "black": BLACK,
    "red": RED,
    "green": GREEN,
    "blue": BLUE,
}


def get_color(color: Union[str, list, ListConfig]) -> TensorType[3]:
    """
    Args:
        color (Union[str, list, ListConfig]): Color as a string or a rgb list

    Returns:
        TensorType[3]: Parsed color
    """
    if isinstance(color, str):
        color = color.lower()
        if color not in COLORS_DICT:
            raise ValueError(f"{color} is not a valid preset color")
        return COLORS_DICT[color]
    if isinstance(color, (list, ListConfig)):
        if len(color) != 3:
            raise ValueError(f"Color should be 3 values (RGB) instead got {color}")
        return torch.tensor(color)

    raise ValueError(f"Color should be an RGB list or string, instead got {type(color)}")

mattport/structures/cameras.py METASEP
"""
Camera Models
"""
from abc import abstractmethod
from dataclasses import dataclass
from typing import Optional

import torch
from torch.nn.functional import normalize
from torchtyping import TensorType

from mattport.structures.rays import CameraRayBundle, RayBundle


@dataclass
class Rays:
    """Camera rays. Can be arbitrary dimension"""

    origin: TensorType[..., 3]
    direction: TensorType[..., 3]


class Camera:
    """Base Camera. Intended to be subclassed"""

    def __init__(self, camera_to_world: Optional[TensorType[3, 4]] = torch.eye(4)[:3]) -> None:
        self.camera_to_world = camera_to_world

    @abstractmethod
    def get_num_intrinsics_params(self) -> int:
        """_summary_

        Returns:
            int: _description_
        """
        return

    @abstractmethod
    def get_intrinsics(self) -> torch.Tensor:
        """_summary_

        Returns:
            torch.Tensor: _description_
        """
        return

    def get_camera_to_world(self) -> TensorType[3, 4]:
        """_summary_

        Returns:
            TensorType[3, 4]: _description_
        """
        return self.camera_to_world

    @abstractmethod
    def get_image_height(self):
        """_summary_"""
        return

    @abstractmethod
    def get_image_width(self):
        """_summary_"""
        return

    def get_image_coords(self, pixel_offset: float = 0.5) -> TensorType["image_height", "image_width", 2]:
        """_summary_

        Returns:
            _type_: _description_
        """
        image_height = self.get_image_height()
        image_width = self.get_image_width()
        image_coords = torch.meshgrid(torch.arange(image_height), torch.arange(image_width), indexing="ij")
        image_coords = torch.stack(image_coords, dim=-1) + pixel_offset  # stored as (y, x) coordinates
        return image_coords

    @classmethod
    @abstractmethod
    def generate_rays(
        cls,
        intrinsics: TensorType["num_rays", "num_intrinsics_params"],
        camera_to_world: TensorType["num_rays", 3, 4],
        coords: TensorType["image_height", "image_width", 2],
    ) -> RayBundle:
        """_summary_

        Args:
            intrinsics (TensorType[&quot;num_rays&quot;, &quot;num_intrinsics_params&quot;]): _description_
            camera_to_world (TensorType[&quot;num_rays&quot;, 3, 4]): _description_
            coords (TensorType[&quot;image_height&quot;, &quot;image_width&quot;, 2]): _description_

        Returns:
            RayBundle: _description_
        """
        return

    def generate_camera_rays(self) -> CameraRayBundle:
        """Generate rays for the camera.

        Returns:
            Rays: Camera rays of shape [height, width]
        """
        image_height = self.get_image_height()
        image_width = self.get_image_width()
        num_rays = image_height * image_width
        intrinsics = self.get_intrinsics().unsqueeze(0).repeat(num_rays, 1)  # (num_rays, num_intrinsics_params)
        camera_to_world = self.camera_to_world.unsqueeze(0).repeat(num_rays, 1, 1)  # (num_rays, 3, 4)
        coords = self.get_image_coords().view(num_rays, 2)  # (num_rays, 2)
        ray_bundle = self.generate_rays(intrinsics, camera_to_world, coords)
        camera_ray_bundle = ray_bundle.to_camera_ray_bundle(image_height, image_width)
        return camera_ray_bundle


class PinholeCamera(Camera):
    """Pinhole camera model."""

    def __init__(
        self, cx: float, cy: float, fx: float, fy: float, camera_to_world: Optional[TensorType[3, 4]] = torch.eye(4)[:3]
    ):
        super().__init__(camera_to_world)
        self.cx = cx
        self.cy = cy
        self.fx = fx
        self.fy = fy

    def get_num_intrinsics_params(self):
        return 4

    def get_image_width(self):
        return int(self.cx * 2.0)

    def get_image_height(self):
        return int(self.cy * 2.0)

    def get_intrinsics_matrix(self):
        """_summary_

        Returns:
            _type_: _description_
        """
        K = torch.tensor(
            [[self.fx, 0.0, self.cx], [0.0, self.fy, self.cy], [0, 0, 1.0]],
            dtype=torch.float32,
        )
        return K

    def get_intrinsics(self) -> torch.Tensor:
        return torch.tensor([self.cx, self.cy, self.fx, self.fy])

    @classmethod
    def fx_index(cls):
        """TODO(ethan): redo this in a better way.
        Ideally we can dynamically grab the focal length parameters depending on
        Simple vs. not Simple Pinhole Model.

        Returns:
            _type_: _description_
        """
        return 2

    @classmethod
    def fy_index(cls):
        """_summary_

        Returns:
            _type_: _description_
        """
        return 3

    @classmethod
    def generate_rays(
        cls,
        intrinsics: TensorType["num_rays", "num_intrinsics_params"],
        camera_to_world: TensorType["num_rays", 3, 4],
        coords: TensorType["num_rays", 2],
    ) -> RayBundle:

        cx = intrinsics[:, 0:1]
        cy = intrinsics[:, 1:2]
        fx = intrinsics[:, cls.fx_index() : cls.fx_index() + 1]
        fy = intrinsics[:, cls.fy_index() : cls.fy_index() + 1]
        y = coords[:, 0:1]  # (num_rays, 1)
        x = coords[:, 1:2]  # (num_rays, 1)
        original_directions = torch.cat([(x - cx) / fx, -(y - cy) / fy, -torch.ones_like(x)], -1)  # (num_rays, 3)
        rotation = camera_to_world[:, :3, :3]  # (num_rays, 3, 3)
        directions = torch.sum(
            original_directions[:, None, :] * rotation, dim=-1
        )  # (num_rays, 1, 3) * (num_rays, 3, 3) -> (num_rays, 3)
        directions = normalize(directions, dim=-1)
        origins = camera_to_world[:, :3, 3]  # (num_rays, 3)
        return RayBundle(origins=origins, directions=directions)


class SimplePinholeCamera(PinholeCamera):
    """Simple Pinhole Camera model."""

    def __init__(self, cx: float, cy: float, f: float, camera_to_world: Optional[TensorType[3, 4]] = torch.eye(4)[:3]):
        super().__init__(cx, cy, f, f, camera_to_world)

    @classmethod
    def fx_index(cls):
        return 2

    @classmethod
    def fy_index(cls):
        return 2


def get_camera_model(num_intrinsics_params):
    """Returns the camera model given the specified number of intrinsics parameters.

    Args:
        num_intrinsics_params (_type_): _description_

    Returns:
        _type_: _description_
    """
    if num_intrinsics_params == 3:
        return SimplePinholeCamera
    raise NotImplementedError

mattport/structures/__init__.py METASEP

mattport/sfm/__init__.py METASEP

setup.py METASEP
import setuptools

setuptools.setup(
    name="mattport",
    version="1.0.0",
    author="MattPort",
    url="https://github.com/ethanweber/mattport.git",
    description="mattport description",
    packages=setuptools.find_packages(),
    python_requires=">=3.8",
)

mattport/nerf/graph/vanilla_nerf.py METASEP
"""
Implementation of vanilla nerf.
"""


from typing import Dict, List, Tuple

import torch
from torch import nn
from torch.nn import Parameter
from torchtyping import TensorType

from mattport.nerf.field_modules.encoding import NeRFEncoding
from mattport.nerf.field_modules.field_heads import DensityFieldHead, FieldHeadNames, RGBFieldHead
from mattport.nerf.field_modules.mlp import MLP
from mattport.nerf.field_modules.ray_generator import RayGenerator
from mattport.nerf.graph.base import Graph
from mattport.nerf.loss import MSELoss
from mattport.nerf.renderers import AccumulationRenderer, RGBRenderer
from mattport.nerf.sampler import PDFSampler, UniformSampler
from mattport.structures import colors
from mattport.structures.rays import RaySamples


class NeRFField(nn.Module):
    """NeRF module"""

    def __init__(self, num_layers=8, layer_width=256, skip_connections: Tuple = (4,)) -> None:
        super().__init__()
        self.num_layers = num_layers
        self.layer_width = layer_width
        self.skip_connections = skip_connections

        self.encoding_xyz = NeRFEncoding(
            in_dim=3, num_frequencies=10, min_freq_exp=0.0, max_freq_exp=8.0, include_input=True
        )
        self.encoding_dir = NeRFEncoding(
            in_dim=3, num_frequencies=4, min_freq_exp=0.0, max_freq_exp=4.0, include_input=True
        )
        self.mlp_base = MLP(
            in_dim=self.encoding_xyz.get_out_dim(),
            out_dim=self.layer_width,
            num_layers=self.num_layers,
            layer_width=self.layer_width,
            skip_connections=self.skip_connections,
            activation=nn.ReLU(),
        )
        self.mlp_rgb = MLP(
            in_dim=self.mlp_base.get_out_dim() + self.encoding_dir.get_out_dim(),
            out_dim=self.layer_width // 2,
            num_layers=1,
            activation=nn.ReLU(),
        )
        self.field_output_rgb = RGBFieldHead(in_dim=self.mlp_rgb.get_out_dim())
        self.field_output_density = DensityFieldHead(in_dim=self.mlp_base.get_out_dim())

    def forward(self, ray_samples: RaySamples):
        """Evaluates the field at points along the ray
        Args:
            xyz: ()
        # TODO(ethan): change the input to be something more abstracted
        e.g., a FieldInput structure
        """
        positions = ray_samples.positions
        directions = ray_samples.directions
        encoded_xyz = self.encoding_xyz(positions)
        encoded_dir = self.encoding_dir(directions)
        base_mlp_out = self.mlp_base(encoded_xyz)
        rgb_mlp_out = self.mlp_rgb(torch.cat([encoded_dir, base_mlp_out], dim=-1))

        field_rgb_output = self.field_output_rgb(rgb_mlp_out)
        field_density_out = self.field_output_density(base_mlp_out)

        field_outputs = {}
        field_outputs.update(field_rgb_output)
        field_outputs.update(field_density_out)
        return field_outputs


class NeRFGraph(Graph):
    """Vanilla NeRF graph"""

    def __init__(
        self,
        intrinsics=None,
        camera_to_world=None,
        near_plane=1.0,
        far_plane=6.0,
        num_coarse_samples=64,
        num_importance_samples=128,
        **kwargs
    ) -> None:
        self.near_plane = near_plane
        self.far_plane = far_plane
        self.num_coarse_samples = num_coarse_samples
        self.num_importance_samples = num_importance_samples
        super().__init__(intrinsics=intrinsics, camera_to_world=camera_to_world, **kwargs)

    def populate_modules(self):
        # ray generator
        self.ray_generator = RayGenerator(self.intrinsics, self.camera_to_world)

        # samplers
        self.sampler_uniform = UniformSampler(
            near_plane=self.near_plane, far_plane=self.far_plane, num_samples=self.num_coarse_samples
        )
        self.sampler_pdf = PDFSampler(num_samples=self.num_importance_samples)

        # field
        self.field_coarse = NeRFField()
        self.field_fine = NeRFField()

        # renderers
        self.renderer_rgb = RGBRenderer(background_color=colors.WHITE)
        self.renderer_accumulation = AccumulationRenderer()

        # losses
        self.rgb_loss = MSELoss()

    def get_param_groups(self) -> Dict[str, List[Parameter]]:
        """Obtain the parameter groups for the optimizers

        Returns:
            Dict[str, List[Parameter]]: Mapping of different parameter groups
        """
        param_groups = {}
        # param_groups["cameras"] = list(self.ray_generator.parameters())
        param_groups["fields"] = list(self.field_coarse.parameters()) + list(self.field_fine.parameters())
        return param_groups

    def forward(self, ray_indices: TensorType["num_rays", 3]):
        """Takes in the ray indices and renders out values."""
        # get the rays:
        ray_bundle = self.ray_generator.forward(ray_indices)  # RayBundle
        # coarse network:
        uniform_ray_samples = self.sampler_uniform(ray_bundle)  # RaySamples

        # time coarse network # xyz -> asd
        coarse_field_outputs = self.field_coarse(uniform_ray_samples)  # FieldOutputs
        # time end coarse network

        coarse_weights = uniform_ray_samples.get_weights(coarse_field_outputs[FieldHeadNames.DENSITY])

        coarse_renderer_outputs = self.renderer_rgb(
            rgb=coarse_field_outputs[FieldHeadNames.RGB],
            weights=coarse_weights,
        )  # RendererOutputs
        coarse_renderer_accumulation = self.renderer_accumulation(coarse_weights)  # RendererOutputs

        # fine network:
        pdf_ray_samples = self.sampler_pdf(uniform_ray_samples, coarse_weights)  # RaySamples
        fine_field_outputs = self.field_fine(pdf_ray_samples)  # FieldOutputs

        fine_weights = pdf_ray_samples.get_weights(fine_field_outputs[FieldHeadNames.DENSITY])

        fine_renderer_outputs = self.renderer_rgb(
            rgb=fine_field_outputs[FieldHeadNames.RGB],
            weights=fine_weights,
        )  # RendererOutputs
        fine_renderer_accumulation = self.renderer_accumulation(fine_weights)  # RendererOutputs

        # outputs:
        outputs = {
            "rgb_coarse": coarse_renderer_outputs.rgb,
            "rgb_fine": fine_renderer_outputs.rgb,
            "accumulation_coarse": coarse_renderer_accumulation.accumulation,
            "accumulation_fine": fine_renderer_accumulation.accumulation,
        }
        return outputs

    def get_losses(self, batch, graph_outputs):
        # batch.pixels # (num_rays, 3)
        losses = {}
        rgb_loss_coarse = self.rgb_loss(batch["pixels"], graph_outputs["rgb_coarse"])
        rgb_loss_fine = self.rgb_loss(batch["pixels"], graph_outputs["rgb_fine"])
        losses = {"rgb_loss_coarse": rgb_loss_coarse, "rgb_loss_fine": rgb_loss_fine}
        return losses

mattport/nerf/graph/tensorf.py METASEP
"""
TensorRF implementation.
TODO:
"""

mattport/nerf/graph/instant_ngp.py METASEP
"""
Implementation of Instant NGP.
"""


from typing import Dict, List

import torch
from torch import nn
from torch.nn import Parameter
from torchtyping import TensorType

from mattport.nerf.field_modules.encoding import HashEncoding, NeRFEncoding
from mattport.nerf.field_modules.field_heads import DensityFieldHead, FieldHeadNames, RGBFieldHead
from mattport.nerf.field_modules.mlp import MLP
from mattport.nerf.field_modules.ray_generator import RayGenerator
from mattport.nerf.graph.base import Graph
from mattport.nerf.loss import MSELoss
from mattport.nerf.renderers import RGBRenderer
from mattport.nerf.sampler import PDFSampler, UniformSampler  # pylint: disable=unused-import
from mattport.structures.rays import RaySamples


class NGPField(nn.Module):
    """NeRF module"""

    def __init__(self, num_layers=3, layer_width=64) -> None:
        super().__init__()
        self.num_layers = num_layers
        self.layer_width = layer_width

        self.encoding_xyz = HashEncoding()
        self.encoding_dir = NeRFEncoding(
            in_dim=3, num_frequencies=4, min_freq_exp=0.0, max_freq_exp=4.0, include_input=True
        )
        self.mlp_base = MLP(
            in_dim=self.encoding_xyz.get_out_dim(),
            out_dim=self.layer_width,
            num_layers=self.num_layers,
            layer_width=self.layer_width,
            activation=nn.ReLU(),
        )
        self.mlp_rgb = MLP(
            in_dim=self.mlp_base.get_out_dim() + self.encoding_dir.get_out_dim(),
            out_dim=self.layer_width // 2,
            num_layers=1,
            activation=nn.ReLU(),
        )
        self.field_output_rgb = RGBFieldHead(in_dim=self.mlp_rgb.get_out_dim())
        self.field_output_density = DensityFieldHead(in_dim=self.mlp_base.get_out_dim())

    def forward(self, ray_samples: RaySamples):
        """Evaluates the field at points along the ray
        Args:
            xyz: ()
        # TODO(ethan): change the input to be something more abstracted
        e.g., a FieldInput structure
        """
        positions = ray_samples.positions
        directions = ray_samples.directions
        encoded_xyz = self.encoding_xyz(positions)
        encoded_dir = self.encoding_dir(directions)
        base_mlp_out = self.mlp_base(encoded_xyz)
        rgb_mlp_out = self.mlp_rgb(torch.cat([encoded_dir, base_mlp_out], dim=-1))

        field_rgb_output = self.field_output_rgb(rgb_mlp_out)
        field_density_out = self.field_output_density(base_mlp_out)

        field_outputs = {}
        field_outputs.update(field_rgb_output)
        field_outputs.update(field_density_out)
        return field_outputs


class NGPGraph(Graph):
    """Vanilla NeRF graph"""

    def __init__(
        self,
        intrinsics=None,
        camera_to_world=None,
        near_plane=1.0,
        far_plane=6.0,
        num_coarse_samples=64,
        num_importance_samples=128,
        **kwargs
    ) -> None:
        self.near_plane = near_plane
        self.far_plane = far_plane
        self.num_coarse_samples = num_coarse_samples
        self.num_importance_samples = num_importance_samples
        super().__init__(intrinsics=intrinsics, camera_to_world=camera_to_world, **kwargs)

    def populate_modules(self):
        # ray generator
        self.ray_generator = RayGenerator(self.intrinsics, self.camera_to_world)

        # samplers
        self.sampler_uniform = UniformSampler(
            near_plane=self.near_plane, far_plane=self.far_plane, num_samples=self.num_coarse_samples
        )
        self.sampler_pdf = PDFSampler(num_samples=self.num_importance_samples)

        # field
        self.field_coarse = NGPField()
        self.field_fine = NGPField()

        # renderers
        self.renderer_rgb = RGBRenderer()

        # losses
        self.rgb_loss = MSELoss()

    def get_param_groups(self) -> Dict[str, List[Parameter]]:
        """Obtain the parameter groups for the optimizers

        Returns:
            Dict[str, List[Parameter]]: Mapping of different parameter groups
        """
        param_groups = {}
        param_groups["fields"] = list(self.field_coarse.parameters()) + list(self.field_fine.parameters())
        return param_groups

    def forward(self, ray_indices: TensorType["num_rays", 3]):
        """Takes in the ray indices and renders out values."""
        # get the rays:
        ray_bundle = self.ray_generator.forward(ray_indices)  # RayBundle
        # coarse network:
        uniform_ray_samples = self.sampler_uniform(ray_bundle)  # RaySamples

        # time coarse network # xyz -> asd
        coarse_field_outputs = self.field_coarse(uniform_ray_samples)  # FieldOutputs
        # time end coarse network

        coarse_weights = uniform_ray_samples.get_weights(coarse_field_outputs[FieldHeadNames.DENSITY])

        coarse_renderer_outputs = self.renderer_rgb(
            rgb=coarse_field_outputs[FieldHeadNames.RGB],
            weights=coarse_weights,
        )  # RendererOutputs
        # fine network:
        pdf_ray_samples = self.sampler_pdf(uniform_ray_samples, coarse_weights)  # RaySamples
        fine_field_outputs = self.field_fine(pdf_ray_samples)  # FieldOutputs

        fine_weights = pdf_ray_samples.get_weights(fine_field_outputs[FieldHeadNames.DENSITY])

        fine_renderer_outputs = self.renderer_rgb(
            rgb=fine_field_outputs[FieldHeadNames.RGB],
            weights=fine_weights,
        )  # RendererOutputs
        # outputs:
        outputs = {"rgb_coarse": coarse_renderer_outputs.rgb, "rgb_fine": fine_renderer_outputs.rgb}
        return outputs

    def get_losses(self, batch, graph_outputs):
        # batch.pixels # (num_rays, 3)
        losses = {}
        rgb_loss_coarse = self.rgb_loss(batch["pixels"], graph_outputs["rgb_coarse"])
        rgb_loss_fine = self.rgb_loss(batch["pixels"], graph_outputs["rgb_fine"])
        losses = {"rgb_loss_coarse": rgb_loss_coarse, "rgb_loss_fine": rgb_loss_fine}
        return losses

mattport/nerf/graph/base.py METASEP
"""
The Graph module contains all trainable parameters.
"""
from abc import abstractmethod
from dataclasses import dataclass
from typing import Dict, List, Optional

from torch import nn
from torch.nn import Parameter
from torchtyping import TensorType


@dataclass
class GraphInputs:
    """Datastucture to encode the inputs to the graph."""

    points: float


@dataclass
class Node:
    """Node datastructure for graph composition."""

    name: str
    children: Dict[str, "Node"]
    parents: Dict[str, "Node"]
    visited_order: Optional[bool] = False
    visited_in_dim: Optional[bool] = False

    def __hash__(self):
        return hash(self.name)


class Graph(nn.Module):
    """_summary_"""

    def __init__(self, intrinsics=None, camera_to_world=None) -> None:
        super().__init__()
        self.intrinsics = intrinsics
        self.camera_to_world = camera_to_world
        self.populate_modules()  # populate the modules

    @abstractmethod
    def populate_modules(self):
        """Initializes the modules that are part of the network."""

    @abstractmethod
    def get_param_groups(self) -> Dict[str, List[Parameter]]:
        """Returns the parameter groups of the network in a dictionary format"""

    def get_in_dim(self, curr_node: Node) -> None:
        """Dynamically calculates and sets the input dimensions of the modules based on dependency graph

        Args:
            curr_node (Node): pointer to current node in process
        """
        curr_node.visited_in_dim = True
        if len(curr_node.parents) > 0:
            in_dim = 0
            for parent_name in curr_node.parents.keys():
                in_dim += self[parent_name].get_out_dim()
            self[curr_node.name].set_in_dim(in_dim)
            self.modules[curr_node.name].meta_data.in_dim = in_dim

        for child_node in curr_node.children.values():
            if not child_node.visited_in_dim:
                self.get_in_dim(child_node)

    @abstractmethod
    def forward(self, ray_indices: TensorType["num_rays", 3]):
        """Forward function that needs to be overridden."""

    @abstractmethod
    def get_losses(self, batch, graph_outputs):
        """Computes and returns the losses."""

mattport/nerf/field_modules/ray_generator.py METASEP
"""
Ray generator.
"""
from torch import nn
from torchtyping import TensorType
from mattport.structures.rays import RayBundle
from mattport.structures.cameras import get_camera_model


class RayGenerator(nn.Module):
    """torch.nn Module for generating rays.
    This class will store the intrinsics and extrinsics parameters of the cameras."""

    def __init__(
        self,
        intrinsics: TensorType["num_cameras", "num_intrinsics_params"],
        camera_to_world: TensorType["num_cameras", 3, 4],
    ) -> None:
        """_summary_

        Args:
            intrinsics (TensorType[&quot;num_cameras&quot;, &quot;num_intrinsics_params&quot;]):
                The intrinsics parameters.
            camera_to_world (TensorType[&quot;num_cameras&quot;, 3, 4]): Camera to world transformation matrix.
        """
        super().__init__()
        self.num_cameras, self.num_intrinsics_params = intrinsics.shape
        assert self.num_cameras >= 0
        self.intrinsics = nn.Parameter(intrinsics, requires_grad=False)
        self.camera_to_world = nn.Parameter(camera_to_world, requires_grad=False)
        # TODO(ethan): add learnable parameters that are deltas on the intrinsics and camera_to_world parameters

    def forward(self, ray_indices: TensorType["num_rays", 3]) -> RayBundle:
        """Index into the cameras to generate the rays.

        Args:
            x (_type_): _description_
        """
        c = ray_indices[:, 0]  # camera indices
        y = ray_indices[:, 1]  # row indices
        x = ray_indices[:, 2]  # col indices
        intrinsics = self.intrinsics[c]
        camera_to_world = self.camera_to_world[c]
        # NOTE(ethan): we currently assume all images have the same height and width
        camera_index = 0
        camera_class = get_camera_model(self.num_intrinsics_params)
        camera = camera_class(*self.intrinsics[camera_index].tolist())
        image_coords = camera.get_image_coords().to(ray_indices.device)
        coords = image_coords[y, x]

        ray_bundle = camera_class.generate_rays(intrinsics=intrinsics, camera_to_world=camera_to_world, coords=coords)
        ray_bundle.camera_indices = c
        return ray_bundle

mattport/nerf/field_modules/mlp.py METASEP
"""
Multi Layer Perceptron
"""
from typing import Tuple, Optional

import torch
from torch import nn
from torchtyping import TensorType

from mattport.nerf.field_modules.base import FieldModule


class MLP(FieldModule):
    """Multilayer perceptron"""

    def __init__(
        self,
        in_dim: int,
        out_dim: int,
        num_layers: int,
        layer_width: int = None,
        skip_connections: Tuple[int] = (),
        activation: Optional[nn.Module] = nn.ReLU(),
        out_activation: Optional[nn.Module] = None,
    ) -> None:
        """Initialize parameters of mulilayer perceptron.

        Args:
            in_dim (int): Input layer dimension
            out_dim (int): Ouput layer dimension
            num_layers (int): Number of network layers
            layer_width (int): Width of each MLP layer
            activation (Optional[nn.Module], optional): intermediate layer activation function. Defaults to nn.ReLU.
            out_activation (Optional[nn.Module], optional): output activation function. Defaults to None.
        """
        super().__init__()
        self.in_dim = in_dim
        assert self.in_dim > 0
        self.out_dim = out_dim
        self.num_layers = num_layers
        self.layer_width = layer_width
        self.skip_connections = skip_connections
        self.activation = activation
        self.out_activation = out_activation
        self.net = None
        self.build_nn_modules()

    def build_nn_modules(self) -> None:
        """Initialize multi-layer perceptron."""
        layers = []
        if self.num_layers == 1:
            layers.append(nn.Linear(self.in_dim, self.out_dim))
        else:
            for i in range(self.num_layers - 1):
                if i == 0:
                    assert i not in list(self.skip_connections), "Skip connection at layer 0 doesn't make sense."
                    layers.append(nn.Linear(self.in_dim, self.layer_width))
                elif i in self.skip_connections:
                    layers.append(nn.Linear(self.layer_width + self.in_dim, self.layer_width))
                else:
                    layers.append(nn.Linear(self.layer_width, self.layer_width))
            layers.append(nn.Linear(self.layer_width, self.out_dim))
        self.layers = nn.ModuleList(layers)

    def forward(self, in_tensor: TensorType[..., "in_dim"]) -> TensorType[..., "out_dim"]:
        """Process input with a multilayer perceptron.

        Args:
            in_tensor (TensorType[..., "in_dim]): Network input

        Returns:
            TensorType[..., "out_dim"]: Network output
        """
        x = in_tensor
        for i, layer in enumerate(self.layers):
            if i in self.skip_connections:
                x = torch.cat([in_tensor, x], -1)
            x = layer(x)
            if self.activation:
                x = self.activation(x)
        return x

mattport/nerf/field_modules/field_losses.py METASEP

mattport/nerf/field_modules/field_heads.py METASEP
"""
Collection of render heads
"""
from enum import Enum
from typing import Dict, Optional

from torch import nn
from torchtyping import TensorType

from mattport.nerf.field_modules.base import FieldModule


class FieldHeadNames(Enum):
    """Possible field outputs"""

    RGB = "rgb"
    DENSITY = "density"


class FieldHead(FieldModule):
    """Base field output"""

    def __init__(
        self, in_dim: int, out_dim: int, field_head_name: FieldHeadNames, activation: Optional[nn.Module] = None
    ) -> None:
        """
        Args:
            in_dim (int): input dimension
            out_dim (int): output dimension for renderer
            field_head_name (FieldHeadNames): name of field head
            activation (Optional[nn.Module]): output head activation
        """
        super().__init__()
        self.in_dim = in_dim
        self.out_dim = out_dim
        # if field_head_name not in FieldHeadNames:
        #     raise ValueError("Incorrect field output name. Should be one of FieldHeadNames.")
        self.field_head_name = field_head_name
        self.activation = activation
        self.net = None
        self.build_nn_modules()

    def build_nn_modules(self) -> None:
        layers = [nn.Linear(self.in_dim, self.out_dim)]
        if self.activation:
            layers.append(self.activation)
        self.net = nn.Sequential(*layers)

    def forward(self, in_tensor: TensorType[..., "in_dim"]) -> Dict[FieldHeadNames, TensorType]:
        """Process network output for renderer

        Args:
            in_tensor (TensorType[..., "in_dim]): Network input

        Returns:
            TensorType[..., "out_dim"]: Render head output
        """
        if not self.field_head_name:
            raise ValueError("field_quantity_name should be set in the child class. E.g., as 'rgb' or 'density'.")
        if not self.net:
            raise SystemError("Render head network not initialized. build_nn_modules() should be called.")
        out_tensor = self.net(in_tensor)
        return {self.field_head_name: out_tensor}


class DensityFieldHead(FieldHead):
    """Density output"""

    def __init__(self, in_dim: int, activation: Optional[nn.Module] = nn.Softplus()) -> None:
        super().__init__(in_dim, out_dim=1, field_head_name=FieldHeadNames.DENSITY, activation=activation)


class RGBFieldHead(FieldHead):
    """RGB output"""

    def __init__(self, in_dim: int, activation: Optional[nn.Module] = nn.Sigmoid()) -> None:
        super().__init__(in_dim, out_dim=3, field_head_name=FieldHeadNames.RGB, activation=activation)

mattport/nerf/field_modules/encoding.py METASEP
"""
Encoding functions
"""
import numpy as np
import torch
from torchtyping import TensorType
from mattport.nerf.field_modules.base import FieldModule


class Encoding(FieldModule):
    """Encode an input tensor. Intended to be subclassed"""

    def __init__(self, in_dim: int) -> None:
        """
        Args:
            in_dim (int): Input dimension of tensor
        """
        super().__init__()
        if in_dim <= 0:
            raise ValueError("Input dimension should be greater than zero")
        self.in_dim = in_dim

    def encode(self, in_tensor: TensorType[..., "input_dim"]) -> TensorType[..., "output_dim"]:
        """Encodes an input tensor.

        Args:
            x (TensorType[..., "input_dim"]): Input tensor to be encoded

        Returns:
            TensorType[..., "output_dim"]: A encoded input tensor
        """
        raise NotImplementedError

    def forward(self, in_tensor: TensorType[..., "input_dim"]) -> TensorType[..., "output_dim"]:
        """Call forward"""
        return self.encode(in_tensor)


class ScalingAndOffset(Encoding):
    """Simple scaling and offet to input"""

    def __init__(self, in_dim: int, scaling: float = 1.0, offset: float = 0.0) -> None:
        """Each input is scaled then offset

        Args:
            in_dim (int): Input dimension of tensor
            scaling (float, optional): Scaling applied to tensor. Defaults to 1.0.
            offset (float, optional): Offset applied to tensor. Defaults to 0.0.
        """
        super().__init__(in_dim)

        self.scaling = scaling
        self.offset = offset

    def get_out_dim(self) -> int:
        return self.in_dim

    def encode(self, in_tensor: TensorType[..., "input_dim"]) -> TensorType[..., "output_dim"]:
        return self.scaling * in_tensor + self.offset


class NeRFEncoding(Encoding):
    """Multi-scale sinousoidal encoding proposed in the original NeRF paper"""

    def __init__(
        self, in_dim: int, num_frequencies: int, min_freq_exp: float, max_freq_exp: float, include_input: bool = False
    ) -> None:
        """Each axis is encoded with frequencies ranging from 2^min_freq_exp to 2^max_freq_exp.

        Args:
            in_dim (int): Input dimension of tensor
            num_frequencies (int): Number of encoded frequencies per axis
            min_freq_exp (float): Minimum frequency exponent
            max_freq_exp (float): Maximum frequency exponent
            include_input (float): Append the input coordinate to the encoding
        """
        super().__init__(in_dim)

        self.num_frequencies = num_frequencies
        self.min_freq = min_freq_exp
        self.max_freq = max_freq_exp
        self.include_input = include_input

    def get_out_dim(self) -> int:
        out_dim = self.in_dim * self.num_frequencies * 2
        if self.include_input:
            out_dim += self.in_dim
        return out_dim

    def encode(self, in_tensor: TensorType[..., "input_dim"]) -> TensorType[..., "output_dim"]:
        """
        Args:
            in_tensor (TensorType[..., "input_dim"]): For best performance, the input tensor should be between 0 and 1.

        Returns:
            TensorType[..., "output_dim"]: Output values will be between -1 and 1
        """
        freqs = 2 ** torch.linspace(self.min_freq, self.max_freq, self.num_frequencies).to(in_tensor.device)
        scaled_inputs = 2 * torch.pi * in_tensor[..., None] * freqs  # [..., "input_dim", "num_scales"]
        scaled_inputs = scaled_inputs.view(*scaled_inputs.shape[:-2], -1)  # [..., "input_dim" * "num_scales"]
        encoded_inputs = torch.cat([torch.sin(scaled_inputs), torch.cos(scaled_inputs)], axis=-1)
        if self.include_input:
            encoded_inputs = torch.cat([encoded_inputs, in_tensor], axis=-1)
        return encoded_inputs


class RFFEncoding(Encoding):
    """Random Fourier Feature encoding"""

    def __init__(self, in_dim: int, num_frequencies: int, scale: float) -> None:
        """

        Args:
            in_dim (int): Input dimension of tensor
            num_frequencies (int): Number of encoding frequencies
            scale (float): Std of Gaussian to sample frequencies. Must be greater than zero
        """
        super().__init__(in_dim)

        self.num_frequencies = num_frequencies
        if not scale > 0:
            raise ValueError("RFF encoding scale should be greater than zero")
        self.scale = scale
        self.b_matrix = torch.normal(mean=0, std=self.scale, size=(self.in_dim, self.num_frequencies))

    def get_out_dim(self) -> int:
        return self.num_frequencies * 2

    def encode(self, in_tensor: TensorType[..., "input_dim"]) -> TensorType[..., "output_dim"]:
        """
        Args:
            in_tensor (TensorType[..., "input_dim"]): For best performance, the input tensor should be between 0 and 1.

        Returns:
            TensorType[..., "output_dim"]: Output values will be between -1 and 1
        """
        scaled_inputs = in_tensor @ self.b_matrix  # [..., "num_frequencies"]
        encoded_inputs = torch.cat([torch.sin(scaled_inputs), torch.cos(scaled_inputs)], axis=-1)
        return encoded_inputs


class HashEncoding(Encoding):
    """Hash encoding"""

    def __init__(
        self,
        num_levels: int = 16,
        min_res: int = 16,
        max_res: int = 1024,
        hash_table_size: int = 2**19,
        features_per_level: int = 2,
        hash_init_scale: float = 0.001,
    ) -> None:
        """_summary_

        Args:
            num_levels (int, optional): Number of feature grids. Defaults to 16.
            min_res (int, optional): Resolution of smallest feature grid. Defaults to 16.
            max_res (int, optional): Resolution of largest feature grid. Defaults to 1024.
            hash_table_size (int, optional): Size of hash table. Defaults to 2**19.
            features_per_level (int, optional): Number of features per level. Defaults to 2.
            hash_init_scale (float, optional): Value to initialize hash grid. Defaults to 0.001.
        """
        super().__init__(in_dim=3)
        self.num_levels = num_levels
        self.features_per_level = features_per_level
        self.hash_table_size = hash_table_size

        levels = torch.arange(num_levels)
        growth_factor = np.exp((np.log(max_res) - np.log(min_res)) / (num_levels - 1))
        self.scalings = torch.floor(min_res * growth_factor**levels)

        self.hash_offset = levels * hash_table_size
        self.hash_table = torch.rand(size=(hash_table_size * num_levels, features_per_level)) * 2 - 1
        self.hash_table *= hash_init_scale
        self.hash_table = torch.nn.Parameter(self.hash_table)

    def get_out_dim(self) -> int:
        return self.num_levels * self.features_per_level

    def hash_fn(self, in_tensor: TensorType[..., "num_levels", 3]) -> TensorType[..., "num_levels"]:
        """Hash tensor using method described in Instant-NGP

        Args:
            in_tensor (TensorType[..., &quot;num_levels&quot;, 3]): Tensor to be hashed

        Returns:
            TensorType[..., &quot;num_levels&quot;]: Hashed tensor
        """
        in_tensor = in_tensor * torch.tensor([1, 2654435761, 805459861]).to(in_tensor.device)
        x = torch.bitwise_xor(in_tensor[..., 0], in_tensor[..., 1])
        x = torch.bitwise_xor(x, in_tensor[..., 2])
        x %= self.hash_table_size
        x += self.hash_offset.to(x.device)
        return x

    def encode(self, in_tensor: TensorType[..., "input_dim"]) -> TensorType[..., "output_dim"]:
        assert in_tensor.shape[-1] == 3

        in_tensor = in_tensor[..., None, :]  # [..., 1, 3]
        scaled = in_tensor * self.scalings.view(1, 1, -1, 1).to(in_tensor.device)  # [..., L, 3]
        scaled_c = torch.ceil(scaled).type(torch.int32)
        scaled_f = torch.floor(scaled).type(torch.int32)

        offset = scaled - scaled_f

        hashed_0 = self.hash_fn(scaled_c)  # [..., num_levels]
        hashed_1 = self.hash_fn(torch.cat([scaled_c[..., 0:1], scaled_f[..., 1:2], scaled_c[..., 2:3]], dim=-1))
        hashed_2 = self.hash_fn(torch.cat([scaled_f[..., 0:1], scaled_f[..., 1:2], scaled_c[..., 2:3]], dim=-1))
        hashed_3 = self.hash_fn(torch.cat([scaled_f[..., 0:1], scaled_c[..., 1:2], scaled_c[..., 2:3]], dim=-1))
        hashed_4 = self.hash_fn(torch.cat([scaled_c[..., 0:1], scaled_c[..., 1:2], scaled_f[..., 2:3]], dim=-1))
        hashed_5 = self.hash_fn(torch.cat([scaled_c[..., 0:1], scaled_f[..., 1:2], scaled_f[..., 2:3]], dim=-1))
        hashed_6 = self.hash_fn(scaled_f)
        hashed_7 = self.hash_fn(torch.cat([scaled_f[..., 0:1], scaled_c[..., 1:2], scaled_f[..., 2:3]], dim=-1))

        f_0 = self.hash_table[hashed_0]  # [..., num_levels, features_per_level]
        f_1 = self.hash_table[hashed_1]
        f_2 = self.hash_table[hashed_2]
        f_3 = self.hash_table[hashed_3]
        f_4 = self.hash_table[hashed_4]
        f_5 = self.hash_table[hashed_5]
        f_6 = self.hash_table[hashed_6]
        f_7 = self.hash_table[hashed_7]

        f_03 = f_0 * offset[..., 0:1] + f_3 * (1 - offset[..., 0:1])
        f_12 = f_1 * offset[..., 0:1] + f_2 * (1 - offset[..., 0:1])
        f_56 = f_5 * offset[..., 0:1] + f_6 * (1 - offset[..., 0:1])
        f_47 = f_4 * offset[..., 0:1] + f_7 * (1 - offset[..., 0:1])

        f0312 = f_03 * offset[..., 1:2] + f_12 * (1 - offset[..., 1:2])
        f4756 = f_47 * offset[..., 1:2] + f_56 * (1 - offset[..., 1:2])

        encoded_value = f0312 * offset[..., 2:3] + f4756 * (
            1 - offset[..., 2:3]
        )  # [..., num_levels, features_per_level]

        return torch.flatten(encoded_value, start_dim=-2, end_dim=-1)  # [..., num_levels * features_per_level]

mattport/nerf/field_modules/base.py METASEP
"""
The field module baseclass.
"""
from abc import abstractmethod

from torch import nn
from torchtyping import TensorType


class FieldModule(nn.Module):
    """Field modules that can be combined to store and compute the fields."""

    def __init__(self) -> None:
        """Default initialization of module"""
        super().__init__()
        self.in_dim = 0
        self.out_dim = 0

    def build_nn_modules(self) -> None:
        """Function instantiates any torch.nn members within the module.
        If none exist, do nothing."""

    def set_in_dim(self, in_dim: int) -> None:
        """Sets input dimension of encoding

        Args:
            in_dim (int): input dimension
        """
        if in_dim <= 0:
            raise ValueError("Input dimension should be greater than zero")
        self.in_dim = in_dim

    def get_out_dim(self) -> int:
        """Calculates output dimension of encoding.

        Returns:
            int: output dimension
        """
        if not hasattr(self, "out_dim"):
            raise ValueError("Output dimension has not been set")
        if self.out_dim <= 0:
            raise ValueError("Output dimension should be greater than zero")
        return self.out_dim

    @abstractmethod
    def forward(self, in_tensor: TensorType[..., "input_dim"]) -> TensorType[..., "output_dim"]:
        """_summary_

        Args:
            x (TensorType[..., "input_dim"]): Input tensor to be encoded

        Raises:
            NotImplementedError: _description_

        Returns:
            TensorType[..., "output_dim"]: A encoded input tensor
        """
        raise NotImplementedError

mattport/nerf/field_modules/__init__.py METASEP
"""init field modules"""
from .encoding import Encoding, ScalingAndOffset
from .mlp import MLP
from .base import FieldModule

mattport/nerf/trainer.py METASEP
"""
Code to train model.
"""
import logging
import os
from time import time
from typing import Dict

import torch
import torch.distributed as dist
from hydra.utils import instantiate
from omegaconf import DictConfig
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader

import mattport.utils.writer
from mattport.nerf.dataset.collate import CollateIterDataset, collate_batch_size_one
from mattport.nerf.dataset.image_dataset import ImageDataset, collate_batch
from mattport.nerf.dataset.utils import DatasetInputs, get_dataset_inputs_dict
from mattport.nerf.metrics import get_psnr
from mattport.nerf.optimizers import Optimizers
from mattport.utils import profiler
from mattport.utils.decorators import check_main_thread
from mattport.utils.stats_tracker import Stats, StatsTracker

logging.getLogger("PIL").setLevel(logging.WARNING)


class Trainer:
    """Training class"""

    def __init__(self, config: DictConfig, local_rank: int = 0, world_size: int = 1, cpu=False):
        """_summary_

        Args:
            config (DictConfig): _description_
            local_rank (int, optional): _description_. Defaults to 0.
            world_size (int, optional): _description_. Defaults to 1.
            cpu (bool, optional): Whether or not to use the CPU.
        """
        self.config = config
        self.local_rank = local_rank
        self.world_size = world_size
        # dataset variables
        self.train_image_dataset = None
        self.train_dataset = None
        self.train_dataloader = None
        self.val_image_dataset = None
        # model variables
        self.graph = None
        self.optimizers = None
        self.start_step = 0
        # logging variables
        self.is_main_thread = world_size != 0 and local_rank % world_size == 0
        writer = getattr(mattport.utils.writer, self.config.logging.writer.type)
        self.writer = writer(self.is_main_thread, save_dir=self.config.logging.writer.save_dir)
        self.stats = StatsTracker(config, self.is_main_thread)
        if not profiler.PROFILER and self.config.logging.enable_profiler:
            profiler.PROFILER = profiler.Profiler(config, self.is_main_thread)
        self.device = f"cuda:{self.local_rank}" if not cpu else "cpu"

    @profiler.time_function
    def setup(self):
        """Setup the Trainer by calling other setup functions."""
        dataset_inputs_dict = get_dataset_inputs_dict(**self.config.data.dataset)
        self.setup_datasets(dataset_inputs_dict)
        self.setup_graph(dataset_inputs_dict["train"])

    def collate_fn(self, batch_list):
        """TODO(ethan): I need to replace this.
        I'm only using this for multiprocess pickle issues for now.
        """
        return collate_batch(batch_list, self.config.data.dataloader.num_rays_per_batch, keep_full_image=False)

    @profiler.time_function
    def setup_datasets(self, dataset_inputs_dict: Dict[str, DatasetInputs]):
        """_summary_"""
        self.train_image_dataset = ImageDataset(
            image_filenames=dataset_inputs_dict["train"].image_filenames,
            downscale_factor=dataset_inputs_dict["train"].downscale_factor,
            alpha_color=dataset_inputs_dict["train"].alpha_color,
        )

        self.train_dataset = CollateIterDataset(
            self.train_image_dataset,
            collate_fn=self.collate_fn,
            num_samples_to_collate=self.config.data.dataloader.num_images_to_sample_from,
            num_times_to_repeat=self.config.data.dataloader.num_times_to_repeat_images,
        )
        self.train_dataloader = DataLoader(
            self.train_dataset,
            batch_size=1,
            num_workers=self.config.data.dataloader.num_workers,
            collate_fn=collate_batch_size_one,
            pin_memory=True,
        )
        self.val_image_dataset = ImageDataset(
            image_filenames=dataset_inputs_dict["val"].image_filenames,
            downscale_factor=dataset_inputs_dict["val"].downscale_factor,
            alpha_color=dataset_inputs_dict["val"].alpha_color,
        )

    @profiler.time_function
    def setup_graph(self, dataset_inputs: DatasetInputs):
        """Setup the graph. The dataset inputs should be set with the training data.

        Args:
            dataset_inputs (DatasetInputs): The inputs which will be used to define the camera parameters.
        """
        self.graph = instantiate(
            self.config.graph.network,
            intrinsics=dataset_inputs.intrinsics,
            camera_to_world=dataset_inputs.camera_to_world,
        ).to(self.device)
        self.setup_optimizers()

        if self.config.graph.resume_train.load_dir:
            self.load_checkpoint(self.config.graph.resume_train)

        if self.world_size > 1:
            self.graph = DDP(self.graph, device_ids=[self.local_rank])
            dist.barrier(device_ids=[self.local_rank])

    def setup_optimizers(self):
        """_summary_"""
        # TODO(ethan): handle different world sizes
        self.optimizers = Optimizers(self.config.graph.param_groups, self.graph.get_param_groups())

    def load_checkpoint(self, load_config: DictConfig) -> int:
        """Load the checkpoint from the given path

        Args:
            load_path (str): path from which to load the model

        Returns:
            int: step iteration associated with the loaded checkpoint
        """
        load_path = os.path.join(load_config.load_dir, f"step-{load_config.load_step:09d}.ckpt")
        assert os.path.exists(load_path), f"Checkpoint {load_path} does not exist"
        loaded_state = torch.load(load_path, map_location="cpu")
        self.graph.load_state_dict({key.replace("module.", ""): value for key, value in loaded_state["model"].items()})
        for k, v in loaded_state["optimizers"].items():
            self.optimizers.optimizers[k].load_state_dict(v)
        self.start_step = loaded_state["step"] + 1
        logging.info("done loading checkpoint from %s", load_path)

    @check_main_thread
    def save_checkpoint(self, output_dir: str, step: int) -> None:
        """Save the model and optimizers

        Args:
            output_dir (str): directory to save the checkpoint
            step (int): number of steps in training for given checkpoint
            model (Graph): Graph model to be saved
            optimizers (Optimizers): Optimizers to be saved
        """
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)
        ckpt_path = os.path.join(output_dir, f"step-{step:09d}.ckpt")
        torch.save(
            {
                "step": step,
                "model": self.graph.module.state_dict() if hasattr(self.graph, "module") else self.graph.state_dict(),
                "optimizers": {k: v.state_dict() for (k, v) in self.optimizers.optimizers.items()},
            },
            ckpt_path,
        )

    @classmethod
    def get_aggregated_loss(cls, losses: Dict[str, torch.tensor]):
        """Returns the aggregated losses and the scalar for calling .backwards() on.
        # TODO: move this out to another file/class/etc.
        """
        loss_sum = 0.0
        loss_dict = {}
        for loss_name in losses.keys():
            # TODO(ethan): add loss weightings here from a config
            loss_sum += losses[loss_name]
            loss_dict[loss_name] = float(losses[loss_name])
        return loss_sum, loss_dict

    def train(self) -> None:
        """_summary_"""
        train_start = time()
        num_iterations = self.config.graph.max_num_iterations
        iter_dataset = iter(self.train_dataloader)
        for i, step in enumerate(range(self.start_step, self.start_step + num_iterations)):
            data_start = time()
            batch = next(iter_dataset)
            self.stats.update_time(Stats.ITER_LOAD_TIME, data_start, time(), step=step)

            iter_start = time()
            loss_dict = self.train_iteration(batch, step)
            self.stats.update_time(
                Stats.RAYS_PER_SEC, iter_start, time(), step=step, batch_size=batch["indices"].shape[0]
            )
            self.stats.update_time(Stats.ITER_TRAIN_TIME, iter_start, time(), step=step)

            if step != 0 and step % self.config.logging.steps_per_log == 0:
                self.writer.write_scalar_dict(loss_dict, step, group="Loss", prefix="train-")
                # TODO: add the learning rates to tensorboard/logging
            if step != 0 and self.config.graph.steps_per_save and step % self.config.graph.steps_per_save == 0:
                self.save_checkpoint(self.config.graph.model_dir, step)
            if step % self.config.graph.steps_per_test == 0:
                for image_idx in self.config.data.validation_image_indices:
                    self.test_image(image_idx=image_idx, step=step)
            self.stats.print_stats(i / num_iterations)

        self.stats.update_time(Stats.TOTAL_TRAIN_TIME, train_start, time(), step=-1)
        self.stats.print_stats(-1)

    @profiler.time_function
    def train_iteration(self, batch: dict, step: int):
        """Run one iteration with a batch of inputs."""
        # move batch to correct device
        ray_indices = batch["indices"].to(self.device)
        graph_outputs = self.graph(ray_indices)
        batch["pixels"] = batch["pixels"].to(self.device)
        losses = (
            self.graph.module.get_losses(batch, graph_outputs)
            if hasattr(self.graph, "module")
            else self.graph.get_losses(batch, graph_outputs)
        )
        loss_sum, loss_dict = self.get_aggregated_loss(losses)
        self.optimizers.zero_grad_all()
        loss_sum.backward()
        self.optimizers.scheduler_step_all(step)  # NOTE(ethan): I think the scheduler needs to know what step we are on
        self.optimizers.optimizer_step_all()
        return loss_dict

    @profiler.time_function
    def test_image(self, image_idx, step):
        """Test a specific image."""
        image = self.val_image_dataset[image_idx]["image"]  # ground truth
        image_height, image_width, _ = image.shape
        pixel_coords = torch.meshgrid(torch.arange(image_height), torch.arange(image_width), indexing="ij")
        pixel_coords = torch.stack(pixel_coords, dim=-1).long()
        all_ray_indices = torch.cat([torch.ones_like(pixel_coords[..., :1]) * image_idx, pixel_coords], dim=-1).view(
            -1, 3
        )
        with torch.no_grad():
            num_rays = all_ray_indices.shape[0]
            chunk_size = 1024
            rgb_coarse = []
            rgb_fine = []
            accumulation_coarse = []
            accumulation_fine = []
            for i in range(0, num_rays, chunk_size):
                ray_indices = all_ray_indices[i : i + chunk_size].to(self.device)
                graph_outputs = self.graph(ray_indices)
                rgb_coarse.append(graph_outputs["rgb_coarse"])
                rgb_fine.append(graph_outputs["rgb_fine"])
                accumulation_coarse.append(graph_outputs["accumulation_coarse"])
                accumulation_fine.append(graph_outputs["accumulation_fine"])
            rgb_coarse = torch.cat(rgb_coarse).view(image_height, image_width, 3).detach().cpu()
            rgb_fine = torch.cat(rgb_fine).view(image_height, image_width, 3).detach().cpu()
            accumulation_coarse = torch.cat(accumulation_coarse).view(image_height, image_width, 1).detach().cpu()
            accumulation_fine = torch.cat(accumulation_fine).view(image_height, image_width, 1).detach().cpu()

        combined_image = torch.cat([image, rgb_coarse, rgb_fine], dim=1)
        self.writer.write_image(f"image_idx_{image_idx}-rgb_coarse_fine", combined_image, step, group="val_img")

        combined_image = torch.cat([accumulation_coarse, accumulation_fine], dim=1)
        self.writer.write_image(f"image_idx_{image_idx}", combined_image, step, group="val_accumulation")

        coarse_psnr = get_psnr(image, rgb_coarse)
        self.writer.write_scalar(f"image_idx_{image_idx}", float(coarse_psnr), step, group="val")

        fine_psnr = get_psnr(image, rgb_fine)
        self.stats.update_value(Stats.CURR_TEST_PSNR, float(fine_psnr), step)
        self.writer.write_scalar(f"image_idx_{image_idx}-fine_psnr", float(fine_psnr), step, group="val")

mattport/nerf/sampler.py METASEP
"""
Collection of sampling strategies
"""

from typing import Optional
import torch
from torch import nn
from torchtyping import TensorType

from mattport.structures.rays import RayBundle, RaySamples


class UniformSampler(nn.Module):
    """Sample uniformly along a ray"""

    def __init__(self, near_plane: float, far_plane: float, num_samples: int, train_stratified=True) -> None:
        """
        Args:
            near_plane (float): Minimum distance along ray to sample
            far_plane (float): Maximum distance along ray to sample
            num_samples (int): Number of samples per ray
            train_stratified (bool): Use stratified sampling during training. Defults to True
        """
        super().__init__()

        self.near_plane = near_plane
        self.far_plane = far_plane
        self.num_samples = num_samples
        self.train_stratified = train_stratified

    @torch.no_grad()
    def forward(
        self,
        ray_bundle: RayBundle,
        near_plane: Optional[float] = None,
        far_plane: Optional[float] = None,
        num_samples: Optional[int] = None,
    ) -> RaySamples:
        """Generates position samples uniformly.

        Args:
            ray_bundle (RayBundle): Rays to generate samples for
            TensorType[..., "output_dim"]: A encoded input tensor
            near_plane (Optional[float]): Minimum distance along ray to sample
            far_plane (Optional[float]): Maximum distance along ray to sample
            num_samples (Optional[int]): Number of samples per ray

        Returns:
            RaySamples: Positions and deltas for samples along a ray
        """
        near_plane = near_plane or self.near_plane
        far_plane = far_plane or self.far_plane
        num_samples = num_samples or self.num_samples

        num_rays = ray_bundle.origins.shape[0]

        bins = torch.linspace(near_plane, far_plane, num_samples + 1).to(ray_bundle.origins.device)  # (num_samples+1)

        if self.train_stratified and self.training:
            t_rand = torch.rand((num_rays, num_samples), dtype=bins.dtype, device=bins.device)
            ts = bins[None, :-1] + t_rand * (bins[None, 1:] - bins[None, :-1])  # (num_rays, num_samples)
        else:
            ts = (bins[1:] + bins[:-1]) / 2
            ts = ts.unsqueeze(0).repeat(num_rays, 1)  # (num_rays, num_samples)

        ray_samples = RaySamples(
            ts=ts,
            ray_bundle=ray_bundle,
        )

        return ray_samples


class PDFSampler(nn.Module):
    """Sample based on probability distribution"""

    def __init__(self, num_samples: int, train_stratified: bool = True, include_original: bool = True) -> None:
        """
        Args:
            num_samples (int): Number of samples per ray
            train_stratified: boolean: Randomize location within each bin during training. Defaults to True
            include_original: Add original samples to ray. Defaults to True
        """
        super().__init__()

        self.num_samples = num_samples
        self.include_original = include_original
        self.train_stratified = train_stratified

    @torch.no_grad()
    def forward(
        self,
        coarse_ray_samples: RaySamples,
        weights: TensorType[..., "num_samples"],
        num_samples: Optional[int] = None,
        eps: float = 1e-5,
    ) -> RaySamples:
        """Generates position samples given a distribution.

        Args:
            ray_bundle (RayBundle): Rays to generate samples for
            TensorType[..., "output_dim"]: A encoded input tensor
            bins (TensorType[..., "num_samples"]): Ray bins
            weights: (TensorType[..., "num_samples"]): Weights for each bin
            num_samples (Optional[int]): Number of samples per ray
            eps: float: Small value to prevent numerical issues. Defaults to 1e-5

        Returns:
            RaySamples: Positions and deltas for samples along a ray
        """
        num_samples = num_samples or self.num_samples
        weights = weights[..., :-1]

        # Add small offset to rays with zero weight to prevent NaNs
        weights_sum = torch.sum(weights, dim=-1, keepdim=True)
        padding = torch.relu(eps - weights_sum)
        weights = weights + padding / weights.shape[-1]
        weights_sum += padding

        pdf = weights / weights_sum
        cdf = torch.min(torch.ones_like(pdf), torch.cumsum(pdf, dim=-1))
        cdf = torch.cat([torch.zeros_like(cdf[..., :1]), cdf], dim=-1)

        if self.train_stratified and self.training:
            u = torch.rand(size=(*cdf.shape[:-1], num_samples), device=cdf.device)
        else:
            u = torch.linspace(0.0, 1.0, steps=num_samples, device=cdf.device)
            u = torch.expand(size=(*cdf.shape[:-1], num_samples))

        u = u.contiguous()
        indicies = torch.searchsorted(cdf, u, right=True)
        below = torch.max(torch.zeros_like(indicies), indicies - 1)
        above = torch.min(cdf.shape[-1] - torch.ones_like(indicies), indicies)

        indicies_g = torch.stack([below, above], -1)
        matched_shape = (indicies_g.shape[0], indicies_g.shape[1], cdf.shape[-1])
        cdf_g = torch.gather(cdf.unsqueeze(1).expand(matched_shape), dim=2, index=indicies_g)
        bins_g = torch.gather(coarse_ray_samples.ts.unsqueeze(1).expand(matched_shape), dim=2, index=indicies_g)

        denom = cdf_g[..., 1] - cdf_g[..., 0]
        denom = torch.where(denom < eps, torch.ones_like(denom), denom)
        t = (u - cdf_g[..., 0]) / denom
        ts = bins_g[..., 0] + t * (bins_g[..., 1] - bins_g[..., 0])

        if self.include_original:
            ts, _ = torch.sort(torch.cat([coarse_ray_samples.ts, ts], -1), -1)
        else:
            ts, _ = torch.sort(ts, -1)

        # Stop gradients
        ts = ts.detach()

        ray_samples = RaySamples(
            ts=ts,
            ray_bundle=coarse_ray_samples.ray_bundle,
        )

        return ray_samples

mattport/nerf/renderers.py METASEP
"""
Collection of renderers
"""
from dataclasses import dataclass
from typing import Optional

import torch
from torch import nn
from torchtyping import TensorType


@dataclass
class RendererOutputs:
    """_summary_"""

    rgb: TensorType["num_rays", 3] = None
    density: TensorType["num_rays", 1] = None
    accumulation: TensorType["num_rays", 1] = None


class RGBRenderer(nn.Module):
    """Standard volumetic rendering."""

    def __init__(self, background_color: Optional[TensorType[3]] = None) -> None:
        """
        Args:
            background_color (TensorType[3], optional): Background color as RGB. Defaults to black.
        """
        super().__init__()
        self.background_color = background_color

    def forward(
        self,
        rgb: TensorType[..., "num_samples", 3],
        weights: TensorType[..., "num_samples"],
    ) -> RendererOutputs:
        """Composite samples along ray and render color image

        Args:
            rgb (TensorType[..., "num_samples", -1]): RGB for each sample
            weights (TensorType[..., "num_samples"]): Weights for each sample

        Returns:
            RendererOutputs: Outputs with rgb values.
        """

        rgb = torch.sum(weights[..., None] * rgb, dim=-2)

        if self.background_color is not None:
            rgb = rgb + self.background_color.to(weights.device)[None, ...] * (
                1.0 - torch.sum(weights, dim=-1)[..., None]
            )

        assert torch.max(rgb) <= 1.0
        assert torch.min(rgb) >= 0.0

        renderer_outputs = RendererOutputs(rgb=rgb)
        return renderer_outputs


class AccumulationRenderer(nn.Module):
    """Accumulated value along a ray."""

    @classmethod
    def forward(
        cls,
        weights: TensorType[..., "num_samples"],
    ) -> RendererOutputs:
        """Composite samples along ray and calculate accumulation.

        Args:
            weights (TensorType[..., "num_samples"]): Weights for each sample

        Returns:
            RendererOutputs: Outputs with accumulated values.
        """

        accumulation = torch.sum(weights, dim=-1)[..., None]

        renderer_outputs = RendererOutputs(accumulation=accumulation)
        return renderer_outputs

mattport/nerf/optimizers.py METASEP
"""
Optimizers class.
"""

from typing import Dict, List
from omegaconf import DictConfig
import torch
from torch.nn import Parameter


class Optimizers:
    """_summary_"""

    def __init__(self, config: DictConfig, param_groups: Dict[str, List[Parameter]]):
        """_summary_

        Args:
            config (DictConfig): _description_
            param_dict (Dict[str, List[Parameter]]): _description_
        """
        self.config = config
        self.optimizers = {}
        self.schedulers = {}
        for param_name, params in param_groups.items():
            optimizer_config = config[param_name].optimizer
            optimizer = getattr(torch.optim, optimizer_config.type)
            kwargs = {k: v for k, v in optimizer_config.items() if k != "type"}
            self.optimizers[param_name] = optimizer(params, **kwargs)
            if config[param_name].scheduler:
                scheduler_config = config[param_name].scheduler
                scheduler = getattr(torch.optim.lr_scheduler, scheduler_config.type)
                kwargs = {k: v for k, v in scheduler_config.items() if k != "type"}
                self.schedulers[param_name] = scheduler(self.optimizers[param_name], **kwargs)

    def optimizer_step(self, param_name: str) -> None:
        """fetch and step corresponding optimizer

        Args:
            param_name (str): name of optimizer to step forward
        """
        self.optimizers[param_name].step()

    def scheduler_step(self, param_name: str) -> None:
        """fetch and step corresponding scheduler

        Args:
            param_name (str): name of scheduler to step forward
        """
        if self.config.param_name.scheduler:
            self.schedulers[param_name].step()

    def zero_grad_all(self):
        """zero the gradients for all optimizer parameters"""
        for _, optimizer in self.optimizers.items():
            optimizer.zero_grad()

    def optimizer_step_all(self):
        """Run step for all optimizers."""
        for _, optimizer in self.optimizers.items():
            # note that they key is the parameter name
            optimizer.step()

    def scheduler_step_all(self, step):
        """Run step for all schedulers."""
        # TODO: this needs to be implemented

mattport/nerf/metrics.py METASEP
"""
Collection of metrics.
"""

import torch


def get_psnr(image_gt, image_pred):
    """
    Returns the psnr for an image. TODO: add masking
    """
    mean = torch.mean((image_gt - image_pred) ** 2)
    return -10 * torch.log10(mean)

mattport/nerf/loss.py METASEP
"""
Collection of Losses
"""
from torch import nn

L1Loss = nn.L1Loss
MSELoss = nn.MSELoss

LOSSES = {"L1": L1Loss, "MSE": MSELoss}

mattport/nerf/__init__.py METASEP

mattport/nerf/dataset/utils.py METASEP
"""
For loading the blender dataset format.
"""

import os
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple, Union

import imageio
import numpy as np
from omegaconf import ListConfig
import torch
from torchtyping import TensorType
from mattport.structures.colors import get_color

from mattport.utils.io import get_absolute_path, load_from_json


@dataclass
class DatasetInputs:
    """Dataset inputs for the image dataset and the ray generator."""

    image_filenames: List[str]
    downscale_factor: float = 1.0
    alpha_color: Optional[TensorType[3]] = None
    intrinsics: torch.tensor = None
    camera_to_world: torch.tensor = None


def load_blender_data(
    basedir: str, downscale_factor: int = 1, alpha_color: Optional[TensorType[3]] = None, split: str = "train"
) -> DatasetInputs:
    """Processes the a blender dataset directory.
    Some of this code comes from https://github.com/yenchenlin/nerf-pytorch/blob/master/load_blender.py#L37.

    Args:
        data_directory (str): Location of data
        downscale_factor (int, optional): How much to downscale images. Defaults to 1.0.
        alpha_color (TensorType[3], optional): Sets transparent regions to specified color, otherwise black.
        split (str, optional): Which dataset split to generate.

    Returns:
        DatasetInputs
    """
    # pylint: disable=unused-argument
    # pylint: disable-msg=too-many-locals
    meta = load_from_json(os.path.join(basedir, f"transforms_{split}.json"))
    image_filenames = []
    poses = []
    for frame in meta["frames"]:
        fname = os.path.join(basedir, frame["file_path"].replace("./", "") + ".png")
        image_filenames.append(fname)
        poses.append(np.array(frame["transform_matrix"]))
    poses = np.array(poses).astype(np.float32)

    img_0 = imageio.imread(image_filenames[0])
    image_height, image_width = img_0.shape[:2]
    camera_angle_x = float(meta["camera_angle_x"])
    focal_length = 0.5 * image_width / np.tan(0.5 * camera_angle_x)

    cx = image_width / 2.0
    cy = image_height / 2.0
    camera_to_world = torch.from_numpy(poses[:, :3])  # camera to world transform
    num_cameras = len(image_filenames)
    num_intrinsics_params = 3
    intrinsics = torch.ones((num_cameras, num_intrinsics_params), dtype=torch.float32)
    intrinsics *= torch.tensor([cx, cy, focal_length])

    dataset_inputs = DatasetInputs(
        image_filenames=image_filenames,
        downscale_factor=downscale_factor,
        alpha_color=alpha_color,
        intrinsics=intrinsics * 1.0 / downscale_factor,  # downscaling the intrinsics here
        camera_to_world=camera_to_world,
    )

    return dataset_inputs


def get_dataset_inputs_dict(
    data_directory: str,
    dataset_type: str,
    downscale_factor: int = 1,
    alpha_color: Optional[Union[str, list, ListConfig]] = None,
    splits: Tuple[str] = ("train", "val"),
) -> Dict[str, DatasetInputs]:
    """Returns the dataset inputs, which will be used with an ImageDataset and RayGenerator.
    # TODO: implement the `test` split, which will have depths and normals, etc.

    Args:
        data_directory (str): Location of data
        dataset_type (str): Name of dataset type
        downscale_factor (int, optional): How much to downscale images. Defaults to 1.0.
        alpha_color (str, list, optional): Sets transparent regions to specified color, otherwise black.

    Returns:
        Dict[str, DatasetInputs]: The inputs needed for generating rays.
    """
    dataset_inputs_dict = {}

    if alpha_color is not None:
        alpha_color = get_color(alpha_color)

    if dataset_type == "blender":
        for split in splits:
            dataset_inputs = load_blender_data(
                get_absolute_path(data_directory),
                downscale_factor=downscale_factor,
                alpha_color=alpha_color,
            )
            dataset_inputs_dict[split] = dataset_inputs
    else:
        raise NotImplementedError(f"{dataset_type} is not a valid dataset type")

    return dataset_inputs_dict

mattport/nerf/dataset/image_dataset.py METASEP
"""
Some dataset code.
"""

from typing import List, Optional

import numpy as np
import numpy.typing as npt
import PIL
import torch
from PIL import Image
from torch.utils.data import default_collate
from torchtyping import TensorType


def collate_batch(batch_list, num_rays_per_batch, keep_full_image: bool = False):
    """_summary_

    Args:
     batch (_type_): _description_
    """
    # TODO(ethan): handle sampling even when in different processes
    # we don't want the same samples for all spawned processed when
    # using distributed training

    batch = default_collate(batch_list)
    num_images, image_height, image_width, _ = batch["image"].shape
    indices = torch.floor(
        torch.rand((num_rays_per_batch, 3)) * torch.tensor([num_images, image_height, image_width])
    ).long()
    c, y, x = [i.flatten() for i in torch.split(indices, 1, dim=-1)]
    pixels = batch["image"][c, y, x]
    mask = batch["mask"][c, y, x]
    assert pixels.shape == (num_rays_per_batch, 3), pixels.shape

    # Needed to correct the random indices to their actual camera idx locations.
    local_indices = indices.clone()
    indices[:, 0] = batch["image_idx"][c]
    collated_batch = {
        "local_indices": local_indices,  # local to the batch returned
        "indices": indices,  # with the abs camera indices
        "pixels": pixels,
        "mask": mask,
    }
    if keep_full_image:
        collated_batch["image"] = batch["image"]

    return collated_batch


class ImageDataset(torch.utils.data.Dataset):
    """Dataset that returns images."""

    def __init__(
        self, image_filenames: List[str], downscale_factor: int = 1, alpha_color: Optional[TensorType[3]] = None
    ):
        """_summary_

        Args:
            image_filenames (List[str]): List of image filenames
            downscale_factor (int, optional): How much to downscale the image. Defaults to 1.
            alpha_color (TensorType[3], optional): Sets transparent regions to specified color, otherwise black.
        """
        super().__init__()
        assert isinstance(downscale_factor, int)
        self.image_filenames = image_filenames
        self.downscale_factor = downscale_factor
        self.alpha_color = alpha_color

    def __len__(self):
        return len(self.image_filenames)

    def get_image(self, image_idx: int) -> npt.NDArray[np.uint8]:
        """Returns the image.

        Args:
            image_idx (int): The image index in the dataset.

        Returns:
            np.uint8: an image of shape (H, W, 3 or 4)
        """
        image_filename = self.image_filenames[image_idx]
        pil_image = Image.open(image_filename)
        if self.downscale_factor != 1.0:
            image_width, image_height = pil_image.size
            if image_width % self.downscale_factor != 0:
                raise ValueError(
                    f"Image width {image_width} is not divisible by downscale_factor {self.downscale_factor}"
                )
            if image_height % self.downscale_factor != 0:
                raise ValueError(
                    f"Image height {image_height} is not divisible by downscale_factor {self.downscale_factor}"
                )
            pil_image = pil_image.resize(
                (image_width // self.downscale_factor, image_height // self.downscale_factor), PIL.Image.BILINEAR
            )
        image = np.array(pil_image, dtype="uint8")  # shape is (h, w, 3 or 4)
        assert len(image.shape) == 3
        assert image.dtype == np.uint8
        assert image.shape[2] in [3, 4], f"Image shape of {image.shape} is in correct."
        return image

    def __getitem__(self, image_idx):
        # the image might be RGB or RGBA, so we separate it
        original_image = torch.from_numpy(self.get_image(image_idx).astype("float32") / 255.0)

        if self.alpha_color is not None:
            image = original_image[:, :, :3] * original_image[:, :, -1:] + self.alpha_color * (
                1.0 - original_image[:, :, -1:]
            )
        else:
            image = original_image[:, :, :3]
        num_channels = original_image.shape[2]
        if num_channels == 4:
            mask = original_image[:, :, 3]
        elif num_channels == 3:
            mask = np.ones_like(original_image[:, :, 0])
        else:
            raise ValueError(f"Image shape of {image.shape} is in correct.")

        data = {
            "image_idx": image_idx,
            "image": image,  # the pixels
            "mask": mask,
        }
        return data

mattport/nerf/dataset/collate.py METASEP
"""
Classes to help with caching data while training.
"""

import random
from typing import Callable

import torch
from torch.utils.data import default_collate


def collate_batch_size_one(batch_list):
    """Use the default collate function but then squeze to avoid the batch dimension being added."""
    assert len(batch_list) == 1
    collated_batch = default_collate(batch_list)
    for key in collated_batch:
        assert collated_batch[key].shape[0] == 1
        collated_batch[key] = collated_batch[key].squeeze(0)
    return collated_batch


class CollateIterDataset(torch.utils.data.IterableDataset):
    """Collated image dataset that implements caching of images.

    Args:
        torch (_type_): _description_
    """

    def __init__(
        self,
        dataset: torch.utils.data.Dataset,
        collate_fn: Callable,
        num_samples_to_collate: int,
        num_times_to_repeat: int = 0,
    ):
        super().__init__()
        self.dataset = dataset
        self.collate_fn = collate_fn
        self.num_samples_to_collate = num_samples_to_collate
        self.num_times_to_repeat = num_times_to_repeat

        self.num_repeated = self.num_times_to_repeat  # starting value
        self.cached_batch_list = None

    def __getitem__(self, idx):
        return self.dataset.__getitem__(idx)

    def get_batch_list(self):
        """Returns a list of batches from the dataset attribute."""
        # sampling without replacement. TODO(ethan): don't use 'random'
        indices = random.sample(range(len(self.dataset)), k=self.num_samples_to_collate)
        batch_list = [self.dataset.__getitem__(idx) for idx in indices]
        return batch_list

    def __iter__(self):
        while True:
            if self.num_repeated >= self.num_times_to_repeat:
                # trigger a reset
                self.num_repeated = 0
                batch_list = self.get_batch_list()
                # possibly save a cached item
                self.cached_batch_list = batch_list if self.num_times_to_repeat != 0 else None
            else:
                batch_list = self.cached_batch_list
                self.num_repeated += 1
            collated_batch = self.collate_fn(batch_list)
            yield collated_batch

mattport/nerf/dataset/__init__.py METASEP

mattport/nerf/dataset/colmap_utils.py METASEP

# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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]
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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_pointsTD_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_pointsTD_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_pointsTD_text(points3D, os.path.join(path, "points3D") + ext)
    else:
        write_cameras_binary(cameras, os.path.join(path, "cameras" + ext))
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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:
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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])
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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:
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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():
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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):
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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")
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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":
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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_pointsTD_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_pointsTD_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))
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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))
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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_pointsTD_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_pointsTD_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_pointsTD_text(points3D, os.path.join(path, "points3D") + ext)
    else:
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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")
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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")
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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]
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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_pointsTD_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_pointsTD_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_pointsTD_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_pointsTD_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="outut 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:
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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:
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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])))
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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():
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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):
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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_pointsTD_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_pointsTD_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_pointsTD_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_pointsTD_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="outut model format", default=".txt")
    args = parser.parse_args()

infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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")
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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:
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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_pointsTD_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_pointsTD_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_pointsTD_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))
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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):
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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:
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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:
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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):
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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_pointsTD_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_pointsTD_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))
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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):
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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_pointsTD_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_pointsTD_binary(os.path.join(path, "points3D") + ext)
    return cameras, images, points3D


def write_model(cameras, images, points3D, path, ext=".bin"):
    if ext == ".txt":
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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")
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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]
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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"
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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
infile
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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:])))
inproject
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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:
inproject
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


CameraModel = collections.namedtuple("CameraModel", ["model_id", "model_name", "num_params"])
inproject
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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)
inproject
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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_pointsTD_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_pointsTD_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_pointsTD_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_pointsTD_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="outut 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__":
commited
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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):
non_informative
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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):
    """
non_informative
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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")


non_informative
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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_pointsTD_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_pointsTD_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_pointsTD_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_pointsTD_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="outut 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()
non_informative
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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
non_informative
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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])))
random
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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()
random
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/reconstruction.cc
        void Reconstruction::ReadPoints3DText(const std::string& path)
        void Reconstruction::WritePoints3DText(const std::string& path)
    """
    if len(points3D) == 0:
random
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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/base/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]
            image_name = ""
            current_char = read_next_bytes(fid, 1, "c")[0]
            while current_char != b"\x00":  # look for the ASCII 0 entry
                image_name += current_char.decode("utf-8")
                current_char = read_next_bytes(fid, 1, "c")[0]
            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/base/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/base/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_pointsTD_text(path):
    """
    see: src/base/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_pointsTD_binary(path_to_model_file):
    """
    see: src/base/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_pointsTD_text(points3D, path):
    """
    see: src/base/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_pointsTD_binary(points3D, path_to_model_file):
    """
    see: src/base/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_pointsTD_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_pointsTD_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_pointsTD_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_pointsTD_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],
random
# pylint: disable-all
"""
Here we have code taken from COLMAP for parsing data in the COLMAP format.
Original file at https://github.com/colmap/colmap/blob/1a4d0bad2e90aa65ce997c9d1779518eaed998d5/scripts/python/read_write_model.py.
"""

# Copyright (c) 2022, 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.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

import os
import collections
import numpy as np
import struct
import argparse


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/base/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/base/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/base/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/base/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/base/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,
random
            track_elems = read_next_bytes(fid, num_bytes=8 * track_length, format_char_sequence="ii" * track_length)         write_images_binary(images, os.path.join(path, "images" + ext))         write_next_bytes(fid, len(cameras), "Q")             track_length = read_next_bytes(fid, num_bytes=8, format_char_sequence="Q")[0]         num_points = read_next_bytes(fid, 8, "Q")[0]             write_next_bytes(fid, img.id, "i")             camera_properties = read_next_bytes(fid, num_bytes=24, format_char_sequence="iiQQ")             write_next_bytes(fid, pt.rgb.tolist(), "BBB")         cameras = read_cameras_text(os.path.join(path, "cameras" + ext))         write_pointsTD_text(points3D, os.path.join(path, "points3D") + ext)         points3D = read_pointsTD_text(os.path.join(path, "points3D") + ext)         write_cameras_binary(cameras, os.path.join(path, "cameras" + ext))             write_next_bytes(fid, pt.xyz.tolist(), "ddd")             num_points2D = read_next_bytes(fid, num_bytes=8, format_char_sequence="Q")[0]                 current_char = read_next_bytes(fid, 1, "c")[0]             write_next_bytes(fid, img.camera_id, "i")             write_next_bytes(fid, track_length, "Q")         write_model(cameras, images, points3D, path=args.output_model, ext=args.output_format)         write_next_bytes(fid, len(points3D), "Q")             images[image_id] = Image(             write_next_bytes(fid, pt.id, "Q")                 write_next_bytes(fid, [image_id, point2D_id], "ii")     cameras, images, points3D = read_model(path=args.input_model, ext=args.input_format)             write_next_bytes(fid, pt.error, "d")                 write_next_bytes(fid, float(p), "d")         write_pointsTD_binary(points3D, os.path.join(path, "points3D") + ext)             binary_image_properties = read_next_bytes(fid, num_bytes=64, format_char_sequence="idddddddi")         num_cameras = read_next_bytes(fid, 8, "Q")[0]         num_reg_images = read_next_bytes(fid, 8, "Q")[0]                 write_next_bytes(fid, [*xy, p3d_id], "ddq")         write_images_text(images, os.path.join(path, "images" + ext))             binary_point_line_properties = read_next_bytes(fid, num_bytes=43, format_char_sequence="QdddBBBd")         write_cameras_text(cameras, os.path.join(path, "cameras" + ext))             write_next_bytes(fid, img.qvec.tolist(), "dddd")             write_next_bytes(fid, camera_properties, "iiQQ")         elif detect_model_format(path, ".txt"):             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, width=width, height=height, params=params)                 write_next_bytes(fid, char.encode("utf-8"), "c") Camera = collections.namedtuple("Camera", ["id", "model", "width", "height", "params"])             cameras[camera_id] = Camera(     main()     """     see: src/base/reconstruction.cc def detect_model_format(path, ext): # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE                 tvec = np.array(tuple(map(float, elems[5:8])))             if len(line) > 0 and line[0] != "#":         mean_track_length = 0             ]                     camera_id=camera_id,