Skip to content

Commit 1c7738e

Browse files
feat: add Peak Signal-to-Noise Ratio (PSNR) to compression (#1028)
1 parent 88a00a6 commit 1c7738e

File tree

3 files changed

+96
-0
lines changed

3 files changed

+96
-0
lines changed

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
* [Huffman Encoding](https://github.com/TheAlgorithms/Rust/blob/master/src/compression/huffman_encoding.rs)
7070
* [LZ77](https://github.com/TheAlgorithms/Rust/blob/master/src/compression/lz77.rs)
7171
* [Move to Front](https://github.com/TheAlgorithms/Rust/blob/master/src/compression/move_to_front.rs)
72+
* [Peak Signal-to-Noise Ratio](https://github.com/TheAlgorithms/Rust/blob/master/src/compression/peak_signal_to_noise_ratio.rs)
7273
* [Run Length Encoding](https://github.com/TheAlgorithms/Rust/blob/master/src/compression/run_length_encoding.rs)
7374
* Conversions
7475
* [Binary to Decimal](https://github.com/TheAlgorithms/Rust/blob/master/src/conversions/binary_to_decimal.rs)

src/compression/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ mod burrows_wheeler_transform;
22
mod huffman_encoding;
33
mod lz77;
44
mod move_to_front;
5+
mod peak_signal_to_noise_ratio;
56
mod run_length_encoding;
67

78
pub use self::burrows_wheeler_transform::{all_rotations, bwt_transform, reverse_bwt, BwtResult};
89
pub use self::huffman_encoding::{huffman_decode, huffman_encode};
910
pub use self::lz77::{LZ77Compressor, Token};
1011
pub use self::move_to_front::{move_to_front_decode, move_to_front_encode};
12+
pub use self::peak_signal_to_noise_ratio::peak_signal_to_noise_ratio;
1113
pub use self::run_length_encoding::{run_length_decode, run_length_encode};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//! # Peak Signal-to-Noise Ratio (PSNR)
2+
//!
3+
//! Measures the quality of a reconstructed or compressed image relative to the original.
4+
//! A higher PSNR generally indicates better quality.
5+
//!
6+
//! Reference: <https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio>
7+
8+
const PIXEL_MAX: f64 = 255.0;
9+
10+
/// Computes the PSNR in decibels (dB) between an original and a compressed image.
11+
///
12+
/// # Arguments
13+
/// * `original` - Pixel values of the original image (u8 slice, any channel layout)
14+
/// * `compressed` - Pixel values of the compressed/reconstructed image (same length)
15+
///
16+
/// # Returns
17+
/// * `f64::INFINITY` when the images are identical (MSE = 0)
18+
/// * Otherwise the PSNR value in dB
19+
///
20+
/// # Panics
21+
/// Panics if `original` and `compressed` have different lengths.
22+
pub fn peak_signal_to_noise_ratio(original: &[u8], compressed: &[u8]) -> f64 {
23+
assert_eq!(
24+
original.len(),
25+
compressed.len(),
26+
"original and compressed images must have the same number of pixels"
27+
);
28+
29+
let mse: f64 = original
30+
.iter()
31+
.zip(compressed.iter())
32+
.map(|(&o, &c)| {
33+
let diff = o as f64 - c as f64;
34+
diff * diff
35+
})
36+
.sum::<f64>()
37+
/ original.len() as f64;
38+
39+
if mse == 0.0 {
40+
return f64::INFINITY;
41+
}
42+
43+
20.0 * (PIXEL_MAX / mse.sqrt()).log10()
44+
}
45+
46+
#[cfg(test)]
47+
mod tests {
48+
use super::*;
49+
50+
#[test]
51+
fn identical_images_returns_infinity() {
52+
let img = vec![100u8, 150, 200, 50, 75, 25];
53+
assert_eq!(peak_signal_to_noise_ratio(&img, &img), f64::INFINITY);
54+
}
55+
56+
#[test]
57+
fn single_pixel_off_by_one() {
58+
// original: [0], compressed: [1] → MSE = 1.0 → PSNR = 20·log10(255) ≈ 48.13 dB
59+
let original = vec![0u8];
60+
let compressed = vec![1u8];
61+
let psnr = peak_signal_to_noise_ratio(&original, &compressed);
62+
let expected = 20.0 * 255.0_f64.log10();
63+
assert!((psnr - expected).abs() < 1e-6, "got {psnr}");
64+
}
65+
66+
#[test]
67+
fn uniform_noise() {
68+
// original all-zero, compressed all-10 → MSE = 100 → PSNR = 20·log10(255/10) ≈ 28.13 dB
69+
let original = vec![0u8; 16];
70+
let compressed = vec![10u8; 16];
71+
let psnr = peak_signal_to_noise_ratio(&original, &compressed);
72+
let expected = 20.0 * (PIXEL_MAX / 10.0).log10();
73+
assert!((psnr - expected).abs() < 1e-6, "got {psnr}");
74+
}
75+
76+
#[test]
77+
fn known_psnr_value() {
78+
// 4 pixels: diffs = [15, 15, 15, 15] → MSE = 225 → PSNR ≈ 24.61 dB
79+
let original = vec![0u8, 0, 0, 0];
80+
let compressed = vec![15u8, 15, 15, 15];
81+
let psnr = peak_signal_to_noise_ratio(&original, &compressed);
82+
let expected = 20.0 * (PIXEL_MAX / 15.0).log10();
83+
assert!((psnr - expected).abs() < 1e-6, "got {psnr}");
84+
}
85+
86+
#[test]
87+
#[should_panic(expected = "same number of pixels")]
88+
fn mismatched_lengths_panics() {
89+
let original = vec![0u8; 4];
90+
let compressed = vec![0u8; 8];
91+
peak_signal_to_noise_ratio(&original, &compressed);
92+
}
93+
}

0 commit comments

Comments
 (0)