src.croco.datasets.transforms

# Copyright (C) 2022-present Naver Corporation. All rights reserved.
# Licensed under CC BY-NC-SA 4.0 (non-commercial use only).

import torch
import torchvision.transforms
import torchvision.transforms.functional as F

# "Pair": apply a transform on a pair
# "Both": apply the exact same transform to both images

class ComposePair(torchvision.transforms.Compose):
    def __call__(self, img1, img2):
        for t in self.transforms:
            img1, img2 = t(img1, img2)
        return img1, img2

class NormalizeBoth(torchvision.transforms.Normalize):
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ToTensorBoth(torchvision.transforms.ToTensor):
    def __call__(self, img1, img2):
        img1 = super().__call__(img1)
        img2 = super().__call__(img2)
        return img1, img2

class RandomCropPair(torchvision.transforms.RandomCrop):
    # the crop will be intentionally different for the two images with this class
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ColorJitterPair(torchvision.transforms.ColorJitter):
    # can be symmetric (same for both images) or assymetric (different jitter params for each image) depending on assymetric_prob
    def __init__(self, assymetric_prob, **kwargs):
        super().__init__(**kwargs)
        self.assymetric_prob = assymetric_prob

    def jitter_one(
        self,
        img,
        fn_idx,
        brightness_factor,
        contrast_factor,
        saturation_factor,
        hue_factor,
    ):
        for fn_id in fn_idx:
            if fn_id == 0 and brightness_factor is not None:
                img = F.adjust_brightness(img, brightness_factor)
            elif fn_id == 1 and contrast_factor is not None:
                img = F.adjust_contrast(img, contrast_factor)
            elif fn_id == 2 and saturation_factor is not None:
                img = F.adjust_saturation(img, saturation_factor)
            elif fn_id == 3 and hue_factor is not None:
                img = F.adjust_hue(img, hue_factor)
        return img

    def forward(self, img1, img2):

        fn_idx, brightness_factor, contrast_factor, saturation_factor, hue_factor = (
            self.get_params(self.brightness, self.contrast, self.saturation, self.hue)
        )
        img1 = self.jitter_one(
            img1,
            fn_idx,
            brightness_factor,
            contrast_factor,
            saturation_factor,
            hue_factor,
        )
        if torch.rand(1) < self.assymetric_prob:  # assymetric:
            (
                fn_idx,
                brightness_factor,
                contrast_factor,
                saturation_factor,
                hue_factor,
            ) = self.get_params(
                self.brightness, self.contrast, self.saturation, self.hue
            )
        img2 = self.jitter_one(
            img2,
            fn_idx,
            brightness_factor,
            contrast_factor,
            saturation_factor,
            hue_factor,
        )
        return img1, img2

def get_pair_transforms(transform_str, totensor=True, normalize=True):
    # transform_str is eg    crop224+color
    trfs = []
    for s in transform_str.split("+"):
        if s.startswith("crop"):
            size = int(s[len("crop") :])
            trfs.append(RandomCropPair(size))
        elif s == "acolor":
            trfs.append(
                ColorJitterPair(
                    assymetric_prob=1.0,
                    brightness=(0.6, 1.4),
                    contrast=(0.6, 1.4),
                    saturation=(0.6, 1.4),
                    hue=0.0,
                )
            )
        elif s == "":  # if transform_str was ""
            pass
        else:
            raise NotImplementedError("Unknown augmentation: " + s)

    if totensor:
        trfs.append(ToTensorBoth())
    if normalize:
        trfs.append(
            NormalizeBoth(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
        )

    if len(trfs) == 0:
        return None
    elif len(trfs) == 1:
        return trfs
    else:
        return ComposePair(trfs)


ComposePair

# Copyright (C) 2022-present Naver Corporation. All rights reserved.
# Licensed under CC BY-NC-SA 4.0 (non-commercial use only).

import torch
import torchvision.transforms
import torchvision.transforms.functional as F

# "Pair": apply a transform on a pair
# "Both": apply the exact same transform to both images

class ComposePair(torchvision.transforms.Compose):
    def __call__(self, img1, img2):
        for t in self.transforms:
            img1, img2 = t(img1, img2)
        return img1, img2

class NormalizeBoth(torchvision.transforms.Normalize):
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ToTensorBoth(torchvision.transforms.ToTensor):
    def __call__(self, img1, img2):
        img1 = super().__call__(img1)
        img2 = super().__call__(img2)
        return img1, img2

class RandomCropPair(torchvision.transforms.RandomCrop):
    # the crop will be intentionally different for the two images with this class
    def forward(self, img1, img2):
        img1 = super().forward(img1)


NormalizeBoth

# Copyright (C) 2022-present Naver Corporation. All rights reserved.
# Licensed under CC BY-NC-SA 4.0 (non-commercial use only).

import torch
import torchvision.transforms
import torchvision.transforms.functional as F

# "Pair": apply a transform on a pair
# "Both": apply the exact same transform to both images

class ComposePair(torchvision.transforms.Compose):
    def __call__(self, img1, img2):
        for t in self.transforms:
            img1, img2 = t(img1, img2)
        return img1, img2

class NormalizeBoth(torchvision.transforms.Normalize):
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ToTensorBoth(torchvision.transforms.ToTensor):
    def __call__(self, img1, img2):
        img1 = super().__call__(img1)
        img2 = super().__call__(img2)
        return img1, img2

class RandomCropPair(torchvision.transforms.RandomCrop):
    # the crop will be intentionally different for the two images with this class
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ColorJitterPair(torchvision.transforms.ColorJitter):
    # can be symmetric (same for both images) or assymetric (different jitter params for each image) depending on assymetric_prob
    def __init__(self, assymetric_prob, **kwargs):


ToTensorBoth

import torchvision.transforms.functional as F

# "Pair": apply a transform on a pair
# "Both": apply the exact same transform to both images

class ComposePair(torchvision.transforms.Compose):
    def __call__(self, img1, img2):
        for t in self.transforms:
            img1, img2 = t(img1, img2)
        return img1, img2

class NormalizeBoth(torchvision.transforms.Normalize):
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ToTensorBoth(torchvision.transforms.ToTensor):
    def __call__(self, img1, img2):
        img1 = super().__call__(img1)
        img2 = super().__call__(img2)
        return img1, img2

class RandomCropPair(torchvision.transforms.RandomCrop):
    # the crop will be intentionally different for the two images with this class
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ColorJitterPair(torchvision.transforms.ColorJitter):
    # can be symmetric (same for both images) or assymetric (different jitter params for each image) depending on assymetric_prob
    def __init__(self, assymetric_prob, **kwargs):
        super().__init__(**kwargs)
        self.assymetric_prob = assymetric_prob

    def jitter_one(
        self,
        img,
        fn_idx,


RandomCropPair

    def __call__(self, img1, img2):
        for t in self.transforms:
            img1, img2 = t(img1, img2)
        return img1, img2

class NormalizeBoth(torchvision.transforms.Normalize):
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ToTensorBoth(torchvision.transforms.ToTensor):
    def __call__(self, img1, img2):
        img1 = super().__call__(img1)
        img2 = super().__call__(img2)
        return img1, img2

class RandomCropPair(torchvision.transforms.RandomCrop):
    # the crop will be intentionally different for the two images with this class
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ColorJitterPair(torchvision.transforms.ColorJitter):
    # can be symmetric (same for both images) or assymetric (different jitter params for each image) depending on assymetric_prob
    def __init__(self, assymetric_prob, **kwargs):
        super().__init__(**kwargs)
        self.assymetric_prob = assymetric_prob

    def jitter_one(
        self,
        img,
        fn_idx,
        brightness_factor,
        contrast_factor,
        saturation_factor,
        hue_factor,
    ):
        for fn_id in fn_idx:
            if fn_id == 0 and brightness_factor is not None:
                img = F.adjust_brightness(img, brightness_factor)


ColorJitterPair

        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ToTensorBoth(torchvision.transforms.ToTensor):
    def __call__(self, img1, img2):
        img1 = super().__call__(img1)
        img2 = super().__call__(img2)
        return img1, img2

class RandomCropPair(torchvision.transforms.RandomCrop):
    # the crop will be intentionally different for the two images with this class
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ColorJitterPair(torchvision.transforms.ColorJitter):
    # can be symmetric (same for both images) or assymetric (different jitter params for each image) depending on assymetric_prob
    def __init__(self, assymetric_prob, **kwargs):
        super().__init__(**kwargs)
        self.assymetric_prob = assymetric_prob

    def jitter_one(
        self,
        img,
        fn_idx,
        brightness_factor,
        contrast_factor,
        saturation_factor,
        hue_factor,
    ):
        for fn_id in fn_idx:
            if fn_id == 0 and brightness_factor is not None:
                img = F.adjust_brightness(img, brightness_factor)
            elif fn_id == 1 and contrast_factor is not None:
                img = F.adjust_contrast(img, contrast_factor)
            elif fn_id == 2 and saturation_factor is not None:
                img = F.adjust_saturation(img, saturation_factor)
            elif fn_id == 3 and hue_factor is not None:
                img = F.adjust_hue(img, hue_factor)
        return img

    def forward(self, img1, img2):

        fn_idx, brightness_factor, contrast_factor, saturation_factor, hue_factor = (
            self.get_params(self.brightness, self.contrast, self.saturation, self.hue)
        )
        img1 = self.jitter_one(
            img1,
            fn_idx,
            brightness_factor,
            contrast_factor,
            saturation_factor,
            hue_factor,
        )
        if torch.rand(1) < self.assymetric_prob:  # assymetric:
            (
                fn_idx,
                brightness_factor,
                contrast_factor,
                saturation_factor,
                hue_factor,
            ) = self.get_params(
                self.brightness, self.contrast, self.saturation, self.hue
            )
        img2 = self.jitter_one(
            img2,
            fn_idx,
            brightness_factor,
            contrast_factor,
            saturation_factor,
            hue_factor,
        )
        return img1, img2

def get_pair_transforms(transform_str, totensor=True, normalize=True):
    # transform_str is eg    crop224+color
    trfs = []
    for s in transform_str.split("+"):
        if s.startswith("crop"):
            size = int(s[len("crop") :])
            trfs.append(RandomCropPair(size))
        elif s == "acolor":
            trfs.append(
                ColorJitterPair(
                    assymetric_prob=1.0,
                    brightness=(0.6, 1.4),
                    contrast=(0.6, 1.4),
                    saturation=(0.6, 1.4),
                    hue=0.0,
                )
            )
        elif s == "":  # if transform_str was ""


            (
                fn_idx,
                brightness_factor,
                contrast_factor,
                saturation_factor,
                hue_factor,
            ) = self.get_params(
                self.brightness, self.contrast, self.saturation, self.hue
            )
        img2 = self.jitter_one(
            img2,
            fn_idx,
            brightness_factor,
            contrast_factor,
            saturation_factor,
            hue_factor,
        )
        return img1, img2

def get_pair_transforms(transform_str, totensor=True, normalize=True):
    # transform_str is eg    crop224+color
    trfs = []
    for s in transform_str.split("+"):
        if s.startswith("crop"):
            size = int(s[len("crop") :])
            trfs.append(RandomCropPair(size))
        elif s == "acolor":
            trfs.append(
                ColorJitterPair(
                    assymetric_prob=1.0,
                    brightness=(0.6, 1.4),
                    contrast=(0.6, 1.4),
                    saturation=(0.6, 1.4),
                    hue=0.0,
                )
            )
        elif s == "":  # if transform_str was ""
            pass
        else:
            raise NotImplementedError("Unknown augmentation: " + s)

    if totensor:
        trfs.append(ToTensorBoth())
    if normalize:
        trfs.append(
            NormalizeBoth(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
        )

    if len(trfs) == 0:
        return None
    elif len(trfs) == 1:
        return trfs
    else:
        return ComposePair(trfs)


# "Pair": apply a transform on a pair
# "Both": apply the exact same transform to both images

class ComposePair(torchvision.transforms.Compose):
    def __call__(self, img1, img2):
        for t in self.transforms:
            img1, img2 = t(img1, img2)
        return img1, img2

class NormalizeBoth(torchvision.transforms.Normalize):
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ToTensorBoth(torchvision.transforms.ToTensor):
    def __call__(self, img1, img2):
        img1 = super().__call__(img1)
        img2 = super().__call__(img2)
        return img1, img2

class RandomCropPair(torchvision.transforms.RandomCrop):
    # the crop will be intentionally different for the two images with this class
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ColorJitterPair(torchvision.transforms.ColorJitter):
    # can be symmetric (same for both images) or assymetric (different jitter params for each image) depending on assymetric_prob
    def __init__(self, assymetric_prob, **kwargs):
        super().__init__(**kwargs)
        self.assymetric_prob = assymetric_prob

    def jitter_one(
        self,
        img,
        fn_idx,


forward

    def jitter_one(
        self,
        img,
        fn_idx,
        brightness_factor,
        contrast_factor,
        saturation_factor,
        hue_factor,
    ):
        for fn_id in fn_idx:
            if fn_id == 0 and brightness_factor is not None:
                img = F.adjust_brightness(img, brightness_factor)
            elif fn_id == 1 and contrast_factor is not None:
                img = F.adjust_contrast(img, contrast_factor)
            elif fn_id == 2 and saturation_factor is not None:
                img = F.adjust_saturation(img, saturation_factor)
            elif fn_id == 3 and hue_factor is not None:
                img = F.adjust_hue(img, hue_factor)
        return img

    def forward(self, img1, img2):

        fn_idx, brightness_factor, contrast_factor, saturation_factor, hue_factor = (
            self.get_params(self.brightness, self.contrast, self.saturation, self.hue)
        )
        img1 = self.jitter_one(
            img1,
            fn_idx,
            brightness_factor,
            contrast_factor,
            saturation_factor,
            hue_factor,
        )
        if torch.rand(1) < self.assymetric_prob:  # assymetric:
            (
                fn_idx,
                brightness_factor,
                contrast_factor,
                saturation_factor,
                hue_factor,
            ) = self.get_params(
                self.brightness, self.contrast, self.saturation, self.hue
            )
        img2 = self.jitter_one(
            img2,
            fn_idx,
            brightness_factor,
            contrast_factor,
            saturation_factor,
            hue_factor,
        )
        return img1, img2

def get_pair_transforms(transform_str, totensor=True, normalize=True):
    # transform_str is eg    crop224+color
    trfs = []
    for s in transform_str.split("+"):
        if s.startswith("crop"):
            size = int(s[len("crop") :])
            trfs.append(RandomCropPair(size))
        elif s == "acolor":
            trfs.append(
                ColorJitterPair(
                    assymetric_prob=1.0,
                    brightness=(0.6, 1.4),
                    contrast=(0.6, 1.4),
                    saturation=(0.6, 1.4),
                    hue=0.0,
                )
            )
        elif s == "":  # if transform_str was ""


        return img1, img2

class ToTensorBoth(torchvision.transforms.ToTensor):
    def __call__(self, img1, img2):
        img1 = super().__call__(img1)
        img2 = super().__call__(img2)
        return img1, img2

class RandomCropPair(torchvision.transforms.RandomCrop):
    # the crop will be intentionally different for the two images with this class
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ColorJitterPair(torchvision.transforms.ColorJitter):
    # can be symmetric (same for both images) or assymetric (different jitter params for each image) depending on assymetric_prob
    def __init__(self, assymetric_prob, **kwargs):
        super().__init__(**kwargs)
        self.assymetric_prob = assymetric_prob

    def jitter_one(
        self,
        img,
        fn_idx,
        brightness_factor,
        contrast_factor,
        saturation_factor,
        hue_factor,
    ):
        for fn_id in fn_idx:
            if fn_id == 0 and brightness_factor is not None:
                img = F.adjust_brightness(img, brightness_factor)
            elif fn_id == 1 and contrast_factor is not None:
                img = F.adjust_contrast(img, contrast_factor)
            elif fn_id == 2 and saturation_factor is not None:
                img = F.adjust_saturation(img, saturation_factor)
            elif fn_id == 3 and hue_factor is not None:
                img = F.adjust_hue(img, hue_factor)
        return img


    def __call__(self, img1, img2):
        img1 = super().__call__(img1)
        img2 = super().__call__(img2)
        return img1, img2

class RandomCropPair(torchvision.transforms.RandomCrop):
    # the crop will be intentionally different for the two images with this class
    def forward(self, img1, img2):
        img1 = super().forward(img1)
        img2 = super().forward(img2)
        return img1, img2

class ColorJitterPair(torchvision.transforms.ColorJitter):
    # can be symmetric (same for both images) or assymetric (different jitter params for each image) depending on assymetric_prob
    def __init__(self, assymetric_prob, **kwargs):
        super().__init__(**kwargs)
        self.assymetric_prob = assymetric_prob

    def jitter_one(
        self,
        img,
        fn_idx,
        brightness_factor,
        contrast_factor,
        saturation_factor,
        hue_factor,
    ):
        for fn_id in fn_idx:
            if fn_id == 0 and brightness_factor is not None:
                img = F.adjust_brightness(img, brightness_factor)
            elif fn_id == 1 and contrast_factor is not None:
                img = F.adjust_contrast(img, contrast_factor)
            elif fn_id == 2 and saturation_factor is not None:
                img = F.adjust_saturation(img, saturation_factor)
            elif fn_id == 3 and hue_factor is not None:
                img = F.adjust_hue(img, hue_factor)
        return img

    def forward(self, img1, img2):

        fn_idx, brightness_factor, contrast_factor, saturation_factor, hue_factor = (
            self.get_params(self.brightness, self.contrast, self.saturation, self.hue)
        )
        img1 = self.jitter_one(
            img1,
            fn_idx,
            brightness_factor,
            contrast_factor,
            saturation_factor,
            hue_factor,
        )
        if torch.rand(1) < self.assymetric_prob:  # assymetric:
            (
                fn_idx,
                brightness_factor,
                contrast_factor,
                saturation_factor,


# Copyright (C) 2022-present Naver Corporation. All rights reserved.
# Licensed under CC BY-NC-SA 4.0 (non-commercial use only).
#
# --------------------------------------------------------
# Extracting crops for pre-training
# --------------------------------------------------------

import os
import argparse
from tqdm import tqdm
from PIL import Image
import functools
from multiprocessing import Pool
import math

def arg_parser():
    parser = argparse.ArgumentParser(
        "Generate cropped image pairs from image crop list"
    )

    parser.add_argument("--crops", type=str, required=True, help="crop file")
    parser.add_argument("--root-dir", type=str, required=True, help="root directory")
    parser.add_argument(
        "--output-dir", type=str, required=True, help="output directory"
    )
    parser.add_argument("--imsize", type=int, default=256, help="size of the crops")
    parser.add_argument(
        "--nthread", type=int, required=True, help="number of simultaneous threads"
    )
    parser.add_argument(
        "--max-subdir-levels",
        type=int,
        default=5,
        help="maximum number of subdirectories",
    )
    parser.add_argument(
        "--ideal-number-pairs-in-dir",
        type=int,
        default=500,
        help="number of pairs stored in a dir",
    )
    return parser

def main(args):
    listing_path = os.path.join(args.output_dir, "listing.txt")

    print(f"Loading list of crops ... ({args.nthread} threads)")
    crops, num_crops_to_generate = load_crop_file(args.crops)

    print(f"Preparing jobs ({len(crops)} candidate image pairs)...")
    num_levels = min(
        math.ceil(math.log(num_crops_to_generate, args.ideal_number_pairs_in_dir)),
        args.max_subdir_levels,
    )
    num_pairs_in_dir = math.ceil(num_crops_to_generate ** (1 / num_levels))

    jobs = prepare_jobs(crops, num_levels, num_pairs_in_dir)
    del crops

    os.makedirs(args.output_dir, exist_ok=True)
    mmap = Pool(args.nthread).imap_unordered if args.nthread > 1 else map
    call = functools.partial(save_image_crops, args)

    print(f"Generating cropped images to {args.output_dir} ...")
    with open(listing_path, "w") as listing:
        listing.write("# pair_path\n")
        for results in tqdm(mmap(call, jobs), total=len(jobs)):
            for path in results:
                listing.write(f"{path}\n")
    print("Finished writing listing to", listing_path)

def load_crop_file(path):
    data = open(path).read().splitlines()
    pairs = []
    num_crops_to_generate = 0
    for line in tqdm(data):
        if line.startswith("#"):
            continue
        line = line.split(", ")
        if len(line) < 8:
            img1, img2, rotation = line
            pairs.append((img1, img2, int(rotation), []))
        else:
            l1, r1, t1, b1, l2, r2, t2, b2 = map(int, line)
            rect1, rect2 = (l1, t1, r1, b1), (l2, t2, r2, b2)
            pairs[-1][-1].append((rect1, rect2))
            num_crops_to_generate += 1
    return pairs, num_crops_to_generate

def prepare_jobs(pairs, num_levels, num_pairs_in_dir):
    jobs = []
    powers = [num_pairs_in_dir**level for level in reversed(range(num_levels))]

    def get_path(idx):
        idx_array = []
        d = idx
        for level in range(num_levels - 1):
            idx_array.append(idx // powers[level])
            idx = idx % powers[level]
        idx_array.append(d)
        return "/".join(map(lambda x: hex(x)[2:], idx_array))

    idx = 0
    for pair_data in tqdm(pairs):
        img1, img2, rotation, crops = pair_data
        if -60 <= rotation and rotation <= 60:
            rotation = 0  # most likely not a true rotation
        paths = [get_path(idx + k) for k in range(len(crops))]
        idx += len(crops)
        jobs.append(((img1, img2), rotation, crops, paths))
    return jobs

def load_image(path):
    try:
        return Image.open(path).convert("RGB")
    except Exception as e:
        print("skipping", path, e)
        raise OSError()

def save_image_crops(args, data):
    # load images
    img_pair, rot, crops, paths = data
    try:
        img1, img2 = [
            load_image(os.path.join(args.root_dir, impath)) for impath in img_pair
        ]
    except OSError as e:
        return []

    def area(sz):
        return sz[0] * sz[1]

    tgt_size = (args.imsize, args.imsize)

    def prepare_crop(img, rect, rot=0):
        # actual crop
        img = img.crop(rect)

        # resize to desired size
        interp = (
            Image.Resampling.LANCZOS
            if area(img.size) > 4 * area(tgt_size)
            else Image.Resampling.BICUBIC
        )
        img = img.resize(tgt_size, resample=interp)

        # rotate the image
        rot90 = (round(rot / 90) % 4) * 90
        if rot90 == 90:
            img = img.transpose(Image.Transpose.ROTATE_90)
        elif rot90 == 180:
            img = img.transpose(Image.Transpose.ROTATE_180)
        elif rot90 == 270:
            img = img.transpose(Image.Transpose.ROTATE_270)
        return img

    results = []
    for (rect1, rect2), path in zip(crops, paths):
        crop1 = prepare_crop(img1, rect1)
        crop2 = prepare_crop(img2, rect2, rot)

        fullpath1 = os.path.join(args.output_dir, path + "_1.jpg")
        fullpath2 = os.path.join(args.output_dir, path + "_2.jpg")
        os.makedirs(os.path.dirname(fullpath1), exist_ok=True)

        assert not os.path.isfile(fullpath1), fullpath1
        assert not os.path.isfile(fullpath2), fullpath2
        crop1.save(fullpath1)
        crop2.save(fullpath2)
        results.append(path)

    return results

if __name__ == "__main__":
    args = arg_parser().parse_args()
    main(args)


# Copyright (C) 2022-present Naver Corporation. All rights reserved.
# Licensed under CC BY-NC-SA 4.0 (non-commercial use only).
#
# --------------------------------------------------------
# Extracting crops for pre-training
# --------------------------------------------------------

import os
import argparse
from tqdm import tqdm
from PIL import Image
import functools
from multiprocessing import Pool
import math

def arg_parser():
    parser = argparse.ArgumentParser(
        "Generate cropped image pairs from image crop list"
    )

    parser.add_argument("--crops", type=str, required=True, help="crop file")
    parser.add_argument("--root-dir", type=str, required=True, help="root directory")
    parser.add_argument(
        "--output-dir", type=str, required=True, help="output directory"
    )
    parser.add_argument("--imsize", type=int, default=256, help="size of the crops")
    parser.add_argument(
        "--nthread", type=int, required=True, help="number of simultaneous threads"
    )
    parser.add_argument(
        "--max-subdir-levels",
        type=int,
        default=5,
        help="maximum number of subdirectories",
    )
    parser.add_argument(
        "--ideal-number-pairs-in-dir",
        type=int,
        default=500,
        help="number of pairs stored in a dir",
    )
    return parser

def main(args):
    listing_path = os.path.join(args.output_dir, "listing.txt")

    print(f"Loading list of crops ... ({args.nthread} threads)")
    crops, num_crops_to_generate = load_crop_file(args.crops)

    print(f"Preparing jobs ({len(crops)} candidate image pairs)...")
    num_levels = min(
        math.ceil(math.log(num_crops_to_generate, args.ideal_number_pairs_in_dir)),
        args.max_subdir_levels,
    )
    num_pairs_in_dir = math.ceil(num_crops_to_generate ** (1 / num_levels))

    jobs = prepare_jobs(crops, num_levels, num_pairs_in_dir)
    del crops

    os.makedirs(args.output_dir, exist_ok=True)
    mmap = Pool(args.nthread).imap_unordered if args.nthread > 1 else map


main

    )
    parser.add_argument("--imsize", type=int, default=256, help="size of the crops")
    parser.add_argument(
        "--nthread", type=int, required=True, help="number of simultaneous threads"
    )
    parser.add_argument(
        "--max-subdir-levels",
        type=int,
        default=5,
        help="maximum number of subdirectories",
    )
    parser.add_argument(
        "--ideal-number-pairs-in-dir",
        type=int,
        default=500,
        help="number of pairs stored in a dir",
    )
    return parser

def main(args):
    listing_path = os.path.join(args.output_dir, "listing.txt")

    print(f"Loading list of crops ... ({args.nthread} threads)")
    crops, num_crops_to_generate = load_crop_file(args.crops)

    print(f"Preparing jobs ({len(crops)} candidate image pairs)...")
    num_levels = min(
        math.ceil(math.log(num_crops_to_generate, args.ideal_number_pairs_in_dir)),
        args.max_subdir_levels,
    )
    num_pairs_in_dir = math.ceil(num_crops_to_generate ** (1 / num_levels))

    jobs = prepare_jobs(crops, num_levels, num_pairs_in_dir)
    del crops

    os.makedirs(args.output_dir, exist_ok=True)
    mmap = Pool(args.nthread).imap_unordered if args.nthread > 1 else map
    call = functools.partial(save_image_crops, args)

    print(f"Generating cropped images to {args.output_dir} ...")
    with open(listing_path, "w") as listing:
        listing.write("# pair_path\n")
        for results in tqdm(mmap(call, jobs), total=len(jobs)):
            for path in results:
                listing.write(f"{path}\n")
    print("Finished writing listing to", listing_path)

def load_crop_file(path):
    data = open(path).read().splitlines()
    pairs = []
    num_crops_to_generate = 0
    for line in tqdm(data):
        if line.startswith("#"):
            continue
        line = line.split(", ")
        if len(line) < 8:
            img1, img2, rotation = line
            pairs.append((img1, img2, int(rotation), []))
        else:
            l1, r1, t1, b1, l2, r2, t2, b2 = map(int, line)
            rect1, rect2 = (l1, t1, r1, b1), (l2, t2, r2, b2)
            pairs[-1][-1].append((rect1, rect2))
            num_crops_to_generate += 1
    return pairs, num_crops_to_generate


        args.max_subdir_levels,
    )
    num_pairs_in_dir = math.ceil(num_crops_to_generate ** (1 / num_levels))

    jobs = prepare_jobs(crops, num_levels, num_pairs_in_dir)
    del crops

    os.makedirs(args.output_dir, exist_ok=True)
    mmap = Pool(args.nthread).imap_unordered if args.nthread > 1 else map
    call = functools.partial(save_image_crops, args)

    print(f"Generating cropped images to {args.output_dir} ...")
    with open(listing_path, "w") as listing:
        listing.write("# pair_path\n")
        for results in tqdm(mmap(call, jobs), total=len(jobs)):
            for path in results:
                listing.write(f"{path}\n")
    print("Finished writing listing to", listing_path)

def load_crop_file(path):
    data = open(path).read().splitlines()
    pairs = []
    num_crops_to_generate = 0
    for line in tqdm(data):
        if line.startswith("#"):
            continue
        line = line.split(", ")
        if len(line) < 8:
            img1, img2, rotation = line
            pairs.append((img1, img2, int(rotation), []))
        else:
            l1, r1, t1, b1, l2, r2, t2, b2 = map(int, line)
            rect1, rect2 = (l1, t1, r1, b1), (l2, t2, r2, b2)
            pairs[-1][-1].append((rect1, rect2))
            num_crops_to_generate += 1
    return pairs, num_crops_to_generate

def prepare_jobs(pairs, num_levels, num_pairs_in_dir):
    jobs = []
    powers = [num_pairs_in_dir**level for level in reversed(range(num_levels))]

    def get_path(idx):
        idx_array = []
        d = idx
        for level in range(num_levels - 1):
            idx_array.append(idx // powers[level])
            idx = idx % powers[level]
        idx_array.append(d)
        return "/".join(map(lambda x: hex(x)[2:], idx_array))

    idx = 0
    for pair_data in tqdm(pairs):
        img1, img2, rotation, crops = pair_data
        if -60 <= rotation and rotation <= 60:
            rotation = 0  # most likely not a true rotation


def load_crop_file(path):
    data = open(path).read().splitlines()
    pairs = []
    num_crops_to_generate = 0
    for line in tqdm(data):
        if line.startswith("#"):
            continue
        line = line.split(", ")
        if len(line) < 8:
            img1, img2, rotation = line
            pairs.append((img1, img2, int(rotation), []))
        else:
            l1, r1, t1, b1, l2, r2, t2, b2 = map(int, line)
            rect1, rect2 = (l1, t1, r1, b1), (l2, t2, r2, b2)
            pairs[-1][-1].append((rect1, rect2))
            num_crops_to_generate += 1
    return pairs, num_crops_to_generate

def prepare_jobs(pairs, num_levels, num_pairs_in_dir):
    jobs = []
    powers = [num_pairs_in_dir**level for level in reversed(range(num_levels))]

    def get_path(idx):
        idx_array = []
        d = idx
        for level in range(num_levels - 1):
            idx_array.append(idx // powers[level])
            idx = idx % powers[level]
        idx_array.append(d)
        return "/".join(map(lambda x: hex(x)[2:], idx_array))

    idx = 0
    for pair_data in tqdm(pairs):
        img1, img2, rotation, crops = pair_data
        if -60 <= rotation and rotation <= 60:
            rotation = 0  # most likely not a true rotation
        paths = [get_path(idx + k) for k in range(len(crops))]
        idx += len(crops)
        jobs.append(((img1, img2), rotation, crops, paths))
    return jobs

def load_image(path):
    try:
        return Image.open(path).convert("RGB")
    except Exception as e:
        print("skipping", path, e)
        raise OSError()

def save_image_crops(args, data):
    # load images
    img_pair, rot, crops, paths = data
    try:
        img1, img2 = [
            load_image(os.path.join(args.root_dir, impath)) for impath in img_pair
        ]
    except OSError as e:
        return []


    def get_path(idx):
        idx_array = []
        d = idx
        for level in range(num_levels - 1):
            idx_array.append(idx // powers[level])
            idx = idx % powers[level]
        idx_array.append(d)
        return "/".join(map(lambda x: hex(x)[2:], idx_array))

    idx = 0
    for pair_data in tqdm(pairs):
        img1, img2, rotation, crops = pair_data
        if -60 <= rotation and rotation <= 60:
            rotation = 0  # most likely not a true rotation
        paths = [get_path(idx + k) for k in range(len(crops))]
        idx += len(crops)
        jobs.append(((img1, img2), rotation, crops, paths))
    return jobs

def load_image(path):
    try:
        return Image.open(path).convert("RGB")
    except Exception as e:
        print("skipping", path, e)
        raise OSError()

def save_image_crops(args, data):
    # load images
    img_pair, rot, crops, paths = data
    try:
        img1, img2 = [
            load_image(os.path.join(args.root_dir, impath)) for impath in img_pair
        ]
    except OSError as e:
        return []

    def area(sz):
        return sz[0] * sz[1]

    tgt_size = (args.imsize, args.imsize)

    def prepare_crop(img, rect, rot=0):
        # actual crop
        img = img.crop(rect)


    idx = 0
    for pair_data in tqdm(pairs):
        img1, img2, rotation, crops = pair_data
        if -60 <= rotation and rotation <= 60:
            rotation = 0  # most likely not a true rotation
        paths = [get_path(idx + k) for k in range(len(crops))]
        idx += len(crops)
        jobs.append(((img1, img2), rotation, crops, paths))
    return jobs

def load_image(path):
    try:
        return Image.open(path).convert("RGB")
    except Exception as e:
        print("skipping", path, e)
        raise OSError()

def save_image_crops(args, data):
    # load images
    img_pair, rot, crops, paths = data
    try:
        img1, img2 = [
            load_image(os.path.join(args.root_dir, impath)) for impath in img_pair
        ]
    except OSError as e:
        return []

    def area(sz):
        return sz[0] * sz[1]

    tgt_size = (args.imsize, args.imsize)

    def prepare_crop(img, rect, rot=0):
        # actual crop
        img = img.crop(rect)

        # resize to desired size
        interp = (
            Image.Resampling.LANCZOS
            if area(img.size) > 4 * area(tgt_size)
            else Image.Resampling.BICUBIC
        )
        img = img.resize(tgt_size, resample=interp)

        # rotate the image
        rot90 = (round(rot / 90) % 4) * 90
        if rot90 == 90:
            img = img.transpose(Image.Transpose.ROTATE_90)
        elif rot90 == 180:
            img = img.transpose(Image.Transpose.ROTATE_180)
        elif rot90 == 270:
            img = img.transpose(Image.Transpose.ROTATE_270)
        return img

    results = []
    for (rect1, rect2), path in zip(crops, paths):
        crop1 = prepare_crop(img1, rect1)
        crop2 = prepare_crop(img2, rect2, rot)

        fullpath1 = os.path.join(args.output_dir, path + "_1.jpg")
        fullpath2 = os.path.join(args.output_dir, path + "_2.jpg")
        os.makedirs(os.path.dirname(fullpath1), exist_ok=True)

        assert not os.path.isfile(fullpath1), fullpath1
        assert not os.path.isfile(fullpath2), fullpath2
        crop1.save(fullpath1)
        crop2.save(fullpath2)
        results.append(path)

    return results

if __name__ == "__main__":
    args = arg_parser().parse_args()
    main(args)


    num_crops_to_generate = 0
    for line in tqdm(data):
        if line.startswith("#"):
            continue
        line = line.split(", ")
        if len(line) < 8:
            img1, img2, rotation = line
            pairs.append((img1, img2, int(rotation), []))
        else:
            l1, r1, t1, b1, l2, r2, t2, b2 = map(int, line)
            rect1, rect2 = (l1, t1, r1, b1), (l2, t2, r2, b2)
            pairs[-1][-1].append((rect1, rect2))
            num_crops_to_generate += 1
    return pairs, num_crops_to_generate

def prepare_jobs(pairs, num_levels, num_pairs_in_dir):
    jobs = []
    powers = [num_pairs_in_dir**level for level in reversed(range(num_levels))]

    def get_path(idx):
        idx_array = []
        d = idx
        for level in range(num_levels - 1):
            idx_array.append(idx // powers[level])
            idx = idx % powers[level]
        idx_array.append(d)
        return "/".join(map(lambda x: hex(x)[2:], idx_array))

    idx = 0
    for pair_data in tqdm(pairs):
        img1, img2, rotation, crops = pair_data
        if -60 <= rotation and rotation <= 60:
            rotation = 0  # most likely not a true rotation
        paths = [get_path(idx + k) for k in range(len(crops))]
        idx += len(crops)
        jobs.append(((img1, img2), rotation, crops, paths))
    return jobs

def load_image(path):
    try:
        return Image.open(path).convert("RGB")
    except Exception as e:
        print("skipping", path, e)
        raise OSError()


area

def load_image(path):
    try:
        return Image.open(path).convert("RGB")
    except Exception as e:
        print("skipping", path, e)
        raise OSError()

def save_image_crops(args, data):
    # load images
    img_pair, rot, crops, paths = data
    try:
        img1, img2 = [
            load_image(os.path.join(args.root_dir, impath)) for impath in img_pair
        ]
    except OSError as e:
        return []

    def area(sz):
        return sz[0] * sz[1]

    tgt_size = (args.imsize, args.imsize)

    def prepare_crop(img, rect, rot=0):
        # actual crop
        img = img.crop(rect)

        # resize to desired size
        interp = (
            Image.Resampling.LANCZOS
            if area(img.size) > 4 * area(tgt_size)
            else Image.Resampling.BICUBIC
        )
        img = img.resize(tgt_size, resample=interp)

        # rotate the image
        rot90 = (round(rot / 90) % 4) * 90
        if rot90 == 90:
            img = img.transpose(Image.Transpose.ROTATE_90)
        elif rot90 == 180:


    except Exception as e:
        print("skipping", path, e)
        raise OSError()

def save_image_crops(args, data):
    # load images
    img_pair, rot, crops, paths = data
    try:
        img1, img2 = [
            load_image(os.path.join(args.root_dir, impath)) for impath in img_pair
        ]
    except OSError as e:
        return []

    def area(sz):
        return sz[0] * sz[1]

    tgt_size = (args.imsize, args.imsize)

    def prepare_crop(img, rect, rot=0):
        # actual crop
        img = img.crop(rect)

        # resize to desired size
        interp = (
            Image.Resampling.LANCZOS
            if area(img.size) > 4 * area(tgt_size)
            else Image.Resampling.BICUBIC
        )
        img = img.resize(tgt_size, resample=interp)

        # rotate the image
        rot90 = (round(rot / 90) % 4) * 90
        if rot90 == 90:
            img = img.transpose(Image.Transpose.ROTATE_90)
        elif rot90 == 180:
            img = img.transpose(Image.Transpose.ROTATE_180)
        elif rot90 == 270:
            img = img.transpose(Image.Transpose.ROTATE_270)
        return img

    results = []
    for (rect1, rect2), path in zip(crops, paths):
        crop1 = prepare_crop(img1, rect1)
        crop2 = prepare_crop(img2, rect2, rot)

        fullpath1 = os.path.join(args.output_dir, path + "_1.jpg")
        fullpath2 = os.path.join(args.output_dir, path + "_2.jpg")
        os.makedirs(os.path.dirname(fullpath1), exist_ok=True)

        assert not os.path.isfile(fullpath1), fullpath1
        assert not os.path.isfile(fullpath2), fullpath2
        crop1.save(fullpath1)
        crop2.save(fullpath2)
        results.append(path)

    return results

if __name__ == "__main__":


# Copyright (C) 2022-present Naver Corporation. All rights reserved.
# Licensed under CC BY-NC-SA 4.0 (non-commercial use only).

"""
Paths to Habitat-Sim scenes
"""

import os
import json
import collections
from tqdm import tqdm

# Hardcoded path to the different scene datasets
SCENES_DATASET = {
    "hm3d": "./data/habitat-sim-data/scene_datasets/hm3d/",
    "gibson": "./data/habitat-sim-data/scene_datasets/gibson/",
    "habitat-test-scenes": "./data/habitat-sim/scene_datasets/habitat-test-scenes/",
    "replica_cad_baked_lighting": "./data/habitat-sim/scene_datasets/replica_cad_baked_lighting/",
    "replica_cad": "./data/habitat-sim/scene_datasets/replica_cad/",
    "replica": "./data/habitat-sim/scene_datasets/ReplicaDataset/",
    "scannet": "./data/habitat-sim/scene_datasets/scannet/",
}

SceneData = collections.namedtuple(
    "SceneData", ["scene_dataset_config_file", "scene", "navmesh", "output_dir"]
)

def list_replicacad_scenes(base_output_dir, base_path=SCENES_DATASET["replica_cad"]):
    scene_dataset_config_file = os.path.join(
        base_path, "replicaCAD.scene_dataset_config.json"
    )
    scenes = [f"apt_{i}" for i in range(6)] + ["empty_stage"]
    navmeshes = [f"navmeshes/apt_{i}_static_furniture.navmesh" for i in range(6)] + [
        "empty_stage.navmesh"
    ]
    scenes_data = []
    for idx in range(len(scenes)):
        output_dir = os.path.join(base_output_dir, "ReplicaCAD", scenes[idx])
        # Add scene
        data = SceneData(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scenes[idx] + ".scene_instance.json",
            navmesh=os.path.join(base_path, navmeshes[idx]),
            output_dir=output_dir,
        )
        scenes_data.append(data)
    return scenes_data

def list_replica_cad_baked_lighting_scenes(
    base_output_dir, base_path=SCENES_DATASET["replica_cad_baked_lighting"]
):
    scene_dataset_config_file = os.path.join(
        base_path, "replicaCAD_baked.scene_dataset_config.json"
    )
    scenes = sum(
        [[f"Baked_sc{i}_staging_{j:02}" for i in range(5)] for j in range(21)], []
    )
    navmeshes = ""  # [f"navmeshes/apt_{i}_static_furniture.navmesh" for i in range(6)] + ["empty_stage.navmesh"]
    scenes_data = []
    for idx in range(len(scenes)):
        output_dir = os.path.join(
            base_output_dir, "replica_cad_baked_lighting", scenes[idx]
        )
        data = SceneData(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scenes[idx],
            navmesh="",
            output_dir=output_dir,
        )
        scenes_data.append(data)
    return scenes_data

def list_replica_scenes(base_output_dir, base_path):
    scenes_data = []
    for scene_id in os.listdir(base_path):
        scene = os.path.join(base_path, scene_id, "mesh.ply")
        navmesh = os.path.join(
            base_path, scene_id, "habitat/mesh_preseg_semantic.navmesh"
        )  # Not sure if I should use it
        scene_dataset_config_file = ""
        output_dir = os.path.join(base_output_dir, scene_id)
        # Add scene only if it does not exist already, or if exist_ok
        data = SceneData(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scene,
            navmesh=navmesh,
            output_dir=output_dir,
        )
        scenes_data.append(data)
    return scenes_data

def list_scenes(base_output_dir, base_path):
    """
    Generic method iterating through a base_path folder to find scenes.
    """
    scenes_data = []
    for root, dirs, files in os.walk(base_path, followlinks=True):
        folder_scenes_data = []
        for file in files:
            name, ext = os.path.splitext(file)
            if ext == ".glb":
                scene = os.path.join(root, name + ".glb")
                navmesh = os.path.join(root, name + ".navmesh")
                if not os.path.exists(navmesh):
                    navmesh = ""
                relpath = os.path.relpath(root, base_path)
                output_dir = os.path.abspath(
                    os.path.join(base_output_dir, relpath, name)
                )
                data = SceneData(
                    scene_dataset_config_file="",
                    scene=scene,
                    navmesh=navmesh,
                    output_dir=output_dir,
                )
                folder_scenes_data.append(data)

        # Specific check for HM3D:
        # When two meshesxxxx.basis.glb and xxxx.glb are present, use the 'basis' version.
        basis_scenes = [
            data.scene[: -len(".basis.glb")]
            for data in folder_scenes_data
            if data.scene.endswith(".basis.glb")
        ]
        if len(basis_scenes) != 0:
            folder_scenes_data = [
                data
                for data in folder_scenes_data
                if not (data.scene[: -len(".glb")] in basis_scenes)
            ]

        scenes_data.extend(folder_scenes_data)
    return scenes_data

def list_scenes_available(base_output_dir, scenes_dataset_paths=SCENES_DATASET):
    scenes_data = []

    # HM3D
    for split in ("minival", "train", "val", "examples"):
        scenes_data += list_scenes(
            base_output_dir=os.path.join(base_output_dir, f"hm3d/{split}/"),
            base_path=f"{scenes_dataset_paths['hm3d']}/{split}",
        )

    # Gibson
    scenes_data += list_scenes(
        base_output_dir=os.path.join(base_output_dir, "gibson"),
        base_path=scenes_dataset_paths["gibson"],
    )

    # Habitat test scenes (just a few)
    scenes_data += list_scenes(
        base_output_dir=os.path.join(base_output_dir, "habitat-test-scenes"),
        base_path=scenes_dataset_paths["habitat-test-scenes"],
    )

    # ReplicaCAD (baked lightning)
    scenes_data += list_replica_cad_baked_lighting_scenes(
        base_output_dir=base_output_dir
    )

    # ScanNet
    scenes_data += list_scenes(
        base_output_dir=os.path.join(base_output_dir, "scannet"),
        base_path=scenes_dataset_paths["scannet"],
    )

    # Replica
    list_replica_scenes(
        base_output_dir=os.path.join(base_output_dir, "replica"),
        base_path=scenes_dataset_paths["replica"],
    )
    return scenes_data


import collections
from tqdm import tqdm

# Hardcoded path to the different scene datasets
SCENES_DATASET = {
    "hm3d": "./data/habitat-sim-data/scene_datasets/hm3d/",
    "gibson": "./data/habitat-sim-data/scene_datasets/gibson/",
    "habitat-test-scenes": "./data/habitat-sim/scene_datasets/habitat-test-scenes/",
    "replica_cad_baked_lighting": "./data/habitat-sim/scene_datasets/replica_cad_baked_lighting/",
    "replica_cad": "./data/habitat-sim/scene_datasets/replica_cad/",
    "replica": "./data/habitat-sim/scene_datasets/ReplicaDataset/",
    "scannet": "./data/habitat-sim/scene_datasets/scannet/",
}

SceneData = collections.namedtuple(
    "SceneData", ["scene_dataset_config_file", "scene", "navmesh", "output_dir"]
)

def list_replicacad_scenes(base_output_dir, base_path=SCENES_DATASET["replica_cad"]):
    scene_dataset_config_file = os.path.join(
        base_path, "replicaCAD.scene_dataset_config.json"
    )
    scenes = [f"apt_{i}" for i in range(6)] + ["empty_stage"]
    navmeshes = [f"navmeshes/apt_{i}_static_furniture.navmesh" for i in range(6)] + [
        "empty_stage.navmesh"
    ]
    scenes_data = []
    for idx in range(len(scenes)):
        output_dir = os.path.join(base_output_dir, "ReplicaCAD", scenes[idx])
        # Add scene
        data = SceneData(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scenes[idx] + ".scene_instance.json",
            navmesh=os.path.join(base_path, navmeshes[idx]),
            output_dir=output_dir,
        )
        scenes_data.append(data)
    return scenes_data

def list_replica_cad_baked_lighting_scenes(
    base_output_dir, base_path=SCENES_DATASET["replica_cad_baked_lighting"]
):
    scene_dataset_config_file = os.path.join(
        base_path, "replicaCAD_baked.scene_dataset_config.json"
    )
    scenes = sum(
        [[f"Baked_sc{i}_staging_{j:02}" for i in range(5)] for j in range(21)], []
    )
    navmeshes = ""  # [f"navmeshes/apt_{i}_static_furniture.navmesh" for i in range(6)] + ["empty_stage.navmesh"]
    scenes_data = []
    for idx in range(len(scenes)):
        output_dir = os.path.join(
            base_output_dir, "replica_cad_baked_lighting", scenes[idx]
        )
        data = SceneData(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scenes[idx],


        base_path, "replicaCAD.scene_dataset_config.json"
    )
    scenes = [f"apt_{i}" for i in range(6)] + ["empty_stage"]
    navmeshes = [f"navmeshes/apt_{i}_static_furniture.navmesh" for i in range(6)] + [
        "empty_stage.navmesh"
    ]
    scenes_data = []
    for idx in range(len(scenes)):
        output_dir = os.path.join(base_output_dir, "ReplicaCAD", scenes[idx])
        # Add scene
        data = SceneData(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scenes[idx] + ".scene_instance.json",
            navmesh=os.path.join(base_path, navmeshes[idx]),
            output_dir=output_dir,
        )
        scenes_data.append(data)
    return scenes_data

def list_replica_cad_baked_lighting_scenes(
    base_output_dir, base_path=SCENES_DATASET["replica_cad_baked_lighting"]
):
    scene_dataset_config_file = os.path.join(
        base_path, "replicaCAD_baked.scene_dataset_config.json"
    )
    scenes = sum(
        [[f"Baked_sc{i}_staging_{j:02}" for i in range(5)] for j in range(21)], []
    )
    navmeshes = ""  # [f"navmeshes/apt_{i}_static_furniture.navmesh" for i in range(6)] + ["empty_stage.navmesh"]
    scenes_data = []
    for idx in range(len(scenes)):
        output_dir = os.path.join(
            base_output_dir, "replica_cad_baked_lighting", scenes[idx]
        )
        data = SceneData(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scenes[idx],
            navmesh="",
            output_dir=output_dir,
        )
        scenes_data.append(data)
    return scenes_data

def list_replica_scenes(base_output_dir, base_path):
    scenes_data = []
    for scene_id in os.listdir(base_path):
        scene = os.path.join(base_path, scene_id, "mesh.ply")
        navmesh = os.path.join(
            base_path, scene_id, "habitat/mesh_preseg_semantic.navmesh"
        )  # Not sure if I should use it
        scene_dataset_config_file = ""
        output_dir = os.path.join(base_output_dir, scene_id)
        # Add scene only if it does not exist already, or if exist_ok
        data = SceneData(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scene,
            navmesh=navmesh,
            output_dir=output_dir,
        )
        scenes_data.append(data)
    return scenes_data


    )
    scenes = sum(
        [[f"Baked_sc{i}_staging_{j:02}" for i in range(5)] for j in range(21)], []
    )
    navmeshes = ""  # [f"navmeshes/apt_{i}_static_furniture.navmesh" for i in range(6)] + ["empty_stage.navmesh"]
    scenes_data = []
    for idx in range(len(scenes)):
        output_dir = os.path.join(
            base_output_dir, "replica_cad_baked_lighting", scenes[idx]
        )
        data = SceneData(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scenes[idx],
            navmesh="",
            output_dir=output_dir,
        )
        scenes_data.append(data)
    return scenes_data

def list_replica_scenes(base_output_dir, base_path):
    scenes_data = []
    for scene_id in os.listdir(base_path):
        scene = os.path.join(base_path, scene_id, "mesh.ply")
        navmesh = os.path.join(
            base_path, scene_id, "habitat/mesh_preseg_semantic.navmesh"
        )  # Not sure if I should use it
        scene_dataset_config_file = ""
        output_dir = os.path.join(base_output_dir, scene_id)
        # Add scene only if it does not exist already, or if exist_ok
        data = SceneData(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scene,
            navmesh=navmesh,
            output_dir=output_dir,
        )
        scenes_data.append(data)
    return scenes_data

def list_scenes(base_output_dir, base_path):
    """
    Generic method iterating through a base_path folder to find scenes.
    """
    scenes_data = []
    for root, dirs, files in os.walk(base_path, followlinks=True):
        folder_scenes_data = []
        for file in files:
            name, ext = os.path.splitext(file)
            if ext == ".glb":
                scene = os.path.join(root, name + ".glb")
                navmesh = os.path.join(root, name + ".navmesh")
                if not os.path.exists(navmesh):
                    navmesh = ""
                relpath = os.path.relpath(root, base_path)
                output_dir = os.path.abspath(
                    os.path.join(base_output_dir, relpath, name)
                )


def list_replica_scenes(base_output_dir, base_path):
    scenes_data = []
    for scene_id in os.listdir(base_path):
        scene = os.path.join(base_path, scene_id, "mesh.ply")
        navmesh = os.path.join(
            base_path, scene_id, "habitat/mesh_preseg_semantic.navmesh"
        )  # Not sure if I should use it
        scene_dataset_config_file = ""
        output_dir = os.path.join(base_output_dir, scene_id)
        # Add scene only if it does not exist already, or if exist_ok
        data = SceneData(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scene,
            navmesh=navmesh,
            output_dir=output_dir,
        )
        scenes_data.append(data)
    return scenes_data

def list_scenes(base_output_dir, base_path):
    """
    Generic method iterating through a base_path folder to find scenes.
    """
    scenes_data = []
    for root, dirs, files in os.walk(base_path, followlinks=True):
        folder_scenes_data = []
        for file in files:
            name, ext = os.path.splitext(file)
            if ext == ".glb":
                scene = os.path.join(root, name + ".glb")
                navmesh = os.path.join(root, name + ".navmesh")
                if not os.path.exists(navmesh):
                    navmesh = ""
                relpath = os.path.relpath(root, base_path)
                output_dir = os.path.abspath(
                    os.path.join(base_output_dir, relpath, name)
                )
                data = SceneData(
                    scene_dataset_config_file="",
                    scene=scene,
                    navmesh=navmesh,
                    output_dir=output_dir,
                )
                folder_scenes_data.append(data)

        # Specific check for HM3D:
        # When two meshesxxxx.basis.glb and xxxx.glb are present, use the 'basis' version.
        basis_scenes = [
            data.scene[: -len(".basis.glb")]
            for data in folder_scenes_data
            if data.scene.endswith(".basis.glb")
        ]
        if len(basis_scenes) != 0:
            folder_scenes_data = [
                data
                for data in folder_scenes_data
                if not (data.scene[: -len(".glb")] in basis_scenes)
            ]

        scenes_data.extend(folder_scenes_data)
    return scenes_data

def list_scenes_available(base_output_dir, scenes_dataset_paths=SCENES_DATASET):
    scenes_data = []

    # HM3D
    for split in ("minival", "train", "val", "examples"):
        scenes_data += list_scenes(
            base_output_dir=os.path.join(base_output_dir, f"hm3d/{split}/"),
            base_path=f"{scenes_dataset_paths['hm3d']}/{split}",
        )

    # Gibson
    scenes_data += list_scenes(
        base_output_dir=os.path.join(base_output_dir, "gibson"),
        base_path=scenes_dataset_paths["gibson"],
    )

    # Habitat test scenes (just a few)
    scenes_data += list_scenes(


                folder_scenes_data.append(data)

        # Specific check for HM3D:
        # When two meshesxxxx.basis.glb and xxxx.glb are present, use the 'basis' version.
        basis_scenes = [
            data.scene[: -len(".basis.glb")]
            for data in folder_scenes_data
            if data.scene.endswith(".basis.glb")
        ]
        if len(basis_scenes) != 0:
            folder_scenes_data = [
                data
                for data in folder_scenes_data
                if not (data.scene[: -len(".glb")] in basis_scenes)
            ]

        scenes_data.extend(folder_scenes_data)
    return scenes_data

def list_scenes_available(base_output_dir, scenes_dataset_paths=SCENES_DATASET):
    scenes_data = []

    # HM3D
    for split in ("minival", "train", "val", "examples"):
        scenes_data += list_scenes(
            base_output_dir=os.path.join(base_output_dir, f"hm3d/{split}/"),
            base_path=f"{scenes_dataset_paths['hm3d']}/{split}",
        )

    # Gibson
    scenes_data += list_scenes(
        base_output_dir=os.path.join(base_output_dir, "gibson"),
        base_path=scenes_dataset_paths["gibson"],
    )

    # Habitat test scenes (just a few)
    scenes_data += list_scenes(
        base_output_dir=os.path.join(base_output_dir, "habitat-test-scenes"),
        base_path=scenes_dataset_paths["habitat-test-scenes"],
    )

    # ReplicaCAD (baked lightning)
    scenes_data += list_replica_cad_baked_lighting_scenes(
        base_output_dir=base_output_dir
    )

    # ScanNet
    scenes_data += list_scenes(
        base_output_dir=os.path.join(base_output_dir, "scannet"),
        base_path=scenes_dataset_paths["scannet"],
    )

    # Replica
    list_replica_scenes(
        base_output_dir=os.path.join(base_output_dir, "replica"),
        base_path=scenes_dataset_paths["replica"],
    )
    return scenes_data


# Copyright (C) 2022-present Naver Corporation. All rights reserved.
# Licensed under CC BY-NC-SA 4.0 (non-commercial use only).

import os
from tqdm import tqdm
import argparse
import PIL.Image
import numpy as np
import json
from datasets.habitat_sim.multiview_habitat_sim_generator import (
    MultiviewHabitatSimGenerator,
    NoNaviguableSpaceError,
)
from datasets.habitat_sim.paths import list_scenes_available
import cv2
import quaternion
import shutil

def generate_multiview_images_for_scene(
    scene_dataset_config_file,
    scene,
    navmesh,
    output_dir,
    views_count,
    size,
    exist_ok=False,
    generate_depth=False,
    **kwargs,
):
    """
    Generate tuples of overlapping views for a given scene.
    generate_depth: generate depth images and camera parameters.
    """
    if os.path.exists(output_dir) and not exist_ok:
        print(f"Scene {scene}: data already generated. Ignoring generation.")
        return
    try:
        print(f"Scene {scene}: {size} multiview acquisitions to generate...")
        os.makedirs(output_dir, exist_ok=exist_ok)

        metadata_filename = os.path.join(output_dir, "metadata.json")

        metadata_template = dict(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scene,
            navmesh=navmesh,
            views_count=views_count,
            size=size,
            generate_depth=generate_depth,
            **kwargs,
        )
        metadata_template["multiviews"] = dict()

        if os.path.exists(metadata_filename):
            print("Metadata file already exists:", metadata_filename)
            print("Loading already generated metadata file...")
            with open(metadata_filename, "r") as f:
                metadata = json.load(f)

            for key in metadata_template.keys():
                if key != "multiviews":
                    assert (
                        metadata_template[key] == metadata[key]
                    ), f"existing file is inconsistent with the input parameters:\nKey: {key}\nmetadata: {metadata[key]}\ntemplate: {metadata_template[key]}."
        else:
            print("No temporary file found. Starting generation from scratch...")
            metadata = metadata_template

        starting_id = len(metadata["multiviews"])
        print(f"Starting generation from index {starting_id}/{size}...")
        if starting_id >= size:
            print("Generation already done.")
            return

        generator = MultiviewHabitatSimGenerator(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scene,
            navmesh=navmesh,
            views_count=views_count,
            size=size,
            **kwargs,
        )

        for idx in tqdm(range(starting_id, size)):
            # Generate / re-generate the observations
            try:
                data = generator[idx]
                observations = data["observations"]
                positions = data["positions"]
                orientations = data["orientations"]

                idx_label = f"{idx:08}"
                for oidx, observation in enumerate(observations):
                    observation_label = (
                        f"{oidx + 1}"  # Leonid is indexing starting from 1
                    )
                    # Color image saved using PIL
                    img = PIL.Image.fromarray(observation["color"][:, :, :3])
                    filename = os.path.join(
                        output_dir, f"{idx_label}_{observation_label}.jpeg"
                    )
                    img.save(filename)
                    if generate_depth:
                        # Depth image as EXR file
                        filename = os.path.join(
                            output_dir, f"{idx_label}_{observation_label}_depth.exr"
                        )
                        cv2.imwrite(
                            filename,
                            observation["depth"],
                            [cv2.IMWRITE_EXR_TYPE, cv2.IMWRITE_EXR_TYPE_HALF],
                        )
                        # Camera parameters
                        camera_params = dict(
                            [
                                (key, observation[key].tolist())
                                for key in (
                                    "camera_intrinsics",
                                    "R_cam2world",
                                    "t_cam2world",
                                )
                            ]
                        )
                        filename = os.path.join(
                            output_dir,
                            f"{idx_label}_{observation_label}_camera_params.json",
                        )
                        with open(filename, "w") as f:
                            json.dump(camera_params, f)
                metadata["multiviews"][idx_label] = {
                    "positions": positions.tolist(),
                    "orientations": orientations.tolist(),
                    "covisibility_ratios": data["covisibility_ratios"].tolist(),
                    "valid_fractions": data["valid_fractions"].tolist(),
                    "pairwise_visibility_ratios": data[
                        "pairwise_visibility_ratios"
                    ].tolist(),
                }
            except RecursionError:
                print(
                    "Recursion error: unable to sample observations for this scene. We will stop there."
                )
                break

            # Regularly save a temporary metadata file, in case we need to restart the generation
            if idx % 10 == 0:
                with open(metadata_filename, "w") as f:
                    json.dump(metadata, f)

        # Save metadata
        with open(metadata_filename, "w") as f:
            json.dump(metadata, f)

        generator.close()
    except NoNaviguableSpaceError:
        pass

def create_commandline(scene_data, generate_depth, exist_ok=False):
    """
    Create a commandline string to generate a scene.
    """

    def my_formatting(val):
        if val is None or val == "":
            return '""'
        else:
            return val

    commandline = f"""python {__file__} --scene {my_formatting(scene_data.scene)}
    --scene_dataset_config_file {my_formatting(scene_data.scene_dataset_config_file)}
    --navmesh {my_formatting(scene_data.navmesh)}
    --output_dir {my_formatting(scene_data.output_dir)}
    --generate_depth {int(generate_depth)}
    --exist_ok {int(exist_ok)}
    """
    commandline = " ".join(commandline.split())
    return commandline

if __name__ == "__main__":
    os.umask(2)

    parser = argparse.ArgumentParser(
        description="""Example of use -- listing commands to generate data for scenes available:
    > python datasets/habitat_sim/generate_multiview_habitat_images.py --list_commands
    """
    )

    parser.add_argument("--output_dir", type=str, required=True)
    parser.add_argument(
        "--list_commands", action="store_true", help="list commandlines to run if true"
    )
    parser.add_argument("--scene", type=str, default="")
    parser.add_argument("--scene_dataset_config_file", type=str, default="")
    parser.add_argument("--navmesh", type=str, default="")

    parser.add_argument("--generate_depth", type=int, default=1)
    parser.add_argument("--exist_ok", type=int, default=0)

    kwargs = dict(resolution=(256, 256), hfov=60, views_count=2, size=1000)

    args = parser.parse_args()
    generate_depth = bool(args.generate_depth)
    exist_ok = bool(args.exist_ok)

    if args.list_commands:
        # Listing scenes available...
        scenes_data = list_scenes_available(base_output_dir=args.output_dir)

        for scene_data in scenes_data:
            print(
                create_commandline(
                    scene_data, generate_depth=generate_depth, exist_ok=exist_ok
                )
            )
    else:
        if args.scene == "" or args.output_dir == "":
            print("Missing scene or output dir argument!")
            print(parser.format_help())
        else:
            generate_multiview_images_for_scene(
                scene=args.scene,
                scene_dataset_config_file=args.scene_dataset_config_file,
                navmesh=args.navmesh,
                output_dir=args.output_dir,
                exist_ok=exist_ok,
                generate_depth=generate_depth,
                **kwargs,
            )


# Copyright (C) 2022-present Naver Corporation. All rights reserved.
# Licensed under CC BY-NC-SA 4.0 (non-commercial use only).

import os
from tqdm import tqdm
import argparse
import PIL.Image
import numpy as np
import json
from datasets.habitat_sim.multiview_habitat_sim_generator import (
    MultiviewHabitatSimGenerator,
    NoNaviguableSpaceError,
)
from datasets.habitat_sim.paths import list_scenes_available
import cv2
import quaternion
import shutil

def generate_multiview_images_for_scene(
    scene_dataset_config_file,
    scene,
    navmesh,
    output_dir,
    views_count,
    size,
    exist_ok=False,
    generate_depth=False,
    **kwargs,
):
    """
    Generate tuples of overlapping views for a given scene.
    generate_depth: generate depth images and camera parameters.
    """
    if os.path.exists(output_dir) and not exist_ok:
        print(f"Scene {scene}: data already generated. Ignoring generation.")
        return
    try:
        print(f"Scene {scene}: {size} multiview acquisitions to generate...")
        os.makedirs(output_dir, exist_ok=exist_ok)

        metadata_filename = os.path.join(output_dir, "metadata.json")

        metadata_template = dict(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scene,
            navmesh=navmesh,
            views_count=views_count,
            size=size,
            generate_depth=generate_depth,
            **kwargs,
        )
        metadata_template["multiviews"] = dict()

        if os.path.exists(metadata_filename):
            print("Metadata file already exists:", metadata_filename)
            print("Loading already generated metadata file...")
            with open(metadata_filename, "r") as f:
                metadata = json.load(f)

            for key in metadata_template.keys():
                if key != "multiviews":
                    assert (
                        metadata_template[key] == metadata[key]
                    ), f"existing file is inconsistent with the input parameters:\nKey: {key}\nmetadata: {metadata[key]}\ntemplate: {metadata_template[key]}."
        else:
            print("No temporary file found. Starting generation from scratch...")
            metadata = metadata_template

        starting_id = len(metadata["multiviews"])
        print(f"Starting generation from index {starting_id}/{size}...")
        if starting_id >= size:
            print("Generation already done.")
            return

        generator = MultiviewHabitatSimGenerator(
            scene_dataset_config_file=scene_dataset_config_file,
            scene=scene,
            navmesh=navmesh,
            views_count=views_count,
            size=size,
            **kwargs,
        )

        for idx in tqdm(range(starting_id, size)):
            # Generate / re-generate the observations
            try:
                data = generator[idx]
                observations = data["observations"]
                positions = data["positions"]
                orientations = data["orientations"]

                idx_label = f"{idx:08}"
                for oidx, observation in enumerate(observations):
                    observation_label = (
                        f"{oidx + 1}"  # Leonid is indexing starting from 1
                    )
                    # Color image saved using PIL
                    img = PIL.Image.fromarray(observation["color"][:, :, :3])
                    filename = os.path.join(
                        output_dir, f"{idx_label}_{observation_label}.jpeg"
                    )
                    img.save(filename)
                    if generate_depth:
                        # Depth image as EXR file
                        filename = os.path.join(
                            output_dir, f"{idx_label}_{observation_label}_depth.exr"
                        )
                        cv2.imwrite(
                            filename,
                            observation["depth"],
                            [cv2.IMWRITE_EXR_TYPE, cv2.IMWRITE_EXR_TYPE_HALF],
                        )
                        # Camera parameters
                        camera_params = dict(
                            [
                                (key, observation[key].tolist())
                                for key in (
                                    "camera_intrinsics",
                                    "R_cam2world",
                                    "t_cam2world",
                                )
                            ]
                        )
                        filename = os.path.join(
                            output_dir,
                            f"{idx_label}_{observation_label}_camera_params.json",
                        )
                        with open(filename, "w") as f:
                            json.dump(camera_params, f)
                metadata["multiviews"][idx_label] = {
                    "positions": positions.tolist(),
                    "orientations": orientations.tolist(),
                    "covisibility_ratios": data["covisibility_ratios"].tolist(),
                    "valid_fractions": data["valid_fractions"].tolist(),
                    "pairwise_visibility_ratios": data[
                        "pairwise_visibility_ratios"
                    ].tolist(),
                }
            except RecursionError:
                print(
                    "Recursion error: unable to sample observations for this scene. We will stop there."
                )
                break

            # Regularly save a temporary metadata file, in case we need to restart the generation
            if idx % 10 == 0:
                with open(metadata_filename, "w") as f:
                    json.dump(metadata, f)

        # Save metadata
        with open(metadata_filename, "w") as f:
            json.dump(metadata, f)

        generator.close()
    except NoNaviguableSpaceError:
        pass

def create_commandline(scene_data, generate_depth, exist_ok=False):
    """
    Create a commandline string to generate a scene.
    """

    def my_formatting(val):
        if val is None or val == "":
            return '""'
        else:
            return val

    commandline = f"""python {__file__} --scene {my_formatting(scene_data.scene)}
    --scene_dataset_config_file {my_formatting(scene_data.scene_dataset_config_file)}
    --navmesh {my_formatting(scene_data.navmesh)}
    --output_dir {my_formatting(scene_data.output_dir)}
    --generate_depth {int(generate_depth)}
    --exist_ok {int(exist_ok)}
    """


            except RecursionError:
                print(
                    "Recursion error: unable to sample observations for this scene. We will stop there."
                )
                break

            # Regularly save a temporary metadata file, in case we need to restart the generation
            if idx % 10 == 0:
                with open(metadata_filename, "w") as f:
                    json.dump(metadata, f)

        # Save metadata
        with open(metadata_filename, "w") as f:
            json.dump(metadata, f)

        generator.close()
    except NoNaviguableSpaceError:
        pass

def create_commandline(scene_data, generate_depth, exist_ok=False):
    """
    Create a commandline string to generate a scene.
    """

    def my_formatting(val):
        if val is None or val == "":
            return '""'
        else:
            return val

    commandline = f"""python {__file__} --scene {my_formatting(scene_data.scene)}
    --scene_dataset_config_file {my_formatting(scene_data.scene_dataset_config_file)}
    --navmesh {my_formatting(scene_data.navmesh)}
    --output_dir {my_formatting(scene_data.output_dir)}
    --generate_depth {int(generate_depth)}
    --exist_ok {int(exist_ok)}
    """
    commandline = " ".join(commandline.split())
    return commandline

if __name__ == "__main__":
    os.umask(2)

    parser = argparse.ArgumentParser(
        description="""Example of use -- listing commands to generate data for scenes available:
    > python datasets/habitat_sim/generate_multiview_habitat_images.py --list_commands
    """
    )

    parser.add_argument("--output_dir", type=str, required=True)
    parser.add_argument(
        "--list_commands", action="store_true", help="list commandlines to run if true"
    )
    parser.add_argument("--scene", type=str, default="")
    parser.add_argument("--scene_dataset_config_file", type=str, default="")
    parser.add_argument("--navmesh", type=str, default="")

    parser.add_argument("--generate_depth", type=int, default=1)


            # Regularly save a temporary metadata file, in case we need to restart the generation
            if idx % 10 == 0:
                with open(metadata_filename, "w") as f:
                    json.dump(metadata, f)

        # Save metadata
        with open(metadata_filename, "w") as f:
            json.dump(metadata, f)

        generator.close()
    except NoNaviguableSpaceError:
        pass

def create_commandline(scene_data, generate_depth, exist_ok=False):
    """
    Create a commandline string to generate a scene.
    """

    def my_formatting(val):
        if val is None or val == "":
            return '""'
        else:
            return val

    commandline = f"""python {__file__} --scene {my_formatting(scene_data.scene)}
    --scene_dataset_config_file {my_formatting(scene_data.scene_dataset_config_file)}
    --navmesh {my_formatting(scene_data.navmesh)}
    --output_dir {my_formatting(scene_data.output_dir)}
    --generate_depth {int(generate_depth)}
    --exist_ok {int(exist_ok)}
    """
    commandline = " ".join(commandline.split())
    return commandline

if __name__ == "__main__":
    os.umask(2)

    parser = argparse.ArgumentParser(
        description="""Example of use -- listing commands to generate data for scenes available:
    > python datasets/habitat_sim/generate_multiview_habitat_images.py --list_commands
    """
    )


# Copyright (C) 2022-present Naver Corporation. All rights reserved.
# Licensed under CC BY-NC-SA 4.0 (non-commercial use only).

import os
import numpy as np
import quaternion
import habitat_sim
import json
from sklearn.neighbors import NearestNeighbors
import cv2

# OpenCV to habitat camera convention transformation
R_OPENCV2HABITAT = np.stack(
    (habitat_sim.geo.RIGHT, -habitat_sim.geo.UP, habitat_sim.geo.FRONT), axis=0
)
R_HABITAT2OPENCV = R_OPENCV2HABITAT.T
DEG2RAD = np.pi / 180

def compute_camera_intrinsics(height, width, hfov):
    f = width / 2 / np.tan(hfov / 2 * np.pi / 180)
    cu, cv = width / 2, height / 2
    return f, cu, cv

def compute_camera_pose_opencv_convention(camera_position, camera_orientation):
    R_cam2world = quaternion.as_rotation_matrix(camera_orientation) @ R_OPENCV2HABITAT
    t_cam2world = np.asarray(camera_position)
    return R_cam2world, t_cam2world

def compute_pointmap(depthmap, hfov):
    """Compute a HxWx3 pointmap in camera frame from a HxW depth map."""
    height, width = depthmap.shape
    f, cu, cv = compute_camera_intrinsics(height, width, hfov)
    # Cast depth map to point
    z_cam = depthmap
    u, v = np.meshgrid(range(width), range(height))
    x_cam = (u - cu) / f * z_cam
    y_cam = (v - cv) / f * z_cam
    X_cam = np.stack((x_cam, y_cam, z_cam), axis=-1)
    return X_cam

def compute_pointcloud(depthmap, hfov, camera_position, camera_rotation):
    """Return a 3D point cloud corresponding to valid pixels of the depth map"""
    R_cam2world, t_cam2world = compute_camera_pose_opencv_convention(
        camera_position, camera_rotation
    )

    X_cam = compute_pointmap(depthmap=depthmap, hfov=hfov)
    valid_mask = X_cam[:, :, 2] != 0.0

    X_cam = X_cam.reshape(-1, 3)[valid_mask.flatten()]
    X_world = X_cam @ R_cam2world.T + t_cam2world.reshape(1, 3)
    return X_world

def compute_pointcloud_overlaps_scikit(
    pointcloud1, pointcloud2, distance_threshold, compute_symmetric=False
):
    """
    Compute 'overlapping' metrics based on a distance threshold between two point clouds.
    """
    nbrs = NearestNeighbors(n_neighbors=1, algorithm="kd_tree").fit(pointcloud2)
    distances, indices = nbrs.kneighbors(pointcloud1)
    intersection1 = np.count_nonzero(distances.flatten() < distance_threshold)

    data = {"intersection1": intersection1, "size1": len(pointcloud1)}
    if compute_symmetric:
        nbrs = NearestNeighbors(n_neighbors=1, algorithm="kd_tree").fit(pointcloud1)
        distances, indices = nbrs.kneighbors(pointcloud2)
        intersection2 = np.count_nonzero(distances.flatten() < distance_threshold)
        data["intersection2"] = intersection2
        data["size2"] = len(pointcloud2)

    return data

def _append_camera_parameters(observation, hfov, camera_location, camera_rotation):
    """
    Add camera parameters to the observation dictionnary produced by Habitat-Sim
    In-place modifications.
    """
    R_cam2world, t_cam2world = compute_camera_pose_opencv_convention(
        camera_location, camera_rotation
    )
    height, width = observation["depth"].shape
    f, cu, cv = compute_camera_intrinsics(height, width, hfov)
    K = np.asarray([[f, 0, cu], [0, f, cv], [0, 0, 1.0]])
    observation["camera_intrinsics"] = K
    observation["t_cam2world"] = t_cam2world
    observation["R_cam2world"] = R_cam2world

def look_at(eye, center, up, return_cam2world=True):
    """
    Return camera pose looking at a given center point.
    Analogous of gluLookAt function, using OpenCV camera convention.
    """
    z = center - eye
    z /= np.linalg.norm(z, axis=-1, keepdims=True)
    y = -up
    y = y - np.sum(y * z, axis=-1, keepdims=True) * z
    y /= np.linalg.norm(y, axis=-1, keepdims=True)
    x = np.cross(y, z, axis=-1)

    if return_cam2world:
        R = np.stack((x, y, z), axis=-1)
        t = eye
    else:
        # World to camera transformation
        # Transposed matrix
        R = np.stack((x, y, z), axis=-2)
        t = -np.einsum("...ij, ...j", R, eye)
    return R, t

def look_at_for_habitat(eye, center, up, return_cam2world=True):
    R, t = look_at(eye, center, up)
    orientation = quaternion.from_rotation_matrix(R @ R_OPENCV2HABITAT.T)
    return orientation, t

def generate_orientation_noise(pan_range, tilt_range, roll_range):
    return (
        quaternion.from_rotation_vector(
            np.random.uniform(*pan_range) * DEG2RAD * habitat_sim.geo.UP
        )
        * quaternion.from_rotation_vector(
            np.random.uniform(*tilt_range) * DEG2RAD * habitat_sim.geo.RIGHT
        )
        * quaternion.from_rotation_vector(
            np.random.uniform(*roll_range) * DEG2RAD * habitat_sim.geo.FRONT
        )
    )

class NoNaviguableSpaceError(RuntimeError):
    def __init__(self, *args):
        super().__init__(*args)

class MultiviewHabitatSimGenerator:
    def __init__(
        self,
        scene,
        navmesh,
        scene_dataset_config_file,
        resolution=(240, 320),
        views_count=2,
        hfov=60,
        gpu_id=0,
        size=10000,
        minimum_covisibility=0.5,
        transform=None,
    ):
        self.scene = scene
        self.navmesh = navmesh
        self.scene_dataset_config_file = scene_dataset_config_file
        self.resolution = resolution
        self.views_count = views_count
        assert self.views_count >= 1
        self.hfov = hfov
        self.gpu_id = gpu_id
        self.size = size
        self.transform = transform

        # Noise added to camera orientation
        self.pan_range = (-3, 3)
        self.tilt_range = (-10, 10)
        self.roll_range = (-5, 5)

        # Height range to sample cameras
        self.height_range = (1.2, 1.8)

        # Random steps between the camera views
        self.random_steps_count = 5
        self.random_step_variance = 2.0

        # Minimum fraction of the scene which should be valid (well defined depth)
        self.minimum_valid_fraction = 0.7

        # Distance threshold to see  to select pairs
        self.distance_threshold = 0.05
        # Minimum IoU of a view point cloud with respect to the reference view to be kept.
        self.minimum_covisibility = minimum_covisibility

        # Maximum number of retries.
        self.max_attempts_count = 100

        self.seed = None
        self._lazy_initialization()

    def _lazy_initialization(self):
        # Lazy random seeding and instantiation of the simulator to deal with multiprocessing properly
        if self.seed == None:
            # Re-seed numpy generator
            np.random.seed()
            self.seed = np.random.randint(2**32 - 1)
            sim_cfg = habitat_sim.SimulatorConfiguration()
            sim_cfg.scene_id = self.scene
            if (
                self.scene_dataset_config_file is not None
                and self.scene_dataset_config_file != ""
            ):
                sim_cfg.scene_dataset_config_file = self.scene_dataset_config_file
            sim_cfg.random_seed = self.seed
            sim_cfg.load_semantic_mesh = False
            sim_cfg.gpu_device_id = self.gpu_id

            depth_sensor_spec = habitat_sim.CameraSensorSpec()
            depth_sensor_spec.uuid = "depth"
            depth_sensor_spec.sensor_type = habitat_sim.SensorType.DEPTH
            depth_sensor_spec.resolution = self.resolution
            depth_sensor_spec.hfov = self.hfov
            depth_sensor_spec.position = [0.0, 0.0, 0]
            depth_sensor_spec.orientation

            rgb_sensor_spec = habitat_sim.CameraSensorSpec()
            rgb_sensor_spec.uuid = "color"
            rgb_sensor_spec.sensor_type = habitat_sim.SensorType.COLOR
            rgb_sensor_spec.resolution = self.resolution
            rgb_sensor_spec.hfov = self.hfov
            rgb_sensor_spec.position = [0.0, 0.0, 0]
            agent_cfg = habitat_sim.agent.AgentConfiguration(
                sensor_specifications=[rgb_sensor_spec, depth_sensor_spec]
            )

            cfg = habitat_sim.Configuration(sim_cfg, [agent_cfg])
            self.sim = habitat_sim.Simulator(cfg)
            if self.navmesh is not None and self.navmesh != "":
                # Use pre-computed navmesh when available (usually better than those generated automatically)
                self.sim.pathfinder.load_nav_mesh(self.navmesh)

            if not self.sim.pathfinder.is_loaded:
                # Try to compute a navmesh
                navmesh_settings = habitat_sim.NavMeshSettings()
                navmesh_settings.set_defaults()
                self.sim.recompute_navmesh(self.sim.pathfinder, navmesh_settings, True)

            # Ensure that the navmesh is not empty
            if not self.sim.pathfinder.is_loaded:
                raise NoNaviguableSpaceError(
                    f"No naviguable location (scene: {self.scene} -- navmesh: {self.navmesh})"
                )

            self.agent = self.sim.initialize_agent(agent_id=0)

    def close(self):
        self.sim.close()

    def __del__(self):
        self.sim.close()

    def __len__(self):
        return self.size

    def sample_random_viewpoint(self):
        """Sample a random viewpoint using the navmesh"""
        nav_point = self.sim.pathfinder.get_random_navigable_point()

        # Sample a random viewpoint height
        viewpoint_height = np.random.uniform(*self.height_range)
        viewpoint_position = nav_point + viewpoint_height * habitat_sim.geo.UP
        viewpoint_orientation = quaternion.from_rotation_vector(
            np.random.uniform(0, 2 * np.pi) * habitat_sim.geo.UP
        ) * generate_orientation_noise(self.pan_range, self.tilt_range, self.roll_range)
        return viewpoint_position, viewpoint_orientation, nav_point

    def sample_other_random_viewpoint(self, observed_point, nav_point):
        """Sample a random viewpoint close to an existing one, using the navmesh and a reference observed point."""
        other_nav_point = nav_point

        walk_directions = self.random_step_variance * np.asarray([1, 0, 1])
        for i in range(self.random_steps_count):
            temp = self.sim.pathfinder.snap_point(
                other_nav_point + walk_directions * np.random.normal(size=3)
            )
            # Snapping may return nan when it fails
            if not np.isnan(temp[0]):
                other_nav_point = temp

        other_viewpoint_height = np.random.uniform(*self.height_range)
        other_viewpoint_position = (
            other_nav_point + other_viewpoint_height * habitat_sim.geo.UP
        )

        # Set viewing direction towards the central point
        rotation, position = look_at_for_habitat(
            eye=other_viewpoint_position,
            center=observed_point,
            up=habitat_sim.geo.UP,
            return_cam2world=True,
        )
        rotation = rotation * generate_orientation_noise(
            self.pan_range, self.tilt_range, self.roll_range
        )
        return position, rotation, other_nav_point

    def is_other_pointcloud_overlapping(self, ref_pointcloud, other_pointcloud):
        """Check if a viewpoint is valid and overlaps significantly with a reference one."""
        # Observation
        pixels_count = self.resolution[0] * self.resolution[1]
        valid_fraction = len(other_pointcloud) / pixels_count
        assert valid_fraction <= 1.0 and valid_fraction >= 0.0
        overlap = compute_pointcloud_overlaps_scikit(
            ref_pointcloud,
            other_pointcloud,
            self.distance_threshold,
            compute_symmetric=True,
        )
        covisibility = min(
            overlap["intersection1"] / pixels_count,
            overlap["intersection2"] / pixels_count,
        )
        is_valid = (valid_fraction >= self.minimum_valid_fraction) and (
            covisibility >= self.minimum_covisibility
        )
        return is_valid, valid_fraction, covisibility

    def is_other_viewpoint_overlapping(
        self, ref_pointcloud, observation, position, rotation
    ):
        """Check if a viewpoint is valid and overlaps significantly with a reference one."""
        # Observation
        other_pointcloud = compute_pointcloud(
            observation["depth"], self.hfov, position, rotation
        )
        return self.is_other_pointcloud_overlapping(ref_pointcloud, other_pointcloud)

    def render_viewpoint(self, viewpoint_position, viewpoint_orientation):
        agent_state = habitat_sim.AgentState()
        agent_state.position = viewpoint_position
        agent_state.rotation = viewpoint_orientation
        self.agent.set_state(agent_state)
        viewpoint_observations = self.sim.get_sensor_observations(agent_ids=0)
        _append_camera_parameters(
            viewpoint_observations, self.hfov, viewpoint_position, viewpoint_orientation
        )
        return viewpoint_observations

    def __getitem__(self, useless_idx):
        ref_position, ref_orientation, nav_point = self.sample_random_viewpoint()
        ref_observations = self.render_viewpoint(ref_position, ref_orientation)
        # Extract point cloud
        ref_pointcloud = compute_pointcloud(
            depthmap=ref_observations["depth"],
            hfov=self.hfov,
            camera_position=ref_position,
            camera_rotation=ref_orientation,
        )

        pixels_count = self.resolution[0] * self.resolution[1]
        ref_valid_fraction = len(ref_pointcloud) / pixels_count
        assert ref_valid_fraction <= 1.0 and ref_valid_fraction >= 0.0
        if ref_valid_fraction < self.minimum_valid_fraction:
            # This should produce a recursion error at some point when something is very wrong.
            return self[0]
        # Pick an reference observed point in the point cloud
        observed_point = np.mean(ref_pointcloud, axis=0)

        # Add the first image as reference
        viewpoints_observations = [ref_observations]
        viewpoints_covisibility = [ref_valid_fraction]
        viewpoints_positions = [ref_position]
        viewpoints_orientations = [quaternion.as_float_array(ref_orientation)]
        viewpoints_clouds = [ref_pointcloud]
        viewpoints_valid_fractions = [ref_valid_fraction]

        for _ in range(self.views_count - 1):
            # Generate an other viewpoint using some dummy random walk
            successful_sampling = False
            for sampling_attempt in range(self.max_attempts_count):
                position, rotation, _ = self.sample_other_random_viewpoint(
                    observed_point, nav_point
                )
                # Observation
                other_viewpoint_observations = self.render_viewpoint(position, rotation)
                other_pointcloud = compute_pointcloud(
                    other_viewpoint_observations["depth"], self.hfov, position, rotation
                )

                is_valid, valid_fraction, covisibility = (
                    self.is_other_pointcloud_overlapping(
                        ref_pointcloud, other_pointcloud
                    )
                )
                if is_valid:
                    successful_sampling = True
                    break
            if not successful_sampling:
                print("WARNING: Maximum number of attempts reached.")
                # Dirty hack, try using a novel original viewpoint
                return self[0]
            viewpoints_observations.append(other_viewpoint_observations)
            viewpoints_covisibility.append(covisibility)
            viewpoints_positions.append(position)
            viewpoints_orientations.append(
                quaternion.as_float_array(rotation)
            )  # WXYZ convention for the quaternion encoding.
            viewpoints_clouds.append(other_pointcloud)
            viewpoints_valid_fractions.append(valid_fraction)

        # Estimate relations between all pairs of images
        pairwise_visibility_ratios = np.ones(
            (len(viewpoints_observations), len(viewpoints_observations))
        )
        for i in range(len(viewpoints_observations)):
            pairwise_visibility_ratios[i, i] = viewpoints_valid_fractions[i]
            for j in range(i + 1, len(viewpoints_observations)):
                overlap = compute_pointcloud_overlaps_scikit(
                    viewpoints_clouds[i],
                    viewpoints_clouds[j],
                    self.distance_threshold,
                    compute_symmetric=True,
                )
                pairwise_visibility_ratios[i, j] = (
                    overlap["intersection1"] / pixels_count
                )
                pairwise_visibility_ratios[j, i] = (
                    overlap["intersection2"] / pixels_count
                )

        # IoU is relative to the image 0
        data = {
            "observations": viewpoints_observations,
            "positions": np.asarray(viewpoints_positions),
            "orientations": np.asarray(viewpoints_orientations),
            "covisibility_ratios": np.asarray(viewpoints_covisibility),
            "valid_fractions": np.asarray(viewpoints_valid_fractions, dtype=float),
            "pairwise_visibility_ratios": np.asarray(
                pairwise_visibility_ratios, dtype=float
            ),
        }

        if self.transform is not None:
            data = self.transform(data)
        return data

    def generate_random_spiral_trajectory(
        self,
        images_count=100,
        max_radius=0.5,
        half_turns=5,
        use_constant_orientation=False,
    ):
        """
        Return a list of images corresponding to a spiral trajectory from a random starting point.
        Useful to generate nice visualisations.
        Use an even number of half turns to get a nice "C1-continuous" loop effect
        """
        ref_position, ref_orientation, navpoint = self.sample_random_viewpoint()
        ref_observations = self.render_viewpoint(ref_position, ref_orientation)
        ref_pointcloud = compute_pointcloud(
            depthmap=ref_observations["depth"],
            hfov=self.hfov,
            camera_position=ref_position,
            camera_rotation=ref_orientation,
        )
        pixels_count = self.resolution[0] * self.resolution[1]
        if len(ref_pointcloud) / pixels_count < self.minimum_valid_fraction:
            # Dirty hack: ensure that the valid part of the image is significant
            return self.generate_random_spiral_trajectory(
                images_count, max_radius, half_turns, use_constant_orientation
            )

        # Pick an observed point in the point cloud
        observed_point = np.mean(ref_pointcloud, axis=0)
        ref_R, ref_t = compute_camera_pose_opencv_convention(
            ref_position, ref_orientation
        )

        images = []
        is_valid = []
        # Spiral trajectory, use_constant orientation
        for i, alpha in enumerate(np.linspace(0, 1, images_count)):
            r = max_radius * np.abs(
                np.sin(alpha * np.pi)
            )  # Increase then decrease the radius
            theta = alpha * half_turns * np.pi
            x = r * np.cos(theta)
            y = r * np.sin(theta)
            z = 0.0
            position = (
                ref_position + (ref_R @ np.asarray([x, y, z]).reshape(3, 1)).flatten()
            )
            if use_constant_orientation:
                orientation = ref_orientation
            else:
                # trajectory looking at a mean point in front of the ref observation
                orientation, position = look_at_for_habitat(
                    eye=position, center=observed_point, up=habitat_sim.geo.UP
                )
            observations = self.render_viewpoint(position, orientation)
            images.append(observations["color"][..., :3])
            _is_valid, valid_fraction, iou = self.is_other_viewpoint_overlapping(
                ref_pointcloud, observations, position, orientation
            )
            is_valid.append(_is_valid)
        ret
# ... truncated ...