use super::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RgbSampling {
BlackTiesEven,
ReplicateHalfUp,
}
pub fn invert_affine(t: [[f32; 3]; 2]) -> Result<[[f32; 3]; 2]> {
let det = t[0][0] * t[1][1] - t[0][1] * t[1][0];
if !t.iter().flatten().all(|v| v.is_finite()) || !det.is_finite() || det.abs() <= 1e-20 {
return Err(Error::Message("degenerate affine transform".into()));
}
let a = t[1][1] / det;
let b = -t[0][1] / det;
let c = -t[1][0] / det;
let d = t[0][0] / det;
let inverse = [
[a, b, -a * t[0][2] - b * t[1][2]],
[c, d, -c * t[0][2] - d * t[1][2]],
];
if !inverse.iter().flatten().all(|v| v.is_finite()) {
return Err(Error::Message("non-finite inverse transform".into()));
}
Ok(inverse)
}
impl ImageOps {
pub fn prepare_affine_rgb(
&self,
input: &TensorDesc,
crops: usize,
height: usize,
width: usize,
sampling: RgbSampling,
) -> Result<Arc<PreparedModel>> {
self.affine_plans
.get_or_prepare((input.clone(), crops, height, width, sampling), || {
self.affine_rgb_fragment(input, crops, height, width, sampling)?
.prepare(3)
})
}
pub fn affine_rgb_fragment(
&self,
input: &TensorDesc,
crops: usize,
height: usize,
width: usize,
sampling: RgbSampling,
) -> Result<ModelFragment> {
self.affine_fragment(input, crops, height, width, sampling, false)
}
pub fn affine_packed_rgb_fragment(
&self,
input: &TensorDesc,
crops: usize,
height: usize,
width: usize,
sampling: RgbSampling,
) -> Result<ModelFragment> {
self.affine_fragment(input, crops, height, width, sampling, true)
}
fn affine_fragment(
&self,
input: &TensorDesc,
crops: usize,
height: usize,
width: usize,
sampling: RgbSampling,
packed: bool,
) -> Result<ModelFragment> {
let shape = input.shape();
if input.dtype() != DType::U8
|| if packed {
input.layout() != Layout::General || shape.len() != 1
} else {
input.layout() != Layout::Nhwc || shape.len() != 4 || shape[0] != 1 || shape[3] != 3
}
|| !input.is_contiguous()
|| input.is_empty()
|| input.elements() > i32::MAX as usize
|| crops == 0
|| height == 0
|| width == 0
{
return Err(Error::Message(
"affine requires one nonempty NHWC RGB image and nonempty crops".into(),
));
}
let output =
TensorDesc::new(DType::U8, vec![crops, height, width, 3])?.with_layout(Layout::Nhwc)?;
let matrices = TensorDesc::new(DType::F32, vec![crops, 2, 3])?;
if output.elements() > i32::MAX as usize || matrices.elements() > i32::MAX as usize {
return Err(Error::Message(
"affine dimensions exceed kernel indexing limits".into(),
));
}
let count = output.elements();
let mut source = include_str!("affine_rgb.loom").to_owned();
source = source.replace(
"@GEOMETRY_ARG@",
if packed { ", %geometry: buffer" } else { "" },
);
source = source.replace("@GEOMETRY@", if packed {
include_str!("affine_packed_geometry.loom")
} else {
"%sw = index.constant @SOURCE_WIDTH@ : index\n%fw = scalar.constant @SOURCE_WIDTH@.0 : f32\n%fh = scalar.constant @SOURCE_HEIGHT@.0 : f32\n%source_offset = index.constant 0 : index\n%geometry_ok = scalar.constant true : i1"
});
for (name, value) in [
("COUNT", count),
("LAST_OUTPUT", count - 1),
("INPUT", input.elements()),
("LAST_INPUT", input.elements() - 1),
("MATRICES", matrices.elements()),
("LAST_MATRIX", matrices.elements() - 1),
("GRID", count.div_ceil(256)),
("WIDTH", width),
("PIXELS", height * width),
(
"SOURCE_WIDTH",
if packed { input.elements() } else { shape[2] },
),
(
"SOURCE_HEIGHT",
if packed { input.elements() } else { shape[1] },
),
("GEOMETRY_COUNT", crops * 3),
("GEOMETRY_LAST", crops * 3 - 1),
] {
source = source.replace(&format!("@{name}@"), &value.to_string());
}
source = source.replace(
"@REPLICATE@",
if sampling == RgbSampling::ReplicateHalfUp {
"true"
} else {
"false"
},
);
source = source.replace(
"@WEIGHTED_VALUE@",
match sampling {
RgbSampling::BlackTiesEven => {
"%vx = scalar.mulf %v, %wx : f32\n%vxy = scalar.mulf %vx, %wy : f32"
}
RgbSampling::ReplicateHalfUp => {
"%weight = scalar.mulf %wx, %wy : f32\n%vxy = scalar.mulf %v, %weight : f32"
}
},
);
source = source.replace("@ROUND@", match sampling {
RgbSampling::BlackTiesEven => "%rounded = scalar.roundevenf %value : f32",
RgbSampling::ReplicateHalfUp => "%half = scalar.constant 0.5 : f32\n%shifted = scalar.addf %value, %half : f32\n%rounded = scalar.floorf %shifted : f32",
});
let mut model = ModelSession::in_context(&self.context)?;
let src = model.allocate(input.bytes())?;
let maps = model.allocate(matrices.bytes())?;
let dst = model.allocate(output.bytes())?;
let mut inputs = vec![(src, input.clone()), (maps, matrices)];
let mut args = vec![src.read(), maps.read(), dst.write()];
if packed {
let desc = TensorDesc::new(DType::U32, vec![crops, 3])?;
let geometry = model.allocate(desc.bytes())?;
inputs.push((geometry, desc));
args.push(geometry.read());
}
let kernel = unsafe { model.compile(&[(&source, Specialization::new("affine_rgb"))])? }[0];
unsafe {
model.freeze(&self.context)?.fragment(
&[Command::Dispatch(Dispatch::indices(
kernel,
[0],
[count.div_ceil(256) as u32, 1, 1],
args,
))],
&inputs,
&[(dst, output)],
)
}
}
pub fn affine_rgb(
&self,
image: &DeviceTensor,
inverse: &DeviceTensor,
height: usize,
width: usize,
sampling: RgbSampling,
) -> Result<Inference> {
self.context.validate(image)?;
self.context.validate(inverse)?;
let shape = inverse.desc().shape();
if shape.len() != 3
|| shape[1..] != [2, 3]
|| inverse.desc().dtype() != DType::F32
|| inverse.desc().layout() != Layout::General
|| !inverse.desc().is_contiguous()
{
return Err(Error::Message(
"affine matrices must be contiguous F32 [crops,2,3] with General layout".into(),
));
}
self.prepare_affine_rgb(image.desc(), shape[0], height, width, sampling)?
.submit(&[image.clone(), inverse.clone()])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inverse_validation() {
assert_eq!(
invert_affine([[2., 0., 4.], [0., 2., -6.]]).unwrap(),
[[0.5, -0., -2.], [-0., 0.5, 3.]]
);
for t in [[[0.; 3]; 2], [[f32::NAN; 3]; 2], [[f32::MAX; 3]; 2]] {
assert!(invert_affine(t).is_err());
}
}
}