Skip to content

Latest commit

 

History

History
256 lines (184 loc) · 7.41 KB

ColorAnalyser.coffee.md

File metadata and controls

256 lines (184 loc) · 7.41 KB

Color Analyser

class ColorAnalyser
  constructor: (img, canvas, maxColorBits=8) ->
    @octree = new Octree maxColorBits
    @loadCanvas img, canvas
    
  loadCanvas: (img, canvas) ->
    context = canvas.getContext '2d'
    
    @imgWidth = img.width
    @imgHeight = img.height

    canvas.width = @imgWidth
    canvas.height = @imgHeight
    
    context.drawImage img, 0, 0
    @imageData = context.getImageData 0, 0, @imgWidth, @imgHeight

  getPixel: (x, y, channels=3) -> 
    idx = (y * @imgWidth + x) * 4
    (@imageData.data[idx + channel] for channel in [0...channels])

  detectBackground: () ->
    top    = (@getPixel x, 0              for x in [0...@imgWidth])
    bottom = (@getPixel x, (@imgHeight-1) for x in [0...@imgWidth])
    left   = (@getPixel 0, y              for y in [0...@imgHeight])
    right  = (@getPixel (@imgWidth-1), y  for y in [0...@imgHeight])

    border = ((top.concat bottom).concat left).concat right

    colorFreqs = {}

    for color in border
      if colorFreqs[color.toString()]?
        colorFreqs[color.toString()]++
      else
        colorFreqs[color.toString()] = 1

    bgColor = top[0]
    mostFrequent = 0
    for color, freq of colorFreqs
      if freq > mostFrequent
        bgColor = color.split(',').map (x) -> parseInt x
        mostFrequent = freq

    return bgColor

  rgbToHsl: (r, g, b) ->
    r /= 255
    g /= 255
    b /= 255
    max = Math.max(r, g, b)
    min = Math.min(r, g, b)
    l = (max + min) / 2
 
    if max == min
      h = s = 0 # achromatic
    else
      d = max - min
      s = if l > 0.5 then d / (2 - max - min) else d / (max + min)
 
      switch max
        when r
          h = (g - b) / d + (if g < b then 6 else 0)
        when g
          h = (b - r) / d + 2
        when b
          h = (r - g) / d + 4
 
      h /= 6
 
    [h, s, l]
 
  #
  # Converts an HSL color value to RGB. Conversion formula
  # adapted from http://en.wikipedia.org/wiki/HSL_color_space.
  # Assumes h, s, and l are contained in the set [0, 1] and
  # returns r, g, and b in the set [0, 255].
  #
  # @param   Number  h       The hue
  # @param   Number  s       The saturation
  # @param   Number  l       The lightness
  # @return  Array           The RGB representation
  #
  hslToRgb: (h, s, l) ->
    if s == 0
      r = g = b = l # achromatic
    else
      hue2rgb = (p, q, t) ->
        if t < 0 then t += 1
        if t > 1 then t -= 1
        if t < 1/6 then return p + (q - p) * 6 * t
        if t < 1/2 then return q
        if t < 2/3 then return p + (q - p) * (2/3 - t) * 6
        return p
 
      q = if l < 0.5 then l * (1 + s) else l + s - l * s
      p = 2 * l - q
      r = hue2rgb(p, q, h + 1/3)
      g = hue2rgb(p, q, h)
      b = hue2rgb(p, q, h - 1/3)
 
    [r * 255, g * 255, b * 255]

  chooseTextColor: (backgroundColor, palette=null) ->
    [r, g, b] = backgroundColor

    [h, s, l] = @rgbToHsl r, g, b

    # rotate hue
    h += 0.5
    if h > 1 then h -= 1

    # invert saturation, and desaturate a little more
    s = (1 - s) * 0.25

    # invert lightness
    l = (1 - l)

    # increase contrast for lightness near grey
    if l < 0.5
      l = -l + 0.5
    else if l > 0.5
      l = -l + 1.5
    else
      l = 1


    # if a color palette has been provided, use it to affect the text color
    if palette?
      lerp = (t, from, to) -> t * to + (1-t) * from

      # find the mode color in the palette
      [modeColor, modeCount] = palette[0]

      for [color, count] in palette
        if count > modeCount
          modeCount = count
          modeColor = color

      # convert to HSL
      [mR, mG, mB] = modeColor
      [mH, mS, mL] = @rgbToHsl mR, mG, mB

      # take the minimum saturation so far
      s = Math.min s, mS

      # shift the hue towards the mode color
      h = lerp 0.75, h, mH

    # convert back to RGB
    rgb = (@hslToRgb h, s, l).map Math.floor

    return rgb

  analyseImage: (paletteSize, background=null, ignoreGrey=false) ->
    if not background? then background = @detectBackground()
    [palette, numVectors] = @getClusteredPalette paletteSize, 1, 1024, background, 32, ignoreGrey
    return [palette, numVectors]

  getClusteredPalette: (numClusters, threshold, paletteSize, exclude, error, ignoreGrey) ->
    [palette, numVectors] = @getThresholdedPalette threshold, paletteSize, exclude, error, ignoreGrey

    clusterer = new KMeans numClusters, 3
    clusterer.setPoints palette
    clusters = clusterer.performCluster()

    colors = ([cluster.getMean(), cluster.size] for cluster in clusters)

    return [colors, numVectors]

  getThresholdedPalette: (threshold, paletteSize, exclude, error, ignoreGrey) ->
    [colors, numVectors] = @getPalette paletteSize, exclude, error, ignoreGrey

    colors.sort (a, b) -> (b[1] - a[1])

    newColors = []

    sum = 0
    for color in colors
      newColors.push color
      sum += color[1]

      break if sum > (numVectors * threshold)

    return [newColors, numVectors] 


  getFilteredPalette: (stdDeviations, paletteSize, exclude, error, ignoreGrey) ->
    [colors, numVectors] = @getPalette paletteSize, exclude, error, ignoreGrey

    numColors = 0
    freqSum = 0
    for i in [0...colors.length]
      [color, freq] = colors[i]

      freqSum += freq
      numColors++

    meanFrequency = freqSum / numColors

    stdDevSum = 0
    for i in [0...colors.length]
      [color, freq] = colors[i]
      meanDiff = (freq - meanFrequency)
      stdDevSum += (meanDiff * meanDiff)

    stdDevFrequency = Math.sqrt (stdDevSum/numColors)

    filteredColors = ([color, freq] for [color, freq] in colors when Math.abs(freq - meanFrequency) < (stdDevFrequency * stdDeviations))

    return [filteredColors, numVectors]


  getPalette: (paletteSize, exclude, error, ignoreGrey) ->
    if not error? then error = 0

    pixelData = @imageData.data

    for i in [0...pixelData.length] by 4
      r = pixelData[i]
      g = pixelData[i+1]
      b = pixelData[i+2]
      a = pixelData[i+3]

      isExcluded = false
      if exclude?
        [er, eg, eb] = exclude
      
        rIsWithinError = (er - error) < r < (er + error)
        gIsWithinError = (eg - error) < g < (eg + error)
        bIsWithinError = (eb - error) < b < (eb + error)

        isExcluded = rIsWithinError and gIsWithinError and bIsWithinError
      
      excludeGrey = false

      if ignoreGrey
        rg = Math.abs( r - g ) < error
        rb = Math.abs( r - b ) < error
        gb = Math.abs( g - b ) < error
        
        excludeGrey = ignoreGrey and rg and rb and gb

        isExcluded = isExcluded or excludeGrey
      
      
      if not isExcluded then @octree.insertVector [r, g, b]

    numVectors = @octree.numVectors
    return [(@octree.reduceToSize paletteSize), numVectors]