///|
/// Individual image channels used by masks, color grading, and analysis.
pub(all) enum ColorChannel {
RedChannel
GreenChannel
BlueChannel
AlphaChannel
LumaChannel
} derive(Eq, Debug)
///|
pub(all) struct ChannelStats {
channel : ColorChannel
minimum : Byte
maximum : Byte
mean : Int
nonzero : Int
histogram : Array[Int]
} derive(Debug)
///|
pub fn ChannelStats::channel(self : ChannelStats) -> ColorChannel {
self.channel
}
///|
pub fn ChannelStats::minimum(self : ChannelStats) -> Byte {
self.minimum
}
///|
pub fn ChannelStats::maximum(self : ChannelStats) -> Byte {
self.maximum
}
///|
pub fn ChannelStats::mean(self : ChannelStats) -> Int {
self.mean
}
///|
pub fn ChannelStats::nonzero(self : ChannelStats) -> Int {
self.nonzero
}
///|
pub fn ChannelStats::histogram(self : ChannelStats) -> ArrayView[Int] {
self.histogram[:]
}
///|
fn selected_channel_value(color : Rgba, channel : ColorChannel) -> Byte {
match channel {
RedChannel => color.red()
GreenChannel => color.green()
BlueChannel => color.blue()
AlphaChannel => color.alpha()
LumaChannel => color.luma()
}
}
///|
fn channel_color(color : Rgba, channel : ColorChannel, value : Byte) -> Rgba {
match channel {
RedChannel => rgba(value, color.green(), color.blue(), color.alpha())
GreenChannel => rgba(color.red(), value, color.blue(), color.alpha())
BlueChannel => rgba(color.red(), color.green(), value, color.alpha())
AlphaChannel => color.with_alpha(value)
LumaChannel => rgba(value, value, value, color.alpha())
}
}
///|
/// Extract a channel as a grayscale canvas.
pub fn Canvas::extract_channel(self : Canvas, channel : ColorChannel) -> Canvas {
let result = Canvas::new(
width=self.width,
height=self.height,
fill=rgba(0, 0, 0, 0),
)
for i in 0.. Canvas {
let result = self.copy()
let width = if result.width < source.width {
result.width
} else {
source.width
}
let height = if result.height < source.height {
result.height
} else {
source.height
}
for y in 0.. Canvas {
let result = self.copy()
for i in 0.. 255 { 255 } else { mapped }
result.pixels[i] = channel_color(result.pixels[i], channel, safe.to_byte())
}
result
}
///|
/// Apply a 256-entry lookup table to one channel.
pub fn Canvas::lookup_channel(
self : Canvas,
channel : ColorChannel,
table : ArrayView[Byte],
) -> Canvas {
let result = self.copy()
for i in 0..= table.length() {
table[table.length() - 1]
} else {
table[old]
}
result.pixels[i] = channel_color(result.pixels[i], channel, value)
}
result
}
///|
fn smoothstep_value(value : Int, low : Int, high : Int) -> Byte {
if high <= low {
if value < low {
0
} else {
255
}
} else if value <= low {
0
} else if value >= high {
255
} else {
let t = (value - low) * 1000 / (high - low)
let curve = t * t * (3000 - 2 * t) / 1000000
(curve * 255 / 1000).to_byte()
}
}
///|
/// Stretch a channel between black and white points.
pub fn Canvas::levels(
self : Canvas,
channel : ColorChannel,
black : Int,
white : Int,
) -> Canvas {
let result = self.copy()
for i in 0.. Canvas {
let safe_gamma = if gamma_per_mille < 1 { 1 } else { gamma_per_mille }
let table : Array[Byte] = []
for value in 0..<=256 {
let scaled = value * 1000 / 255
let exponent = if safe_gamma > 2000 { 2000 } else { safe_gamma }
let mapped = if scaled == 0 {
0
} else {
let repeated = scaled * scaled / 1000
if exponent < 1000 {
(repeated + scaled) / 2
} else {
repeated
}
}
let output = if mapped > 1000 { 255 } else { mapped * 255 / 1000 }
table.push(output.to_byte())
}
self.lookup_channel(channel, table[:])
}
///|
/// Calculate histogram statistics for one color channel.
pub fn Canvas::channel_stats(
self : Canvas,
channel : ColorChannel,
) -> ChannelStats {
let histogram = Array::make(256, 0)
let mut minimum = 255
let mut maximum = 0
let mut total = 0
let mut nonzero = 0
for color in self.pixels {
let value = selected_channel_value(color, channel).to_int()
histogram[value] += 1
if value < minimum {
minimum = value
}
if value > maximum {
maximum = value
}
total += value
if value > 0 {
nonzero += 1
}
}
ChannelStats::{
channel,
minimum: (if self.pixels.length() == 0 { 0 } else { minimum }).to_byte(),
maximum: maximum.to_byte(),
mean: if self.pixels.length() == 0 {
0
} else {
total / self.pixels.length()
},
nonzero,
histogram,
}
}
///|
/// Calculate all five standard channels in one pass.
pub fn Canvas::all_channel_stats(self : Canvas) -> Array[ChannelStats] {
[RedChannel, GreenChannel, BlueChannel, AlphaChannel, LumaChannel].map(fn(
channel,
) {
self.channel_stats(channel)
})
}
///|
/// Mix an RGB channel image with a tint while retaining the source alpha.
pub fn Canvas::colorize_channel(
self : Canvas,
channel : ColorChannel,
tint : Rgba,
strength : Int,
) -> Canvas {
let safe_strength = if strength < 0 {
0
} else if strength > 1000 {
1000
} else {
strength
}
let result = self.copy()
for i in 0.. Canvas {
let result = Canvas::new(
width=self.width,
height=self.height,
fill=rgba(0, 0, 0, 0),
)
for i in 0..= low.to_int() &&
value.to_int() <= high.to_int() {
rgba(255, 255, 255, 255)
} else {
rgba(0, 0, 0, 0)
}
}
result
}
///|
/// Combine two masks with a minimum operation.
pub fn mask_intersection(left : Canvas, right : Canvas) -> Canvas {
let width = if left.width < right.width { left.width } else { right.width }
let height = if left.height < right.height {
left.height
} else {
right.height
}
let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
for y in 0.. Canvas {
let width = if left.width < right.width { left.width } else { right.width }
let height = if left.height < right.height {
left.height
} else {
right.height
}
let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
for y in 0.. b {
a
} else {
b
},
)
}
}
result
}
///|
/// Subtract one alpha mask from another.
pub fn mask_subtract(left : Canvas, right : Canvas) -> Canvas {
let width = if left.width < right.width { left.width } else { right.width }
let height = if left.height < right.height {
left.height
} else {
right.height
}
let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
for y in 0.. b {
(a - b).to_byte()
} else {
0
},
)
}
}
result
}