use crate::error::{NdimageError, NdimageResult};
use scirs2_core::ndarray::Array2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RlmDirection {
Horizontal,
Diagonal45,
Vertical,
Diagonal135,
}
#[derive(Debug, Clone)]
pub struct RlmFeatures {
pub short_run_emphasis: f64,
pub long_run_emphasis: f64,
pub gray_level_non_uniformity: f64,
pub run_length_non_uniformity: f64,
pub run_percentage: f64,
pub low_gray_level_run_emphasis: f64,
pub high_gray_level_run_emphasis: f64,
}
#[derive(Debug, Clone)]
pub struct RlmResult {
pub matrix: Array2<f64>,
pub features: RlmFeatures,
}
pub fn compute_rlm(
image: &Array2<u8>,
direction: RlmDirection,
n_levels: usize,
) -> NdimageResult<Array2<f64>> {
if n_levels < 2 {
return Err(NdimageError::InvalidInput(
"n_levels must be at least 2".into(),
));
}
let (rows, cols) = image.dim();
if rows == 0 || cols == 0 {
return Err(NdimageError::InvalidInput("Image must not be empty".into()));
}
let runs = collect_runs(image, direction, rows, cols, n_levels);
let max_run = runs.iter().map(|&(_, l)| l).max().unwrap_or(1);
let mut matrix = Array2::<f64>::zeros((n_levels, max_run));
for &(g, l) in &runs {
matrix[[g, l - 1]] += 1.0;
}
Ok(matrix)
}
pub fn rlm_features(rlm: &Array2<f64>, n_pixels: usize) -> NdimageResult<RlmFeatures> {
let (n_levels, max_run) = rlm.dim();
if n_levels == 0 || max_run == 0 {
return Err(NdimageError::InvalidInput("RLM must be non-empty".into()));
}
if n_pixels == 0 {
return Err(NdimageError::InvalidInput("n_pixels must be > 0".into()));
}
let n_runs: f64 = rlm.iter().sum();
if n_runs < 1e-15 {
return Ok(RlmFeatures {
short_run_emphasis: 0.0,
long_run_emphasis: 0.0,
gray_level_non_uniformity: 0.0,
run_length_non_uniformity: 0.0,
run_percentage: 0.0,
low_gray_level_run_emphasis: 0.0,
high_gray_level_run_emphasis: 0.0,
});
}
let mut sre = 0.0f64;
let mut lre = 0.0f64;
let mut lgre = 0.0f64;
let mut hgre = 0.0f64;
for i in 0..n_levels {
for j in 0..max_run {
let r = rlm[[i, j]];
if r < 1e-15 {
continue;
}
let run_len = (j + 1) as f64;
let gray = (i + 1) as f64;
sre += r / (run_len * run_len);
lre += r * run_len * run_len;
lgre += r / (gray * gray);
hgre += r * gray * gray;
}
}
let mut gln = 0.0f64;
for i in 0..n_levels {
let row_sum: f64 = (0..max_run).map(|j| rlm[[i, j]]).sum();
gln += row_sum * row_sum;
}
let mut rln = 0.0f64;
for j in 0..max_run {
let col_sum: f64 = (0..n_levels).map(|i| rlm[[i, j]]).sum();
rln += col_sum * col_sum;
}
Ok(RlmFeatures {
short_run_emphasis: sre / n_runs,
long_run_emphasis: lre / n_runs,
gray_level_non_uniformity: gln / n_runs,
run_length_non_uniformity: rln / n_runs,
run_percentage: n_runs / n_pixels as f64,
low_gray_level_run_emphasis: lgre / n_runs,
high_gray_level_run_emphasis: hgre / n_runs,
})
}
fn collect_runs(
image: &Array2<u8>,
direction: RlmDirection,
rows: usize,
cols: usize,
n_levels: usize,
) -> Vec<(usize, usize)> {
let lines = scan_lines(direction, rows, cols);
let mut runs = Vec::new();
for line in &lines {
if line.is_empty() {
continue;
}
let mut idx = 0;
while idx < line.len() {
let (r, c) = line[idx];
let g = (image[[r, c]] as usize).min(n_levels - 1);
let start = idx;
while idx < line.len() {
let (rr, cc) = line[idx];
if (image[[rr, cc]] as usize).min(n_levels - 1) != g {
break;
}
idx += 1;
}
runs.push((g, idx - start));
}
}
runs
}
fn scan_lines(direction: RlmDirection, rows: usize, cols: usize) -> Vec<Vec<(usize, usize)>> {
match direction {
RlmDirection::Horizontal => (0..rows)
.map(|r| (0..cols).map(|c| (r, c)).collect())
.collect(),
RlmDirection::Vertical => (0..cols)
.map(|c| (0..rows).map(|r| (r, c)).collect())
.collect(),
RlmDirection::Diagonal45 => {
let mut lines = Vec::new();
for c0 in 0..cols {
let line: Vec<(usize, usize)> = (0..)
.map(|k| (rows as i64 - 1 - k, c0 as i64 + k))
.take_while(|&(r, c)| r >= 0 && c < cols as i64)
.map(|(r, c)| (r as usize, c as usize))
.collect();
if !line.is_empty() {
lines.push(line);
}
}
for r0 in (0..rows - 1).rev() {
let line: Vec<(usize, usize)> = (0..)
.map(|k| (r0 as i64 - k, k))
.take_while(|&(r, c)| r >= 0 && c < cols as i64)
.map(|(r, c)| (r as usize, c as usize))
.collect();
if !line.is_empty() {
lines.push(line);
}
}
lines
}
RlmDirection::Diagonal135 => {
let mut lines = Vec::new();
for c0 in 0..cols {
let line: Vec<(usize, usize)> = (0..)
.map(|k| (k as i64, c0 as i64 + k as i64))
.take_while(|&(r, c)| r < rows as i64 && c < cols as i64)
.map(|(r, c)| (r as usize, c as usize))
.collect();
if !line.is_empty() {
lines.push(line);
}
}
for r0 in 1..rows {
let line: Vec<(usize, usize)> = (0..)
.map(|k| (r0 as i64 + k as i64, k as i64))
.take_while(|&(r, c)| r < rows as i64 && c < cols as i64)
.map(|(r, c)| (r as usize, c as usize))
.collect();
if !line.is_empty() {
lines.push(line);
}
}
lines
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_rlm_uniform_image() {
let img = Array2::from_elem((4, 6), 2u8);
let rlm = compute_rlm(&img, RlmDirection::Horizontal, 4).expect("rlm");
assert_eq!(rlm.dim(), (4, 6));
assert!(
(rlm[[2, 5]] - 4.0).abs() < 1e-10,
"Expected 4 runs of length 6"
);
let feats = rlm_features(&rlm, 24).expect("features");
assert!(feats.run_percentage > 0.0);
assert!(feats.short_run_emphasis > 0.0);
assert!(feats.long_run_emphasis > 0.0);
}
#[test]
fn test_rlm_checkerboard() {
let img = Array2::from_shape_fn((4, 4), |(i, j)| ((i + j) % 2) as u8);
let rlm = compute_rlm(&img, RlmDirection::Horizontal, 2).expect("rlm");
assert_eq!(rlm.dim(), (2, 1));
let total: f64 = rlm.iter().sum();
assert!((total - 16.0).abs() < 1e-10, "Should have 16 unit runs");
let feats = rlm_features(&rlm, 16).expect("features");
assert!(
(feats.short_run_emphasis - 1.0).abs() < 1e-10,
"SRE should be 1.0 for all unit runs, got {}",
feats.short_run_emphasis
);
assert!(
(feats.long_run_emphasis - 1.0).abs() < 1e-10,
"LRE should be 1.0 for all unit runs, got {}",
feats.long_run_emphasis
);
assert!(
(feats.run_percentage - 1.0).abs() < 1e-10,
"RP should be 1.0, got {}",
feats.run_percentage
);
}
#[test]
fn test_rlm_vertical_runs() {
let img = Array2::from_shape_fn((6, 4), |(_, j)| j as u8);
let rlm = compute_rlm(&img, RlmDirection::Vertical, 4).expect("rlm");
assert!(rlm.dim().1 >= 6);
for g in 0..4 {
assert!(
(rlm[[g, 5]] - 1.0).abs() < 1e-10,
"Each gray level should have 1 vertical run of length 6"
);
}
}
#[test]
fn test_rlm_diagonal_directions() {
let img = Array2::from_elem((5, 5), 0u8);
let rlm_45 = compute_rlm(&img, RlmDirection::Diagonal45, 2).expect("rlm 45");
let rlm_135 = compute_rlm(&img, RlmDirection::Diagonal135, 2).expect("rlm 135");
let total_45: f64 = rlm_45.iter().sum();
let total_135: f64 = rlm_135.iter().sum();
assert!(
(total_45 - total_135).abs() < 1e-10,
"Uniform image should have same run count in both diagonal dirs"
);
assert!(
(total_45 - 9.0).abs() < 1e-10,
"5x5 image should have 9 diagonal runs, got {}",
total_45
);
}
#[test]
fn test_rlm_known_image() {
let img = Array2::from_shape_vec((3, 3), vec![0, 0, 1, 0, 1, 1, 1, 1, 1]).expect("ok");
let rlm = compute_rlm(&img, RlmDirection::Horizontal, 2).expect("rlm");
assert!((rlm[[0, 1]] - 1.0).abs() < 1e-10); assert!((rlm[[0, 0]] - 1.0).abs() < 1e-10); assert!((rlm[[1, 0]] - 1.0).abs() < 1e-10); assert!((rlm[[1, 1]] - 1.0).abs() < 1e-10); assert!((rlm[[1, 2]] - 1.0).abs() < 1e-10); }
#[test]
fn test_rlm_features_bounds() {
let img = Array2::from_shape_fn((8, 8), |(i, j)| ((i * 8 + j) % 4) as u8);
let rlm = compute_rlm(&img, RlmDirection::Horizontal, 4).expect("rlm");
let feats = rlm_features(&rlm, 64).expect("features");
assert!(feats.short_run_emphasis >= 0.0);
assert!(feats.long_run_emphasis >= 0.0);
assert!(feats.gray_level_non_uniformity >= 0.0);
assert!(feats.run_length_non_uniformity >= 0.0);
assert!(feats.run_percentage > 0.0 && feats.run_percentage <= 1.0);
assert!(feats.low_gray_level_run_emphasis >= 0.0);
assert!(feats.high_gray_level_run_emphasis >= 0.0);
}
#[test]
fn test_rlm_errors() {
let img = Array2::from_elem((4, 4), 0u8);
assert!(compute_rlm(&img, RlmDirection::Horizontal, 1).is_err());
let empty = Array2::<u8>::zeros((0, 0));
assert!(compute_rlm(&empty, RlmDirection::Horizontal, 2).is_err());
let rlm = Array2::<f64>::zeros((0, 0));
assert!(rlm_features(&rlm, 16).is_err());
}
}