use box_image_pyramid::PyramidParams;
use chess_corners_core::{
CenterOfMassConfig, ChessParams, ForstnerConfig, OrientationMethod, PeakFitMode,
RadonDetectorParams, RadonPeakConfig, RefinerKind, SaddlePointConfig,
};
use serde::{Deserialize, Serialize};
use crate::multiscale::CoarseToFineParams;
use crate::upscale::UpscaleConfig;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Threshold {
Absolute(f32),
Relative(f32),
}
impl Default for Threshold {
fn default() -> Self {
Threshold::Absolute(0.0)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ChessRing {
#[default]
Canonical,
Broad,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DescriptorRing {
#[default]
FollowDetector,
Canonical,
Broad,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ChessRefiner {
CenterOfMass(CenterOfMassConfig),
Forstner(ForstnerConfig),
SaddlePoint(SaddlePointConfig),
#[cfg(feature = "ml-refiner")]
Ml,
}
impl Default for ChessRefiner {
fn default() -> Self {
Self::CenterOfMass(CenterOfMassConfig::default())
}
}
impl ChessRefiner {
pub fn center_of_mass() -> Self {
Self::CenterOfMass(CenterOfMassConfig::default())
}
pub fn forstner() -> Self {
Self::Forstner(ForstnerConfig::default())
}
pub fn saddle_point() -> Self {
Self::SaddlePoint(SaddlePointConfig::default())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RadonRefiner {
RadonPeak(RadonPeakConfig),
CenterOfMass(CenterOfMassConfig),
}
impl Default for RadonRefiner {
fn default() -> Self {
Self::RadonPeak(RadonPeakConfig::default())
}
}
impl RadonRefiner {
pub fn radon_peak() -> Self {
Self::RadonPeak(RadonPeakConfig::default())
}
pub fn center_of_mass() -> Self {
Self::CenterOfMass(CenterOfMassConfig::default())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum MultiscaleConfig {
#[default]
SingleScale,
Pyramid {
levels: u8,
min_size: usize,
refinement_radius: u32,
},
}
impl MultiscaleConfig {
pub const fn pyramid_default() -> Self {
Self::Pyramid {
levels: 3,
min_size: 128,
refinement_radius: 3,
}
}
pub const fn pyramid(levels: u8, min_size: usize, refinement_radius: u32) -> Self {
Self::Pyramid {
levels,
min_size,
refinement_radius,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
#[non_exhaustive]
pub struct ChessConfig {
pub ring: ChessRing,
pub descriptor_ring: DescriptorRing,
pub nms_radius: u32,
pub min_cluster_size: u32,
pub refiner: ChessRefiner,
}
impl Default for ChessConfig {
fn default() -> Self {
Self {
ring: ChessRing::Canonical,
descriptor_ring: DescriptorRing::FollowDetector,
nms_radius: 2,
min_cluster_size: 2,
refiner: ChessRefiner::default(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
#[non_exhaustive]
pub struct RadonConfig {
pub ray_radius: u32,
pub image_upsample: u32,
pub response_blur_radius: u32,
pub peak_fit: PeakFitMode,
pub nms_radius: u32,
pub min_cluster_size: u32,
pub refiner: RadonRefiner,
}
impl Default for RadonConfig {
fn default() -> Self {
Self {
ray_radius: 4,
image_upsample: 2,
response_blur_radius: 1,
peak_fit: PeakFitMode::Gaussian,
nms_radius: 4,
min_cluster_size: 2,
refiner: RadonRefiner::default(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DetectionStrategy {
Chess(ChessConfig),
Radon(RadonConfig),
}
impl Default for DetectionStrategy {
fn default() -> Self {
DetectionStrategy::Chess(ChessConfig::default())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
#[non_exhaustive]
pub struct DetectorConfig {
pub strategy: DetectionStrategy,
pub threshold: Threshold,
pub multiscale: MultiscaleConfig,
pub upscale: UpscaleConfig,
pub orientation_method: OrientationMethod,
pub merge_radius: f32,
}
impl Default for DetectorConfig {
fn default() -> Self {
Self::chess()
}
}
impl DetectorConfig {
pub fn chess() -> Self {
Self {
strategy: DetectionStrategy::Chess(ChessConfig::default()),
threshold: Threshold::Absolute(0.0),
multiscale: MultiscaleConfig::SingleScale,
upscale: UpscaleConfig::Disabled,
orientation_method: OrientationMethod::default(),
merge_radius: 3.0,
}
}
pub fn chess_multiscale() -> Self {
Self {
multiscale: MultiscaleConfig::pyramid_default(),
..Self::chess()
}
}
pub fn radon() -> Self {
Self {
strategy: DetectionStrategy::Radon(RadonConfig::default()),
threshold: Threshold::Relative(0.01),
multiscale: MultiscaleConfig::SingleScale,
..Self::chess()
}
}
pub fn radon_multiscale() -> Self {
Self {
strategy: DetectionStrategy::Radon(RadonConfig::default()),
threshold: Threshold::Relative(0.01),
multiscale: MultiscaleConfig::pyramid_default(),
..Self::chess()
}
}
pub fn with_chess<F: FnOnce(&mut ChessConfig)>(mut self, f: F) -> Self {
let mut chess = match self.strategy {
DetectionStrategy::Chess(c) => c,
DetectionStrategy::Radon(_) => ChessConfig::default(),
};
f(&mut chess);
self.strategy = DetectionStrategy::Chess(chess);
self
}
pub fn with_radon<F: FnOnce(&mut RadonConfig)>(mut self, f: F) -> Self {
let mut radon = match self.strategy {
DetectionStrategy::Radon(r) => r,
DetectionStrategy::Chess(_) => RadonConfig::default(),
};
f(&mut radon);
self.strategy = DetectionStrategy::Radon(radon);
self
}
pub fn with_threshold(mut self, threshold: Threshold) -> Self {
self.threshold = threshold;
self
}
pub fn with_multiscale(mut self, multiscale: MultiscaleConfig) -> Self {
self.multiscale = multiscale;
self
}
pub fn with_upscale(mut self, upscale: UpscaleConfig) -> Self {
self.upscale = upscale;
self
}
pub fn with_orientation_method(mut self, method: OrientationMethod) -> Self {
self.orientation_method = method;
self
}
pub fn with_merge_radius(mut self, radius: f32) -> Self {
self.merge_radius = radius;
self
}
pub(crate) fn chess_params(&self) -> ChessParams {
let mut params = ChessParams::default();
if let DetectionStrategy::Chess(chess) = &self.strategy {
params.use_radius10 = matches!(chess.ring, ChessRing::Broad);
params.nms_radius = chess.nms_radius;
params.min_cluster_size = chess.min_cluster_size;
params.descriptor_use_radius10 = match chess.descriptor_ring {
DescriptorRing::FollowDetector => None,
DescriptorRing::Canonical => Some(false),
DescriptorRing::Broad => Some(true),
};
params.refiner = chess_refiner_to_kind(chess.refiner);
}
apply_threshold(&mut params, self.threshold);
params.orientation_method = self.orientation_method;
params
}
pub(crate) fn radon_detector_params(&self) -> RadonDetectorParams {
let mut params = RadonDetectorParams::default();
if let DetectionStrategy::Radon(radon) = &self.strategy {
params.ray_radius = radon.ray_radius;
params.image_upsample = radon.image_upsample;
params.response_blur_radius = radon.response_blur_radius;
params.peak_fit = radon.peak_fit;
params.nms_radius = radon.nms_radius;
params.min_cluster_size = radon.min_cluster_size;
params.refiner = radon_refiner_to_kind(radon.refiner);
}
apply_threshold(&mut params, self.threshold);
params
}
pub(crate) fn coarse_to_fine_params(&self) -> Option<CoarseToFineParams> {
let MultiscaleConfig::Pyramid {
levels,
min_size,
refinement_radius,
} = self.multiscale
else {
return None;
};
let mut cfg = CoarseToFineParams::default();
let mut pyramid = PyramidParams::default();
pyramid.num_levels = levels;
pyramid.min_size = min_size;
cfg.pyramid = pyramid;
cfg.refinement_radius = refinement_radius;
cfg.merge_radius = self.merge_radius;
Some(cfg)
}
}
pub(crate) fn chess_refiner_to_kind(refiner: ChessRefiner) -> RefinerKind {
match refiner {
ChessRefiner::CenterOfMass(cfg) => RefinerKind::CenterOfMass(cfg),
ChessRefiner::Forstner(cfg) => RefinerKind::Forstner(cfg),
ChessRefiner::SaddlePoint(cfg) => RefinerKind::SaddlePoint(cfg),
#[cfg(feature = "ml-refiner")]
ChessRefiner::Ml => RefinerKind::CenterOfMass(CenterOfMassConfig::default()),
}
}
pub(crate) fn radon_refiner_to_kind(refiner: RadonRefiner) -> RefinerKind {
match refiner {
RadonRefiner::RadonPeak(cfg) => RefinerKind::RadonPeak(cfg),
RadonRefiner::CenterOfMass(cfg) => RefinerKind::CenterOfMass(cfg),
}
}
trait HasThreshold {
fn set_threshold_abs(&mut self, value: Option<f32>);
fn set_threshold_rel(&mut self, value: f32);
}
impl HasThreshold for ChessParams {
#[inline]
fn set_threshold_abs(&mut self, value: Option<f32>) {
self.threshold_abs = value;
}
#[inline]
fn set_threshold_rel(&mut self, value: f32) {
self.threshold_rel = value;
}
}
impl HasThreshold for RadonDetectorParams {
#[inline]
fn set_threshold_abs(&mut self, value: Option<f32>) {
self.threshold_abs = value;
}
#[inline]
fn set_threshold_rel(&mut self, value: f32) {
self.threshold_rel = value;
}
}
fn apply_threshold<T: HasThreshold>(params: &mut T, threshold: Threshold) {
match threshold {
Threshold::Absolute(value) => {
params.set_threshold_abs(Some(value));
}
Threshold::Relative(frac) => {
params.set_threshold_abs(None);
params.set_threshold_rel(frac);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_strategy_chess(cfg: &DetectorConfig) -> &ChessConfig {
match &cfg.strategy {
DetectionStrategy::Chess(c) => c,
other => panic!("expected ChESS strategy, got {other:?}"),
}
}
fn assert_strategy_radon(cfg: &DetectorConfig) -> &RadonConfig {
match &cfg.strategy {
DetectionStrategy::Radon(r) => r,
other => panic!("expected Radon strategy, got {other:?}"),
}
}
#[test]
fn default_is_single_scale_chess_with_paper_threshold() {
let cfg = DetectorConfig::default();
let chess = assert_strategy_chess(&cfg);
assert_eq!(chess.ring, ChessRing::Canonical);
assert_eq!(chess.descriptor_ring, DescriptorRing::FollowDetector);
assert_eq!(chess.nms_radius, 2);
assert_eq!(chess.min_cluster_size, 2);
assert_eq!(
chess.refiner,
ChessRefiner::CenterOfMass(CenterOfMassConfig::default())
);
assert_eq!(cfg.multiscale, MultiscaleConfig::SingleScale);
assert_eq!(cfg.upscale, UpscaleConfig::Disabled);
assert_eq!(cfg.threshold, Threshold::Absolute(0.0));
assert_eq!(cfg.merge_radius, 3.0);
assert!(cfg.coarse_to_fine_params().is_none());
let params = cfg.chess_params();
assert!(!params.use_radius10);
assert_eq!(params.descriptor_use_radius10, None);
assert_eq!(params.threshold_abs, Some(0.0));
assert_eq!(params.nms_radius, 2);
assert_eq!(params.min_cluster_size, 2);
assert_eq!(
params.refiner,
RefinerKind::CenterOfMass(CenterOfMassConfig::default())
);
}
#[test]
fn relative_threshold_clears_absolute() {
let cfg = DetectorConfig {
threshold: Threshold::Relative(0.15),
..DetectorConfig::chess()
};
let params = cfg.chess_params();
assert_eq!(params.threshold_abs, None);
assert!((params.threshold_rel - 0.15).abs() < f32::EPSILON);
}
#[test]
fn absolute_threshold_overrides_relative() {
let cfg = DetectorConfig {
threshold: Threshold::Absolute(7.5),
..DetectorConfig::chess()
};
let params = cfg.chess_params();
assert_eq!(params.threshold_abs, Some(7.5));
}
#[test]
fn chess_multiscale_preset_carries_pyramid_params() {
let cfg = DetectorConfig::chess_multiscale();
let MultiscaleConfig::Pyramid {
levels,
min_size,
refinement_radius,
} = cfg.multiscale
else {
panic!("chess_multiscale preset must carry Pyramid params");
};
assert_eq!(levels, 3);
assert_eq!(min_size, 128);
assert_eq!(refinement_radius, 3);
let cf = cfg
.coarse_to_fine_params()
.expect("chess_multiscale config must produce CoarseToFineParams");
assert_eq!(cf.pyramid.num_levels, 3);
assert_eq!(cf.pyramid.min_size, 128);
assert_eq!(cf.refinement_radius, 3);
assert_eq!(cf.merge_radius, 3.0);
}
#[test]
fn radon_preset_uses_radon_config_and_relative_threshold() {
let cfg = DetectorConfig::radon();
let radon = assert_strategy_radon(&cfg);
assert_eq!(radon.ray_radius, 4);
assert_eq!(radon.image_upsample, 2);
assert_eq!(radon.response_blur_radius, 1);
assert_eq!(radon.peak_fit, PeakFitMode::Gaussian);
assert_eq!(radon.nms_radius, 4);
assert_eq!(radon.min_cluster_size, 2);
assert_eq!(
radon.refiner,
RadonRefiner::RadonPeak(RadonPeakConfig::default())
);
assert_eq!(cfg.threshold, Threshold::Relative(0.01));
assert_eq!(cfg.multiscale, MultiscaleConfig::SingleScale);
assert!(cfg.coarse_to_fine_params().is_none());
let radon_params = cfg.radon_detector_params();
assert_eq!(radon_params.ray_radius, 4);
assert_eq!(radon_params.image_upsample, 2);
assert_eq!(radon_params.threshold_abs, None);
assert!((radon_params.threshold_rel - 0.01).abs() < f32::EPSILON);
assert_eq!(
radon_params.refiner,
RefinerKind::RadonPeak(RadonPeakConfig::default())
);
}
#[test]
fn radon_multiscale_preset_carries_pyramid_params() {
let cfg = DetectorConfig::radon_multiscale();
assert_strategy_radon(&cfg);
assert_eq!(cfg.threshold, Threshold::Relative(0.01));
let MultiscaleConfig::Pyramid {
levels,
min_size,
refinement_radius,
} = cfg.multiscale
else {
panic!("radon_multiscale preset must carry Pyramid params");
};
assert_eq!(levels, 3);
assert_eq!(min_size, 128);
assert_eq!(refinement_radius, 3);
let cf = cfg
.coarse_to_fine_params()
.expect("radon_multiscale config must produce CoarseToFineParams");
assert_eq!(cf.pyramid.num_levels, 3);
assert_eq!(cf.pyramid.min_size, 128);
assert_eq!(cf.refinement_radius, 3);
assert_eq!(cf.merge_radius, 3.0);
}
#[test]
fn broad_ring_and_forstner_refiner_propagate_to_params() {
let cfg = DetectorConfig {
strategy: DetectionStrategy::Chess(ChessConfig {
ring: ChessRing::Broad,
descriptor_ring: DescriptorRing::Canonical,
refiner: ChessRefiner::Forstner(ForstnerConfig {
max_offset: 2.0,
..ForstnerConfig::default()
}),
..ChessConfig::default()
}),
..DetectorConfig::chess()
};
let params = cfg.chess_params();
assert!(params.use_radius10);
assert_eq!(params.descriptor_use_radius10, Some(false));
assert_eq!(
params.refiner,
RefinerKind::Forstner(ForstnerConfig {
max_offset: 2.0,
..ForstnerConfig::default()
})
);
}
#[test]
fn radon_center_of_mass_refiner_round_trips_to_params() {
let cfg = DetectorConfig {
strategy: DetectionStrategy::Radon(RadonConfig {
refiner: RadonRefiner::CenterOfMass(CenterOfMassConfig::default()),
..RadonConfig::default()
}),
..DetectorConfig::radon()
};
let params = cfg.radon_detector_params();
assert_eq!(
params.refiner,
RefinerKind::CenterOfMass(CenterOfMassConfig::default())
);
}
#[test]
fn chess_preset_round_trips_through_serde() {
let cfg = DetectorConfig::chess();
let json = serde_json::to_string(&cfg).expect("serialize chess config");
let decoded: DetectorConfig =
serde_json::from_str(&json).expect("deserialize chess config");
assert_eq!(decoded, cfg);
}
#[test]
fn chess_multiscale_preset_round_trips_through_serde() {
let cfg = DetectorConfig::chess_multiscale();
let json = serde_json::to_string(&cfg).expect("serialize chess_multiscale config");
let decoded: DetectorConfig =
serde_json::from_str(&json).expect("deserialize chess_multiscale config");
assert_eq!(decoded, cfg);
}
#[test]
fn radon_preset_round_trips_through_serde() {
let cfg = DetectorConfig::radon();
let json = serde_json::to_string(&cfg).expect("serialize radon config");
let decoded: DetectorConfig =
serde_json::from_str(&json).expect("deserialize radon config");
assert_eq!(decoded, cfg);
}
#[test]
fn radon_multiscale_preset_round_trips_through_serde() {
let cfg = DetectorConfig::radon_multiscale();
let json = serde_json::to_string(&cfg).expect("serialize radon_multiscale config");
let decoded: DetectorConfig =
serde_json::from_str(&json).expect("deserialize radon_multiscale config");
assert_eq!(decoded, cfg);
}
#[test]
fn threshold_round_trips_with_externally_tagged_payload() {
let abs = Threshold::Absolute(3.5);
let abs_json = serde_json::to_string(&abs).expect("serialize absolute threshold");
assert!(abs_json.contains("absolute"));
let abs_decoded: Threshold =
serde_json::from_str(&abs_json).expect("deserialize absolute threshold");
assert_eq!(abs_decoded, abs);
let rel = Threshold::Relative(0.42);
let rel_json = serde_json::to_string(&rel).expect("serialize relative threshold");
assert!(rel_json.contains("relative"));
let rel_decoded: Threshold =
serde_json::from_str(&rel_json).expect("deserialize relative threshold");
assert_eq!(rel_decoded, rel);
}
#[test]
fn multiscale_config_round_trips_with_externally_tagged_payload() {
let single = MultiscaleConfig::SingleScale;
let single_json = serde_json::to_string(&single).expect("serialize single-scale");
assert!(single_json.contains("single_scale"));
let decoded: MultiscaleConfig =
serde_json::from_str(&single_json).expect("deserialize single-scale");
assert_eq!(decoded, single);
let pyramid = MultiscaleConfig::Pyramid {
levels: 3,
min_size: 128,
refinement_radius: 3,
};
let pyramid_json = serde_json::to_string(&pyramid).expect("serialize pyramid");
assert!(pyramid_json.contains("pyramid"));
let decoded: MultiscaleConfig =
serde_json::from_str(&pyramid_json).expect("deserialize pyramid");
assert_eq!(decoded, pyramid);
}
#[test]
fn chess_refiner_round_trips_each_variant() {
let variants = [
ChessRefiner::CenterOfMass(CenterOfMassConfig::default()),
ChessRefiner::Forstner(ForstnerConfig::default()),
ChessRefiner::SaddlePoint(SaddlePointConfig::default()),
];
for v in variants {
let json = serde_json::to_string(&v).expect("serialize chess refiner");
let decoded: ChessRefiner =
serde_json::from_str(&json).expect("deserialize chess refiner");
assert_eq!(decoded, v);
}
}
#[test]
fn radon_refiner_round_trips_each_variant() {
let variants = [
RadonRefiner::RadonPeak(RadonPeakConfig::default()),
RadonRefiner::CenterOfMass(CenterOfMassConfig::default()),
];
for v in variants {
let json = serde_json::to_string(&v).expect("serialize radon refiner");
let decoded: RadonRefiner =
serde_json::from_str(&json).expect("deserialize radon refiner");
assert_eq!(decoded, v);
}
}
#[test]
fn unit_enum_variants_serialize_as_bare_strings() {
let json = serde_json::to_string(&MultiscaleConfig::SingleScale).unwrap();
assert_eq!(json, "\"single_scale\"");
let json = serde_json::to_string(&UpscaleConfig::Disabled).unwrap();
assert_eq!(json, "\"disabled\"");
}
#[test]
fn with_chess_mutates_in_place_when_strategy_is_chess() {
let cfg = DetectorConfig::chess().with_chess(|c| c.nms_radius = 7);
let chess = assert_strategy_chess(&cfg);
assert_eq!(chess.nms_radius, 7);
assert_eq!(chess.min_cluster_size, 2);
}
#[test]
fn with_chess_replaces_radon_preserves_threshold() {
let cfg = DetectorConfig::radon()
.with_threshold(Threshold::Absolute(5.0))
.with_chess(|c| c.nms_radius = 3);
let chess = assert_strategy_chess(&cfg);
assert_eq!(chess.nms_radius, 3);
assert_eq!(cfg.threshold, Threshold::Absolute(5.0));
}
#[test]
fn with_radon_mutates_in_place_when_strategy_is_radon() {
let cfg = DetectorConfig::radon().with_radon(|r| r.nms_radius = 9);
let radon = assert_strategy_radon(&cfg);
assert_eq!(radon.nms_radius, 9);
assert_eq!(radon.min_cluster_size, 2);
}
#[test]
fn with_radon_replaces_chess_preserves_threshold() {
let cfg = DetectorConfig::chess()
.with_threshold(Threshold::Relative(0.5))
.with_radon(|r| r.nms_radius = 6);
let radon = assert_strategy_radon(&cfg);
assert_eq!(radon.nms_radius, 6);
assert_eq!(cfg.threshold, Threshold::Relative(0.5));
}
#[test]
fn chained_builder_produces_expected_state() {
let cfg = DetectorConfig::chess()
.with_threshold(Threshold::Relative(0.15))
.with_chess(|c| c.refiner = ChessRefiner::forstner());
assert_eq!(cfg.threshold, Threshold::Relative(0.15));
let chess = assert_strategy_chess(&cfg);
assert_eq!(
chess.refiner,
ChessRefiner::Forstner(ForstnerConfig::default())
);
}
#[test]
fn with_multiscale_sets_multiscale() {
let cfg = DetectorConfig::chess().with_multiscale(MultiscaleConfig::pyramid_default());
assert_eq!(
cfg.multiscale,
MultiscaleConfig::Pyramid {
levels: 3,
min_size: 128,
refinement_radius: 3
}
);
}
#[test]
fn with_upscale_sets_upscale() {
let cfg = DetectorConfig::chess().with_upscale(UpscaleConfig::Fixed(2));
assert_eq!(cfg.upscale, UpscaleConfig::Fixed(2));
}
#[test]
fn with_orientation_method_sets_method() {
let method = OrientationMethod::DiskFit;
let cfg = DetectorConfig::chess().with_orientation_method(method);
assert_eq!(cfg.orientation_method, method);
}
#[test]
fn with_merge_radius_sets_radius() {
let cfg = DetectorConfig::chess().with_merge_radius(5.0);
assert!((cfg.merge_radius - 5.0).abs() < f32::EPSILON);
}
#[test]
fn chess_refiner_shortcuts_equal_full_constructors() {
assert_eq!(
ChessRefiner::center_of_mass(),
ChessRefiner::CenterOfMass(CenterOfMassConfig::default())
);
assert_eq!(
ChessRefiner::forstner(),
ChessRefiner::Forstner(ForstnerConfig::default())
);
assert_eq!(
ChessRefiner::saddle_point(),
ChessRefiner::SaddlePoint(SaddlePointConfig::default())
);
}
#[test]
fn radon_refiner_shortcuts_equal_full_constructors() {
assert_eq!(
RadonRefiner::radon_peak(),
RadonRefiner::RadonPeak(RadonPeakConfig::default())
);
assert_eq!(
RadonRefiner::center_of_mass(),
RadonRefiner::CenterOfMass(CenterOfMassConfig::default())
);
}
#[test]
fn multiscale_config_pyramid_default_equals_literal() {
assert_eq!(
MultiscaleConfig::pyramid_default(),
MultiscaleConfig::Pyramid {
levels: 3,
min_size: 128,
refinement_radius: 3
}
);
}
#[cfg(feature = "ml-refiner")]
#[test]
fn chess_refiner_ml_serializes_as_bare_string() {
let json = serde_json::to_string(&ChessRefiner::Ml).unwrap();
assert_eq!(json, "\"ml\"");
let decoded: ChessRefiner = serde_json::from_str(&json).expect("deserialize ml refiner");
assert_eq!(decoded, ChessRefiner::Ml);
}
}