Last active
January 8, 2018 11:12
-
-
Save little-bobby-tables/c8abd29b15667425fb32234915b5be5b to your computer and use it in GitHub Desktop.
Based on Parametric fuzzy sets for automatic color naming
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* DOES NOT RETURN CORRECT RESULTS, | |
* current implementation violates the unity-sum constraint | |
* (sum of all memberships sometimes exceeds 1) and skips | |
* achromatic category memberships. */ | |
extern crate nalgebra; | |
use nalgebra::{U1, U3, Matrix, MatrixArray}; | |
use std::ops::{Mul, Div}; | |
type Matrix3x3 = Matrix<f32, U3, U3, MatrixArray<f32, U3, U3>>; | |
type RowMatrix3 = Matrix<f32, U1, U3, MatrixArray<f32, U1, U3>>; | |
type ColumnMatrix3 = Matrix<f32, U3, U1, MatrixArray<f32, U3, U1>>; | |
type Point3d = ColumnMatrix3; // (a, b, 1)^T | |
type Point2d = (f32, f32); // (a, b) | |
type Translation2d = (f32, f32); // (t_a, t_b) | |
type Rotation2d = (Rotation, Rotation); // (alpha_a, alpha_b) | |
type Slope2d = (Slope, Slope); // (beta_a, beta_b) | |
type Semiaxes = (f32, f32); | |
type Rotation = f32; // alpha | |
type Slope = f32; // beta | |
macro_rules! scalar { | |
($one_by_one_matrix: expr) => { $one_by_one_matrix.as_slice()[0] } | |
} | |
fn sigmoid_2d(p: Point3d, t: Translation2d, a: Rotation, b: Slope, axis_vector: RowMatrix3) -> f32 { | |
let transform = axis_vector | |
.map(|e| e * -b) | |
.mul(rotation_matrix(a)) | |
.mul(translation_matrix(t)); | |
(1.0 + scalar!(transform.mul(p)).exp()).recip() | |
} | |
fn double_sigmoid((p_a, p_b): Point2d, t: Translation2d, (a_a, a_b): Rotation2d, (b_a, b_b): Slope2d) -> f32 { | |
let point_3d = ColumnMatrix3::new(p_a, p_b, 1.0); | |
let axis_a = RowMatrix3::new(1.0, 0.0, 0.0); | |
let axis_b = RowMatrix3::new(0.0, 1.0, 0.0); | |
sigmoid_2d(point_3d, t, a_b, b_b, axis_a) * sigmoid_2d(point_3d, t, a_a, b_a, axis_b) | |
} | |
fn elliptic_sigmoid((p_a, p_b): Point2d, t: Translation2d, (e_a, e_b): Semiaxes, phi: Rotation, b_e: Slope) -> f32 { | |
let point_3d = ColumnMatrix3::new(p_a, p_b, 1.0); | |
let axis_a = RowMatrix3::new(1.0, 0.0, 0.0); | |
let axis_b = RowMatrix3::new(0.0, 1.0, 0.0); | |
let curve = { | |
let transform_matrix = rotation_matrix(phi).mul(translation_matrix(t)); | |
(scalar!(axis_a.mul(transform_matrix).mul(point_3d)).div(e_a).powi(2) + | |
scalar!(axis_b.mul(transform_matrix).mul(point_3d)).div(e_b).powi(2) - 1.0) | |
}; | |
(1.0 + (-b_e * curve).exp()).recip() | |
} | |
fn triple_sigmoid_with_elliptical_center(p: Point2d, t: Translation2d, /* DS */ a2d: Rotation2d, b2d: Slope2d, /* ES */ e: Semiaxes, phi: Rotation, b_e: Slope) -> f32 { | |
double_sigmoid(p, t, a2d, b2d) * elliptic_sigmoid(p, t, e, phi, b_e) | |
} | |
type ColorSample = (f32, f32, f32); // (l, a, b) | |
type Category = usize; | |
#[derive(Debug)] | |
struct PlaneParameters { | |
t: Translation2d, | |
/* ES params */ | |
e: Semiaxes, | |
phi: Rotation, | |
b_e: Slope, | |
/* chromatic categories: red, orange, brown, yellow, green, blue, purple, pink */ | |
categories: [Option<(Rotation2d, Slope2d)>; 8] | |
} | |
const PLANES: [PlaneParameters; 6] = [ | |
PlaneParameters { | |
t: (0.42, 0.25), | |
e: (5.89, 7.47), | |
phi: 2.32, | |
b_e: 9.84, | |
categories: [ | |
Some(((-2.24, -56.55), (0.90, 1.72))), | |
None, | |
Some(((33.45, 14.56), (1.72, 0.84))), | |
None, | |
Some(((104.56, 134.59), (0.84, 1.95))), | |
Some(((224.59, -147.15), (1.95, 1.01))), | |
Some(((-57.15, -92.24), (1.01, 0.90))), | |
None | |
] | |
}, | |
PlaneParameters { | |
t: (0.23, 0.66), | |
e: (6.46, 7.87), | |
phi: 17.59, | |
b_e: 6.03, | |
categories: [ | |
Some(((2.21, -48.81), (0.52, 5.00))), | |
None, | |
Some(((41.19, 6.87), (5.00, 0.69))), | |
None, | |
Some(((96.87, 120.46), (0.69, 0.96))), | |
Some(((210.46, -148.48), (0.96, 0.92))), | |
Some(((-58.48, -105.72), (0.92, 1.10))), | |
Some(((-15.72, -87.79), (1.10, 0.52))) | |
] | |
}, | |
PlaneParameters { | |
t: (-0.12, 0.52), | |
e: (5.38, 6.98), | |
phi: 19.58, | |
b_e: 6.81, | |
categories: [ | |
Some(((13.57, -45.54), (1.00, 0.57))), | |
Some(((44.45, -28.76), (0.57, 0.52))), | |
Some(((61.24, 6.65), (0.52, 0.84))), | |
None, | |
Some(((96.65, 109.38), (0.84, 0.60))), | |
Some(((199.38, -148.24), (0.60, 0.80))), | |
Some(((-58.24, -112.63), (0.80, 0.62))), | |
Some(((-22.63, -76.43), (0.62, 1.00))) | |
] | |
}, | |
PlaneParameters { | |
t: (-0.47, 1.02), | |
e: (5.99, 7.51), | |
phi: 23.92, | |
b_e: 7.76, | |
categories: [ | |
Some(((26.70, -56.88), (0.91, 0.76))), | |
Some(((33.12, -9.90), (0.76, 0.48))), | |
None, | |
Some(((80.10, 5.63), (0.48, 0.73))), | |
Some(((95.63, 108.14), (0.73, 0.64))), | |
Some(((198.14, -148.59), (0.64, 0.76))), | |
Some(((-58.59, -123.68), (0.76, 5.00))), | |
Some(((-33.68, -63.30), (5.00, 0.91))) | |
] | |
}, | |
PlaneParameters { | |
t: (-0.57, 1.16), | |
e: (5.37, 6.90), | |
phi: 24.75, | |
b_e: 100.00, | |
categories: [ | |
None, | |
Some(((25.75, -15.85), (2.00, 0.84))), | |
None, | |
Some(((74.15, 12.27), (0.84, 0.86))), | |
Some(((102.27, 98.57), (0.86, 0.74))), | |
Some(((188.57, -150.83), (0.74, 0.47))), | |
Some(((-60.83, -122.55), (0.47, 1.74))), | |
Some(((-32.55, -64.25), (1.74, 2.00))) | |
] | |
}, | |
PlaneParameters { | |
t: (-1.26, 1.81), | |
e: (6.04, 7.39), | |
phi: -1.19, | |
b_e: 100.00, | |
categories: [ | |
None, | |
Some(((25.74, -17.56), (1.03, 0.79))), | |
None, | |
Some(((72.44, 16.24), (0.79, 0.96))), | |
Some(((106.24, 100.05), (0.96, 0.90))), | |
Some(((190.05, -149.43), (0.90, 0.60))), | |
Some(((-59.43, -122.37), (0.60, 1.93))), | |
Some(((-32.37, -64.26), (1.93, 1.03))) | |
] | |
} | |
]; | |
pub fn membership(k: Category, (l, a, b): ColorSample) -> f32 { | |
let plane = { | |
if l <= 31.0 { 0 } | |
else if l <= 41.0 { 1 } | |
else if l <= 51.0 { 2 } | |
else if l <= 66.0 { 3 } | |
else if l <= 76.0 { 4 } | |
else { 5 } | |
}; | |
let PlaneParameters { t, e, phi, b_e, categories } = PLANES[plane]; | |
match categories[k] { | |
Some((a2d, b2d)) => | |
triple_sigmoid_with_elliptical_center((a, b), t, a2d, b2d, e, phi, b_e), | |
None => | |
0.0 | |
} | |
} | |
fn translation_matrix((t_a, t_b): Translation2d) -> Matrix3x3 { | |
Matrix3x3::new(1.0, 0.0, -t_a, | |
0.0, 1.0, -t_b, | |
0.0, 0.0, 1.0) | |
} | |
fn rotation_matrix(a: Rotation) -> Matrix3x3 { | |
Matrix3x3::new( a.cos(), a.sin(), 0.0, | |
-a.sin(), a.cos(), 0.0, | |
0.0, 0.0, 1.0) | |
} | |
#[cfg(test)] | |
mod tests { | |
use membership; | |
#[test] | |
fn it_works() { | |
for k in 0..7 { | |
println!("k = {}, m = {}", k, membership(k, (84.22, 15.75, 13.75))); | |
} | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Same algorithm, implemented in Ruby, | |
# which has the same problems as the Rust version. | |
# Example: | |
# Here are the results for #00dd6d, | |
# a light, highly saturated green: | |
# | |
# orange: 0.9999998431800354, | |
# blue: 0.999999999941878, | |
# ... | |
# sum = 1.9999998431219137 | |
# | |
# Bluish orange is not a color we could perceive, now is it? | |
require 'matrix' | |
# Based on https://www.osapublishing.org/josaa/abstract.cfm?uri=josaa-25-10-2582 | |
# (pdf link is in the right corner) | |
AXIS_A = Matrix.row_vector [1.0, 0.0, 0.0] | |
AXIS_B = Matrix.row_vector [0.0, 1.0, 0.0] | |
Tup = Struct.new(:a, :b) | |
def scalar(single_el_matrix) | |
single_el_matrix[0, 0] | |
end | |
# Eq. 7 | |
def S(p, t_tup, α, β, axis) | |
transform = axis.map { |e| e * -β } * translation_m(t_tup) * rotation_m(α) | |
1.0 / (1.0 + Math.exp(scalar(transform * p))) | |
end | |
# Eq. 8 | |
def translation_m(t_tup) | |
Matrix[ [1.0, 0.0, -t_tup.a], | |
[0.0, 1.0, -t_tup.b], | |
[0.0, 0.0, 1.0] ] | |
end | |
# Eq. 8 | |
def rotation_m(α) | |
Matrix[ [Math.cos(α), Math.sin(α), 0.0], | |
[-Math.sin(α), Math.cos(α), 0.0], | |
[0.0, 0.0, 1.0] ] | |
end | |
# Eq. 9 | |
def DS(p, t_tup, α_tup, β_tup) | |
S(p, t_tup, α_tup.b, β_tup.b, AXIS_A) * S(p, t_tup, α_tup.a, β_tup.a, AXIS_B) | |
end | |
# Eq. 10 | |
def ES(p, t_tup, e_tup, ϕ, βe) | |
curve = ( | |
(scalar(AXIS_A * rotation_m(ϕ) * translation_m(t_tup) * p) / e_tup.a) ** 2 + | |
(scalar(AXIS_B * rotation_m(ϕ) * translation_m(t_tup) * p) / e_tup.b) ** 2 - 1.0) | |
1.0 / (1.0 + Math.exp(-βe * curve)) | |
end | |
# Eq. 11 | |
def TSE(p, t_tup, α_tup, β_tup, e_tup, ϕ, βe) | |
DS(p, t_tup, α_tup, β_tup) * ES(p, t_tup, e_tup, ϕ, βe) | |
end | |
def membership(category, l, a, b) | |
# p = (a, b, 1)^T | |
p = Matrix.column_vector [a, b, 1.0] | |
# See Parameter Estimation section of | |
# https://hal.inria.fr/hal-00640930/document | |
plane = if l <= 31 | |
0 | |
elsif l <= 41 | |
1 | |
elsif l <= 51 | |
2 | |
elsif l <= 66 | |
3 | |
elsif l <= 76 | |
4 | |
else | |
5 | |
end | |
params = CHROMATICITY_PLANES[plane] | |
return 0.0 if params == nil | |
TSE(p, params[:t], | |
params[:categories][category][:α], | |
params[:categories][category][:β], | |
params[:e], | |
params[:ϕ], | |
params[:βe]) | |
end | |
CHROMATICITY_PLANES = [ | |
{}, | |
{}, | |
{}, | |
{}, | |
{}, | |
{t: Tup.new(-1.26, 1.81), | |
e: Tup.new(6.04, 7.39), | |
ϕ: -1.19, | |
βe: 100.0, | |
categories: { | |
orange: { α: Tup.new(25.74, -17.56), | |
β: Tup.new(1.03, 0.79) }, | |
yellow: { α: Tup.new(72.44, 16.24), | |
β: Tup.new(0.79, 0.96) }, | |
green: { α: Tup.new(106.24, 100.05), | |
β: Tup.new(0.96, 0.90) }, | |
blue: { α: Tup.new(190.05, -149.43), | |
β: Tup.new(0.96, 0.90) }, | |
purple: { α: Tup.new(-59.43, -122.37), | |
β: Tup.new(0.60, 1.93) }, | |
pink: { α: Tup.new(-32.37, -64.26), | |
β: Tup.new(1.93, 1.03) } | |
}} | |
] | |
# A random color with L component > 76 in CIELAB colorspace | |
# (I haven't inserted the rest of the planes here, but my Rust | |
# version had them and didn't work either) | |
cl, ca, cb = [77, -75, 41] | |
# Memberships for categories not present in the plane is assumed to be 0 | |
categories = CHROMATICITY_PLANES.last[:categories].keys | |
memberships = categories.map { |k| membership(k, cl, ca, cb) } | |
memberships.zip(categories).each { |m, k| puts "#{k}: #{m}" } | |
# This doesn't fulfill the unity-sum constraint. | |
# Sum of all memberships should be equal to 0, | |
# and as I didn't include achromatic categories, | |
# it could even be less than that, but not more. | |
sum = memberships.inject(0) {|sum, m| sum + m } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment