// Largely inspired by the descriptions in http://lab.medialab.sciences-po.fr/iwanthue/// but written from scratch.package colorfulimport ()// The algorithm works in L*a*b* color space and converts to RGB in the end.// L* in [0..1], a* and b* in [-1..1]type lab_t struct { L, A, B float64}typeSoftPaletteSettingsstruct {// A function which can be used to restrict the allowed color-space. CheckColor func(l, a, b float64) bool// The higher, the better quality but the slower. Usually two figures. Iterations int// Use up to 160000 or 8000 samples of the L*a*b* space (and thus calls to CheckColor). // Set this to true only if your CheckColor shapes the Lab space weirdly. ManySamples bool}// Yeah, windows-stype Foo, FooEx, screw you golang...// Uses K-means to cluster the color-space and return the means of the clusters// as a new palette of distinctive colors. Falls back to K-medoid if the mean// happens to fall outside of the color-space, which can only happen if you// specify a CheckColor function.func ( int, SoftPaletteSettings) ([]Color, error) {// Checks whether it's a valid RGB and also fulfills the potentially provided constraint. := func( lab_t) bool { := Lab(.L, .A, .B)return .IsValid() && (.CheckColor == nil || .CheckColor(.L, .A, .B)) }// Sample the color space. These will be the points k-means is run on. := 0.05 := 0.1if .ManySamples { = 0.01 = 0.05 } := make([]lab_t, 0, int(1.0/*2.0/*2.0/))for := 0.0; <= 1.0; += {for := -1.0; <= 1.0; += {for := -1.0; <= 1.0; += {if (lab_t{, , }) { = append(, lab_t{, , }) } } } }// That would cause some infinite loops down there...iflen() < {returnnil, fmt.Errorf("palettegen: more colors requested (%v) than samples available (%v). Your requested color count may be wrong, you might want to use many samples or your constraint function makes the valid color space too small", , len()) } elseiflen() == {returnlabs2cols(), nil// Oops? }// We take the initial means out of the samples, so they are in fact medoids. // This helps us avoid infinite loops or arbitrary cutoffs with too restrictive constraints. := make([]lab_t, )for := 0; < ; ++ {for [] = [rand.Intn(len())]; in(, , []); [] = [rand.Intn(len())] { } } := make([]int, len()) := make([]bool, len())// The actual k-means/medoid iterationsfor := 0; < .Iterations; ++ {// Reassing the samples to clusters, i.e. to their closest mean. // By the way, also check if any sample is used as a medoid and if so, mark that.for , := range { [] = false := math.Inf(+1)for , := range { := lab_dist(, )if < { = [] = }// Mark samples which are used as a medoid.iflab_eq(, ) { [] = true } } }// Compute new means according to the samples.for := range {// The new mean is the average of all samples belonging to it.. := 0 := lab_t{0.0, 0.0, 0.0}for , := range {if [] == { ++ .L += .L .A += .A .B += .B } }if > 0 { .L /= float64() .A /= float64() .B /= float64() } else {// That mean doesn't have any samples? Get a new mean from the sample list!varintfor = rand.Intn(len()); []; = rand.Intn(len()) { } = [] [] = true }// But now we still need to check whether the new mean is an allowed color.if > 0 && () {// It does, life's good (TM) [] = } else {// New mean isn't an allowed color or doesn't have any samples! // Switch to medoid mode and pick the closest (unused) sample. // This should always find something thanks to len(samples) >= colorsCount := math.Inf(+1)for , := range {if ![] { := lab_dist(, )if < { = = } } } } } }returnlabs2cols(), nil}// A wrapper which uses common parameters.func ( int) ([]Color, error) {returnSoftPaletteEx(, SoftPaletteSettings{nil, 50, false})}func in( []lab_t, int, lab_t) bool {for := 0; < && < len(); ++ {if [] == {returntrue } }returnfalse}constLAB_DELTA = 1e-6func lab_eq(, lab_t) bool {returnmath.Abs(.L-.L) < LAB_DELTA &&math.Abs(.A-.A) < LAB_DELTA &&math.Abs(.B-.B) < LAB_DELTA}// That's faster than using colorful's DistanceLab since we would have to// convert back and forth for that. Here is no conversion.func lab_dist(, lab_t) float64 {returnmath.Sqrt(sq(.L-.L) + sq(.A-.A) + sq(.B-.B))}func labs2cols( []lab_t) ( []Color) { = make([]Color, len())for , := range { [] = Lab(.L, .A, .B) }return}
The pages are generated with Goldsv0.8.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.