/
colour-extractor.coffee
116 lines (98 loc) 路 3.29 KB
/
colour-extractor.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
gm = require('gm')
fs = require('fs')
temp = require('temp')
MAX_W = 14
MIFF_START = 'comment={'
MIF_END = '\x0A}\x0A\x0C\x0A'
exports.topColours = (sourceFilename, sorted, cb) ->
img = gm(sourceFilename)
tmpFilename = temp.path({suffix: '.miff'})
img.size((err, wh) ->
ratio = wh.width/MAX_W
w2 = wh.width/2
h2 = wh.height/2
img.bitdepth(8) # Initial colour reduction, prob. smarter than our 'algorithm'
.crop(w2, h2, w2/2, w2/2) # Center should be the most interesting
.scale(Math.ceil(wh.height/ratio), MAX_W) # Scales the image, histogram generation can take some time
.write('histogram:' + tmpFilename, (err) ->
histogram = ''
miffRS = fs.createReadStream(tmpFilename, {encoding: 'utf8'})
miffRS.addListener('data', (chunk) ->
endDelimPos = chunk.indexOf(MIFF_END)
if endDelimPos != -1
histogram += chunk.slice(0, endDelimPos + MIFF_END.length)
miffRS.destroy()
else
histogram += chunk
)
miffRS.addListener('close', ->
fs.unlink(tmpFilename)
colours = reduceSimilar(histogram.slice(histogram.indexOf(MIFF_START) + MIFF_START.length)
.split('\n')
.slice(1, -3)
.map(parseHistogramLine))
colours = colours.sort(sortByFrequency) if sorted
cb(colours)
)
)
)
exports.colourKey = (path, cb) ->
exports.topColours(path, false, (xs) ->
M = xs.length
m = Math.ceil(M/2)
cb([
xs[0], xs[1], xs[2],
xs[m-1], xs[m], xs[m+1],
xs[M-3], xs[M-2], xs[M-1]
])
)
exports.rgb2hex = (r, g, b) ->
rgb = if arguments.length is 1 then r else [r, g, b]
'#' + rgb.map((x) -> (if x < 16 then '0' else '') + x.toString(16)).join('')
exports.hex2rgb = (xs) ->
xs = xs.slice(1) if xs[0] is '#'
[xs.slice(0, 2), xs.slice(2, -2), xs.slice(-2)].map((x) -> parseInt(x, 16))
# PRIVATE FUNCTIONS
include = (x, xs) ->
xs.push(x) if xs.indexOf(x) is -1
xs
sortByFrequency = ([a, _], [b, _]) ->
return -1 if a > b
return 1 if a < b
return 0
distance = ([r1, g1, b1], [r2, g2, b2]) ->
Math.sqrt(Math.pow(r1 - r2, 2) + Math.pow(g1 - g2, 2) + Math.pow(b1 - b2, 2))
###
Example line:
f: (rrr, ggg, bbb) #rrggbb\n
\ \ \_____________ Hex code / "black" / "white"
\ \______________________________ RGB triplet
\_________________________________ Frequency at which colour appears
###
parseHistogramLine = (xs) ->
xs = xs.trim().split(':')
[+xs[0], xs[1].split('(')[1].split(')')[0].split(',').map((x) -> +x.trim())]
# Magic
reduceSimilar = (xs, r) ->
minD = Infinity
maxD = 0
maxF = 0
n = 0
N = xs.length - 1
tds = for x in xs
break if n is N
d = distance(x[1], xs[++n][1])
minD = d if d < minD
maxD = d if d > maxD
d
# Geometric mean helps us detecting similar colours appearing at lower frequencies
avgD = Math.sqrt(minD * maxD)
n = 0
rs = []
for d in tds
if d > avgD
include(xs[n], rs)
maxF = xs[n][0] if xs[n][0] > maxF
n++
# Normalise values, [0, maxF] => [0, 1]
rs.map(([f, c]) -> [f/maxF, c])