use edgefirst_image::{CPUProcessor, Crop, Flip, ImageProcessorTrait, Region, Rotation};
use edgefirst_tensor::{
CpuAccess, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
};
const POISON: u8 = 0xAB;
const SRC_W: usize = 16;
const SRC_H: usize = 8;
const PARENT_W: usize = 64;
const PARENT_H: usize = 48;
fn mem_image(w: usize, h: usize, fmt: PixelFormat) -> TensorDyn {
TensorDyn::image(
w,
h,
fmt,
DType::U8,
Some(TensorMemory::Mem),
CpuAccess::ReadWrite,
)
.expect("allocate image")
}
fn make_src(w: usize, h: usize, fmt: PixelFormat) -> TensorDyn {
let mut t = mem_image(w, h, fmt);
{
let u8t = t.as_u8_mut().unwrap();
let mut map = u8t.map_mut().unwrap();
for (i, b) in map.as_mut_slice().iter_mut().enumerate() {
*b = (i.wrapping_mul(37).wrapping_add(11) % 251) as u8;
}
}
t
}
fn fill(t: &mut TensorDyn, byte: u8) {
let u8t = t.as_u8_mut().unwrap();
let mut map = u8t.map_mut().unwrap();
map.as_mut_slice().fill(byte);
}
fn bytes(t: &TensorDyn) -> Vec<u8> {
let u8t = t.as_u8().unwrap();
let map = t.as_u8().map(|_| u8t.map_read().unwrap()).unwrap();
map.as_slice().to_vec()
}
fn assert_view_dst_matches_standalone(
label: &str,
src: &TensorDyn,
dst_fmt: PixelFormat,
region: Region,
crop: Crop,
) {
let mut cpu = CPUProcessor::new();
let mut reference = mem_image(region.width, region.height, dst_fmt);
fill(&mut reference, POISON);
cpu.convert(src, &mut reference, Rotation::None, Flip::None, crop)
.unwrap_or_else(|e| panic!("{label}: standalone convert failed: {e}"));
let ref_bytes = bytes(&reference);
let ref_stride = reference
.effective_row_stride()
.expect("reference has a stride");
let mut parent = mem_image(PARENT_W, PARENT_H, dst_fmt);
fill(&mut parent, POISON);
let parent_stride = parent.effective_row_stride().expect("parent has a stride");
{
let mut view = parent.view(region).unwrap_or_else(|e| {
panic!("{label}: view({region:?}) of a {dst_fmt:?} parent failed: {e}")
});
assert_eq!(
view.effective_row_stride(),
Some(parent_stride),
"{label}: a multi-row view must report the PARENT row pitch"
);
cpu.convert(src, &mut view, Rotation::None, Flip::None, crop)
.unwrap_or_else(|e| panic!("{label}: view convert failed: {e}"));
}
let got = bytes(&parent);
let bpp = dst_fmt.channels();
let row_bytes = region.width * bpp;
let x_off = region.x * bpp;
for row in 0..region.height {
let start = (region.y + row) * parent_stride + x_off;
let got_row = &got[start..start + row_bytes];
let want_row = &ref_bytes[row * ref_stride..row * ref_stride + row_bytes];
assert_eq!(
got_row, want_row,
"{label}: row {row} of the view region does not match the standalone convert \
(parent_stride={parent_stride}, offset={start})"
);
}
for y in 0..PARENT_H {
for x in 0..PARENT_W * bpp {
let inside = y >= region.y
&& y < region.y + region.height
&& x >= x_off
&& x < x_off + row_bytes;
if inside {
continue;
}
let off = y * parent_stride + x;
assert_eq!(
got[off], POISON,
"{label}: byte outside the view region was overwritten at \
(row {y}, byte {x}), offset {off}"
);
}
}
}
fn full_view() -> Region {
Region::new(0, 0, SRC_W, SRC_H)
}
#[test]
fn rgb_to_rgba_view_of_wider_parent() {
let src = make_src(SRC_W, SRC_H, PixelFormat::Rgb);
assert_view_dst_matches_standalone(
"rgb→rgba",
&src,
PixelFormat::Rgba,
full_view(),
Crop::default(),
);
}
#[test]
fn nv12_to_rgba_view_of_wider_parent() {
let src = make_src(SRC_W, SRC_H, PixelFormat::Nv12);
assert_view_dst_matches_standalone(
"nv12→rgba",
&src,
PixelFormat::Rgba,
full_view(),
Crop::default(),
);
}
#[test]
fn nv12_to_rgb_view_of_wider_parent() {
let src = make_src(SRC_W, SRC_H, PixelFormat::Nv12);
assert_view_dst_matches_standalone(
"nv12→rgb",
&src,
PixelFormat::Rgb,
full_view(),
Crop::default(),
);
}
#[test]
fn rgba_to_rgba_view_of_wider_parent() {
let src = make_src(SRC_W, SRC_H, PixelFormat::Rgba);
assert_view_dst_matches_standalone(
"rgba→rgba",
&src,
PixelFormat::Rgba,
full_view(),
Crop::default(),
);
}
#[test]
fn rgb_to_rgb_view_of_wider_parent() {
let src = make_src(SRC_W, SRC_H, PixelFormat::Rgb);
assert_view_dst_matches_standalone(
"rgb→rgb",
&src,
PixelFormat::Rgb,
full_view(),
Crop::default(),
);
}
#[test]
fn rgba_to_rgb_view_of_wider_parent() {
let src = make_src(SRC_W, SRC_H, PixelFormat::Rgba);
assert_view_dst_matches_standalone(
"rgba→rgb",
&src,
PixelFormat::Rgb,
full_view(),
Crop::default(),
);
}
#[test]
fn rgb_to_bgra_view_of_wider_parent() {
let src = make_src(SRC_W, SRC_H, PixelFormat::Rgb);
assert_view_dst_matches_standalone(
"rgb→bgra",
&src,
PixelFormat::Bgra,
full_view(),
Crop::default(),
);
}
#[test]
fn rgb_to_grey_view_of_wider_parent() {
let src = make_src(SRC_W, SRC_H, PixelFormat::Rgb);
assert_view_dst_matches_standalone(
"rgb→grey",
&src,
PixelFormat::Grey,
full_view(),
Crop::default(),
);
}
#[test]
fn rgb_to_yuyv_view_of_wider_parent() {
let src = make_src(SRC_W, SRC_H, PixelFormat::Rgb);
assert_view_dst_matches_standalone(
"rgb→yuyv",
&src,
PixelFormat::Yuyv,
full_view(),
Crop::default(),
);
}
#[test]
fn yuyv_to_rgb_view_of_wider_parent() {
let src = make_src(SRC_W, SRC_H, PixelFormat::Yuyv);
assert_view_dst_matches_standalone(
"yuyv→rgb",
&src,
PixelFormat::Rgb,
full_view(),
Crop::default(),
);
}
#[test]
fn rgb_to_rgba_offset_view_of_wider_parent() {
let src = make_src(SRC_W, SRC_H, PixelFormat::Rgb);
assert_view_dst_matches_standalone(
"rgb→rgba@(16,8)",
&src,
PixelFormat::Rgba,
Region::new(16, 8, SRC_W, SRC_H),
Crop::default(),
);
}
#[test]
fn scaled_rgb_to_rgba_view_of_wider_parent() {
let src = make_src(SRC_W / 2, SRC_H / 2, PixelFormat::Rgb);
assert_view_dst_matches_standalone(
"rgb(8x4)→rgba(16x8) scaled",
&src,
PixelFormat::Rgba,
full_view(),
Crop::default(),
);
}
#[test]
fn scaled_nv12_to_rgba_view_of_wider_parent() {
let src = make_src(SRC_W / 2, SRC_H / 2, PixelFormat::Nv12);
assert_view_dst_matches_standalone(
"nv12(8x4)→rgba(16x8) scaled",
&src,
PixelFormat::Rgba,
full_view(),
Crop::default(),
);
}
#[test]
fn letterbox_rgb_to_rgba_view_of_wider_parent() {
let src = make_src(SRC_W, SRC_H / 2, PixelFormat::Rgb);
assert_view_dst_matches_standalone(
"rgb→rgba letterbox",
&src,
PixelFormat::Rgba,
full_view(),
Crop::letterbox([114, 114, 114, 255]),
);
}
fn poison(t: &mut TensorDyn) {
match t {
TensorDyn::I8(t) => t.map_mut().unwrap().as_mut_slice().fill(-91),
TensorDyn::F32(t) => t.map_mut().unwrap().as_mut_slice().fill(-7.5),
TensorDyn::F16(t) => t
.map_mut()
.unwrap()
.as_mut_slice()
.fill(half::f16::from_f32(-7.5)),
other => panic!("poison: unhandled dtype {:?}", other.dtype()),
}
}
fn raw_bytes(t: &TensorDyn) -> Vec<u8> {
match t {
TensorDyn::I8(t) => t
.map_read()
.unwrap()
.as_slice()
.iter()
.map(|v| *v as u8)
.collect(),
TensorDyn::F32(t) => t
.map_read()
.unwrap()
.as_slice()
.iter()
.flat_map(|v| v.to_le_bytes())
.collect(),
TensorDyn::F16(t) => t
.map_read()
.unwrap()
.as_slice()
.iter()
.flat_map(|v| v.to_le_bytes())
.collect(),
other => panic!("raw_bytes: unhandled dtype {:?}", other.dtype()),
}
}
fn assert_typed_view_dst_matches_standalone(label: &str, dtype: DType, elem: usize) {
let mut cpu = CPUProcessor::new();
let src = make_src(SRC_W, SRC_H, PixelFormat::Rgb);
let region = full_view();
let dst_fmt = PixelFormat::Rgba;
let alloc = |w: usize, h: usize| {
TensorDyn::image(
w,
h,
dst_fmt,
dtype,
Some(TensorMemory::Mem),
CpuAccess::ReadWrite,
)
.expect("allocate typed image")
};
let mut reference = alloc(region.width, region.height);
cpu.convert(
&src,
&mut reference,
Rotation::None,
Flip::None,
Crop::default(),
)
.unwrap_or_else(|e| panic!("{label}: standalone convert failed: {e}"));
let mut parent = alloc(PARENT_W, PARENT_H);
poison(&mut parent);
let parent_stride = parent.effective_row_stride().expect("parent has a stride");
let before = raw_bytes(&parent);
{
let mut view = parent
.view(region)
.unwrap_or_else(|e| panic!("{label}: view failed: {e}"));
cpu.convert(&src, &mut view, Rotation::None, Flip::None, Crop::default())
.unwrap_or_else(|e| panic!("{label}: view convert failed: {e}"));
}
let want = raw_bytes(&reference);
let got = raw_bytes(&parent);
let row_bytes = region.width * dst_fmt.channels() * elem;
let ref_stride = row_bytes;
for row in 0..region.height {
assert_eq!(
&got[row * parent_stride..row * parent_stride + row_bytes],
&want[row * ref_stride..row * ref_stride + row_bytes],
"{label}: row {row} does not match the standalone convert \
(parent_stride={parent_stride})"
);
}
for (off, (g, b)) in got.iter().zip(before.iter()).enumerate() {
let (y, x) = (off / parent_stride, off % parent_stride);
if y < region.height && x < row_bytes {
continue;
}
assert_eq!(
g, b,
"{label}: element byte outside the view region changed at \
(row {y}, byte {x}), offset {off}"
);
}
}
#[test]
fn rgb_to_int8_rgba_view_of_wider_parent() {
assert_typed_view_dst_matches_standalone("rgb→i8 rgba", DType::I8, 1);
}
#[test]
fn rgb_to_f32_rgba_view_of_wider_parent() {
assert_typed_view_dst_matches_standalone("rgb→f32 rgba", DType::F32, 4);
}
#[test]
fn rgb_to_f16_rgba_view_of_wider_parent() {
assert_typed_view_dst_matches_standalone("rgb→f16 rgba", DType::F16, 2);
}
#[test]
fn padded_planar_dst_honours_stride() {
let src = make_src(SRC_W, SRC_H, PixelFormat::Nv12);
let mut cpu = CPUProcessor::new();
assert!(
mem_image(PARENT_W, PARENT_H, PixelFormat::PlanarRgb)
.view(full_view())
.is_err(),
"Tensor::view must keep rejecting planar layouts — if this starts \
succeeding, planar dst views need their own coverage here"
);
let stride = PARENT_W * 4;
let mut padded = mem_image(stride, SRC_H, PixelFormat::PlanarRgb);
fill(&mut padded, POISON);
padded
.configure_image(SRC_W, SRC_H, PixelFormat::PlanarRgb)
.expect("shrink logical geometry");
padded.set_row_stride(stride).expect("declare padded pitch");
cpu.convert(
&src,
&mut padded,
Rotation::None,
Flip::None,
Crop::default(),
)
.expect("planar convert");
let mut reference = mem_image(SRC_W, SRC_H, PixelFormat::PlanarRgb);
cpu.convert(
&src,
&mut reference,
Rotation::None,
Flip::None,
Crop::default(),
)
.expect("planar reference convert");
let got = bytes(&padded);
let want = bytes(&reference);
for plane in 0..3 {
for row in 0..SRC_H {
let g = &got[(plane * SRC_H + row) * stride..][..SRC_W];
let w = &want[(plane * SRC_H + row) * SRC_W..][..SRC_W];
assert_eq!(g, w, "planar plane {plane} row {row} mismatch");
}
}
}