#![cfg(target_os = "linux")]
use edgefirst_image::{
CPUProcessor, ConvertOptions, Crop, Error, Flip, ImageProcessorTrait, MaskOverlay, Rotation,
};
use edgefirst_tensor::{
CpuAccess, DType, DetectBox, Error as TensorError, PixelFormat, PixelLayout, Region, Tensor,
TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
};
const W: usize = 64;
const H: usize = 48;
#[path = "../../tensor/tests/support/dma_require.rs"]
#[allow(dead_code)]
mod dma_require;
#[path = "../../tensor/tests/support/outcome.rs"]
#[allow(dead_code)]
mod outcome;
fn dma_or_skip(what: &str) -> bool {
dma_require::available_or_skip(edgefirst_tensor::is_dma_available(), what)
}
fn source(format: PixelFormat) -> TensorDyn {
let t = TensorDyn::image(
W,
H,
format,
DType::U8,
Some(TensorMemory::Mem),
CpuAccess::ReadWrite,
)
.unwrap();
for (i, b) in t
.as_u8()
.unwrap()
.map_mut()
.unwrap()
.as_mut_slice()
.iter_mut()
.enumerate()
{
*b = (i * 7 % 253) as u8;
}
t
}
fn destination(
format: PixelFormat,
dtype: DType,
access: CpuAccess,
) -> Option<(TensorDyn, Tensor<u8>)> {
let dst = outcome::allocated(
TensorDyn::image(W, H, format, dtype, Some(TensorMemory::DmaBuf), access),
&format!("{format:?}/{dtype:?} {access:?} DMA-BUF image"),
outcome::expect_dma(edgefirst_tensor::is_dma_available()),
)?;
let fd = dst.clone_fd().unwrap();
let len = std::fs::File::from(fd.try_clone().unwrap())
.metadata()
.unwrap()
.len() as usize;
let alias = Tensor::<u8>::from_fd(fd, &[len], None).unwrap();
Some((dst, alias))
}
fn fill(alias: &Tensor<u8>, byte: u8) {
alias.map().unwrap().as_mut_slice().fill(byte);
}
fn bytes(alias: &Tensor<u8>) -> Vec<u8> {
alias.map().unwrap().as_slice().to_vec()
}
fn pixels(dst: &TensorDyn, alias: &Tensor<u8>) -> Vec<u8> {
let format = dst.format().unwrap();
let elem = dst.dtype().size();
let (row, rows) = match format.layout() {
PixelLayout::Packed => (W * format.channels() * elem, H),
PixelLayout::Planar => (W * elem, format.channels() * H),
_ => (W * elem, dst.shape()[0]),
};
let stride = dst.effective_row_stride().unwrap_or(row);
let all = bytes(alias);
(0..rows)
.flat_map(|r| all[r * stride..r * stride + row].iter().copied())
.collect()
}
fn is_denied(e: &Error) -> bool {
e.cpu_access_denied().is_some()
}
#[test]
fn cpu_converts_never_read_a_write_only_destination() {
if !dma_or_skip("cpu_converts_never_read_a_write_only_destination") {
return;
}
let half = Region::new(8, 4, W / 2, H / 2);
let options: [(&str, Option<ConvertOptions>); 5] = [
("plain", None),
(
"letterbox",
Some(
ConvertOptions::new()
.source(Some(half))
.letterbox([114, 114, 114, 255]),
),
),
(
"crop",
Some(ConvertOptions::new().crop(Crop::new()).source(Some(half))),
),
("flip", Some(ConvertOptions::new().flip(Flip::Horizontal))),
(
"rotate",
Some(ConvertOptions::new().rotation(Rotation::Rotate180)),
),
];
let mut cpu = CPUProcessor::new();
let mut ran = 0;
for src_format in [
PixelFormat::Rgba,
PixelFormat::Rgb,
PixelFormat::Yuyv,
PixelFormat::Nv12,
] {
let src = source(src_format);
for (dst_format, dtype) in [
(PixelFormat::Rgba, DType::U8),
(PixelFormat::Rgb, DType::U8),
(PixelFormat::Bgra, DType::U8),
(PixelFormat::Grey, DType::U8),
(PixelFormat::PlanarRgb, DType::U8),
(PixelFormat::Rgb, DType::I8),
(PixelFormat::PlanarRgb, DType::I8),
] {
for (name, opts) in &options {
let label = format!("{src_format:?} -> {dst_format:?}/{dtype:?} {name}");
let Some((mut dst, alias)) = destination(dst_format, dtype, CpuAccess::Write)
else {
return;
};
let mut outputs = Vec::new();
for prefill in [0x00, 0xA5] {
fill(&alias, prefill);
match cpu.convert(&src, &mut dst, *opts) {
Ok(()) => outputs.push(pixels(&dst, &alias)),
Err(e) if is_denied(&e) => break,
Err(Error::NotSupported(_) | Error::NotImplemented(_)) => break,
Err(e) => panic!("{label}: {e}"),
}
}
if let [a, b] = &outputs[..] {
ran += 1;
assert!(
a == b,
"{label}: the output depends on the destination's earlier bytes"
);
}
}
}
}
assert!(ran > 0, "no convert ran into a write-only destination");
}
#[test]
fn cpu_draw_refuses_destinations_it_cannot_read() {
if !dma_or_skip("cpu_draw_refuses_destinations_it_cannot_read") {
return;
}
let mut cpu = CPUProcessor::new();
let boxes = [DetectBox {
bbox: [0.1, 0.1, 0.6, 0.6].into(),
score: 0.9,
label: 0,
}];
for access in [CpuAccess::Write, CpuAccess::None] {
let Some((mut dst, alias)) = destination(PixelFormat::Rgba, DType::U8, access) else {
return;
};
fill(&alias, 0x5A);
match cpu.draw_decoded_masks(&mut dst, &boxes, &[], MaskOverlay::default()) {
Err(Error::Tensor(TensorError::CpuAccessDenied { declared, .. })) => {
assert_eq!(declared, access)
}
other => panic!("{access:?}: {other:?}"),
}
assert!(
bytes(&alias).iter().all(|&b| b == 0x5A),
"{access:?}: a refused draw wrote the destination"
);
}
}
#[test]
fn cpu_convert_refuses_a_hardware_only_source() {
if !dma_or_skip("cpu_convert_refuses_a_hardware_only_source") {
return;
}
let mut cpu = CPUProcessor::new();
for access in [CpuAccess::None, CpuAccess::Write] {
let Some((src, _alias)) = destination(PixelFormat::Rgba, DType::U8, access) else {
return;
};
let mut dst = TensorDyn::image(
W,
H,
PixelFormat::Rgba,
DType::U8,
Some(TensorMemory::Mem),
CpuAccess::ReadWrite,
)
.unwrap();
match cpu.convert(&src, &mut dst, None) {
Err(e) if is_denied(&e) => {}
other => panic!("{access:?}: {other:?}"),
}
}
}