Skip to content

Commit

Permalink
Merge pull request #15 from indragiek/color-diff
Browse files Browse the repository at this point in the history
Add CIE76 and CIE94 algorithms
  • Loading branch information
Emmanuel Odeke committed Dec 25, 2014
2 parents 4def5f5 + 00f174a commit f3e09f9
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 99 deletions.
6 changes: 6 additions & 0 deletions DominantColor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
723DE5C51A4939A200C357E3 /* KMeans.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723DE51A1A49348D00C357E3 /* KMeans.swift */; };
723DE5C61A4939A300C357E3 /* ColorSpaceConversion.m in Sources */ = {isa = PBXBuildFile; fileRef = 723DE5161A49348D00C357E3 /* ColorSpaceConversion.m */; };
723DE5C71A4939A500C357E3 /* INVector3.m in Sources */ = {isa = PBXBuildFile; fileRef = 723DE5191A49348D00C357E3 /* INVector3.m */; };
72431C0F1A4BCF3B00470BD7 /* INVector3SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72431C0E1A4BCF3B00470BD7 /* INVector3SwiftExtensions.swift */; };
72431C101A4BCF3B00470BD7 /* INVector3SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72431C0E1A4BCF3B00470BD7 /* INVector3SwiftExtensions.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -111,6 +113,7 @@
723DE5741A49388400C357E3 /* ExampleiOS-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ExampleiOS-Bridging-Header.h"; path = "DominantColor/iOS/ExampleiOS-Bridging-Header.h"; sourceTree = SOURCE_ROOT; };
723DE59F1A4938DD00C357E3 /* DominantColor.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DominantColor.framework; sourceTree = BUILT_PRODUCTS_DIR; };
723DE5BE1A49394D00C357E3 /* GLKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GLKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.1.sdk/System/Library/Frameworks/GLKit.framework; sourceTree = DEVELOPER_DIR; };
72431C0E1A4BCF3B00470BD7 /* INVector3SwiftExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = INVector3SwiftExtensions.swift; sourceTree = "<group>"; };
72D797B81A43F44D00D32E7C /* ExampleMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleMac.app; sourceTree = BUILT_PRODUCTS_DIR; };
72D797DE1A43F89000D32E7C /* GLKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GLKit.framework; path = System/Library/Frameworks/GLKit.framework; sourceTree = SDKROOT; };
898845D21A490CE000003EF2 /* ExampleiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -162,6 +165,7 @@
723DE5171A49348D00C357E3 /* DominantColors.swift */,
723DE5181A49348D00C357E3 /* INVector3.h */,
723DE5191A49348D00C357E3 /* INVector3.m */,
72431C0E1A4BCF3B00470BD7 /* INVector3SwiftExtensions.swift */,
723DE51A1A49348D00C357E3 /* KMeans.swift */,
723DE5381A49353C00C357E3 /* Supporting Files */,
2F3D09851A4C212A001ED0BF /* Memoization.swift */,
Expand Down Expand Up @@ -443,6 +447,7 @@
files = (
2F3D09861A4C212A001ED0BF /* Memoization.swift in Sources */,
723DE55C1A49360F00C357E3 /* ColorDifference.swift in Sources */,
72431C0F1A4BCF3B00470BD7 /* INVector3SwiftExtensions.swift in Sources */,
723DE55D1A49361100C357E3 /* DominantColors.swift in Sources */,
723DE55E1A49361200C357E3 /* KMeans.swift in Sources */,
723DE5601A49361900C357E3 /* ColorSpaceConversion.m in Sources */,
Expand All @@ -456,6 +461,7 @@
files = (
2F3D09871A4C212A001ED0BF /* Memoization.swift in Sources */,
723DE5C31A49399E00C357E3 /* ColorDifference.swift in Sources */,
72431C101A4BCF3B00470BD7 /* INVector3SwiftExtensions.swift in Sources */,
723DE5C41A4939A000C357E3 /* DominantColors.swift in Sources */,
723DE5C51A4939A200C357E3 /* KMeans.swift in Sources */,
723DE5C61A4939A300C357E3 /* ColorSpaceConversion.m in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions DominantColor/Mac/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, DragAndDropImageViewDelegate
let CGImage = image.CGImageForProposedRect(nil, context: nil, hints: nil)!.takeUnretainedValue()
for n in nValues {
let ns = dispatch_benchmark(5) {
dominantColorsInImage(CGImage, n, 98251)
dominantColorsInImage(CGImage, maxSampledPixels: n)
return
}
println("n = \(n) averaged \(ns/1000000) ms")
Expand All @@ -49,7 +49,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, DragAndDropImageViewDelegate

self.image = image
let CGImage = image.CGImageForProposedRect(nil, context: nil, hints: nil)!.takeUnretainedValue()
let colors = dominantColorsInImage(CGImage, 1000, 98251)
let colors = dominantColorsInImage(CGImage)
let boxes = [box1, box2, box3, box4, box5, box6]

for box in boxes {
Expand Down
74 changes: 53 additions & 21 deletions DominantColor/Shared/ColorDifference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,61 @@
// Copyright (c) 2014 Indragie Karunaratne. All rights reserved.
//

private func degToRad(deg: Float) -> Float {
return deg * Float(M_PI) / 180
import GLKit.GLKMath

// These functions return the squared color difference because for distance
// calculations it doesn't matter and saves an unnecessary computation.

// From http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE76.html
func CIE76SquaredColorDifference(lab1: INVector3, lab2: INVector3) -> Float {
let (L1, a1, b1) = lab1.unpack()
let (L2, a2, b2) = lab2.unpack()

return pow(L2 - L1, 2) + pow(a2 - a1, 2) + pow(b2 - b1, 2)
}

private func radToDeg(rad: Float) -> Float {
return rad * 180 / Float(M_PI)
private func C(a: Float, b: Float) -> Float {
return sqrt(pow(a, 2) + pow(b, 2))
}

// From http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html
func CIE94SquaredColorDifference(
kL: Float = 1,
kC: Float = 1,
kH: Float = 1,
K1: Float = 0.045,
K2: Float = 0.015
)(colors: (INVector3, INVector3)) -> Float {

let (L1, a1, b1) = colors.0.unpack()
let (L2, a2, b2) = colors.1.unpack()
let ΔL = L1 - L2

let (C1, C2) = (C(a1, b1), C(a2, b2))
let ΔC = C1 - C2

let ΔH = sqrt(pow(a1 - a2, 2) + pow(b1 - b2, 2) - pow(ΔC, 2))

let Sl: Float = 1
let Sc = 1 + K1 * C1
let Sh = 1 + K2 * C1

return pow(ΔL / (kL * Sl), 2) + pow(ΔC / (kC * Sc), 2) + pow(ΔH / (kH * Sh), 2)
}

// From http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE2000.html
//
// NOTE: This returns the *squared* color difference to save a sqrt() call because
// we don't care about the absolute metric. To get the actual value of the CIE 2000
// delta E, the output from this function must be square rooted.
public func CIE2000SquaredColorDifference(lab1: INVector3, lab2: INVector3, kL: Float = 1, kC: Float = 1, kH: Float = 1) -> Float {
let (L1, a1, b1) = (lab1.x, lab1.y, lab1.z)
let (L2, a2, b2) = (lab2.x, lab2.y, lab2.z)
func CIE2000SquaredColorDifference(
kL: Float = 1,
kC: Float = 1,
kH: Float = 1
)(colors: (INVector3, INVector3)) -> Float {

let (L1, a1, b1) = colors.0.unpack()
let (L2, a2, b2) = colors.1.unpack()

let ΔLp = L2 - L1
let Lbp = (L1 + L2) / 2

let C: (Float, Float) -> Float = { a, b in
return sqrt(pow(a, 2) + pow(b, 2))
}
let (C1, C2) = (C(a1, b1), C(a2, b2))
let Cb = (C1 + C2) / 2

Expand All @@ -44,7 +76,7 @@ public func CIE2000SquaredColorDifference(lab1: INVector3, lab2: INVector3, kL:

let hp: (Float, Float) -> Float = { ap, b in
if ap == 0 && b == 0 { return 0 }
let θ = radToDeg(atan2(b, ap))
let θ = GLKMathRadiansToDegrees(atan2(b, ap))
return fmod(θ < 0 ? (θ + 360) : θ, 360)
}
let (h1p, h2p) = (hp(a1p, b1), hp(a2p, b2))
Expand All @@ -61,7 +93,7 @@ public func CIE2000SquaredColorDifference(lab1: INVector3, lab2: INVector3, kL:
}
}()

let ΔHp = 2 * sqrt(C1p * C2p) * sin(degToRad(Δhp / 2))
let ΔHp = 2 * sqrt(C1p * C2p) * sin(GLKMathDegreesToRadians(Δhp / 2))
let Hbp: Float = {
if (C1p == 0 || C2p == 0) {
return h1p + h2p
Expand All @@ -73,18 +105,18 @@ public func CIE2000SquaredColorDifference(lab1: INVector3, lab2: INVector3, kL:
}()

let T = 1
- 0.17 * cos(degToRad(Hbp - 30))
+ 0.24 * cos(degToRad(2 * Hbp))
+ 0.32 * cos(degToRad(3 * Hbp + 6))
- 0.20 * cos(degToRad(4 * Hbp - 63))
- 0.17 * cos(GLKMathDegreesToRadians(Hbp - 30))
+ 0.24 * cos(GLKMathDegreesToRadians(2 * Hbp))
+ 0.32 * cos(GLKMathDegreesToRadians(3 * Hbp + 6))
- 0.20 * cos(GLKMathDegreesToRadians(4 * Hbp - 63))

let Sl = 1 + (0.015 * pow(Lbp - 50, 2)) / sqrt(20 + pow(Lbp - 50, 2))
let Sc = 1 + 0.045 * Cbp
let Sh = 1 + 0.015 * Cbp * T

let Δθ = 30 * exp(-pow((Hbp - 275) / 25, 2))
let Rc = 2 * sqrt(pow(Cbp, 7) / (pow(Cbp, 7) + pow(25, 7)))
let Rt = -Rc * sin(degToRad(2 * Δθ))
let Rt = -Rc * sin(GLKMathDegreesToRadians(2 * Δθ))

let Lterm = ΔLp / (kL * Sl)
let Cterm = ΔCp / (kC * Sc)
Expand Down
74 changes: 40 additions & 34 deletions DominantColor/Shared/DominantColors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,43 +75,23 @@ private extension RGBAPixel {

// MARK: Clustering

public func +(lhs: INVector3, rhs: INVector3) -> INVector3 {
return INVector3Add(lhs, rhs)
}

extension INVector3 : ClusteredType {
public func distance(to: INVector3) -> Float {
return CIE2000SquaredColorDifference(self, to)
}

public func divideScalar(scalar: Int) -> INVector3 {
return INVector3DivideScalar(self, Float(scalar))
}

public static var identity: INVector3 {
return INVector3(x: 0, y: 0, z: 0)
}
}

private func selectKForElements<T>(elements: [T]) -> Int {
// Seems like a magic number...
return 16
}
extension INVector3 : ClusteredType {}

// MARK: Main

// Computes the proportionally scaled dimensions such that the
// total number of pixels does not exceed the specified limit.
private func scaledDimensionsForPixelLimit(limit: UInt, width: UInt, height: UInt) -> (UInt, UInt) {
if (width * height > limit) {
let ratio = Float(width) / Float(height)
let maxWidth = sqrtf(ratio * Float(limit))
return (UInt(maxWidth), UInt(Float(limit) / maxWidth))
}
return (width, height)
public enum GroupingAccuracy {
case Low // CIE 76 - Euclidian distance
case Medium // CIE 94 - Perceptual non-uniformity corrections
case High // CIE 2000 - Additional corrections for neutral colors, lightness, chroma, and hue
}

public func dominantColorsInImage(image: CGImage, maxSampledPixels: UInt, seed: Int) -> [CGColor] {
public func dominantColorsInImage(
image: CGImage,
maxSampledPixels: UInt = 1000,
accuracy: GroupingAccuracy = .Medium,
seed: UInt32 = 3571
) -> [CGColor] {

let (width, height) = (CGImageGetWidth(image), CGImageGetHeight(image))
let (scaledWidth, scaledHeight) = scaledDimensionsForPixelLimit(maxSampledPixels, width, height)

Expand All @@ -132,14 +112,40 @@ public func dominantColorsInImage(image: CGImage, maxSampledPixels: UInt, seed:
labValues.append(memoizedRGBToLAB(pixel))
}
}

// Cluster the colors using the k-means algorithm
let k = selectKForElements(labValues)
var clusters = kmeans(labValues, k, seed)
var clusters = kmeans(labValues, k, seed, distanceForAccuracy(accuracy))

// Sort the clusters by size in descending order so that the
// most dominant colors come first.
clusters.sort { $0.size > $1.size }

return clusters.map { RGBVectorToCGColor(IN_LABToRGB($0.centroid)) }
}

private func distanceForAccuracy(accuracy: GroupingAccuracy) -> (INVector3, INVector3) -> Float {
switch accuracy {
case .Low:
return CIE76SquaredColorDifference
case .Medium:
return CIE94SquaredColorDifference()
case .High:
return CIE2000SquaredColorDifference()
}
}

// Computes the proportionally scaled dimensions such that the
// total number of pixels does not exceed the specified limit.
private func scaledDimensionsForPixelLimit(limit: UInt, width: UInt, height: UInt) -> (UInt, UInt) {
if (width * height > limit) {
let ratio = Float(width) / Float(height)
let maxWidth = sqrtf(ratio * Float(limit))
return (UInt(maxWidth), UInt(Float(limit) / maxWidth))
}
return (width, height)
}

private func selectKForElements<T>(elements: [T]) -> Int {
// Seems like a magic number...
return 16
}
3 changes: 0 additions & 3 deletions DominantColor/Shared/INVector3.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,3 @@ typedef struct {

GLKVector3 INVector3ToGLKVector3(INVector3 vector);
INVector3 GLKVector3ToINVector3(GLKVector3 vector);

INVector3 INVector3Add(INVector3 v1, INVector3 v2);
INVector3 INVector3DivideScalar(INVector3 vector, float scalar);
8 changes: 0 additions & 8 deletions DominantColor/Shared/INVector3.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,3 @@ GLKVector3 INVector3ToGLKVector3(INVector3 vector) {
INVector3 GLKVector3ToINVector3(GLKVector3 vector) {
return (INVector3){ vector.x, vector.y, vector.z };
}

INVector3 INVector3Add(INVector3 v1, INVector3 v2) {
return GLKVector3ToINVector3(GLKVector3Add(INVector3ToGLKVector3(v1), INVector3ToGLKVector3(v2)));
}

INVector3 INVector3DivideScalar(INVector3 vector, float scalar) {
return GLKVector3ToINVector3(GLKVector3DivideScalar(INVector3ToGLKVector3(vector), scalar));
}
29 changes: 29 additions & 0 deletions DominantColor/Shared/INVector3SwiftExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// INVector3SwiftExtensions.swift
// DominantColor
//
// Created by Indragie on 12/24/14.
// Copyright (c) 2014 Indragie Karunaratne. All rights reserved.
//

extension INVector3 {
func unpack() -> (Float, Float, Float) {
return (x, y, z)
}

static var identity: INVector3 {
return INVector3(x: 0, y: 0, z: 0)
}
}

func +(lhs: INVector3, rhs: INVector3) -> INVector3 {
return INVector3(x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z)
}

func /(lhs: INVector3, rhs: Float) -> INVector3 {
return INVector3(x: lhs.x / rhs, y: lhs.y / rhs, z: lhs.z / rhs)
}

func /(lhs: INVector3, rhs: Int) -> INVector3 {
return lhs / Float(rhs)
}
Loading

0 comments on commit f3e09f9

Please sign in to comment.