Skip to content

Commit 43b23f7

Browse files
committed
feat: add quad bayer support
1 parent 8c1ddf2 commit 43b23f7

13 files changed

Lines changed: 1597 additions & 20 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "demosaic"
3-
version = "0.1.0"
4-
edition = "2021"
3+
version = "0.2.0"
4+
edition = "2026"
55
rust-version = "1.85"
66
license = "MIT OR Apache-2.0"
77
description = "Demosaicing algorithms for Bayer and X-Trans CFA sensors"

README.md

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,27 @@
44
[![docs.rs](https://docs.rs/demosaic/badge.svg)](https://docs.rs/demosaic)
55
[![CI](https://github.com/naorunaoru/demosaic/actions/workflows/ci.yml/badge.svg)](https://github.com/naorunaoru/demosaic/actions/workflows/ci.yml)
66

7-
Demosaicing algorithms for Bayer and X-Trans CFA sensors. Pure Rust, `no_std` compatible. *Not* blazingly fast, just moderately. Use at your own pace.
7+
Demosaicing algorithms for Bayer, Quad Bayer, and X-Trans CFA sensors. Pure Rust, `no_std` compatible. *Not* blazingly fast, just moderately. Use at your own pace.
88

99
## Algorithms
1010

11-
| Algorithm | Bayer | X-Trans | Quality | Speed |
12-
|-----------|:-----:|:-------:|---------|-------|
13-
| Bilinear | x | x | Low | Fastest |
14-
| MHC | x | | Medium | Fast |
15-
| PPG | x | | Medium-high | Medium |
16-
| AHD | x | | High | Slow |
17-
| Markesteijn (1-pass) | | x | Medium-high | Medium |
18-
| Markesteijn (3-pass) | | x | High | Slow |
19-
| DHT | | x | Medium-high | Medium |
11+
| Algorithm | Bayer | Quad Bayer | X-Trans | Quality | Speed |
12+
|-----------|:-----:|:----------:|:-------:|---------|-------|
13+
| Bilinear | x | x | x | Low | Fastest |
14+
| MHC | x | | | Medium | Fast |
15+
| PPG | x | | | Medium-high | Medium |
16+
| AHD | x | | | High | Slow |
17+
| Quad-PPG | | x | | Medium-high | Medium |
18+
| Markesteijn (1-pass) | | | x | Medium-high | Medium |
19+
| Markesteijn (3-pass) | | | x | High | Slow |
20+
| DHT | | | x | Medium-high | Medium |
21+
22+
Quad Bayer sensors also support two additional processing strategies:
23+
24+
| Strategy | Output resolution | Description |
25+
|----------|-------------------|-------------|
26+
| Binning + Bayer | Half (W/2 x H/2) | Average 2x2 same-color blocks, then apply any Bayer algorithm |
27+
| Remosaic + Bayer | Full (W x H) | Convert to standard Bayer, then apply any Bayer algorithm |
2028

2129
## Usage
2230

@@ -55,6 +63,9 @@ space conversion.
5563
// Bayer (2x2)
5664
let cfa = CfaPattern::bayer_rggb(); // also bayer_bggr, bayer_grbg, bayer_gbrg
5765

66+
// Quad Bayer (4x4)
67+
let cfa = CfaPattern::quad_bayer_rggb(); // also quad_bayer_bggr, quad_bayer_grbg, quad_bayer_gbrg
68+
5869
// X-Trans (6x6)
5970
let cfa = CfaPattern::xtrans_default(); // standard Fujifilm ILC pattern
6071
let cfa = CfaPattern::xtrans(pattern); // custom 6x6 pattern
@@ -63,6 +74,60 @@ let cfa = CfaPattern::xtrans(pattern); // custom 6x6 pattern
6374
let cfa = CfaPattern::bayer_rggb().shift(dy, dx);
6475
```
6576

77+
### Quad Bayer
78+
79+
Quad Bayer sensors (Sony IMX586, Samsung ISOCELL GN1, etc.) use a 4x4 repeating pattern where each standard Bayer pixel is replaced by a 2x2 same-color block:
80+
81+
```
82+
R R G G
83+
R R G G
84+
G G B B
85+
G G B B
86+
```
87+
88+
There are four ways to process Quad Bayer data, depending on your quality/resolution needs:
89+
90+
**Direct demosaic** (full resolution):
91+
```rust
92+
use demosaic::{demosaic, Algorithm, CfaPattern};
93+
94+
let cfa = CfaPattern::quad_bayer_rggb();
95+
let mut output = vec![0.0f32; 3 * width * height];
96+
97+
// Bilinear (fast, low quality)
98+
demosaic(&input, width, height, &cfa, Algorithm::Bilinear, &mut output).unwrap();
99+
100+
// Quad-PPG (gradient-based, higher quality)
101+
demosaic(&input, width, height, &cfa, Algorithm::QuadPpg, &mut output).unwrap();
102+
```
103+
104+
**Binning** (half resolution, best SNR):
105+
```rust
106+
use demosaic::{demosaic_quad_binned, BayerAlgorithm, CfaPattern};
107+
108+
let cfa = CfaPattern::quad_bayer_rggb();
109+
let (ow, oh) = (width / 2, height / 2);
110+
let mut output = vec![0.0f32; 3 * ow * oh];
111+
112+
demosaic_quad_binned(&input, width, height, &cfa, BayerAlgorithm::Ppg, &mut output).unwrap();
113+
```
114+
115+
**Remosaic** (full resolution, leverages all existing Bayer algorithms):
116+
```rust
117+
use demosaic::{demosaic, remosaic, Algorithm, CfaPattern};
118+
119+
let cfa = CfaPattern::quad_bayer_rggb();
120+
let bayer_cfa = cfa.quad_to_bayer().unwrap();
121+
122+
// Step 1: convert Quad Bayer to standard Bayer
123+
let mut bayer_data = vec![0.0f32; width * height];
124+
remosaic(&input, width, height, &cfa, &mut bayer_data).unwrap();
125+
126+
// Step 2: demosaic with any Bayer algorithm
127+
let mut output = vec![0.0f32; 3 * width * height];
128+
demosaic(&bayer_data, width, height, &bayer_cfa, Algorithm::Ahd, &mut output).unwrap();
129+
```
130+
66131
## Features
67132

68133
| Feature | Default | Description |

benches/demosaic.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use criterion::{criterion_group, criterion_main, Criterion};
2-
use demosaic::{demosaic, Algorithm, CfaPattern, Channel};
2+
use demosaic::{demosaic, demosaic_quad_binned, Algorithm, BayerAlgorithm, CfaPattern, Channel};
33

44
fn synthetic_input(width: usize, height: usize, cfa: &CfaPattern) -> Vec<f32> {
55
let mut input = vec![0.0f32; width * height];
@@ -46,5 +46,32 @@ fn bench_xtrans(c: &mut Criterion) {
4646
group.finish();
4747
}
4848

49-
criterion_group!(benches, bench_bayer, bench_xtrans);
49+
fn bench_quad_bayer(c: &mut Criterion) {
50+
let cfa = CfaPattern::quad_bayer_rggb();
51+
let (w, h) = (4032, 3024);
52+
let input = synthetic_input(w, h, &cfa);
53+
let mut output = vec![0.0f32; 3 * w * h];
54+
55+
let mut group = c.benchmark_group("quad_bayer_4032x3024");
56+
for algo in [Algorithm::Bilinear, Algorithm::QuadPpg] {
57+
group.bench_function(format!("{algo}"), |b| {
58+
b.iter(|| demosaic(&input, w, h, &cfa, algo, &mut output).unwrap());
59+
});
60+
}
61+
62+
// Binned demosaic (output is half resolution)
63+
let ow = w / 2;
64+
let oh = h / 2;
65+
let mut binned_output = vec![0.0f32; 3 * ow * oh];
66+
group.bench_function("Binned+PPG", |b| {
67+
b.iter(|| {
68+
demosaic_quad_binned(&input, w, h, &cfa, BayerAlgorithm::Ppg, &mut binned_output)
69+
.unwrap()
70+
});
71+
});
72+
73+
group.finish();
74+
}
75+
76+
criterion_group!(benches, bench_bayer, bench_xtrans, bench_quad_bayer);
5077
criterion_main!(benches);

src/cfa.rs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,70 @@ impl CfaPattern {
8383
Self { pattern: XTRANS_DEFAULT, width: 6, height: 6 }
8484
}
8585

86+
/// Create a Quad Bayer RGGB pattern (4x4 repeating tile).
87+
///
88+
/// ```text
89+
/// R R G G
90+
/// R R G G
91+
/// G G B B
92+
/// G G B B
93+
/// ```
94+
pub fn quad_bayer_rggb() -> Self {
95+
Self::quad_bayer([Red, Green, Green, Blue])
96+
}
97+
98+
/// Create a Quad Bayer BGGR pattern (4x4 repeating tile).
99+
///
100+
/// ```text
101+
/// B B G G
102+
/// B B G G
103+
/// G G R R
104+
/// G G R R
105+
/// ```
106+
pub fn quad_bayer_bggr() -> Self {
107+
Self::quad_bayer([Blue, Green, Green, Red])
108+
}
109+
110+
/// Create a Quad Bayer GRBG pattern (4x4 repeating tile).
111+
///
112+
/// ```text
113+
/// G G R R
114+
/// G G R R
115+
/// B B G G
116+
/// B B G G
117+
/// ```
118+
pub fn quad_bayer_grbg() -> Self {
119+
Self::quad_bayer([Green, Red, Blue, Green])
120+
}
121+
122+
/// Create a Quad Bayer GBRG pattern (4x4 repeating tile).
123+
///
124+
/// ```text
125+
/// G G B B
126+
/// G G B B
127+
/// R R G G
128+
/// R R G G
129+
/// ```
130+
pub fn quad_bayer_gbrg() -> Self {
131+
Self::quad_bayer([Green, Blue, Red, Green])
132+
}
133+
134+
fn quad_bayer(base: [Channel; 4]) -> Self {
135+
let mut pattern = [Green; 36];
136+
for r in 0..2 {
137+
for c in 0..2 {
138+
let ch = base[r * 2 + c];
139+
let r0 = 2 * r;
140+
let c0 = 2 * c;
141+
pattern[r0 * 4 + c0] = ch;
142+
pattern[r0 * 4 + c0 + 1] = ch;
143+
pattern[(r0 + 1) * 4 + c0] = ch;
144+
pattern[(r0 + 1) * 4 + c0 + 1] = ch;
145+
}
146+
}
147+
Self { pattern, width: 4, height: 4 }
148+
}
149+
86150
/// Return a shifted view of this pattern.
87151
///
88152
/// The shift (dy, dx) follows the additive convention:
@@ -119,10 +183,31 @@ impl CfaPattern {
119183
self.width == 2
120184
}
121185

186+
/// Returns `true` if this is a 4x4 Quad Bayer pattern.
187+
pub fn is_quad_bayer(&self) -> bool {
188+
self.width == 4 && self.height == 4
189+
}
190+
122191
/// Returns `true` if this is a 6x6 X-Trans pattern.
123192
pub fn is_xtrans(&self) -> bool {
124193
self.width == 6
125194
}
195+
196+
/// For a Quad Bayer pattern, derive the corresponding 2x2 Bayer pattern
197+
/// that results from 2x2 binning.
198+
///
199+
/// Returns `None` if this is not a Quad Bayer pattern.
200+
pub fn quad_to_bayer(&self) -> Option<CfaPattern> {
201+
if !self.is_quad_bayer() {
202+
return None;
203+
}
204+
Some(Self::bayer([
205+
self.color_at(0, 0),
206+
self.color_at(0, 2),
207+
self.color_at(2, 0),
208+
self.color_at(2, 2),
209+
]))
210+
}
126211
}
127212

128213
impl fmt::Display for CfaPattern {
@@ -132,6 +217,10 @@ impl fmt::Display for CfaPattern {
132217
write!(f, "{}", self.pattern[i])?;
133218
}
134219
Ok(())
220+
} else if self.is_quad_bayer() {
221+
write!(f, "Quad Bayer ")?;
222+
// Show the 2x2 base pattern (top-left of each quadrant).
223+
write!(f, "{}{}{}{}", self.pattern[0], self.pattern[2], self.pattern[8], self.pattern[10])
135224
} else {
136225
write!(f, "X-Trans 6x6")
137226
}
@@ -177,6 +266,117 @@ mod tests {
177266
assert_eq!(shifted.color_at(2, 3), cfa.color_at(3, 4));
178267
}
179268

269+
#[test]
270+
fn quad_bayer_rggb_pattern() {
271+
let cfa = CfaPattern::quad_bayer_rggb();
272+
assert_eq!(cfa.width(), 4);
273+
assert_eq!(cfa.height(), 4);
274+
assert!(cfa.is_quad_bayer());
275+
assert!(!cfa.is_bayer());
276+
assert!(!cfa.is_xtrans());
277+
278+
// Row 0: R R G G
279+
assert_eq!(cfa.color_at(0, 0), Red);
280+
assert_eq!(cfa.color_at(0, 1), Red);
281+
assert_eq!(cfa.color_at(0, 2), Green);
282+
assert_eq!(cfa.color_at(0, 3), Green);
283+
// Row 1: R R G G
284+
assert_eq!(cfa.color_at(1, 0), Red);
285+
assert_eq!(cfa.color_at(1, 1), Red);
286+
assert_eq!(cfa.color_at(1, 2), Green);
287+
assert_eq!(cfa.color_at(1, 3), Green);
288+
// Row 2: G G B B
289+
assert_eq!(cfa.color_at(2, 0), Green);
290+
assert_eq!(cfa.color_at(2, 1), Green);
291+
assert_eq!(cfa.color_at(2, 2), Blue);
292+
assert_eq!(cfa.color_at(2, 3), Blue);
293+
// Row 3: G G B B
294+
assert_eq!(cfa.color_at(3, 0), Green);
295+
assert_eq!(cfa.color_at(3, 1), Green);
296+
assert_eq!(cfa.color_at(3, 2), Blue);
297+
assert_eq!(cfa.color_at(3, 3), Blue);
298+
299+
// Tiling
300+
assert_eq!(cfa.color_at(4, 0), Red);
301+
assert_eq!(cfa.color_at(0, 4), Red);
302+
assert_eq!(cfa.color_at(6, 6), Blue);
303+
}
304+
305+
#[test]
306+
fn quad_bayer_all_variants() {
307+
let rggb = CfaPattern::quad_bayer_rggb();
308+
let bggr = CfaPattern::quad_bayer_bggr();
309+
let grbg = CfaPattern::quad_bayer_grbg();
310+
let gbrg = CfaPattern::quad_bayer_gbrg();
311+
312+
// Top-left 2x2 block color
313+
assert_eq!(rggb.color_at(0, 0), Red);
314+
assert_eq!(bggr.color_at(0, 0), Blue);
315+
assert_eq!(grbg.color_at(0, 0), Green);
316+
assert_eq!(gbrg.color_at(0, 0), Green);
317+
318+
// Bottom-right 2x2 block color
319+
assert_eq!(rggb.color_at(2, 2), Blue);
320+
assert_eq!(bggr.color_at(2, 2), Red);
321+
assert_eq!(grbg.color_at(2, 2), Green);
322+
assert_eq!(gbrg.color_at(2, 2), Green);
323+
324+
// All should be quad Bayer
325+
for cfa in &[&rggb, &bggr, &grbg, &gbrg] {
326+
assert!(cfa.is_quad_bayer());
327+
}
328+
}
329+
330+
#[test]
331+
fn quad_bayer_shift() {
332+
let cfa = CfaPattern::quad_bayer_rggb();
333+
let shifted = cfa.shift(1, 1);
334+
// shifted.color_at(0,0) == cfa.color_at(1,1)
335+
assert_eq!(shifted.color_at(0, 0), cfa.color_at(1, 1));
336+
assert_eq!(shifted.color_at(2, 3), cfa.color_at(3, 0));
337+
}
338+
339+
#[test]
340+
fn quad_to_bayer_mapping() {
341+
assert_eq!(
342+
CfaPattern::quad_bayer_rggb().quad_to_bayer().unwrap(),
343+
CfaPattern::bayer_rggb()
344+
);
345+
assert_eq!(
346+
CfaPattern::quad_bayer_bggr().quad_to_bayer().unwrap(),
347+
CfaPattern::bayer_bggr()
348+
);
349+
assert_eq!(
350+
CfaPattern::quad_bayer_grbg().quad_to_bayer().unwrap(),
351+
CfaPattern::bayer_grbg()
352+
);
353+
assert_eq!(
354+
CfaPattern::quad_bayer_gbrg().quad_to_bayer().unwrap(),
355+
CfaPattern::bayer_gbrg()
356+
);
357+
}
358+
359+
#[test]
360+
fn quad_to_bayer_returns_none_for_non_quad() {
361+
assert!(CfaPattern::bayer_rggb().quad_to_bayer().is_none());
362+
assert!(CfaPattern::xtrans_default().quad_to_bayer().is_none());
363+
}
364+
365+
#[test]
366+
fn quad_bayer_color_count_per_tile() {
367+
// Each 4x4 tile should have 4R, 8G, 4B
368+
let cfa = CfaPattern::quad_bayer_rggb();
369+
let mut counts = [0u32; 3];
370+
for r in 0..4 {
371+
for c in 0..4 {
372+
counts[cfa.color_at(r, c) as usize] += 1;
373+
}
374+
}
375+
assert_eq!(counts[Red as usize], 4);
376+
assert_eq!(counts[Green as usize], 8);
377+
assert_eq!(counts[Blue as usize], 4);
378+
}
379+
180380
#[test]
181381
fn xtrans_every_3x3_has_all_colors() {
182382
let cfa = CfaPattern::xtrans_default();

0 commit comments

Comments
 (0)