use ndarray::{Array2, ArrayD, Axis, Ix2};
use crate::error::{OcrError, Result};
use crate::inference::{ModelBackend, Tensor};
const CHANNEL_EXTENT: usize = 2;
const REGION_CHANNEL: usize = 0;
const LINK_CHANNEL: usize = 1;
const BATCH_AXIS: usize = 0;
const BATCH_EXTENT: usize = 1;
const SPATIAL_RANK: usize = 3;
#[derive(Debug)]
pub(super) struct HeatMaps {
pub region: Array2<f32>,
pub link: Array2<f32>,
}
pub(super) fn run_craft(backend: &dyn ModelBackend, tensor: Tensor) -> Result<HeatMaps> {
let output = backend.run(tensor)?;
split_channels(output)
}
fn split_channels(output: ArrayD<f32>) -> Result<HeatMaps> {
let spatial = squeeze_batch(output)?;
let channel_axis = find_channel_axis(spatial.shape())?;
let region = extract_channel(&spatial, channel_axis, REGION_CHANNEL)?;
let link = extract_channel(&spatial, channel_axis, LINK_CHANNEL)?;
Ok(HeatMaps { region, link })
}
fn squeeze_batch(output: ArrayD<f32>) -> Result<ArrayD<f32>> {
match output.ndim() {
4 => {
let batch = output.shape()[BATCH_AXIS];
if batch != BATCH_EXTENT {
return Err(OcrError::inference(format!(
"CRAFT output batch axis must have extent {BATCH_EXTENT}, got {batch} in shape {:?}",
output.shape()
)));
}
Ok(output.index_axis_move(Axis(BATCH_AXIS), 0))
}
SPATIAL_RANK => Ok(output),
other => Err(OcrError::inference(format!(
"CRAFT output must be rank 3 or 4, got rank {other} with shape {:?}",
output.shape()
))),
}
}
fn find_channel_axis(shape: &[usize]) -> Result<usize> {
let candidates: Vec<usize> = shape
.iter()
.enumerate()
.filter(|&(_, &extent)| extent == CHANNEL_EXTENT)
.map(|(axis, _)| axis)
.collect();
match candidates.as_slice() {
[only] => Ok(*only),
[] => Err(OcrError::inference(format!(
"CRAFT output shape {shape:?} has no channel axis of extent {CHANNEL_EXTENT}"
))),
_ => Err(OcrError::inference(format!(
"CRAFT output shape {shape:?} is ambiguous: multiple axes have extent {CHANNEL_EXTENT}"
))),
}
}
fn extract_channel(spatial: &ArrayD<f32>, channel_axis: usize, channel: usize) -> Result<Array2<f32>> {
spatial
.index_axis(Axis(channel_axis), channel)
.to_owned()
.into_dimensionality::<Ix2>()
.map_err(|error| OcrError::inference(format!("CRAFT channel is not a 2-D map: {error}")))
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::IxDyn;
struct FixedBackend {
output: ArrayD<f32>,
}
impl ModelBackend for FixedBackend {
fn name(&self) -> &str {
"fixed"
}
fn run(&self, _input: Tensor) -> Result<Tensor> {
Ok(self.output.clone())
}
}
#[test]
fn should_split_channel_first_output_into_region_and_link() {
let data: Vec<f32> = (0..24).map(|value| value as f32).collect();
let output = ArrayD::from_shape_vec(IxDyn(&[1, 2, 3, 4]), data).expect("valid shape");
let heat = split_channels(output).expect("splits channel-first output");
assert_eq!(heat.region.dim(), (3, 4));
assert_eq!(heat.link.dim(), (3, 4));
assert_eq!(heat.region[[0, 0]], 0.0);
assert_eq!(heat.region[[2, 3]], 11.0);
assert_eq!(heat.link[[0, 0]], 12.0);
assert_eq!(heat.link[[2, 3]], 23.0);
}
#[test]
fn should_split_channel_last_output_into_region_and_link() {
let data: Vec<f32> = (0..24).map(|value| value as f32).collect();
let output = ArrayD::from_shape_vec(IxDyn(&[1, 3, 4, 2]), data).expect("valid shape");
let heat = split_channels(output).expect("splits channel-last output");
assert_eq!(heat.region.dim(), (3, 4));
assert_eq!(heat.link.dim(), (3, 4));
assert_eq!(heat.region[[0, 0]], 0.0);
assert_eq!(heat.region[[0, 1]], 2.0);
assert_eq!(heat.link[[0, 0]], 1.0);
assert_eq!(heat.link[[0, 1]], 3.0);
}
#[test]
fn should_error_when_channel_axis_is_ambiguous() {
let data: Vec<f32> = vec![0.0; 16];
let output = ArrayD::from_shape_vec(IxDyn(&[1, 2, 2, 4]), data).expect("valid shape");
let error = split_channels(output).expect_err("ambiguous channel axis must error");
assert!(matches!(error, OcrError::Inference { .. }));
}
#[test]
fn should_error_when_no_channel_axis_has_extent_two() {
let data: Vec<f32> = vec![0.0; 12];
let output = ArrayD::from_shape_vec(IxDyn(&[1, 3, 4]), data).expect("valid shape");
let error = split_channels(output).expect_err("absent channel axis must error");
assert!(matches!(error, OcrError::Inference { .. }));
}
#[test]
fn should_error_when_batch_extent_is_not_one() {
let data: Vec<f32> = vec![0.0; 48];
let output = ArrayD::from_shape_vec(IxDyn(&[2, 3, 4, 2]), data).expect("valid shape");
let error = split_channels(output).expect_err("non-unit batch must error");
assert!(matches!(error, OcrError::Inference { .. }));
}
#[test]
fn should_run_backend_and_split_its_output() {
let data: Vec<f32> = (0..16).map(|value| value as f32).collect();
let output = ArrayD::from_shape_vec(IxDyn(&[1, 2, 1, 8]), data).expect("valid shape");
let backend = FixedBackend { output };
let heat = run_craft(&backend, ArrayD::zeros(IxDyn(&[1, 3, 8, 8]))).expect("runs and splits");
assert_eq!(heat.region.dim(), (1, 8));
assert_eq!(heat.link.dim(), (1, 8));
assert_eq!(heat.region[[0, 0]], 0.0);
assert_eq!(heat.link[[0, 0]], 8.0);
}
}