Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement small colmap interop crate #91

Open
edgarriba opened this issue Jul 3, 2024 · 0 comments
Open

implement small colmap interop crate #91

edgarriba opened this issue Jul 3, 2024 · 0 comments
Labels
enhancement New feature or request

Comments

@edgarriba
Copy link
Member

use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::path::Path;

#[derive(Debug, thiserror::Error)]
pub enum ColmapError {
    #[error("error reading or writing file")]
    IoError(#[from] std::io::Error),

    #[error("Invalid number of camera parameters")]
    InvalidNumCameraParams(usize),
}

/// Represents a 2D vector.
///
/// # Attributes
///
/// * `x` - x coordinate.
/// * `y` - y coordinate.
///
#[derive(Clone)]
pub struct Vector2d {
    pub x: f64,
    pub y: f64,
}

/// Represents a Colmap camera model id.
///
/// # Variants
///
/// * `CameraModelInvalid` - Invalid camera model.
/// * `CameraModelSimplePinhole` - Simple pinhole camera model.
/// * `CameraModelPinhole` - Pinhole camera model.
/// * `CameraModelSimplifiedRadial` - Simplified radial camera model.
/// * `CameraModelRadial` - Radial camera model.
/// * `CameraModelOpenCV` - OpenCV camera model.
/// * `CameraModelOpenCVFisheye` - OpenCV fisheye camera model.
/// * `CameraModelFullOpenCV` - Full OpenCV camera model.
/// * `CameraModelFOV` - Field of view camera model.
/// * `CameraModelSimpleRadialFisheye` - Simple radial fisheye camera model.
/// * `CameraModelRadialFisheye` - Radial fisheye camera model.
/// * `CameraModelThinPrismFisheye` - Thin prism fisheye camera model.
/// * `CameraModelCount` - Number of camera models.
///
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum CameraModelId {
    CameraModelInvalid = -1,
    CameraModelSimplePinhole = 0,
    CameraModelPinhole = 1,
    CameraModelSimplifiedRadial = 2,
    CameraModelRadial = 3,
    CameraModelOpenCV = 4,
    CameraModelOpenCVFisheye = 5,
    CameraModelFullOpenCV = 6,
    CameraModelFOV = 7,
    CameraModelSimpleRadialFisheye = 8,
    CameraModelRadialFisheye = 9,
    CameraModelThinPrismFisheye = 10,
    CameraModelCount = 11,
}

/// Represents a camera in the Colmap system.
///
/// # Attributes
///
/// * `camera_id` - Unique camera id.
/// * `model_id` - Camera model id.
/// * `width` - Image width.
/// * `height` - Image height.
/// * `params` - Camera parameters.
///
pub struct ColmapCamera {
    pub camera_id: u32,
    pub model_id: CameraModelId,
    pub width: usize,
    pub height: usize,
    pub params: Vec<f64>,
}

/// Represents an image in the Colmap system.
///
/// # Attributes
///
/// * `name` - Image name.
/// * `image_id` - Unique image id.
/// * `camera_id` - Camera id.
/// * `rotation` - Rotation quaternion (qw, qx, qy, qz).
/// * `translation` - Translation vector (x, y, z).
/// * `points2d` - 2D points.
/// * `point3d_ids` - 3D point ids.
///
pub struct ColmapImage {
    pub name: String,
    pub image_id: u32,
    pub camera_id: u32,
    pub rotation: [f64; 4],    // qw, qx, qy, qz
    pub translation: [f64; 3], // x, y, z
    pub points2d: Vec<Vector2d>,
    pub point3d_ids: Vec<u64>,
}

impl ColmapCamera {
    /// Create a new ColmapCamera.
    ///
    /// # Arguments
    ///
    /// * `camera_id` - Unique camera id.
    /// * `model_id` - Camera model id.
    /// * `width` - Image width.
    /// * `height` - Image height.
    /// * `params` - Camera parameters.
    ///
    /// # Returns
    ///
    /// * Result containing the new ColmapCamera.
    ///
    /// # Errors
    ///
    /// * If the number of camera parameters is invalid.
    pub fn new(
        camera_id: u32,
        model_id: CameraModelId,
        width: usize,
        height: usize,
        params: Vec<f64>,
    ) -> Result<Self, ColmapError> {
        if params.len() != camera_model_num_params(model_id) {
            return Err(ColmapError::InvalidNumCameraParams(params.len()));
        }
        Ok(ColmapCamera {
            camera_id,
            model_id,
            width,
            height,
            params,
        })
    }
}

fn camera_model_num_params(model_id: CameraModelId) -> usize {
    match model_id {
        CameraModelId::CameraModelSimplePinhole => 3,
        CameraModelId::CameraModelPinhole => 4,
        CameraModelId::CameraModelSimplifiedRadial => 4,
        CameraModelId::CameraModelRadial => 5,
        CameraModelId::CameraModelOpenCV => 8,
        CameraModelId::CameraModelOpenCVFisheye => 8,
        CameraModelId::CameraModelFullOpenCV => 12,
        CameraModelId::CameraModelFOV => 5,
        CameraModelId::CameraModelSimpleRadialFisheye => 4,
        CameraModelId::CameraModelRadialFisheye => 5,
        CameraModelId::CameraModelThinPrismFisheye => 12,
        _ => 0,
    }
}

/// Write colmap images to binary file.
///
/// # Arguments
///
/// * `path` - Path to the output file.
/// * `images` - Vector of colmap images.
///
pub fn write_images_binary(path: &Path, images: &Vec<ColmapImage>) -> Result<(), ColmapError> {
    let mut f = std::fs::File::create(path)?;

    f.write_u64::<LittleEndian>(images.len() as u64)?;

    for image in images {
        f.write_u32::<LittleEndian>(image.image_id)?;

        // write qw, qx, qy, qz
        for i in 0..4 {
            f.write_f64::<LittleEndian>(image.rotation[i])?;
        }

        // write x, y, z
        for i in 0..3 {
            f.write_f64::<LittleEndian>(image.translation[i])?;
        }

        f.write_u32::<LittleEndian>(image.camera_id)?;

        // image name
        for c in image.name.chars() {
            f.write_u8(c as u8)?;
        }
        f.write_u8('\0' as u8)?;

        f.write_u64::<LittleEndian>(image.points2d.len() as u64)?;

        for j in 0..image.points2d.len() {
            f.write_f64::<LittleEndian>(image.points2d[j].x)?;
            f.write_f64::<LittleEndian>(image.points2d[j].y)?;
            f.write_u64::<LittleEndian>(image.point3d_ids[j])?;
        }
    }

    Ok(())
}

/// Write colmap cameras to binary file.
///
/// # Arguments
///
/// * `path` - Path to the output file.
/// * `cameras` - Vector of colmap cameras.
///
pub fn write_cameras_binary(path: &Path, cameras: &Vec<ColmapCamera>) -> Result<(), ColmapError> {
    let mut f = std::fs::File::create(path)?;

    f.write_u64::<LittleEndian>(cameras.len() as u64)?;

    for camera in cameras {
        f.write_u32::<LittleEndian>(camera.camera_id)?;
        f.write_i32::<LittleEndian>(camera.model_id as i32)?; // enum to i32 for c++ compatibility
        f.write_u64::<LittleEndian>(camera.width as u64)?;
        f.write_u64::<LittleEndian>(camera.height as u64)?;
        for param in &camera.params {
            f.write_f64::<LittleEndian>(*param)?;
        }
    }

    Ok(())
}

/// Read colmap cameras from binary file.
///
/// # Arguments
///
/// * `path` - Path to the input file.
///
/// # Returns
///
/// * Vector of colmap cameras.
///
pub fn read_cameras_binary(path: &Path) -> Result<Vec<ColmapCamera>, ColmapError> {
    let mut f = std::fs::File::open(path)?;

    let mut cameras = Vec::new();

    let num_cameras = f.read_u64::<LittleEndian>()?;

    for _ in 0..num_cameras {
        let camera_id = f.read_u32::<LittleEndian>()?;
        let model_id = match f.read_i32::<LittleEndian>()? {
            0 => CameraModelId::CameraModelSimplePinhole,
            1 => CameraModelId::CameraModelPinhole,
            2 => CameraModelId::CameraModelSimplifiedRadial,
            3 => CameraModelId::CameraModelRadial,
            4 => CameraModelId::CameraModelOpenCV,
            5 => CameraModelId::CameraModelOpenCVFisheye,
            6 => CameraModelId::CameraModelFullOpenCV,
            7 => CameraModelId::CameraModelFOV,
            8 => CameraModelId::CameraModelSimpleRadialFisheye,
            9 => CameraModelId::CameraModelRadialFisheye,
            10 => CameraModelId::CameraModelThinPrismFisheye,
            11 => CameraModelId::CameraModelCount,
            _ => CameraModelId::CameraModelInvalid,
        };
        let width = f.read_u64::<LittleEndian>()? as usize;
        let height = f.read_u64::<LittleEndian>()? as usize;

        let mut params = Vec::new();
        for _ in 0..camera_model_num_params(model_id) {
            params.push(f.read_f64::<LittleEndian>()?);
        }

        cameras.push(ColmapCamera::new(
            camera_id, model_id, width, height, params,
        )?);
    }

    Ok(cameras)
}

/// Read colmap images from binary file.
///
/// # Arguments
///
/// * `path` - Path to the input file.
///
/// # Returns
///
/// * Vector of colmap images.
///
pub fn read_images_binary(path: &Path) -> Result<Vec<ColmapImage>, ColmapError> {
    let mut f = std::fs::File::open(path)?;

    let mut images = Vec::new();

    let num_images = f.read_u64::<LittleEndian>()?;

    for _ in 0..num_images {
        let image_id = f.read_u32::<LittleEndian>()?;

        let mut rotation = [0.0; 4];
        for i in 0..4 {
            rotation[i] = f.read_f64::<LittleEndian>()?;
        }

        let mut translation = [0.0; 3];
        for i in 0..3 {
            translation[i] = f.read_f64::<LittleEndian>()?;
        }

        let camera_id = f.read_u32::<LittleEndian>()?;

        let mut name = String::new();
        loop {
            let c = f.read_u8()?;
            if c == 0 {
                break;
            }
            name.push(c as char);
        }

        let num_points_2d = f.read_u64::<LittleEndian>()?;

        let mut points2d = Vec::with_capacity(num_points_2d as usize);
        let mut point3d_ids = Vec::with_capacity(num_points_2d as usize);

        for _ in 0..num_points_2d {
            let x = f.read_f64::<LittleEndian>()?;
            let y = f.read_f64::<LittleEndian>()?;
            let point3d_id = f.read_u64::<LittleEndian>()?;

            points2d.push(Vector2d { x, y });
            point3d_ids.push(point3d_id);
        }

        images.push(ColmapImage {
            name,
            image_id,
            camera_id,
            rotation,
            translation,
            points2d,
            point3d_ids,
        });
    }

    Ok(images)
}

#[cfg(test)]
mod tests {
    use super::*;
    use anyhow::Result;

    #[test]
    fn test_write_images_binary() -> Result<()> {
        let images = vec![
            ColmapImage {
                name: "image1".to_string(),
                image_id: 1,
                camera_id: 1,
                rotation: [1.0, 0.0, 0.0, 0.0],
                translation: [0.0, 0.0, 0.0],
                point3d_ids: vec![1, 2, 3],
                points2d: vec![
                    Vector2d { x: 1.0, y: 2.0 },
                    Vector2d { x: 3.0, y: 4.0 },
                    Vector2d { x: 5.0, y: 6.0 },
                ],
            },
            ColmapImage {
                name: "image2".to_string(),
                image_id: 2,
                camera_id: 1,
                rotation: [1.0, 0.0, 0.0, 0.0],
                translation: [0.0, 0.0, 0.0],
                point3d_ids: vec![4, 5, 6],
                points2d: vec![Vector2d { x: 2.0, y: 3.0 }, Vector2d { x: 4.0, y: 5.0 }],
            },
        ];

        // write to file
        let temp_dir = tempfile::TempDir::new()?;
        let path = temp_dir.path().join("test_images.bin");

        write_images_binary(&path, &images).unwrap();

        // read back from file
        let read_images = read_images_binary(&path).unwrap();

        assert_eq!(images.len(), read_images.len());

        for i in 0..images.len() {
            assert_eq!(images[i].name, read_images[i].name);
            assert_eq!(images[i].image_id, read_images[i].image_id);
            assert_eq!(images[i].camera_id, read_images[i].camera_id);
            for j in 0..4 {
                assert_eq!(images[i].rotation[j], read_images[i].rotation[j]);
            }
            for j in 0..3 {
                assert_eq!(images[i].translation[j], read_images[i].translation[j]);
            }
            for j in 0..images[i].points2d.len() {
                assert_eq!(images[i].points2d[j].x, read_images[i].points2d[j].x);
                assert_eq!(images[i].points2d[j].y, read_images[i].points2d[j].y);
                assert_eq!(images[i].point3d_ids[j], read_images[i].point3d_ids[j]);
            }
        }

        Ok(())
    }

    #[test]
    fn test_write_cameras_binary() -> Result<()> {
        let cameras = vec![
            ColmapCamera::new(
                1,
                CameraModelId::CameraModelSimplePinhole,
                1920,
                1080,
                vec![1.0, 2.0, 3.0],
            )?,
            ColmapCamera::new(
                2,
                CameraModelId::CameraModelOpenCV,
                1920,
                1080,
                vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
            )?,
        ];

        // write to file
        //let path = Path::new("test_cameras.bin");
        let temp_dir = tempfile::TempDir::new()?;
        let path = temp_dir.path().join("test_cameras.bin");

        write_cameras_binary(&path, &cameras)?;

        // read back from file
        let read_cameras = read_cameras_binary(&path)?;

        assert_eq!(cameras.len(), read_cameras.len());

        for i in 0..cameras.len() {
            println!("{:?}", cameras[i].model_id);
            println!("{:?}", read_cameras[i].model_id);
            assert_eq!(cameras[i].camera_id, read_cameras[i].camera_id);
            assert_eq!(cameras[i].model_id, read_cameras[i].model_id);
            assert_eq!(cameras[i].width, read_cameras[i].width);
            assert_eq!(cameras[i].height, read_cameras[i].height);
            for j in 0..cameras[i].params.len() {
                assert_eq!(cameras[i].params[j], read_cameras[i].params[j]);
            }
        }

        Ok(())
    }
}
@edgarriba edgarriba added the enhancement New feature or request label Jul 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant