macro_rules! features_table {
(
$(
$(#[$variant_attr:meta])*
$variant:ident = $id:literal : $ty:ty => $field:ident
),* $(,)?
) => {
#[non_exhaustive]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[repr(u16)]
pub enum AnalysisFeature {
$(
$(#[$variant_attr])*
$variant = $id,
)*
}
impl AnalysisFeature {
#[inline]
pub const fn id(self) -> u16 {
self as u16
}
#[inline]
#[allow(unused_doc_comments)]
pub const fn from_u16(n: u16) -> Option<Self> {
match n {
$(
$(#[$variant_attr])*
$id => Some(Self::$variant),
)*
_ => None,
}
}
#[inline]
pub(crate) const fn is_active(self) -> bool {
match self {
_ => true,
}
}
#[allow(unused_doc_comments)]
pub const fn name(self) -> &'static str {
match self {
$(
$(#[$variant_attr])*
Self::$variant => stringify!($field),
)*
}
}
}
impl FeatureSet {
#[allow(unused_doc_comments, unused_mut, unused_assignments)]
pub const SUPPORTED: Self = {
let mut s = Self::new();
$(
$(#[$variant_attr])*
{
s = s.with(AnalysisFeature::$variant);
}
)*
s
};
}
#[derive(Default, Debug, Clone, Copy)]
pub(crate) struct RawAnalysis {
$(
$(#[$variant_attr])*
pub $field: $ty,
)*
}
impl RawAnalysis {
#[allow(unused_doc_comments)]
pub(crate) fn into_results(
self,
requested: FeatureSet,
geometry: ImageGeometry,
source_descriptor: zenpixels::PixelDescriptor,
) -> AnalysisResults {
let mut r = AnalysisResults::new(requested, geometry, source_descriptor);
$(
$(#[$variant_attr])*
{
if requested.contains(AnalysisFeature::$variant) {
r.set(AnalysisFeature::$variant, self.$field);
}
}
)*
r
}
}
};
}
features_table! {
Variance = 0 : f32 => variance,
EdgeDensity = 1 : f32 => edge_density,
ChromaComplexity = 2 : f32 => chroma_complexity,
CbSharpness = 3 : f32 => cb_sharpness,
CrSharpness = 4 : f32 => cr_sharpness,
Uniformity = 5 : f32 => uniformity,
FlatColorBlockRatio = 6 : f32 => flat_color_block_ratio,
#[cfg(feature = "experimental")]
Colourfulness = 7 : f32 => colourfulness,
#[cfg(feature = "experimental")]
LaplacianVariance = 8 : f32 => laplacian_variance,
#[cfg(feature = "experimental")]
VarianceSpread = 9 : f32 => variance_spread,
DistinctColorBins = 10 : u32 => distinct_color_bins,
#[cfg(feature = "experimental")]
PaletteDensity = 12 : f32 => palette_density,
CbHorizSharpness = 13 : f32 => cb_horiz_sharpness,
CbVertSharpness = 14 : f32 => cb_vert_sharpness,
CbPeakSharpness = 15 : f32 => cb_peak_sharpness,
CrHorizSharpness = 16 : f32 => cr_horiz_sharpness,
CrVertSharpness = 17 : f32 => cr_vert_sharpness,
CrPeakSharpness = 18 : f32 => cr_peak_sharpness,
HighFreqEnergyRatio = 19 : f32 => high_freq_energy_ratio,
LumaHistogramEntropy = 20 : f32 => luma_histogram_entropy,
#[cfg(feature = "experimental")]
DctCompressibilityY = 21 : f32 => dct_compressibility_y,
#[cfg(feature = "experimental")]
DctCompressibilityUV = 22 : f32 => dct_compressibility_uv,
#[cfg(feature = "experimental")]
PatchFraction = 23 : f32 => patch_fraction,
AlphaPresent = 24 : bool => alpha_present,
AlphaUsedFraction = 25 : f32 => alpha_used_fraction,
AlphaBimodalScore = 26 : f32 => alpha_bimodal_score,
#[cfg(feature = "composites")]
TextLikelihood = 27 : f32 => text_likelihood,
#[cfg(feature = "composites")]
ScreenContentLikelihood = 28 : f32 => screen_content_likelihood,
#[cfg(feature = "composites")]
NaturalLikelihood = 29 : f32 => natural_likelihood,
#[cfg(feature = "experimental")]
IndexedPaletteWidth = 30 : u32 => indexed_palette_width,
#[cfg(feature = "experimental")]
PaletteFitsIn256 = 31 : bool => palette_fits_in_256,
#[cfg(feature = "experimental")]
PeakLuminanceNits = 32 : f32 => peak_luminance_nits,
#[cfg(feature = "experimental")]
P99LuminanceNits = 33 : f32 => p99_luminance_nits,
#[cfg(feature = "experimental")]
HdrHeadroomStops = 34 : f32 => hdr_headroom_stops,
#[cfg(feature = "experimental")]
HdrPixelFraction = 35 : f32 => hdr_pixel_fraction,
#[cfg(feature = "experimental")]
WideGamutPeak = 36 : f32 => wide_gamut_peak,
#[cfg(feature = "experimental")]
WideGamutFraction = 37 : f32 => wide_gamut_fraction,
#[cfg(feature = "experimental")]
EffectiveBitDepth = 38 : u32 => effective_bit_depth,
#[cfg(feature = "experimental")]
HdrPresent = 39 : bool => hdr_present,
#[cfg(feature = "experimental")]
GamutCoverageSrgb = 46 : f32 => gamut_coverage_srgb,
#[cfg(feature = "experimental")]
GamutCoverageP3 = 47 : f32 => gamut_coverage_p3,
#[cfg(feature = "experimental")]
GradientFraction = 48 : f32 => gradient_fraction,
#[cfg(feature = "experimental")]
GrayscaleScore = 40 : f32 => grayscale_score,
#[cfg(feature = "experimental")]
AqMapMean = 41 : f32 => aq_map_mean,
#[cfg(feature = "experimental")]
AqMapStd = 42 : f32 => aq_map_std,
#[cfg(feature = "experimental")]
NoiseFloorY = 43 : f32 => noise_floor_y,
#[cfg(feature = "experimental")]
NoiseFloorUV = 44 : f32 => noise_floor_uv,
#[cfg(feature = "composites")]
LineArtScore = 45 : f32 => line_art_score,
#[cfg(feature = "experimental")]
SkinToneFraction = 49 : f32 => skin_tone_fraction,
#[cfg(feature = "experimental")]
EdgeSlopeStdev = 50 : f32 => edge_slope_stdev,
}
#[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum FeatureValue {
F32(f32),
U32(u32),
Bool(bool),
}
impl FeatureValue {
#[inline]
pub const fn as_f32(self) -> Option<f32> {
match self {
Self::F32(x) => Some(x),
_ => None,
}
}
#[inline]
pub const fn as_u32(self) -> Option<u32> {
match self {
Self::U32(x) => Some(x),
_ => None,
}
}
#[inline]
pub const fn as_bool(self) -> Option<bool> {
match self {
Self::Bool(x) => Some(x),
_ => None,
}
}
#[inline]
pub fn to_f32(self) -> f32 {
match self {
Self::F32(x) => x,
Self::U32(x) => x as f32,
Self::Bool(false) => 0.0,
Self::Bool(true) => 1.0,
}
}
}
impl From<f32> for FeatureValue {
fn from(x: f32) -> Self {
Self::F32(x)
}
}
impl From<u32> for FeatureValue {
fn from(x: u32) -> Self {
Self::U32(x)
}
}
impl From<bool> for FeatureValue {
fn from(x: bool) -> Self {
Self::Bool(x)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct FeatureSet {
bits: [u64; 4],
}
impl FeatureSet {
pub const fn new() -> Self {
Self { bits: [0; 4] }
}
pub const fn just(f: AnalysisFeature) -> Self {
Self::new().with(f)
}
pub const fn with(mut self, f: AnalysisFeature) -> Self {
let id = f as u16 as usize;
self.bits[id >> 6] |= 1u64 << (id & 63);
self
}
pub const fn without(mut self, f: AnalysisFeature) -> Self {
let id = f as u16 as usize;
self.bits[id >> 6] &= !(1u64 << (id & 63));
self
}
pub const fn union(mut self, other: Self) -> Self {
let mut i = 0;
while i < 4 {
self.bits[i] |= other.bits[i];
i += 1;
}
self
}
pub const fn intersect(mut self, other: Self) -> Self {
let mut i = 0;
while i < 4 {
self.bits[i] &= other.bits[i];
i += 1;
}
self
}
pub const fn difference(mut self, other: Self) -> Self {
let mut i = 0;
while i < 4 {
self.bits[i] &= !other.bits[i];
i += 1;
}
self
}
pub const fn intersects(self, other: Self) -> bool {
let mut i = 0;
while i < 4 {
if self.bits[i] & other.bits[i] != 0 {
return true;
}
i += 1;
}
false
}
pub const fn contains_all(self, other: Self) -> bool {
let mut i = 0;
while i < 4 {
if self.bits[i] & other.bits[i] != other.bits[i] {
return false;
}
i += 1;
}
true
}
pub const fn contains(self, f: AnalysisFeature) -> bool {
let id = f as u16 as usize;
(self.bits[id >> 6] >> (id & 63)) & 1 != 0
}
pub const fn is_empty(self) -> bool {
let mut i = 0;
while i < 4 {
if self.bits[i] != 0 {
return false;
}
i += 1;
}
true
}
pub const fn len(self) -> u32 {
let mut total = 0;
let mut i = 0;
while i < 4 {
total += self.bits[i].count_ones();
i += 1;
}
total
}
pub fn iter(self) -> FeatureSetIter {
FeatureSetIter {
bits: self.bits,
next_id: 0,
}
}
}
pub struct FeatureSetIter {
bits: [u64; 4],
next_id: u16,
}
impl Iterator for FeatureSetIter {
type Item = AnalysisFeature;
fn next(&mut self) -> Option<Self::Item> {
loop {
let id = self.next_id as usize;
if id >= 256 {
return None;
}
let bit = (self.bits[id >> 6] >> (id & 63)) & 1;
self.next_id += 1;
if bit == 1
&& let Some(f) = AnalysisFeature::from_u16(id as u16)
{
return Some(f);
}
}
}
}
impl IntoIterator for FeatureSet {
type Item = AnalysisFeature;
type IntoIter = FeatureSetIter;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl Default for FeatureSet {
fn default() -> Self {
Self::new()
}
}
pub(crate) const PALETTE_FULL_FEATURES: FeatureSet = {
let mut s = FeatureSet::new();
s = s.with(AnalysisFeature::DistinctColorBins);
#[cfg(feature = "experimental")]
{
s = s.with(AnalysisFeature::PaletteDensity);
s = s.with(AnalysisFeature::GrayscaleScore);
}
s
};
#[allow(unused_mut, unused_assignments)]
pub(crate) const PALETTE_QUICK_FEATURES: FeatureSet = {
let mut s = FeatureSet::new();
#[cfg(feature = "experimental")]
{
s = s.with(AnalysisFeature::IndexedPaletteWidth);
s = s.with(AnalysisFeature::PaletteFitsIn256);
}
s
};
pub(crate) const PALETTE_FEATURES: FeatureSet = PALETTE_FULL_FEATURES.union(PALETTE_QUICK_FEATURES);
#[allow(unused_mut, unused_assignments)]
pub(crate) const TIER1_EXTRAS_FEATURES: FeatureSet = {
let mut s = FeatureSet::new();
s = s.with(AnalysisFeature::Variance);
#[cfg(feature = "experimental")]
{
s = s.with(AnalysisFeature::Colourfulness);
s = s.with(AnalysisFeature::LaplacianVariance);
s = s.with(AnalysisFeature::SkinToneFraction);
s = s.with(AnalysisFeature::EdgeSlopeStdev);
}
s
};
#[allow(unused_mut, unused_assignments)]
pub(crate) const TIER1_FULL_FEATURES: FeatureSet = {
let mut s = FeatureSet::new();
s = s.with(AnalysisFeature::Variance);
#[cfg(feature = "experimental")]
{
s = s.with(AnalysisFeature::Colourfulness);
s = s.with(AnalysisFeature::LaplacianVariance);
s = s.with(AnalysisFeature::EdgeSlopeStdev);
}
s
};
#[allow(unused_mut, unused_assignments)]
pub(crate) const TIER1_SKIN_FEATURES: FeatureSet = {
let mut s = FeatureSet::new();
#[cfg(feature = "experimental")]
{
s = s.with(AnalysisFeature::SkinToneFraction);
}
s
};
pub(crate) const TIER2_FEATURES: FeatureSet = FeatureSet::new()
.with(AnalysisFeature::CbHorizSharpness)
.with(AnalysisFeature::CbVertSharpness)
.with(AnalysisFeature::CbPeakSharpness)
.with(AnalysisFeature::CrHorizSharpness)
.with(AnalysisFeature::CrVertSharpness)
.with(AnalysisFeature::CrPeakSharpness);
#[allow(unused_mut, unused_assignments)]
pub(crate) const TIER3_FEATURES: FeatureSet = {
let mut s = FeatureSet::new();
s = s.with(AnalysisFeature::HighFreqEnergyRatio);
s = s.with(AnalysisFeature::LumaHistogramEntropy);
#[cfg(feature = "experimental")]
{
s = s.with(AnalysisFeature::DctCompressibilityY);
s = s.with(AnalysisFeature::DctCompressibilityUV);
s = s.with(AnalysisFeature::PatchFraction);
s = s.with(AnalysisFeature::AqMapMean);
s = s.with(AnalysisFeature::AqMapStd);
s = s.with(AnalysisFeature::NoiseFloorY);
s = s.with(AnalysisFeature::NoiseFloorUV);
s = s.with(AnalysisFeature::GradientFraction);
}
#[cfg(feature = "composites")]
{
s = s.with(AnalysisFeature::LineArtScore);
}
s
};
pub(crate) const ALPHA_FEATURES: FeatureSet = FeatureSet::new()
.with(AnalysisFeature::AlphaPresent)
.with(AnalysisFeature::AlphaUsedFraction)
.with(AnalysisFeature::AlphaBimodalScore);
#[allow(unused_mut, unused_assignments)]
pub(crate) const DEPTH_FEATURES: FeatureSet = {
let mut s = FeatureSet::new();
#[cfg(feature = "experimental")]
{
s = s.with(AnalysisFeature::PeakLuminanceNits);
s = s.with(AnalysisFeature::P99LuminanceNits);
s = s.with(AnalysisFeature::HdrHeadroomStops);
s = s.with(AnalysisFeature::HdrPixelFraction);
s = s.with(AnalysisFeature::WideGamutPeak);
s = s.with(AnalysisFeature::WideGamutFraction);
s = s.with(AnalysisFeature::EffectiveBitDepth);
s = s.with(AnalysisFeature::HdrPresent);
s = s.with(AnalysisFeature::GamutCoverageSrgb);
s = s.with(AnalysisFeature::GamutCoverageP3);
}
s
};
#[allow(unused_mut, unused_assignments)]
pub(crate) const DERIVED_FEATURES: FeatureSet = {
let mut s = FeatureSet::new();
#[cfg(feature = "composites")]
{
s = s.with(AnalysisFeature::TextLikelihood);
s = s.with(AnalysisFeature::ScreenContentLikelihood);
s = s.with(AnalysisFeature::NaturalLikelihood);
}
s
};
#[allow(unused_mut, unused_assignments)]
pub(crate) const T3_NEEDED_BY: FeatureSet = {
let mut s = TIER3_FEATURES;
#[cfg(feature = "composites")]
{
s = s.with(AnalysisFeature::TextLikelihood);
s = s.with(AnalysisFeature::NaturalLikelihood);
}
s
};
#[allow(unused_mut, unused_assignments)]
pub(crate) const PAL_NEEDED_BY: FeatureSet = {
let mut s = PALETTE_FEATURES;
#[cfg(feature = "composites")]
{
s = s.with(AnalysisFeature::ScreenContentLikelihood);
s = s.with(AnalysisFeature::NaturalLikelihood);
}
s
};
#[derive(Clone, Debug)]
pub struct AnalysisQuery {
features: FeatureSet,
}
impl AnalysisQuery {
pub const fn new(features: FeatureSet) -> Self {
Self { features }
}
pub const fn features(&self) -> FeatureSet {
self.features
}
}
pub(crate) const DEFAULT_PIXEL_BUDGET: usize = 500_000;
pub(crate) const DEFAULT_HF_MAX_BLOCKS: usize = 1024;
#[doc(hidden)]
impl AnalysisQuery {
pub fn __internal_with_overrides(
features: FeatureSet,
pixel_budget: usize,
hf_max_blocks: usize,
) -> InternalQuery {
InternalQuery {
features,
pixel_budget,
hf_max_blocks,
}
}
}
#[doc(hidden)]
pub struct InternalQuery {
pub(crate) features: FeatureSet,
pub(crate) pixel_budget: usize,
pub(crate) hf_max_blocks: usize,
}
#[derive(Copy, Clone, Debug)]
pub struct ImageGeometry {
width: u32,
height: u32,
}
impl ImageGeometry {
pub(crate) const fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
pub const fn width(self) -> u32 {
self.width
}
pub const fn height(self) -> u32 {
self.height
}
pub const fn pixels(self) -> u64 {
self.width as u64 * self.height as u64
}
pub fn megapixels(self) -> f32 {
self.pixels() as f32 / 1_000_000.0
}
pub fn aspect_ratio(self) -> f32 {
if self.height == 0 {
0.0
} else {
self.width as f32 / self.height as f32
}
}
}
pub struct AnalysisResults {
requested: FeatureSet,
geometry: ImageGeometry,
source_descriptor: zenpixels::PixelDescriptor,
values: Vec<(AnalysisFeature, FeatureValue)>,
}
impl AnalysisResults {
pub(crate) fn new(
requested: FeatureSet,
geometry: ImageGeometry,
source_descriptor: zenpixels::PixelDescriptor,
) -> Self {
Self {
requested,
geometry,
source_descriptor,
values: Vec::with_capacity(requested.len() as usize),
}
}
pub(crate) fn set(&mut self, f: AnalysisFeature, v: impl Into<FeatureValue>) {
debug_assert!(
self.requested.contains(f),
"analyzer wrote unrequested feature {:?} (id={}) — dispatcher gating is broken",
f,
f.id()
);
if !self.requested.contains(f) {
return;
}
let v = v.into();
let mut i = 0;
while i < self.values.len() {
match self.values[i].0.id().cmp(&f.id()) {
core::cmp::Ordering::Less => i += 1,
core::cmp::Ordering::Equal => {
self.values[i].1 = v;
return;
}
core::cmp::Ordering::Greater => break,
}
}
self.values.insert(i, (f, v));
}
pub const fn requested(&self) -> FeatureSet {
self.requested
}
pub const fn geometry(&self) -> ImageGeometry {
self.geometry
}
#[inline]
pub const fn source_descriptor(&self) -> zenpixels::PixelDescriptor {
self.source_descriptor
}
#[inline]
pub fn get(&self, f: AnalysisFeature) -> Option<FeatureValue> {
self.values.iter().find(|(k, _)| *k == f).map(|(_, v)| *v)
}
#[inline]
pub fn get_f32(&self, f: AnalysisFeature) -> Option<f32> {
self.get(f).map(FeatureValue::to_f32)
}
}
impl core::fmt::Debug for AnalysisResults {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let mut d = f.debug_struct("AnalysisResults");
d.field("requested", &self.requested);
d.field("geometry", &self.geometry);
for (feature, v) in &self.values {
d.field(feature.name(), v);
}
d.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn feature_set_basic_ops() {
let a = FeatureSet::just(AnalysisFeature::Variance);
let b = FeatureSet::just(AnalysisFeature::EdgeDensity);
let u = a.union(b);
assert!(u.contains(AnalysisFeature::Variance));
assert!(u.contains(AnalysisFeature::EdgeDensity));
assert!(!u.contains(AnalysisFeature::ChromaComplexity));
assert_eq!(u.len(), 2);
assert!(a.intersects(u));
assert!(u.contains_all(a));
assert!(!a.contains_all(u));
}
#[test]
fn feature_value_roundtrip() {
let v: FeatureValue = 1.5f32.into();
assert_eq!(v.as_f32(), Some(1.5));
assert_eq!(v.as_u32(), None);
assert_eq!(v.to_f32(), 1.5);
let v: FeatureValue = 7u32.into();
assert_eq!(v.as_u32(), Some(7));
assert_eq!(v.to_f32(), 7.0);
let v: FeatureValue = true.into();
assert_eq!(v.as_bool(), Some(true));
assert_eq!(v.to_f32(), 1.0);
}
const RESERVED_RETIRED_IDS: &[u16] = &[
11,
];
#[test]
fn discriminants_round_trip() {
for id in 0..64u16 {
if RESERVED_RETIRED_IDS.contains(&id) {
assert!(
AnalysisFeature::from_u16(id).is_none(),
"id {id} is retired but from_u16 returned Some — \
don't recycle retired discriminants"
);
continue;
}
if let Some(f) = AnalysisFeature::from_u16(id) {
assert_eq!(f.id(), id);
}
}
assert!(AnalysisFeature::from_u16(64).is_none());
assert!(AnalysisFeature::from_u16(255).is_none());
}
#[test]
fn analysis_query_constructor_only() {
let q = AnalysisQuery::new(FeatureSet::just(AnalysisFeature::Variance));
assert!(q.features().contains(AnalysisFeature::Variance));
assert!(!q.features().contains(AnalysisFeature::EdgeDensity));
}
#[test]
fn raw_analysis_round_trip() {
let raw = RawAnalysis {
variance: 12.5,
distinct_color_bins: 4096,
alpha_present: true,
edge_density: 0.5, ..Default::default()
};
let requested = FeatureSet::just(AnalysisFeature::Variance)
.with(AnalysisFeature::DistinctColorBins)
.with(AnalysisFeature::AlphaPresent);
let r = raw.into_results(
requested,
ImageGeometry::new(64, 64),
zenpixels::PixelDescriptor::RGB8_SRGB,
);
assert_eq!(r.get_f32(AnalysisFeature::Variance), Some(12.5));
assert_eq!(
r.get(AnalysisFeature::DistinctColorBins),
Some(FeatureValue::U32(4096))
);
assert_eq!(
r.get(AnalysisFeature::AlphaPresent),
Some(FeatureValue::Bool(true))
);
assert_eq!(r.get(AnalysisFeature::EdgeDensity), None);
}
#[test]
fn supported_set_covers_all_active_variants() {
let mut active = 0u32;
for id in 0..64u16 {
if RESERVED_RETIRED_IDS.contains(&id) {
continue;
}
let Some(f) = AnalysisFeature::from_u16(id) else {
continue;
};
assert!(
FeatureSet::SUPPORTED.contains(f),
"{:?} (id={}) is missing from FeatureSet::SUPPORTED",
f,
id
);
active += 1;
}
assert_eq!(FeatureSet::SUPPORTED.len(), active);
}
#[test]
fn analysis_results_get_and_set() {
let requested = FeatureSet::just(AnalysisFeature::Variance)
.with(AnalysisFeature::DistinctColorBins)
.with(AnalysisFeature::AlphaPresent);
let mut r = AnalysisResults::new(
requested,
ImageGeometry::new(1920, 1080),
zenpixels::PixelDescriptor::RGB8_SRGB,
);
r.set(AnalysisFeature::AlphaPresent, true);
r.set(AnalysisFeature::Variance, 42.0_f32);
r.set(AnalysisFeature::DistinctColorBins, 1234_u32);
r.set(AnalysisFeature::Variance, 43.0_f32);
assert_eq!(
r.get(AnalysisFeature::Variance),
Some(FeatureValue::F32(43.0))
);
assert_eq!(
r.get(AnalysisFeature::DistinctColorBins),
Some(FeatureValue::U32(1234))
);
assert_eq!(
r.get(AnalysisFeature::AlphaPresent),
Some(FeatureValue::Bool(true))
);
assert_eq!(r.get(AnalysisFeature::ChromaComplexity), None);
assert_eq!(r.get(AnalysisFeature::EdgeDensity), None);
assert_eq!(r.get_f32(AnalysisFeature::Variance), Some(43.0));
assert_eq!(r.get_f32(AnalysisFeature::DistinctColorBins), Some(1234.0));
assert_eq!(r.get_f32(AnalysisFeature::AlphaPresent), Some(1.0));
assert_eq!(r.geometry().width(), 1920);
assert_eq!(r.geometry().pixels(), 1920 * 1080);
assert!((r.geometry().aspect_ratio() - 1920.0 / 1080.0).abs() < 1e-6);
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "analyzer wrote unrequested feature")]
fn set_unrequested_feature_panics_in_debug() {
let mut r = AnalysisResults::new(
FeatureSet::just(AnalysisFeature::Variance),
ImageGeometry::new(64, 64),
zenpixels::PixelDescriptor::RGB8_SRGB,
);
r.set(AnalysisFeature::EdgeDensity, 0.0_f32);
}
#[test]
fn feature_set_iter_visits_every_set_member_in_id_order() {
let s = FeatureSet::just(AnalysisFeature::DistinctColorBins)
.with(AnalysisFeature::Variance)
.with(AnalysisFeature::EdgeDensity);
let collected: Vec<_> = s.iter().collect();
assert_eq!(
collected,
vec![
AnalysisFeature::Variance,
AnalysisFeature::EdgeDensity,
AnalysisFeature::DistinctColorBins,
]
);
let n = FeatureSet::SUPPORTED.iter().count();
assert_eq!(n as u32, FeatureSet::SUPPORTED.len());
assert_eq!(FeatureSet::new().iter().count(), 0);
let s2 = FeatureSet::just(AnalysisFeature::Variance);
let v: Vec<_> = s2.into_iter().collect();
assert_eq!(v, vec![AnalysisFeature::Variance]);
}
#[test]
fn analysis_feature_id_is_public_and_stable() {
assert_eq!(AnalysisFeature::Variance.id(), 0);
assert_eq!(AnalysisFeature::EdgeDensity.id(), 1);
assert_eq!(AnalysisFeature::DistinctColorBins.id(), 10);
assert_eq!(AnalysisFeature::AlphaPresent.id(), 24);
assert_eq!(
AnalysisFeature::from_u16(0),
Some(AnalysisFeature::Variance)
);
assert_eq!(AnalysisFeature::from_u16(11), None); assert_eq!(AnalysisFeature::from_u16(9999), None);
}
#[test]
fn tier_bundles_are_disjoint() {
let bundles = [
PALETTE_FEATURES,
TIER2_FEATURES,
TIER3_FEATURES,
ALPHA_FEATURES,
DERIVED_FEATURES,
];
for (i, a) in bundles.iter().enumerate() {
for b in bundles.iter().skip(i + 1) {
assert!(!a.intersects(*b), "tier bundles overlap (this is a bug)");
}
}
}
}