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 <keavon@keavon.com>
This commit is contained in:
Elbert Ronnie 2024-09-06 03:48:58 +05:30 committed by GitHub
parent 70dacac474
commit d9ae01bf90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 147 additions and 8 deletions

View file

@ -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<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> 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,

View file

@ -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<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> 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,

View file

@ -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<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> 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,

View file

@ -67,6 +67,7 @@ fn linear_demosaic_rggb(raw_image: RawImage) -> Image<u16> {
data: image,
width: raw_image.width,
height: raw_image.height,
transform: raw_image.transform,
rgb_to_camera: raw_image.rgb_to_camera,
histogram: None,
}

View file

@ -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<CameraModel>,
@ -42,6 +44,7 @@ pub struct Image<T> {
/// We can assume this will be 3 for all non-obscure, modern cameras.
/// See <https://github.com/GraphiteEditor/Graphite/pull/1923#discussion_r1725070342> 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<R: Read + Seek>(reader: &mut R) -> Result<RawImage, DecoderError>
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::<Orientation, _>(&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<R: Read + Seek>(reader: &mut R) -> Result<RawImage, DecoderError>
};
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<u8> {
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<u16> {
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)
}

View file

@ -1,2 +1,3 @@
pub mod convert_to_rgb;
pub mod gamma_correction;
pub mod transform;

View file

@ -0,0 +1,67 @@
use crate::Image;
use crate::Transform;
pub fn transform(mut image: Image<u16>) -> Image<u16> {
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
}

View file

@ -22,6 +22,7 @@ pub enum TagId {
Make = 0x10f,
Model = 0x110,
StripOffsets = 0x111,
Orientation = 0x112,
SamplesPerPixel = 0x115,
RowsPerStrip = 0x116,
StripByteCounts = 0x117,

View file

@ -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;

View file

@ -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<T: PrimitiveType, const N: usize> TagType for ConstArray<T, N> {
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<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
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),
})
}
}

View file

@ -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,
}
}
}

View file

@ -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);