use std::io;
use serde::Serialize;
use vernier_core::lrp::LrpReport;
use vernier_core::partition::{PartitionedLrpReport, PartitionedSummary};
use vernier_core::summarize::{Metric, StatLine};
use vernier_core::{ParityMode, Summary};
use crate::error::CliError;
use crate::format::{EvalArtifact, FormatContext, FormatName, Formatter};
pub(crate) const SCHEMA_VERSION: &str = "1";
pub(crate) const SCHEMA_VERSION_V2: &str = "2";
#[derive(Debug, Serialize)]
struct SchemaV1<'a> {
version: &'a str,
iou_type: &'a str,
parity_mode: &'a str,
max_dets: &'a [usize],
use_cats: bool,
lines: Vec<LineV1<'a>>,
stats: Vec<f64>,
}
#[derive(Debug, Serialize)]
struct LineV1<'a> {
metric: &'static str,
iou_threshold: Option<f64>,
iou_threshold_label: String,
area: &'a str,
max_dets: usize,
value: f64,
}
pub(crate) struct Json;
impl Formatter for Json {
fn name(&self) -> &'static str {
"json"
}
fn id(&self) -> FormatName {
FormatName::Json
}
fn render(
&self,
artifact: &EvalArtifact<'_>,
ctx: &FormatContext<'_>,
out: &mut dyn io::Write,
) -> Result<(), CliError> {
match artifact {
EvalArtifact::Ap(summary) => render_ap(summary, ctx, out),
EvalArtifact::Lrp(report) => render_lrp(report, ctx, out),
EvalArtifact::Partitioned { summary, label } => {
render_partitioned(summary, *label, ctx, out)
}
EvalArtifact::PartitionedLrp { summary, label } => {
render_partitioned_lrp(summary, *label, ctx, out)
}
}
}
}
fn render_ap(
summary: &Summary,
ctx: &FormatContext<'_>,
out: &mut dyn io::Write,
) -> Result<(), CliError> {
let lines: Vec<LineV1<'_>> = summary.lines.iter().map(line_to_v1).collect();
let stats = summary.stats();
let doc = SchemaV1 {
version: SCHEMA_VERSION,
iou_type: ctx.iou_type.as_str(),
parity_mode: parity_mode_str(ctx.parity_mode),
max_dets: ctx.max_dets,
use_cats: ctx.use_cats,
lines,
stats,
};
serde_json::to_writer(&mut *out, &doc)?;
writeln!(out)?;
Ok(())
}
fn render_lrp(
report: &LrpReport,
ctx: &FormatContext<'_>,
out: &mut dyn io::Write,
) -> Result<(), CliError> {
let per_class: Vec<LrpClassV1> = report
.per_class
.iter()
.map(|c| LrpClassV1 {
category_id: c.category_id,
olrp: c.olrp,
olrp_loc: c.olrp_loc,
olrp_fp: c.olrp_fp,
olrp_fn: c.olrp_fn,
tau: c.tau,
})
.collect();
let doc = LrpSchemaV1 {
version: SCHEMA_VERSION,
metric: "olrp",
iou_type: ctx.iou_type.as_str(),
parity_mode: parity_mode_str(ctx.parity_mode),
max_dets: ctx.max_dets,
use_cats: ctx.use_cats,
olrp: report.olrp,
olrp_loc: report.olrp_loc,
olrp_fp: report.olrp_fp,
olrp_fn: report.olrp_fn,
per_class,
n_empty_classes: report.n_empty_classes,
kernel: report.config.kernel.as_str(),
tp_threshold: report.config.tp_threshold,
tau_grid_len: report.config.tau_grid_len,
};
serde_json::to_writer(&mut *out, &doc)?;
writeln!(out)?;
Ok(())
}
#[derive(Debug, Serialize)]
struct LrpSchemaV1<'a> {
version: &'a str,
metric: &'static str,
iou_type: &'a str,
parity_mode: &'a str,
max_dets: &'a [usize],
use_cats: bool,
olrp: f64,
olrp_loc: f64,
olrp_fp: f64,
olrp_fn: f64,
per_class: Vec<LrpClassV1>,
n_empty_classes: u32,
kernel: &'a str,
tp_threshold: f64,
tau_grid_len: usize,
}
#[derive(Debug, Serialize)]
struct LrpClassV1 {
category_id: i64,
olrp: Option<f64>,
olrp_loc: Option<f64>,
olrp_fp: Option<f64>,
olrp_fn: Option<f64>,
tau: Option<f64>,
}
fn line_to_v1(line: &StatLine) -> LineV1<'_> {
let iou_threshold_label = match line.iou_threshold {
Some(t) => format!("{t:0.2}"),
None => "0.50:0.95".to_string(),
};
LineV1 {
metric: metric_str(line.metric),
iou_threshold: line.iou_threshold,
iou_threshold_label,
area: line.area.label.as_ref(),
max_dets: line.max_dets,
value: line.value,
}
}
fn metric_str(m: Metric) -> &'static str {
match m {
Metric::AveragePrecision => "AP",
Metric::AverageRecall => "AR",
}
}
fn parity_mode_str(m: ParityMode) -> &'static str {
match m {
ParityMode::Strict => "strict",
ParityMode::Corrected => "corrected",
}
}
#[derive(Debug, Serialize)]
struct SchemaV2<'a> {
version: &'a str,
label: Option<&'a str>,
iou_type: &'a str,
parity_mode: &'a str,
max_dets: &'a [usize],
use_cats: bool,
overall: OverallV2<'a>,
slices: Vec<SliceV2<'a>>,
}
#[derive(Debug, Serialize)]
struct OverallV2<'a> {
lines: Vec<LineV1<'a>>,
stats: Vec<f64>,
n_images: u64,
n_detections: u64,
}
#[derive(Debug, Serialize)]
struct SliceV2<'a> {
axis: &'a str,
value: &'a str,
n_images: u64,
n_detections: u64,
lines: Vec<LineV1<'a>>,
stats: Vec<f64>,
}
fn render_partitioned(
summary: &PartitionedSummary,
label: Option<&str>,
ctx: &FormatContext<'_>,
out: &mut dyn io::Write,
) -> Result<(), CliError> {
let overall_lines: Vec<LineV1<'_>> = summary.overall.lines.iter().map(line_to_v1).collect();
let overall_stats = summary.overall.stats();
let overall = OverallV2 {
lines: overall_lines,
stats: overall_stats,
n_images: summary.overall_n_images,
n_detections: summary.overall_n_detections,
};
let mut slices: Vec<SliceV2<'_>> = Vec::with_capacity(summary.slices.len());
for sr in &summary.slices {
let lines: Vec<LineV1<'_>> = sr.summary.lines.iter().map(line_to_v1).collect();
let stats = sr.summary.stats();
slices.push(SliceV2 {
axis: sr.slice.axis.as_str(),
value: sr.slice.value.as_str(),
n_images: sr.n_images,
n_detections: sr.n_detections,
lines,
stats,
});
}
let doc = SchemaV2 {
version: SCHEMA_VERSION_V2,
label,
iou_type: ctx.iou_type.as_str(),
parity_mode: parity_mode_str(ctx.parity_mode),
max_dets: ctx.max_dets,
use_cats: ctx.use_cats,
overall,
slices,
};
serde_json::to_writer(&mut *out, &doc)?;
writeln!(out)?;
Ok(())
}
#[derive(Debug, Serialize)]
struct LrpSchemaV2<'a> {
version: &'a str,
metric: &'static str,
label: Option<&'a str>,
iou_type: &'a str,
parity_mode: &'a str,
use_cats: bool,
overall: LrpOverallV2<'a>,
slices: Vec<LrpSliceV2<'a>>,
}
#[derive(Debug, Serialize)]
struct LrpOverallV2<'a> {
olrp: f64,
olrp_loc: f64,
olrp_fp: f64,
olrp_fn: f64,
n_empty_classes: u32,
n_images: u64,
n_detections: u64,
config: LrpConfigV2<'a>,
}
#[derive(Debug, Serialize)]
struct LrpSliceV2<'a> {
axis: &'a str,
value: &'a str,
n_images: u64,
n_detections: u64,
olrp: f64,
olrp_loc: f64,
olrp_fp: f64,
olrp_fn: f64,
n_empty_classes: u32,
}
#[derive(Debug, Serialize)]
struct LrpConfigV2<'a> {
tp_threshold: f64,
tau_grid_len: usize,
kernel: &'a str,
}
fn render_partitioned_lrp(
summary: &PartitionedLrpReport,
label: Option<&str>,
ctx: &FormatContext<'_>,
out: &mut dyn io::Write,
) -> Result<(), CliError> {
let overall = LrpOverallV2 {
olrp: summary.overall.olrp,
olrp_loc: summary.overall.olrp_loc,
olrp_fp: summary.overall.olrp_fp,
olrp_fn: summary.overall.olrp_fn,
n_empty_classes: summary.overall.n_empty_classes,
n_images: summary.overall_n_images,
n_detections: summary.overall_n_detections,
config: LrpConfigV2 {
tp_threshold: summary.overall.config.tp_threshold,
tau_grid_len: summary.overall.config.tau_grid_len,
kernel: summary.overall.config.kernel.as_str(),
},
};
let mut slices: Vec<LrpSliceV2<'_>> = Vec::with_capacity(summary.slices.len());
for sr in &summary.slices {
slices.push(LrpSliceV2 {
axis: sr.slice.axis.as_str(),
value: sr.slice.value.as_str(),
n_images: sr.n_images,
n_detections: sr.n_detections,
olrp: sr.report.olrp,
olrp_loc: sr.report.olrp_loc,
olrp_fp: sr.report.olrp_fp,
olrp_fn: sr.report.olrp_fn,
n_empty_classes: sr.report.n_empty_classes,
});
}
let doc = LrpSchemaV2 {
version: SCHEMA_VERSION_V2,
metric: "olrp",
label,
iou_type: ctx.iou_type.as_str(),
parity_mode: parity_mode_str(ctx.parity_mode),
use_cats: ctx.use_cats,
overall,
slices,
};
serde_json::to_writer(&mut *out, &doc)?;
writeln!(out)?;
Ok(())
}