Skip to content

Commit 2e2fff2

Browse files
authored
Fix alpha bleed algorithm (#93)
This PR makes a couple of fixes / adjustments to the alpha bleed algorithm. 1. It only bleeds one pixel now. See this [README](https://github.com/EgoMoose/alpha-bleed/blob/main/README.md) for why. 2. Fully transparent pixels that are not bled into are converted to pure black for better encoding. 3. Fixes a mistake in the algorithm where pixels could sample neighbors from the same queue cycle. The first pass iterates over pixels left-to-right, top-to-bottom so this effect naturally impacts pixels that come further along in the iteration (right and bottom). However, due to the order of the neighbor offsets this effect is somewhat muddied. Regardless, pixels should not be sampling their bled neighbors from the same cycle. Given an input image: ![tree](https://github.com/user-attachments/assets/05152ece-c1f6-47a8-925b-25c019adba6a) The results go from looking like this (everything fully opaque): ![image](https://github.com/user-attachments/assets/f6f7df8e-c2a5-45b3-8895-51f70c5e2142) To this: ![image](https://github.com/user-attachments/assets/efd832ac-c90b-4760-a795-d73776455ba1)
1 parent 2482ba7 commit 2e2fff2

File tree

2 files changed

+48
-30
lines changed

2 files changed

+48
-30
lines changed

src/asset.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ impl Asset {
132132

133133
if let AssetKind::Decal(_) = &kind {
134134
let mut image: DynamicImage = image::load_from_memory(&data)?;
135-
alpha_bleed(&mut image);
135+
alpha_bleed(&mut image, 1);
136136

137137
let format = ImageFormat::from_extension(ext)
138138
.context("Failed to get image format from extension")?;

src/util/alpha_bleed.rs

+47-29
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::collections::VecDeque;
77
use bit_vec::BitVec;
88
use image::{DynamicImage, GenericImage, GenericImageView, Rgba};
99

10-
pub(crate) fn alpha_bleed(img: &mut DynamicImage) {
10+
pub(crate) fn alpha_bleed(img: &mut DynamicImage, thickness: usize) {
1111
let (w, h) = img.dimensions();
1212

1313
// Tells whether a given position has been touched by the bleeding algorithm
@@ -68,41 +68,59 @@ pub(crate) fn alpha_bleed(img: &mut DynamicImage) {
6868
visited.set(x, y);
6969
to_visit.push_back((x, y));
7070
}
71+
72+
img.put_pixel(x, y, Rgba([0, 0, 0, 0]));
7173
}
7274
}
7375

74-
while let Some((x, y)) = to_visit.pop_front() {
75-
// Compute the average color from all surrounding pixels that are
76-
// eligible to be sampled from.
77-
let mut new_color = (0, 0, 0);
78-
let mut contributing = 0;
79-
80-
for (x_source, y_source) in adjacent_positions(x, y) {
81-
if can_be_sampled.get(x_source, y_source) {
82-
let source = img.get_pixel(x_source, y_source);
76+
for _ in 0..thickness {
77+
let queue_length = to_visit.len();
78+
if queue_length == 0 {
79+
break;
80+
}
8381

84-
contributing += 1;
85-
new_color.0 += source[0] as u16;
86-
new_color.1 += source[1] as u16;
87-
new_color.2 += source[2] as u16;
88-
} else if !visited.get(x_source, y_source) {
89-
visited.set(x_source, y_source);
90-
to_visit.push_back((x_source, y_source));
82+
let mut mutated_coords: Vec<(u32, u32)> = vec![(0, 0); queue_length];
83+
for _ in 0..queue_length {
84+
if let Some((x, y)) = to_visit.pop_front() {
85+
// Compute the average color from all surrounding pixels that are
86+
// eligible to be sampled from.
87+
let mut new_color = (0, 0, 0);
88+
let mut contributing = 0;
89+
90+
for (x_source, y_source) in adjacent_positions(x, y) {
91+
if can_be_sampled.get(x_source, y_source) {
92+
let source = img.get_pixel(x_source, y_source);
93+
94+
contributing += 1;
95+
new_color.0 += source[0] as u16;
96+
new_color.1 += source[1] as u16;
97+
new_color.2 += source[2] as u16;
98+
} else if !visited.get(x_source, y_source) {
99+
visited.set(x_source, y_source);
100+
to_visit.push_back((x_source, y_source));
101+
}
102+
}
103+
104+
let denominator = u16::max(1, contributing);
105+
let pixel = Rgba([
106+
(new_color.0 / denominator) as u8,
107+
(new_color.1 / denominator) as u8,
108+
(new_color.2 / denominator) as u8,
109+
0,
110+
]);
111+
112+
img.put_pixel(x, y, pixel);
113+
mutated_coords.push((x, y));
91114
}
92115
}
93116

94-
let pixel = Rgba([
95-
(new_color.0 / contributing) as u8,
96-
(new_color.1 / contributing) as u8,
97-
(new_color.2 / contributing) as u8,
98-
0,
99-
]);
100-
101-
img.put_pixel(x, y, pixel);
102-
103-
// Now that we've bled this pixel, it's eligible to be sampled from for
104-
// future iterations.
105-
can_be_sampled.set(x, y);
117+
for _ in 0..queue_length {
118+
if let Some((x, y)) = mutated_coords.pop() {
119+
// Now that we've bled this pixel, it's eligible to be sampled from for
120+
// future iterations.
121+
can_be_sampled.set(x, y);
122+
}
123+
}
106124
}
107125
}
108126

0 commit comments

Comments
 (0)