use crate::error::{AlgorithmError, Result};
use oxigdal_core::buffer::RasterBuffer;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Horizontal,
Vertical,
Diagonal45,
Diagonal135,
Custom(i64, i64),
}
impl Direction {
#[must_use]
pub fn offset(&self, distance: u32) -> (i64, i64) {
let d = distance as i64;
match self {
Self::Horizontal => (d, 0),
Self::Vertical => (0, d),
Self::Diagonal45 => (d, -d),
Self::Diagonal135 => (d, d),
Self::Custom(dx, dy) => (*dx * d, *dy * d),
}
}
#[must_use]
pub fn all_standard() -> Vec<Self> {
vec![
Self::Horizontal,
Self::Vertical,
Self::Diagonal45,
Self::Diagonal135,
]
}
}
#[derive(Debug, Clone)]
pub struct GlcmParams {
pub gray_levels: usize,
pub normalize: bool,
pub symmetric: bool,
pub window_size: Option<usize>,
}
impl Default for GlcmParams {
fn default() -> Self {
Self {
gray_levels: 256,
normalize: true,
symmetric: true,
window_size: None,
}
}
}
#[derive(Debug, Clone)]
pub struct Glcm {
matrix: Vec<Vec<f64>>,
gray_levels: usize,
direction: Direction,
distance: u32,
normalized: bool,
}
impl Glcm {
#[must_use]
pub fn new(gray_levels: usize, direction: Direction, distance: u32) -> Self {
Self {
matrix: vec![vec![0.0; gray_levels]; gray_levels],
gray_levels,
direction,
distance,
normalized: false,
}
}
#[must_use]
pub fn get(&self, i: usize, j: usize) -> f64 {
if i < self.gray_levels && j < self.gray_levels {
self.matrix[i][j]
} else {
0.0
}
}
pub fn set(&mut self, i: usize, j: usize, value: f64) {
if i < self.gray_levels && j < self.gray_levels {
self.matrix[i][j] = value;
}
}
pub fn increment(&mut self, i: usize, j: usize) {
if i < self.gray_levels && j < self.gray_levels {
self.matrix[i][j] += 1.0;
}
}
pub fn normalize(&mut self) {
let sum: f64 = self.matrix.iter().flat_map(|row| row.iter()).sum();
if sum > 0.0 {
for row in &mut self.matrix {
for val in row {
*val /= sum;
}
}
self.normalized = true;
}
}
pub fn make_symmetric(&mut self) {
for i in 0..self.gray_levels {
for j in 0..self.gray_levels {
let avg = (self.matrix[i][j] + self.matrix[j][i]) / 2.0;
self.matrix[i][j] = avg;
self.matrix[j][i] = avg;
}
}
}
#[must_use]
pub fn gray_levels(&self) -> usize {
self.gray_levels
}
#[must_use]
pub fn direction(&self) -> Direction {
self.direction
}
#[must_use]
pub fn distance(&self) -> u32 {
self.distance
}
#[must_use]
pub fn is_normalized(&self) -> bool {
self.normalized
}
#[must_use]
pub fn matrix(&self) -> &[Vec<f64>] {
&self.matrix
}
}
#[derive(Debug, Clone, Default)]
pub struct HaralickFeatures {
pub contrast: f64,
pub correlation: f64,
pub energy: f64,
pub homogeneity: f64,
pub entropy: f64,
pub dissimilarity: f64,
pub variance: f64,
pub sum_average: f64,
pub sum_entropy: f64,
pub difference_entropy: f64,
pub info_measure_corr1: f64,
pub info_measure_corr2: f64,
pub max_correlation_coeff: f64,
pub cluster_shade: f64,
pub cluster_prominence: f64,
}
pub fn compute_glcm(
src: &RasterBuffer,
direction: Direction,
distance: u32,
params: &GlcmParams,
) -> Result<Glcm> {
let width = src.width();
let height = src.height();
if params.gray_levels == 0 {
return Err(AlgorithmError::InvalidParameter {
parameter: "gray_levels",
message: "Gray levels must be greater than zero".to_string(),
});
}
let quantized = quantize_image(src, params.gray_levels)?;
let (dx, dy) = direction.offset(distance);
let mut glcm = Glcm::new(params.gray_levels, direction, distance);
for y in 0..height {
for x in 0..width {
let nx = x as i64 + dx;
let ny = y as i64 + dy;
if nx >= 0 && nx < width as i64 && ny >= 0 && ny < height as i64 {
let i = quantized.get_pixel(x, y).map_err(AlgorithmError::Core)? as usize;
let j = quantized
.get_pixel(nx as u64, ny as u64)
.map_err(AlgorithmError::Core)? as usize;
glcm.increment(i, j);
}
}
}
if params.symmetric {
glcm.make_symmetric();
}
if params.normalize {
glcm.normalize();
}
Ok(glcm)
}
pub fn compute_glcm_multi_direction(
src: &RasterBuffer,
directions: &[Direction],
distance: u32,
params: &GlcmParams,
) -> Result<Glcm> {
if directions.is_empty() {
return Err(AlgorithmError::InvalidParameter {
parameter: "directions",
message: "At least one direction required".to_string(),
});
}
let mut avg_glcm = Glcm::new(params.gray_levels, directions[0], distance);
for direction in directions {
let glcm = compute_glcm(src, *direction, distance, params)?;
for i in 0..params.gray_levels {
for j in 0..params.gray_levels {
let val = avg_glcm.get(i, j) + glcm.get(i, j);
avg_glcm.set(i, j, val);
}
}
}
for i in 0..params.gray_levels {
for j in 0..params.gray_levels {
let val = avg_glcm.get(i, j) / directions.len() as f64;
avg_glcm.set(i, j, val);
}
}
Ok(avg_glcm)
}
#[must_use]
pub fn compute_haralick_features(glcm: &Glcm) -> HaralickFeatures {
let n = glcm.gray_levels();
let matrix = glcm.matrix();
let mut px = vec![0.0; n];
let mut py = vec![0.0; n];
for i in 0..n {
for j in 0..n {
px[i] += matrix[i][j];
py[j] += matrix[i][j];
}
}
let mut mu_x = 0.0;
let mut mu_y = 0.0;
for i in 0..n {
mu_x += i as f64 * px[i];
mu_y += i as f64 * py[i];
}
let mut sigma_x = 0.0;
let mut sigma_y = 0.0;
for i in 0..n {
sigma_x += (i as f64 - mu_x).powi(2) * px[i];
sigma_y += (i as f64 - mu_y).powi(2) * py[i];
}
sigma_x = sigma_x.sqrt();
sigma_y = sigma_y.sqrt();
let max_sum = 2 * (n - 1);
let mut p_x_plus_y = vec![0.0; max_sum + 1];
let mut p_x_minus_y = vec![0.0; n];
for i in 0..n {
for j in 0..n {
p_x_plus_y[i + j] += matrix[i][j];
let diff = (i as i64 - j as i64).unsigned_abs() as usize;
p_x_minus_y[diff] += matrix[i][j];
}
}
let mut features = HaralickFeatures::default();
features.contrast = (0..n).map(|k| k.pow(2) as f64 * p_x_minus_y[k]).sum();
if sigma_x > 0.0 && sigma_y > 0.0 {
features.correlation = (0..n)
.flat_map(|i| {
(0..n).map(move |j| {
(i as f64 - mu_x) * (j as f64 - mu_y) * matrix[i][j] / (sigma_x * sigma_y)
})
})
.sum();
}
features.energy = (0..n)
.flat_map(|i| (0..n).map(move |j| matrix[i][j].powi(2)))
.sum();
features.homogeneity = (0..n)
.flat_map(|i| (0..n).map(move |j| matrix[i][j] / (1.0 + (i as f64 - j as f64).powi(2))))
.sum();
features.entropy = -(0..n)
.flat_map(|i| {
(0..n).map(move |j| {
let p = matrix[i][j];
if p > 0.0 { p * p.ln() } else { 0.0 }
})
})
.sum::<f64>();
features.dissimilarity = (0..n)
.flat_map(|i| (0..n).map(move |j| (i as f64 - j as f64).abs() * matrix[i][j]))
.sum();
let mu = (0..n)
.flat_map(|i| (0..n).map(move |j| (i + j) as f64 * matrix[i][j]))
.sum::<f64>()
/ 2.0;
features.variance = (0..n)
.flat_map(|i| (0..n).map(move |j| ((i + j) as f64 / 2.0 - mu).powi(2) * matrix[i][j]))
.sum();
features.sum_average = (0..=max_sum).map(|k| k as f64 * p_x_plus_y[k]).sum();
features.sum_entropy = -(0..=max_sum)
.map(|k| {
let p = p_x_plus_y[k];
if p > 0.0 { p * p.ln() } else { 0.0 }
})
.sum::<f64>();
features.difference_entropy = -(0..n)
.map(|k| {
let p = p_x_minus_y[k];
if p > 0.0 { p * p.ln() } else { 0.0 }
})
.sum::<f64>();
let hx = -px
.iter()
.map(|&p| if p > 0.0 { p * p.ln() } else { 0.0 })
.sum::<f64>();
let hy = -py
.iter()
.map(|&p| if p > 0.0 { p * p.ln() } else { 0.0 })
.sum::<f64>();
let hxy = features.entropy;
let px_clone = px.clone();
let py_clone = py.clone();
let hxy1 = -(0..n)
.flat_map(|i| {
let px = px_clone.clone();
let py = py_clone.clone();
(0..n).map(move |j| {
let p = matrix[i][j];
if p > 0.0 && px[i] > 0.0 && py[j] > 0.0 {
p * (px[i] * py[j]).ln()
} else {
0.0
}
})
})
.sum::<f64>();
let hxy2 = -(0..n)
.flat_map(|i| {
let px = px.clone();
let py = py.clone();
(0..n).map(move |j| {
let p = px[i] * py[j];
if p > 0.0 { p * p.ln() } else { 0.0 }
})
})
.sum::<f64>();
let max_hxy = hx.max(hy);
if max_hxy > 0.0 {
features.info_measure_corr1 = (hxy - hxy1) / max_hxy;
}
if hxy2 > hxy {
features.info_measure_corr2 = (1.0 - (-2.0 * (hxy2 - hxy)).exp()).sqrt();
}
features.max_correlation_coeff = features.correlation.abs();
features.cluster_shade = (0..n)
.flat_map(|i| {
(0..n).map(move |j| (i as f64 + j as f64 - mu_x - mu_y).powi(3) * matrix[i][j])
})
.sum();
features.cluster_prominence = (0..n)
.flat_map(|i| {
(0..n).map(move |j| (i as f64 + j as f64 - mu_x - mu_y).powi(4) * matrix[i][j])
})
.sum();
features
}
pub fn compute_texture_feature_image(
src: &RasterBuffer,
feature_name: &str,
direction: Direction,
distance: u32,
window_size: usize,
params: &GlcmParams,
) -> Result<RasterBuffer> {
if window_size % 2 == 0 {
return Err(AlgorithmError::InvalidParameter {
parameter: "window_size",
message: "Window size must be odd".to_string(),
});
}
let width = src.width();
let height = src.height();
let mut dst = RasterBuffer::zeros(width, height, oxigdal_core::types::RasterDataType::Float64);
let hw = (window_size / 2) as i64;
#[cfg(feature = "parallel")]
{
let results: Result<Vec<_>> = (hw as u64..(height - hw as u64))
.into_par_iter()
.map(|y| {
let mut row_data = Vec::new();
for x in hw as u64..(width - hw as u64) {
let value = compute_local_texture_feature(
src,
x,
y,
window_size,
feature_name,
direction,
distance,
params,
)?;
row_data.push((x, value));
}
Ok((y, row_data))
})
.collect();
for (y, row_data) in results? {
for (x, value) in row_data {
dst.set_pixel(x, y, value).map_err(AlgorithmError::Core)?;
}
}
}
#[cfg(not(feature = "parallel"))]
{
for y in hw as u64..(height - hw as u64) {
for x in hw as u64..(width - hw as u64) {
let value = compute_local_texture_feature(
src,
x,
y,
window_size,
feature_name,
direction,
distance,
params,
)?;
dst.set_pixel(x, y, value).map_err(AlgorithmError::Core)?;
}
}
}
Ok(dst)
}
fn compute_local_texture_feature(
src: &RasterBuffer,
cx: u64,
cy: u64,
window_size: usize,
feature_name: &str,
direction: Direction,
distance: u32,
params: &GlcmParams,
) -> Result<f64> {
use oxigdal_core::types::RasterDataType;
let hw = (window_size / 2) as i64;
let mut window = RasterBuffer::zeros(
window_size as u64,
window_size as u64,
RasterDataType::Float64,
);
for wy in 0..window_size {
for wx in 0..window_size {
let sx = (cx as i64 + wx as i64 - hw) as u64;
let sy = (cy as i64 + wy as i64 - hw) as u64;
let val = src.get_pixel(sx, sy).map_err(AlgorithmError::Core)?;
window
.set_pixel(wx as u64, wy as u64, val)
.map_err(AlgorithmError::Core)?;
}
}
let glcm = compute_glcm(&window, direction, distance, params)?;
let features = compute_haralick_features(&glcm);
let value = match feature_name {
"contrast" => features.contrast,
"correlation" => features.correlation,
"energy" => features.energy,
"homogeneity" => features.homogeneity,
"entropy" => features.entropy,
"dissimilarity" => features.dissimilarity,
"variance" => features.variance,
"sum_average" => features.sum_average,
"sum_entropy" => features.sum_entropy,
"difference_entropy" => features.difference_entropy,
"info_measure_corr1" => features.info_measure_corr1,
"info_measure_corr2" => features.info_measure_corr2,
"max_correlation_coeff" => features.max_correlation_coeff,
"cluster_shade" => features.cluster_shade,
"cluster_prominence" => features.cluster_prominence,
_ => {
return Err(AlgorithmError::InvalidParameter {
parameter: "feature_name",
message: format!("Unknown feature: {}", feature_name),
});
}
};
Ok(value)
}
fn quantize_image(src: &RasterBuffer, gray_levels: usize) -> Result<RasterBuffer> {
use oxigdal_core::types::RasterDataType;
let width = src.width();
let height = src.height();
let mut min_val = f64::INFINITY;
let mut max_val = f64::NEG_INFINITY;
for y in 0..height {
for x in 0..width {
let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
min_val = min_val.min(val);
max_val = max_val.max(val);
}
}
let range = max_val - min_val;
if range == 0.0 {
return Ok(RasterBuffer::zeros(width, height, RasterDataType::UInt8));
}
let mut quantized = RasterBuffer::zeros(width, height, RasterDataType::UInt8);
let scale = (gray_levels - 1) as f64 / range;
for y in 0..height {
for x in 0..width {
let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
let level = ((val - min_val) * scale).round() as u8;
let clamped = level.min((gray_levels - 1) as u8);
quantized
.set_pixel(x, y, f64::from(clamped))
.map_err(AlgorithmError::Core)?;
}
}
Ok(quantized)
}
#[allow(clippy::type_complexity)]
pub fn compute_all_texture_features(
src: &RasterBuffer,
direction: Direction,
distance: u32,
window_size: usize,
params: &GlcmParams,
) -> Result<Vec<(&'static str, RasterBuffer)>> {
let features = [
"contrast",
"correlation",
"energy",
"homogeneity",
"entropy",
"dissimilarity",
"variance",
"sum_average",
"sum_entropy",
"difference_entropy",
];
let mut results = Vec::new();
for &feature in &features {
let image =
compute_texture_feature_image(src, feature, direction, distance, window_size, params)?;
results.push((feature, image));
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use oxigdal_core::types::RasterDataType;
#[test]
fn test_direction_offset() {
assert_eq!(Direction::Horizontal.offset(1), (1, 0));
assert_eq!(Direction::Vertical.offset(1), (0, 1));
assert_eq!(Direction::Diagonal45.offset(1), (1, -1));
assert_eq!(Direction::Diagonal135.offset(1), (1, 1));
}
#[test]
fn test_glcm_params_default() {
let params = GlcmParams::default();
assert_eq!(params.gray_levels, 256);
assert!(params.normalize);
assert!(params.symmetric);
assert!(params.window_size.is_none());
}
#[test]
fn test_glcm_creation() {
let glcm = Glcm::new(256, Direction::Horizontal, 1);
assert_eq!(glcm.gray_levels(), 256);
assert_eq!(glcm.direction(), Direction::Horizontal);
assert_eq!(glcm.distance(), 1);
assert!(!glcm.is_normalized());
}
#[test]
fn test_glcm_get_set() {
let mut glcm = Glcm::new(8, Direction::Horizontal, 1);
glcm.set(2, 3, 5.0);
assert_abs_diff_eq!(glcm.get(2, 3), 5.0);
}
#[test]
fn test_glcm_normalize() {
let mut glcm = Glcm::new(2, Direction::Horizontal, 1);
glcm.set(0, 0, 2.0);
glcm.set(0, 1, 3.0);
glcm.set(1, 0, 3.0);
glcm.set(1, 1, 2.0);
glcm.normalize();
assert!(glcm.is_normalized());
assert_abs_diff_eq!(glcm.get(0, 0), 0.2);
assert_abs_diff_eq!(glcm.get(0, 1), 0.3);
}
#[test]
fn test_compute_glcm() {
let mut src = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
for y in 0..10 {
for x in 0..10 {
let val = if (x + y) % 2 == 0 { 0.0 } else { 255.0 };
src.set_pixel(x, y, val)
.expect("setting pixel should succeed in test");
}
}
let params = GlcmParams {
gray_levels: 2,
normalize: true,
symmetric: true,
window_size: None,
};
let glcm = compute_glcm(&src, Direction::Horizontal, 1, ¶ms)
.expect("compute_glcm should succeed in test");
assert!(glcm.is_normalized());
assert_eq!(glcm.gray_levels(), 2);
}
#[test]
fn test_haralick_features() {
let mut glcm = Glcm::new(2, Direction::Horizontal, 1);
glcm.set(0, 0, 0.25);
glcm.set(0, 1, 0.25);
glcm.set(1, 0, 0.25);
glcm.set(1, 1, 0.25);
let features = compute_haralick_features(&glcm);
assert!(features.energy > 0.0);
assert!(features.entropy > 0.0);
assert_abs_diff_eq!(features.energy, 0.25, epsilon = 0.01);
}
#[test]
fn test_quantize_image() {
let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float64);
for y in 0..10 {
for x in 0..10 {
src.set_pixel(x, y, (x * 10 + y) as f64)
.expect("setting pixel should succeed in test");
}
}
let quantized = quantize_image(&src, 8).expect("quantize_image should succeed in test");
for y in 0..10 {
for x in 0..10 {
let val = quantized
.get_pixel(x, y)
.expect("getting pixel should succeed in test");
assert!((0.0..8.0).contains(&val));
}
}
}
#[test]
fn test_multi_direction_glcm() {
let mut src = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
for y in 0..10 {
for x in 0..10 {
src.set_pixel(x, y, ((x + y) % 4 * 64) as f64)
.expect("setting pixel should succeed in test");
}
}
let params = GlcmParams {
gray_levels: 4,
normalize: true,
symmetric: true,
window_size: None,
};
let directions = Direction::all_standard();
let glcm = compute_glcm_multi_direction(&src, &directions, 1, ¶ms)
.expect("compute_glcm_multi_direction should succeed in test");
assert_eq!(glcm.gray_levels(), 4);
}
#[test]
fn test_texture_feature_names() {
let mut glcm = Glcm::new(4, Direction::Horizontal, 1);
glcm.set(0, 0, 0.5);
glcm.set(1, 1, 0.3);
glcm.set(2, 2, 0.2);
let features = compute_haralick_features(&glcm);
assert!(features.contrast.is_finite());
assert!(features.correlation.is_finite());
assert!(features.energy.is_finite());
assert!(features.homogeneity.is_finite());
assert!(features.entropy.is_finite());
assert!(features.dissimilarity.is_finite());
assert!(features.variance.is_finite());
}
}