Instantly share code, notes, and snippets.

ericnormand/00 RGB color mixing.md

Last active March 19, 2021 04:26
Show Gist options
• Save ericnormand/24704c0e4804580c991b68aab29b1c30 to your computer and use it in GitHub Desktop.

RGB Color Mixing

Here's an algorithm for mixing multiple RGB colors to create a single color.

1. Separate the colors into Red, Green, and Blue components.
2. Average all the Reds together, average all the Greens together, average all the Blues together.
3. Put the average values back together into a resulting RGB.

Is this the right way to do it? I don't know! But it's the way we're going to implement in this problem. Your task is to take a collection of RGB strings of the form `"#FF021A"`, mix them like the algorithm above, and return the resulting RGB string.

Note that each RGB string contains two hexadecimal digits for each component. You can round the averages to integers however you want.

Examples

```(mix ["#FFFFFF" "#000000"]) ;=> "#7F7F7F" or "#808080" depending on how you round
(mix ["#FF0000" "#00FF00" "#0000FF"]) ;=> "#555555"```

Thanks to this site for the challenge idea where it is considered Very Hard level in JavaScript.

jintwo commented Oct 19, 2020 • edited

```(defn mix [colors]
mix-components #(quot (reduce + (map read-hex-int %1)) (count %1))
colors-lists (map #(re-seq #"\w{2}" %) colors)
groups (partition (count colors) (apply interleave colors-lists))
hex-color (map mix-components groups)]
(str "#" (str/join "" (map #(format "%02X" %) hex-color)))))```

nbardiuk commented Oct 19, 2020 • edited

```(defn parse-rgb [rgb]
(let [i (Integer/parseInt (subs rgb 1) 16)]
{:r (bit-shift-right i 16)
:g (bit-shift-right (bit-and i 0x00FF00) 8)
:b (bit-and i 0x0000FF)}))

(defn format-rgb [{:keys [r g b]}]
(format "#%X%X%X" r g b))

(defn avg-by-component [colors]
(let [avg #(quot (reduce + %) (count %))
avg-by #(->> colors (map %) avg)]
{:r (avg-by :r)
:g (avg-by :g)
:b (avg-by :b)}))

(defn mix [colors]
(->> colors
(map parse-rgb)
avg-by-component
format-rgb))```

nwjsmith commented Oct 19, 2020

```(defn numeric-components
"Returns a [red green blue] tuple of numbers parsed from the RGB string."
[rgb-string]
(map #(Integer/parseInt % 16) (re-seq #"\w{2}" rgb-string)))

(defn mix
[colours]
(apply format
"#%02X%02X%02X"
(apply map
(fn [& component] (quot (reduce + component) (count component)))
(map numeric-components colours))))
```

steffan-westcott commented Oct 19, 2020 • edited

```(defn rgb->hex [rgb]
(apply format "#%02X%02X%02X" rgb))

(defn hex->rgb [s]
(map #(Integer/parseInt % 16) (re-seq #"\w\w" s)))

(defn mean [& ns]
(quot (reduce + ns) (count ns)))

(defn mix [colours]
(->> colours (map hex->rgb) (apply map mean) rgb->hex))```

michelemendel commented Oct 19, 2020

```(defn pad [n] (if (= 2 (count n)) n (str "0" n)))
(defn hex->rgb [h] (Integer/parseInt h 16))
(defn rgb->hex [rgb] (Integer/toString rgb 16))
(defn hex->rgbs [h] (->> h (re-seq #"\w\w") (map hex->rgb)))
(defn rgbs->hex [rgbs] (->> rgbs (map (comp pad rgb->hex)) (clojure.string/join "") (str "#")))
(defn avgs [xs] (->> xs (apply map +) (map #(/ % (count xs)))))

(defn mix
[hexs]
(->> hexs (map hex->rgbs) avgs rgbs->hex))

(mix ["#FFFFFF" "#000000"])                          ; "#7F7F7F"
(mix ["#FF0000" "#00FF00" "#0000FF"])                ; "#555555"
(mix ["#FFFF00", "#FF0000"])                         ; "##ff7f00"
(mix ["#FFFF00", "#0000FF"])                         ; "#7f7f7f"
(mix ["#B60E73", "#0EAEB6"])                         ; "#625E94"```

nbardiuk commented Oct 19, 2020

@steffan-westcott I really enjoy clear solution. It looks like something I had in mind but could not achive with maps

mchampine commented Oct 19, 2020 • edited

```(defn mix [cs]
(letfn [(avg [& cn] (quot (apply + cn) (count cn)))
(rgbs [s] (map read-string (map #(apply str "0x" %) (partition 2 (rest s)))))]
(apply format "#%X%X%X" (apply map avg (map rgbs cs)))))```

stuartstein777 commented Oct 19, 2020 • edited

```(defn pad [hex]
(if (= 2 (count hex))
hex
(str "0" hex)))

(defn hex->rgb [hex]
(->> (subs hex 1)
(partition 2)
(map (partial apply str))
(map #(str "0x" %))

(defn mix [colours]
(->> (map hex->rgb colours)
(apply map +)
(map #(/ % (count colours)))
(map #(format "%x" (int %)))
(map str/upper-case)
(apply str)
(str "#")))

(mix ["#FFFFFF" "#000000"])                          ; "#7F7F7F"
(mix ["#FF0000" "#00FF00" "#0000FF"])                ; "#555555"
(mix ["#FFFF00", "#FF0000"])                         ; "#ff7f00"```

hby commented Oct 19, 2020

```(defn mix [rgbs]
(let [is (partition 2 1 [1 3 5 7])]
(->> rgbs
(map (fn [rgb] (map #(Integer/parseInt (apply subs rgb %) 16) is)))
(apply map vector)
(map #(int (/ (apply + %) (count rgbs))))
(map #(format "%2x" %))
(apply str "#"))))```

aggerdom commented Oct 20, 2020 • edited

```(defn rgb-components [hex-color]
(->> (rest (re-matches #"(?i)#?([A-F0-9]{2})([A-F0-9]{2})([A-F0-9]{2})" hex-color))
(map #(Integer/parseInt % 16))))

(defn rgb-format [r g b]
(format "#%02X%02X%02X" r g b))

(defn mix [hex-colors]
(->> hex-colors
(map rgb-components) ;; [(r1 g1 b1) (r2 g2 b2) (r3 g3 b3)]
(apply map vector)   ;; [(r1 r2 r3) (g1 g2 g3) (b1 b2 b3)]
(map mean)           ;; [MEAN(r) MEAN(g) MEAN(b)]
(map int)            ;; round
(apply rgb-format))) ;; Format as hex

```

sztamas commented Oct 20, 2020

```(defn- rgb-str->int [rgb]
(if-let [groups (re-matches #"(?i)(?:#)([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})" rgb)]
(map (comp read-string (partial str "0x")) (rest groups))
(throw (IllegalArgumentException. (format "Invalid RGB string '%s'" rgb)))))

(defn mix [rgbs]
(->> rgbs
(map rgb-str->int)
(apply map +)
(map #(quot % (count rgbs)))
(map (partial format "%02X"))
(apply str "#")))
```

kthu commented Oct 20, 2020 • edited

```  (defn hexcolor->ints
[color]
(->> color
rest
(partition 2)
(map #(str "0x" (apply str %)))

(defn ints->hexcolor
[[r g b]]
(format "#%02x%02x%02x" r g b))

(defn average-segment
[segment]
(int (/ (reduce + segment)
(count segment))))

(def transpose (partial apply map list))

(defn mix
[colors]
(->> colors
(map hexcolor->ints)
transpose
(map average-segment)
ints->hexcolor))
```

zelark commented Oct 20, 2020 • edited

```(defn mix [hexs]
(->> hexs
(map #(re-seq #"\w\w" %))
(apply map #(map (fn [h] (Integer/parseInt h 16)) %&))
(map #(quot (apply + %) (count %)))
(apply format "#%02X%02X%02X"))) ;; fixed after steffan-westcott's comment below```

@mchampine, thanks for the idea of using `(apply format "#%X%X%X")`. Btw, in your solution you could just use `#(apply str "0x" %)` and it would work.

mchampine commented Oct 20, 2020

@zelark, thanks, yes I missed that simplification! (using 'format' in various ways for the hex conversion was pioneered here by others though :) )

steffan-westcott commented Oct 20, 2020

Each colour component should be expressed as a 2 digit, `0` padded hex string. To get the desired output format, you can use `%02X` (rather than `%X`) for each component:

```(apply format "#%02X%02X%02X" [1 2 3])
=> "#010203"```

RedPenguin101 commented Oct 20, 2020

```(defn from-hex [color-string]
(->> (partition 2 (subs color-string 1))
(map #(apply str %))
(map #(Integer/parseInt % 16))))

(defn avg [& nums]
(quot (/ (apply + nums) (count nums))))

(defn mix [colors]
(->> colors
(map from-hex)
(apply map avg)
(apply format "#%02X%02X%02X")))

(mix ["#FF0000" "#00FF00" "#0000FF"])
;; => "#555555"

(mix ["#FFFFFF" "#000000"])
;; => "#808080"```

treydavis commented Oct 21, 2020

```(defn mix [hex-colors]
(->> hex-colors
(map #(Integer/decode %1))
(map #(vector
(bit-shift-right (bit-and 0xFF0000 %1) 16)
(bit-shift-right (bit-and 0x00FF00 %1) 8)
(bit-and 0x0000FF %1)))
(apply map +)
(map #(quot %1 (count hex-colors)))
(map #(format "%02X" %1))
(apply str "#")))```

KingCode commented Oct 21, 2020 • edited

"If it was very hard in Javascript, let it be hard in Clojure, too!" :)
Here is my labored version - the price to pay to be free of JVM/JS platform dependencies

``````(def hex-pows (iterate #(* 16 %) 1))
(def num->char (->> "0123456789ABCDEF" (zipmap (range))))
(def char->num (zipmap (vals num->char) (keys num->char)))

(defn ->dec [h]
(->> h (map char->num)
reverse
(reduce (fn [[n pow] x]
[(-> hex-pows (nth pow) (* x) (+ n)),
(inc pow)])
[0 0])
first))

(defn ->hex [n]
(if (zero? n)
"00"
(->>
(loop [acc []
num n
denom (->> hex-pows (take-while #(<= % n)) last)]
(if (< denom 1)
acc
(let [[q r] ((juxt quot rem) num denom)]
(recur (conj acc (num->char q))
r
(/ denom 16)))))
(apply str))))

(def sum-parts
(fn [rf]
(fn
([tally] (->> tally (take 3) (mapv #(quot % (last tally)))))
([[rt gt bt k] [r g b]]
(rf [(+ rt r) (+ gt g) (+ bt b) (inc k)])))))

(defn mix [RGBs]
(let [xf (comp (map rest)
(map #(partition 2 %))
(map #(map (fn [hh] (->dec hh)) %))
sum-parts)
format #(->> % (map ->hex) (apply str "#"))]
(->> RGBs
(transduce xf conj [0 0 0 0])
format)))
``````

proush42 commented Oct 21, 2020

```(defn mix [colors]
(let [hexstr->i (fn [s] (Long/parseLong s 16))
i->hexstr (fn [i] (Long/toHexString i))
parse-rgb (fn [c] (->> (re-find #"#(..)(..)(..)" c)
rest
(map hexstr->i)))
avg       (fn [ns] (/ (reduce + ns) (count ns) 1.0))
avg-color (fn [cs]
[(Math/round (avg (map #(nth % 0) cs)))
(Math/round (avg (map #(nth % 1) cs)))
(Math/round (avg (map #(nth % 2) cs)))])]
(->> (mapv parse-rgb colors)
avg-color
(map i->hexstr)
(apply (partial str "#")))))```

g7s commented Oct 23, 2020

```(defn mix
[colors]
(let [base-16 #(Integer/parseInt % 16)
avg     #(Math/round (float (/ (apply + %&) (count %&))))
rgb     #(map base-16 (re-seq #"\w\w" %))
to-hex  #(Integer/toHexString (+ (bit-shift-left %1 16) (bit-shift-left %2 8) %3))]
(->> colors
(map rgb)
(apply map avg)
(apply to-hex)
(str "#"))))```

HughPowell commented Oct 24, 2020 • edited

I'm trying to use this to expand my property testing skills. Does anyone have any thoughts on the properties that can be tested? The only one I've managed to come up with so far is idempotence.

```(require '[clojure.string :as string]
'[clojure.spec.alpha :as spec]
'[clojure.spec.gen.alpha :as spec-gen]
'[clojure.test.check.clojure-test :refer [defspec]]
'[clojure.test.check.properties :as check-properties])

(def hex-characters
(set (map char (concat (range (int \A) (int \F))
(range (int \0) (int \9))))))

(spec/def ::rgb-string (spec/with-gen
(spec/and
string?
#(re-matches #"#[\p{XDigit}]{6}" %))
#(spec-gen/fmap
(fn [h] (apply str "#" h))
(spec-gen/vector (spec/gen hex-characters) 6))))

(spec/def ::rgb-strings (spec/coll-of ::rgb-string :kind sequential? :min-count 1))

(defn- rgb-string->tuple [rgb]
(->> rgb
(re-seq #"[\p{XDigit}]{2}")
(map (fn [h] (read-string (apply str "0x" h))))))

(defn- tuple->rgb-string [t]
(string/replace (apply format "#%2H%2H%2H" t) #" " "0"))

(defn mix
"Average out the provided colours"
[rgb-strings]
(->> rgb-strings
(map rgb-string->tuple)
(apply map (fn [& colour-values]
(int (/ (apply + colour-values) (count colour-values)))))
tuple->rgb-string))

(defspec mix-is-idempotent
100
(check-properties/for-all [rgb-strings (spec/gen ::rgb-strings)]
(let [result (mix rgb-strings)]
(= result (mix [result])))))```

KingCode commented Oct 24, 2020

@HughPowell Nice work!...Other than idempotency, the only one I can think of is commutativity (changing the input list ordering). Regarding associativity, it does not apply because e.g. (avg 1 5 10) together weighs the values evenly as opposed to (avg (avg 1 5) 10).

HughPowell commented Oct 25, 2020 • edited

Ah ha. Good point @KingCode :-)

```(defspec mix-is-commutative
100
(check-properties/for-all [[original shuffled] (spec-gen/bind
(spec/gen ::rgb-strings)
(fn [xs]
(spec-gen/fmap (fn [ys] [xs ys])
(spec-gen/shuffle xs))))]
(= (mix original) (mix shuffled))))```

This still isn't quite sufficient. We could replace the implementation of `mix` with

`(first (sort rgb-strings))`

and both the current property tests would pass 🤔

germ13 commented Oct 29, 2020

```(defn rgbs [rgb]
(let [a (subs rgb 1 3)
b (subs rgb 3 5)
c (subs rgb 5)]
(map #(read-string (str "0x" %)) [a b c])))

(defn mix[colors]
(let [[& color] (map rgbs colors)]
(str "#" (clojure.string/join ""
(map #(format "%X" %)
(map byte (map #(/ % (count colors))
(apply map + color))))))))```

andyfry01 commented Mar 2, 2021 • edited

I'm a bit late to the party on this one, but here's my approach:

```(defn average
"Util for averaging many numbers, and rounding them down to the nearest whole number"
[& nums]
(int (Math/floor (/ (apply + nums) (count nums)))))

(defn hex->numtriple
"Takes a hex number and turns it into a vector of base-10 numbers, e.g.  #FFFFFF -> [255 255 255]"
[hex]
(as-> hex \$
(drop 1 \$)
(partition 2 \$)
(mapv (fn [chars]
(->> (apply str chars)
(str "0x")

(defn numtriple->hex
"Takes a vector of base-10 numbers and turns them into a hex color, e.g. [255 255 255] -> #FFFFFF"
[numtriple]
(reduce str (cons "#" (map #(format "%02X" %) numtriple))))

(defn mix
"Magic happens here, e.g (mix '#FFFFFF' '#000000') -> '#7F7F7F'"
[& stuff]
(let [rgb-nums (map hex->numtriple stuff)]
(numtriple->hex (apply map average rgb-nums))))

(comment
;; Function demos!
;;
(mix "#000000" "#FFFFFF") ; -> "#7F7F7F"
(mix "#000000" "#7F7F7F" "#111111") ; -> "#303030"
(mix "#000000") ; -> "#000000"
)```

Solution also available here, along with a bunch of other challenges I've done: https://github.com/andyfry01/clojure-coding-challenges/blob/master/src/main/color-averaging.clj

Sinha-Ujjawal commented Mar 19, 2021

My solution to the problem-

```(defn hex->num [s]
(Integer/parseInt s 16))

(defn filter-index [pred coll]
(map second (filter (fn [[i x]] (pred i)) (map-indexed vector coll))))

(defn pick-even [coll]
(filter-index (fn [i] (= 0 (bit-and i 1))) coll))

(defn pick-odd [coll]
(filter-index (fn [i] (= 1 (bit-and i 1))) coll))

(defn hex->rgb [s]
(let [s (rest s)] (map hex->num (map str (pick-even s) (pick-odd s)))))

(defn rgb->hex [[r g b]]
(str "#" (clojure.string/join #"" (map (fn [x] (format "%x" x)) [r g b]))))

(defn mix [colors]
(rgb->hex (let [n (count colors)] (reduce (fn [[x y z] [a b c]] (map (fn [[x y]] (+ x (int (/ y n)))) [[x a] [y b] [z c]])) [0 0 0] (map hex->rgb colors)))))```

Sinha-Ujjawal commented Mar 19, 2021 • edited

Another solution (thx @andyfry01)-

```(defn average [& ns]
(reduce + (map (fn [n] (/ n (count ns))) ns)))

(defn hex->num [s]
(Integer/parseInt s 16))

(defn hex->rgb [s]
(map hex->num (map (fn [[x y]] (str x y)) (partition 2 (rest s)))))

(defn rgb->hex [[r g b]]
(str "#" (clojure.string/join #"" (map (fn [x] (format "%x" x)) [r g b]))))

(defn mix [colors]
(rgb->hex (apply map (fn int-average [& ns] (int (Math/floor (apply average ns)))) (map hex->rgb colors))))```