mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
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:
parent
70dacac474
commit
d9ae01bf90
12 changed files with 147 additions and 8 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
pub mod convert_to_rgb;
|
||||
pub mod gamma_correction;
|
||||
pub mod transform;
|
||||
|
|
67
libraries/raw-rs/src/postprocessing/transform.rs
Normal file
67
libraries/raw-rs/src/postprocessing/transform.rs
Normal 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
|
||||
}
|
|
@ -22,6 +22,7 @@ pub enum TagId {
|
|||
Make = 0x10f,
|
||||
Model = 0x110,
|
||||
StripOffsets = 0x111,
|
||||
Orientation = 0x112,
|
||||
SamplesPerPixel = 0x115,
|
||||
RowsPerStrip = 0x116,
|
||||
StripByteCounts = 0x117,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue