use candle_core::{Result as CandleResult, Tensor};
use candle_nn::{Conv2d, Conv2dConfig, Module, VarBuilder, conv2d};
use super::ops::max_pool2d_padded;
const BACKBONE: [(usize, usize, usize, usize, usize, bool); 14] = [
(3, 64, 3, 1, 1, true),
(64, 64, 3, 1, 1, true),
(64, 128, 3, 1, 1, true),
(128, 128, 3, 1, 1, true),
(128, 256, 3, 1, 1, true),
(256, 256, 3, 1, 1, true),
(256, 256, 3, 1, 1, true),
(256, 512, 3, 1, 1, true),
(512, 512, 3, 1, 1, true),
(512, 512, 3, 1, 1, true),
(512, 512, 3, 1, 1, true),
(512, 512, 3, 1, 1, false),
(512, 1024, 3, 6, 6, false),
(1024, 1024, 1, 0, 1, false),
];
const DECODER: [(usize, usize, usize); 4] = [(1536, 512, 256), (768, 256, 128), (384, 128, 64), (192, 64, 32)];
const HEAD: [(usize, usize, usize, bool); 5] = [
(32, 32, 3, true),
(32, 32, 3, true),
(32, 16, 3, true),
(16, 16, 1, true),
(16, 2, 1, false),
];
const DECODER_CONV_OFFSET: usize = BACKBONE.len();
const HEAD_CONV_OFFSET: usize = DECODER_CONV_OFFSET + 2 * DECODER.len();
struct UpConv {
mix: Conv2d,
refine: Conv2d,
}
impl UpConv {
fn forward(&self, input: &Tensor) -> CandleResult<Tensor> {
self.refine.forward(&self.mix.forward(input)?.relu()?)?.relu()
}
}
pub(super) struct CraftNet {
backbone: Vec<Conv2d>,
decoder: Vec<UpConv>,
head: Vec<Conv2d>,
}
impl CraftNet {
pub(super) fn new(vb: VarBuilder) -> CandleResult<Self> {
let mut backbone = Vec::with_capacity(BACKBONE.len());
for (index, (in_channels, out_channels, kernel, padding, dilation, _)) in BACKBONE.iter().enumerate() {
backbone.push(conv2d(
*in_channels,
*out_channels,
*kernel,
Conv2dConfig {
padding: *padding,
dilation: *dilation,
..Conv2dConfig::default()
},
vb.pp(format!("conv.{index}")),
)?);
}
let mut decoder = Vec::with_capacity(DECODER.len());
for (stage, (concatenated, intermediate, output)) in DECODER.iter().enumerate() {
let index = DECODER_CONV_OFFSET + 2 * stage;
decoder.push(UpConv {
mix: conv2d(
*concatenated,
*intermediate,
1,
Conv2dConfig::default(),
vb.pp(format!("conv.{index}")),
)?,
refine: conv2d(
*intermediate,
*output,
3,
Conv2dConfig {
padding: 1,
..Conv2dConfig::default()
},
vb.pp(format!("conv.{}", index + 1)),
)?,
});
}
let mut head = Vec::with_capacity(HEAD.len());
for (offset, (in_channels, out_channels, kernel, _)) in HEAD.iter().enumerate() {
head.push(conv2d(
*in_channels,
*out_channels,
*kernel,
Conv2dConfig {
padding: kernel / 2,
..Conv2dConfig::default()
},
vb.pp(format!("conv.{}", HEAD_CONV_OFFSET + offset)),
)?);
}
Ok(Self {
backbone,
decoder,
head,
})
}
pub(super) fn forward(&self, input: &Tensor) -> CandleResult<Tensor> {
let (features, skips) = self.run_backbone(input)?;
let decoded = self.run_decoder(features, &skips)?;
let scores = self.run_head(&decoded)?;
scores.permute((0, 2, 3, 1))?.contiguous()
}
fn run_backbone(&self, input: &Tensor) -> CandleResult<(Tensor, Vec<Tensor>)> {
let apply = |index: usize, tensor: &Tensor| -> CandleResult<Tensor> {
let output = self.backbone[index].forward(tensor)?;
if BACKBONE[index].5 { output.relu() } else { Ok(output) }
};
let halve = |tensor: &Tensor| max_pool2d_padded(tensor, (2, 2), (2, 2), (0, 0));
let mut skips = Vec::with_capacity(3);
let mut hidden = apply(1, &apply(0, input)?)?;
hidden = halve(&hidden)?;
hidden = apply(3, &apply(2, &hidden)?)?;
skips.push(hidden.clone());
hidden = halve(&hidden)?;
hidden = apply(5, &apply(4, &hidden)?)?;
skips.push(hidden.clone());
hidden = apply(6, &hidden)?;
hidden = halve(&hidden)?;
hidden = apply(8, &apply(7, &hidden)?)?;
skips.push(hidden.clone());
hidden = apply(9, &hidden)?;
hidden = halve(&hidden)?;
hidden = apply(11, &apply(10, &hidden)?)?;
let stage_five = max_pool2d_padded(&hidden, (3, 3), (1, 1), (1, 1))?;
let features = apply(13, &apply(12, &stage_five)?)?;
Ok((Tensor::cat(&[features, hidden], 1)?, skips))
}
fn run_decoder(&self, features: Tensor, skips: &[Tensor]) -> CandleResult<Tensor> {
let mut hidden = self.decoder[0].forward(&features)?;
for (stage, skip) in self.decoder[1..].iter().zip(skips.iter().rev()) {
let (_, _, height, width) = skip.dims4()?;
let upsampled = hidden.upsample_bilinear2d(height, width, false)?;
hidden = stage.forward(&Tensor::cat(&[&upsampled, skip], 1)?)?;
}
Ok(hidden)
}
fn run_head(&self, input: &Tensor) -> CandleResult<Tensor> {
let mut hidden = input.clone();
for (index, convolution) in self.head.iter().enumerate() {
hidden = convolution.forward(&hidden)?;
if HEAD[index].3 {
hidden = hidden.relu()?;
}
}
Ok(hidden)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_chain_the_backbone_channels() {
for window in BACKBONE.windows(2) {
assert_eq!(
window[0].1, window[1].0,
"each backbone convolution must consume the previous one's channels"
);
}
assert_eq!(BACKBONE[0].0, 3, "CRAFT reads an RGB image");
}
#[test]
fn should_size_each_decoder_stage_to_its_skip_connection() {
let backbone_tail = BACKBONE[BACKBONE.len() - 1].1;
let unrectified_stage = BACKBONE[11].1;
assert_eq!(
DECODER[0].0,
backbone_tail + unrectified_stage,
"the first stage joins the backbone output with the fifth stage input"
);
let skips = [BACKBONE[8].1, BACKBONE[5].1, BACKBONE[3].1];
let upsampled = [DECODER[0].2, DECODER[1].2, DECODER[2].2];
for ((stage, skip), incoming) in DECODER[1..].iter().zip(skips).zip(upsampled) {
assert_eq!(
stage.0,
incoming + skip,
"a decoder stage consumes the previous stage's output joined with its skip"
);
}
}
#[test]
fn should_place_the_head_after_every_other_convolution() {
assert_eq!(HEAD_CONV_OFFSET, 22);
assert_eq!(
HEAD_CONV_OFFSET + HEAD.len(),
27,
"the exported detector carries 27 Conv nodes in total"
);
assert_eq!(HEAD[HEAD.len() - 1].1, 2, "the head emits the region and link maps");
}
}