From d9ae01bf90f07c43edfe3e509a1a5c799168f229 Mon Sep 17 00:00:00 2001 From: Elbert Ronnie <103196773+elbertronnie@users.noreply.github.com> Date: Fri, 6 Sep 2024 03:48:58 +0530 Subject: [PATCH] Raw-rs: Flip and rotate image based on camera orientation (#1954) * add Orientation tag * fix errors * Apply the transformation to the image * Fix BASE_PATH in raw-rs tests * Create output folder if it doesn't exist * Nit --------- Co-authored-by: Keavon Chambers --- libraries/raw-rs/src/decoder/arw1.rs | 3 +- libraries/raw-rs/src/decoder/arw2.rs | 3 +- libraries/raw-rs/src/decoder/uncompressed.rs | 3 +- .../src/demosaicing/linear_demosaicing.rs | 1 + libraries/raw-rs/src/lib.rs | 9 ++- libraries/raw-rs/src/postprocessing/mod.rs | 1 + .../raw-rs/src/postprocessing/transform.rs | 67 +++++++++++++++++++ libraries/raw-rs/src/tiff/mod.rs | 1 + libraries/raw-rs/src/tiff/tags.rs | 10 ++- libraries/raw-rs/src/tiff/types.rs | 21 +++++- libraries/raw-rs/src/tiff/values.rs | 27 ++++++++ libraries/raw-rs/tests/tests.rs | 9 ++- 12 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 libraries/raw-rs/src/postprocessing/transform.rs diff --git a/libraries/raw-rs/src/decoder/arw1.rs b/libraries/raw-rs/src/decoder/arw1.rs index aecda5da..03ca47a2 100644 --- a/libraries/raw-rs/src/decoder/arw1.rs +++ b/libraries/raw-rs/src/decoder/arw1.rs @@ -1,7 +1,7 @@ use crate::tiff::file::TiffRead; use crate::tiff::tags::SonyDataOffset; use crate::tiff::Ifd; -use crate::{RawImage, SubtractBlack}; +use crate::{RawImage, SubtractBlack, Transform}; use bitstream_io::{BitRead, BitReader, Endianness, BE}; use std::io::{Read, Seek}; @@ -26,6 +26,7 @@ pub fn decode_a100(ifd: Ifd, file: &mut TiffRead) -> RawImage #[allow(unreachable_code)] maximum: (1 << 12) - 1, black: SubtractBlack::None, + transform: Transform::Horizontal, camera_model: None, camera_white_balance_multiplier: None, white_balance_multiplier: None, diff --git a/libraries/raw-rs/src/decoder/arw2.rs b/libraries/raw-rs/src/decoder/arw2.rs index 32f03af1..8ee39346 100644 --- a/libraries/raw-rs/src/decoder/arw2.rs +++ b/libraries/raw-rs/src/decoder/arw2.rs @@ -2,7 +2,7 @@ use crate::tiff::file::{Endian, TiffRead}; use crate::tiff::tags::{BitsPerSample, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, SonyToneCurve, StripByteCounts, StripOffsets, Tag, WhiteBalanceRggbLevels}; use crate::tiff::values::CurveLookupTable; use crate::tiff::{Ifd, TiffError}; -use crate::{RawImage, SubtractBlack}; +use crate::{RawImage, SubtractBlack, Transform}; use std::io::{Read, Seek}; use tag_derive::Tag; @@ -50,6 +50,7 @@ pub fn decode(ifd: Ifd, file: &mut TiffRead) -> RawImage { cfa_pattern: ifd.cfa_pattern.try_into().unwrap(), maximum: (1 << 14) - 1, black: SubtractBlack::CfaGrid([512, 512, 512, 512]), // TODO: Find the correct way to do this + transform: Transform::Horizontal, camera_model: None, camera_white_balance_multiplier: ifd.white_balance_levels.map(|arr| arr.map(|x| x as f64)), white_balance_multiplier: None, diff --git a/libraries/raw-rs/src/decoder/uncompressed.rs b/libraries/raw-rs/src/decoder/uncompressed.rs index a2b882f8..e2842787 100644 --- a/libraries/raw-rs/src/decoder/uncompressed.rs +++ b/libraries/raw-rs/src/decoder/uncompressed.rs @@ -1,7 +1,7 @@ use crate::tiff::file::TiffRead; use crate::tiff::tags::{BitsPerSample, BlackLevel, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, RowsPerStrip, StripByteCounts, StripOffsets, Tag, WhiteBalanceRggbLevels}; use crate::tiff::{Ifd, TiffError}; -use crate::{RawImage, SubtractBlack}; +use crate::{RawImage, SubtractBlack, Transform}; use std::io::{Read, Seek}; use tag_derive::Tag; @@ -58,6 +58,7 @@ pub fn decode(ifd: Ifd, file: &mut TiffRead) -> RawImage { cfa_pattern: ifd.cfa_pattern.try_into().unwrap(), maximum: if bits_per_sample == 16 { u16::MAX } else { (1 << bits_per_sample) - 1 }, black: SubtractBlack::CfaGrid(ifd.black_level), + transform: Transform::Horizontal, camera_model: None, camera_white_balance_multiplier: ifd.white_balance_levels.map(|arr| arr.map(|x| x as f64)), white_balance_multiplier: None, diff --git a/libraries/raw-rs/src/demosaicing/linear_demosaicing.rs b/libraries/raw-rs/src/demosaicing/linear_demosaicing.rs index 81fdcda2..4f60c378 100644 --- a/libraries/raw-rs/src/demosaicing/linear_demosaicing.rs +++ b/libraries/raw-rs/src/demosaicing/linear_demosaicing.rs @@ -67,6 +67,7 @@ fn linear_demosaic_rggb(raw_image: RawImage) -> Image { data: image, width: raw_image.width, height: raw_image.height, + transform: raw_image.transform, rgb_to_camera: raw_image.rgb_to_camera, histogram: None, } diff --git a/libraries/raw-rs/src/lib.rs b/libraries/raw-rs/src/lib.rs index 6ab2edc3..5d7fc7b8 100644 --- a/libraries/raw-rs/src/lib.rs +++ b/libraries/raw-rs/src/lib.rs @@ -9,7 +9,8 @@ use crate::metadata::identify::CameraModel; use tag_derive::Tag; use tiff::file::TiffRead; -use tiff::tags::{Compression, ImageLength, ImageWidth, StripByteCounts, SubIfd, Tag}; +use tiff::tags::{Compression, ImageLength, ImageWidth, Orientation, StripByteCounts, SubIfd, Tag}; +use tiff::values::Transform; use tiff::{Ifd, TiffError}; use std::io::{Read, Seek}; @@ -26,6 +27,7 @@ pub struct RawImage { pub width: usize, pub height: usize, pub cfa_pattern: [u8; 4], + pub transform: Transform, pub maximum: u16, pub black: SubtractBlack, pub camera_model: Option, @@ -42,6 +44,7 @@ pub struct Image { /// We can assume this will be 3 for all non-obscure, modern cameras. /// See for more information. pub channels: u8, + pub transform: Transform, pub rgb_to_camera: Option<[[f64; 3]; 3]>, pub(crate) histogram: Option<[[usize; 0x2000]; 3]>, } @@ -60,6 +63,7 @@ pub fn decode(reader: &mut R) -> Result let ifd = Ifd::new_first_ifd(&mut file)?; let camera_model = metadata::identify::identify_camera_model(&ifd, &mut file).unwrap(); + let transform = ifd.get_value::(&mut file)?; let mut raw_image = if camera_model.model == "DSLR-A100" { decoder::arw1::decode_a100(ifd, &mut file) @@ -78,6 +82,7 @@ pub fn decode(reader: &mut R) -> Result }; raw_image.camera_model = Some(camera_model); + raw_image.transform = transform; Ok(raw_image) } @@ -90,6 +95,7 @@ pub fn process_8bit(raw_image: RawImage) -> Image { data: image.data.iter().map(|x| (x >> 8) as u8).collect(), width: image.width, height: image.height, + transform: image.transform, rgb_to_camera: image.rgb_to_camera, histogram: image.histogram, } @@ -101,6 +107,7 @@ pub fn process_16bit(raw_image: RawImage) -> Image { let raw_image = crate::preprocessing::scale_colors::scale_colors(raw_image); let image = crate::demosaicing::linear_demosaicing::linear_demosaic(raw_image); let image = crate::postprocessing::convert_to_rgb::convert_to_rgb(image); + let image = crate::postprocessing::transform::transform(image); crate::postprocessing::gamma_correction::gamma_correction(image) } diff --git a/libraries/raw-rs/src/postprocessing/mod.rs b/libraries/raw-rs/src/postprocessing/mod.rs index d2892271..d2227226 100644 --- a/libraries/raw-rs/src/postprocessing/mod.rs +++ b/libraries/raw-rs/src/postprocessing/mod.rs @@ -1,2 +1,3 @@ pub mod convert_to_rgb; pub mod gamma_correction; +pub mod transform; diff --git a/libraries/raw-rs/src/postprocessing/transform.rs b/libraries/raw-rs/src/postprocessing/transform.rs new file mode 100644 index 00000000..45b24593 --- /dev/null +++ b/libraries/raw-rs/src/postprocessing/transform.rs @@ -0,0 +1,67 @@ +use crate::Image; +use crate::Transform; + +pub fn transform(mut image: Image) -> Image { + if image.transform.is_identity() { + return image; + } + + let channels = image.channels as usize; + let mut data = vec![0; channels * image.width * image.height]; + + let (final_width, final_height) = if image.transform.will_swap_coordinates() { + (image.height, image.width) + } else { + (image.width, image.height) + }; + + let mut initial_index = inverse_transform_index(image.transform, 0, 0, image.width, image.height); + let column_step = inverse_transform_index(image.transform, 0, 1, image.width, image.height) as i64 - initial_index as i64; + let row_step = inverse_transform_index(image.transform, 1, 0, image.width, image.height) as i64 - inverse_transform_index(image.transform, 0, final_width, image.width, image.height) as i64; + + for row in 0..final_height { + for col in 0..final_width { + let transformed_index = final_width * row + col; + + let copy_from_range = channels * initial_index..channels * (initial_index + 1); + let copy_to_range = channels * transformed_index..channels * (transformed_index + 1); + data[copy_to_range].copy_from_slice(&image.data[copy_from_range]); + + initial_index = (initial_index as i64 + column_step) as usize; + } + initial_index = (initial_index as i64 + row_step) as usize; + } + + image.data = data; + image.width = final_width; + image.height = final_height; + + image +} + +pub fn inverse_transform_index(transform: Transform, mut row: usize, mut column: usize, width: usize, height: usize) -> usize { + let value = match transform { + Transform::Horizontal => 0, + Transform::MirrorHorizontal => 1, + Transform::Rotate180 => 3, + Transform::MirrorVertical => 2, + Transform::MirrorHorizontalRotate270 => 4, + Transform::Rotate90 => 6, + Transform::MirrorHorizontalRotate90 => 7, + Transform::Rotate270 => 5, + }; + + if value & 4 != 0 { + std::mem::swap(&mut row, &mut column) + } + + if value & 2 != 0 { + row = height - 1 - row; + } + + if value & 1 != 0 { + column = width - 1 - column; + } + + width * row + column +} diff --git a/libraries/raw-rs/src/tiff/mod.rs b/libraries/raw-rs/src/tiff/mod.rs index fd97f3f1..0eff5b61 100644 --- a/libraries/raw-rs/src/tiff/mod.rs +++ b/libraries/raw-rs/src/tiff/mod.rs @@ -22,6 +22,7 @@ pub enum TagId { Make = 0x10f, Model = 0x110, StripOffsets = 0x111, + Orientation = 0x112, SamplesPerPixel = 0x115, RowsPerStrip = 0x116, StripByteCounts = 0x117, diff --git a/libraries/raw-rs/src/tiff/tags.rs b/libraries/raw-rs/src/tiff/tags.rs index d5efcee7..8d3e6f66 100644 --- a/libraries/raw-rs/src/tiff/tags.rs +++ b/libraries/raw-rs/src/tiff/tags.rs @@ -1,4 +1,4 @@ -use super::types::{Array, ConstArray, TagType, TypeByte, TypeIfd, TypeLong, TypeNumber, TypeSRational, TypeSShort, TypeShort, TypeSonyToneCurve, TypeString}; +use super::types::{Array, ConstArray, TagType, TypeByte, TypeIfd, TypeLong, TypeNumber, TypeOrientation, TypeSRational, TypeSShort, TypeShort, TypeSonyToneCurve, TypeString}; use super::{Ifd, TagId, TiffError, TiffRead}; use std::io::{Read, Seek}; @@ -18,6 +18,7 @@ pub struct PhotometricInterpretation; pub struct Make; pub struct Model; pub struct StripOffsets; +pub struct Orientation; pub struct SamplesPerPixel; pub struct RowsPerStrip; pub struct StripByteCounts; @@ -89,6 +90,13 @@ impl SimpleTag for StripOffsets { const NAME: &'static str = "Strip Offsets"; } +impl SimpleTag for Orientation { + type Type = TypeOrientation; + + const ID: TagId = TagId::Orientation; + const NAME: &'static str = "Orientation"; +} + impl SimpleTag for SamplesPerPixel { type Type = TypeShort; diff --git a/libraries/raw-rs/src/tiff/types.rs b/libraries/raw-rs/src/tiff/types.rs index ad06d7cb..4533f6b3 100644 --- a/libraries/raw-rs/src/tiff/types.rs +++ b/libraries/raw-rs/src/tiff/types.rs @@ -1,7 +1,7 @@ use std::io::{Read, Seek}; use super::file::TiffRead; -use super::values::{CurveLookupTable, Rational}; +use super::values::{CurveLookupTable, Rational, Transform}; use super::{Ifd, IfdTagType, TiffError}; pub struct TypeAscii; @@ -357,6 +357,7 @@ impl TagType for ConstArray { pub struct TypeString; pub struct TypeSonyToneCurve; +pub struct TypeOrientation; impl TagType for TypeString { type Output = String; @@ -378,3 +379,21 @@ impl TagType for TypeSonyToneCurve { Ok(CurveLookupTable::from_sony_tone_table(values)) } } + +impl TagType for TypeOrientation { + type Output = Transform; + + fn read(file: &mut TiffRead) -> Result { + Ok(match TypeShort::read(file)? { + 1 => Transform::Horizontal, + 2 => Transform::MirrorHorizontal, + 3 => Transform::Rotate180, + 4 => Transform::MirrorVertical, + 5 => Transform::MirrorHorizontalRotate270, + 6 => Transform::Rotate90, + 7 => Transform::MirrorHorizontalRotate90, + 8 => Transform::Rotate270, + _ => return Err(TiffError::InvalidValue), + }) + } +} diff --git a/libraries/raw-rs/src/tiff/values.rs b/libraries/raw-rs/src/tiff/values.rs index f107c5e8..01e44981 100644 --- a/libraries/raw-rs/src/tiff/values.rs +++ b/libraries/raw-rs/src/tiff/values.rs @@ -50,3 +50,30 @@ impl CurveLookupTable { self.table[x] } } + +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum Transform { + Horizontal, + MirrorHorizontal, + Rotate180, + MirrorVertical, + MirrorHorizontalRotate270, + Rotate90, + MirrorHorizontalRotate90, + Rotate270, +} + +impl Transform { + pub fn is_identity(&self) -> bool { + *self == Transform::Horizontal + } + + pub fn will_swap_coordinates(&self) -> bool { + use Transform as Tr; + + match *self { + Tr::Horizontal | Tr::MirrorHorizontal | Tr::Rotate180 | Tr::MirrorVertical => false, + Tr::MirrorHorizontalRotate270 | Tr::Rotate90 | Tr::MirrorHorizontalRotate90 | Tr::Rotate270 => true, + } + } +} diff --git a/libraries/raw-rs/tests/tests.rs b/libraries/raw-rs/tests/tests.rs index e15f8af3..acbca20c 100644 --- a/libraries/raw-rs/tests/tests.rs +++ b/libraries/raw-rs/tests/tests.rs @@ -8,14 +8,14 @@ use image::{ColorType, ImageEncoder}; use libraw::Processor; use std::collections::HashMap; use std::fmt::Write; -use std::fs::{read_dir, File}; +use std::fs::{create_dir, metadata, read_dir, File}; use std::io::{BufWriter, Cursor, Read}; use std::path::{Path, PathBuf}; use std::time::Duration; const TEST_FILES: [&str; 3] = ["ILCE-7M3-ARW2.3.5-blossoms.arw", "ILCE-7RM4-ARW2.3.5-kestrel.arw", "ILCE-6000-ARW2.3.1-windsock.arw"]; const BASE_URL: &str = "https://static.graphite.rs/test-data/libraries/raw-rs/"; -const BASE_PATH: &str = "./tests/images"; +const BASE_PATH: &str = "./tests/images/"; #[test] fn test_images_match_with_libraw() { @@ -73,6 +73,11 @@ fn store_image(path: &Path, suffix: &str, data: &mut [u8], width: usize, height: output_path.push(parent); } output_path.push("output"); + + if metadata(&output_path).is_err() { + create_dir(&output_path).unwrap(); + } + if let Some(filename) = path.file_stem() { let new_filename = format!("{}_{}.{}", filename.to_string_lossy(), suffix, "png"); output_path.push(new_filename);