Skip to content

Commit

Permalink
Remove GLKit dependence
Browse files Browse the repository at this point in the history
Moved to using SIMD vector operations instead
  • Loading branch information
dagronf committed Jan 16, 2021
1 parent 46f3901 commit aeb40a5
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 73 deletions.
6 changes: 0 additions & 6 deletions DominantColor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,13 @@
723DE55C1A49360F00C357E3 /* ColorDifference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723DE5141A49348D00C357E3 /* ColorDifference.swift */; };
723DE55D1A49361100C357E3 /* DominantColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723DE5171A49348D00C357E3 /* DominantColors.swift */; };
723DE55E1A49361200C357E3 /* KMeans.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723DE51A1A49348D00C357E3 /* KMeans.swift */; };
723DE5611A49372D00C357E3 /* GLKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 72D797DE1A43F89000D32E7C /* GLKit.framework */; };
723DE5691A49385C00C357E3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723DE5661A49385C00C357E3 /* AppDelegate.swift */; };
723DE56A1A49385C00C357E3 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 723DE5671A49385C00C357E3 /* Images.xcassets */; };
723DE56B1A49385C00C357E3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723DE5681A49385C00C357E3 /* ViewController.swift */; };
723DE5701A49386B00C357E3 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 723DE56C1A49386B00C357E3 /* LaunchScreen.xib */; };
723DE5711A49386B00C357E3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 723DE56E1A49386B00C357E3 /* Main.storyboard */; };
723DE5B61A4938DD00C357E3 /* DominantColor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 723DE59F1A4938DD00C357E3 /* DominantColor.framework */; };
723DE5B71A4938DD00C357E3 /* DominantColor.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 723DE59F1A4938DD00C357E3 /* DominantColor.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
723DE5BF1A49394D00C357E3 /* GLKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 723DE5BE1A49394D00C357E3 /* GLKit.framework */; };
723DE5C01A49395A00C357E3 /* DominantColor.h in Headers */ = {isa = PBXBuildFile; fileRef = 723DE5581A49358F00C357E3 /* DominantColor.h */; settings = {ATTRIBUTES = (Public, ); }; };
723DE5C31A49399E00C357E3 /* ColorDifference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723DE5141A49348D00C357E3 /* ColorDifference.swift */; };
723DE5C41A4939A000C357E3 /* DominantColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723DE5171A49348D00C357E3 /* DominantColors.swift */; };
Expand Down Expand Up @@ -118,15 +116,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
723DE5611A49372D00C357E3 /* GLKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
723DE59B1A4938DD00C357E3 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
723DE5BF1A49394D00C357E3 /* GLKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -527,7 +523,6 @@
723DE5501A49353C00C357E3 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_IDENTITY = "";
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2;
Expand Down Expand Up @@ -558,7 +553,6 @@
723DE5511A49353C00C357E3 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_IDENTITY = "";
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2;
Expand Down
34 changes: 21 additions & 13 deletions DominantColor/Shared/ColorDifference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@
// Copyright (c) 2014 Indragie Karunaratne. All rights reserved.
//

import GLKit.GLKMath
import simd

@inlinable func SIMDMathDegreesToRadians(_ degrees: Float) -> Float {
return degrees * (Float.pi / 180.0)
}

@inlinable func SIMDMathRadiansToDegrees(_ radians: Float) -> Float {
return radians * (180.0 / Float.pi)
}

// 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: GLKVector3, lab2: GLKVector3) -> Float {
func CIE76SquaredColorDifference(_ lab1: simd_float3, lab2: simd_float3) -> Float {
let (L1, a1, b1) = lab1.unpack()
let (L2, a2, b2) = lab2.unpack()

Expand All @@ -30,9 +38,9 @@ func CIE94SquaredColorDifference(
kH: Float = 1,
K1: Float = 0.045,
K2: Float = 0.015
) -> (_ lab1:GLKVector3, _ lab2:GLKVector3) -> Float {
) -> (_ lab1:simd_float3, _ lab2:simd_float3) -> Float {

return { (lab1:GLKVector3, lab2:GLKVector3) -> Float in
return { (lab1:simd_float3, lab2:simd_float3) -> Float in

let (L1, a1, b1) = lab1.unpack()
let (L2, a2, b2) = lab2.unpack()
Expand All @@ -56,9 +64,9 @@ func CIE2000SquaredColorDifference(
_ kL: Float = 1,
kC: Float = 1,
kH: Float = 1
) -> (_ lab1:GLKVector3, _ lab2:GLKVector3) -> Float {
) -> (_ lab1:simd_float3, _ lab2:simd_float3) -> Float {

return { (lab1:GLKVector3, lab2:GLKVector3) -> Float in
return { (lab1:simd_float3, lab2:simd_float3) -> Float in
let (L1, a1, b1) = lab1.unpack()
let (L2, a2, b2) = lab2.unpack()

Expand All @@ -80,7 +88,7 @@ func CIE2000SquaredColorDifference(

let hp: (Float, Float) -> Float = { ap, b in
if ap == 0 && b == 0 { return 0 }
let θ = GLKMathRadiansToDegrees(atan2(b, ap))
let θ = SIMDMathRadiansToDegrees(atan2(b, ap))
return fmod(θ < 0 ? (θ + 360) : θ, 360)
}
let (h1p, h2p) = (hp(a1p, b1), hp(a2p, b2))
Expand All @@ -97,7 +105,7 @@ func CIE2000SquaredColorDifference(
}
}()

let ΔHp = 2 * sqrt(C1p * C2p) * sin(GLKMathDegreesToRadians(Δhp / 2))
let ΔHp = 2 * sqrt(C1p * C2p) * sin(SIMDMathDegreesToRadians(Δhp / 2))
let Hbp: Float = {
if (C1p == 0 || C2p == 0) {
return h1p + h2p
Expand All @@ -109,20 +117,20 @@ func CIE2000SquaredColorDifference(
}()

var T = 1
- 0.17 * cos(GLKMathDegreesToRadians(Hbp - 30))
+ 0.24 * cos(GLKMathDegreesToRadians(2 * Hbp))
- 0.17 * cos(SIMDMathDegreesToRadians(Hbp - 30))
+ 0.24 * cos(SIMDMathDegreesToRadians(2 * Hbp))

T = T
+ 0.32 * cos(GLKMathDegreesToRadians(3 * Hbp + 6))
- 0.20 * cos(GLKMathDegreesToRadians(4 * Hbp - 63))
+ 0.32 * cos(SIMDMathDegreesToRadians(3 * Hbp + 6))
- 0.20 * cos(SIMDMathDegreesToRadians(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(GLKMathDegreesToRadians(2 * Δθ))
let Rt = -Rc * sin(SIMDMathDegreesToRadians(2 * Δθ))

let Lterm = ΔLp / (kL * Sl)
let Cterm = ΔCp / (kC * Sc)
Expand Down
71 changes: 39 additions & 32 deletions DominantColor/Shared/ColorSpaceConversion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,29 @@
// Copyright © 2019 Indragie Karunaratne. All rights reserved.
//

import GLKit
#if os(iOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif

import simd

// MARK: - RGB

func RGBToSRGB(_ rgbVector: GLKVector3) -> GLKVector3 {
func RGBToSRGB(_ rgbVector: simd_float3) -> simd_float3 {
#if os(iOS)
return rgbVector
#elseif os(OSX)
let rgbColor = NSColor(deviceRed: CGFloat(rgbVector.x), green: CGFloat(rgbVector.y), blue: CGFloat(rgbVector.z), alpha: 1.0)
guard let srgbColor = rgbColor.usingColorSpace(.sRGB) else {
fatalError("Could not convert color space")
}
return GLKVector3Make(Float(srgbColor.redComponent), Float(srgbColor.greenComponent), Float(srgbColor.blueComponent))
return simd_float3(Float(srgbColor.redComponent), Float(srgbColor.greenComponent), Float(srgbColor.blueComponent))
#endif
}

func SRGBToRGB(_ srgbVector: GLKVector3) -> GLKVector3 {
func SRGBToRGB(_ srgbVector: simd_float3) -> simd_float3 {
#if os(iOS)
return srgbVector
#elseif os(OSX)
Expand All @@ -31,63 +37,64 @@ func SRGBToRGB(_ srgbVector: GLKVector3) -> GLKVector3 {
guard let rgbColor = srgbColor.usingColorSpace(.deviceRGB) else {
fatalError("Could not convert color space")
}
return GLKVector3Make(Float(rgbColor.redComponent), Float(rgbColor.greenComponent), Float(rgbColor.blueComponent))
return simd_float3(Float(rgbColor.redComponent), Float(rgbColor.greenComponent), Float(rgbColor.blueComponent))
#endif
}

// MARK: - SRGB

func SRGBToLinearSRGB(_ srgbVector: GLKVector3) -> GLKVector3 {
func SRGBToLinearSRGB(_ srgbVector: simd_float3) -> simd_float3 {
func f(_ c: Float) -> Float {
if (c <= 0.04045) {
return c / 12.92
} else {
return powf((c + 0.055) / 1.055, 2.4)
}
}
return GLKVector3Make(f(srgbVector.x), f(srgbVector.y), f(srgbVector.z))
return simd_float3(f(srgbVector.x), f(srgbVector.y), f(srgbVector.z))
}

func LinearSRGBToSRGB(_ lSrgbVector: GLKVector3) -> GLKVector3 {
func LinearSRGBToSRGB(_ lSrgbVector: simd_float3) -> simd_float3 {
func f(_ c: Float) -> Float {
if (c <= 0.0031308) {
return c * 12.92
} else {
return (1.055 * powf(c, 1.0 / 2.4)) - 0.055
}
};
return GLKVector3Make(f(lSrgbVector.x), f(lSrgbVector.y), f(lSrgbVector.z));
return simd_float3(f(lSrgbVector.x), f(lSrgbVector.y), f(lSrgbVector.z));
}

// MARK: - XYZ (CIE 1931)
// http://en.wikipedia.org/wiki/CIE_1931_color_space#Construction_of_the_CIE_XYZ_color_space_from_the_Wright.E2.80.93Guild_data

let LinearSRGBToXYZMatrix = GLKMatrix3(m: (
0.4124, 0.2126, 0.0193,
0.3576, 0.7152, 0.1192,
0.1805, 0.0722, 0.9505
))
let LinearSRGBToXYZMatrix = simd_float3x3([
SIMD3(0.4124, 0.2126, 0.0193),
SIMD3(0.3576, 0.7152, 0.1192),
SIMD3(0.1805, 0.0722, 0.9505)
])

func LinearSRGBToXYZ(_ linearSrgbVector: GLKVector3) -> GLKVector3 {
let unscaledXYZVector = GLKMatrix3MultiplyVector3(LinearSRGBToXYZMatrix, linearSrgbVector);
return GLKVector3MultiplyScalar(unscaledXYZVector, 100.0);
func LinearSRGBToXYZ(_ linearSrgbVector: simd_float3) -> simd_float3 {
let unscaledXYZVector = LinearSRGBToXYZMatrix * linearSrgbVector
return unscaledXYZVector * 100.0
}

let XYZToLinearSRGBMatrix = GLKMatrix3(m: (
3.2406, -0.9689, 0.0557,
-1.5372, 1.8758, -0.2040,
-0.4986, 0.0415, 1.0570
))
let XYZToLinearSRGBMatrix = simd_float3x3([
SIMD3(3.2406, -0.9689, 0.0557),
SIMD3(-1.5372, 1.8758, -0.2040),
SIMD3(-0.4986, 0.0415, 1.0570)
])

func XYZToLinearSRGB(_ xyzVector: GLKVector3) -> GLKVector3 {
let scaledXYZVector = GLKVector3DivideScalar(xyzVector, 100.0);
return GLKMatrix3MultiplyVector3(XYZToLinearSRGBMatrix, scaledXYZVector);
func XYZToLinearSRGB(_ xyzVector: simd_float3) -> simd_float3 {
let scaledXYZVector = xyzVector / 100.0
return XYZToLinearSRGBMatrix * scaledXYZVector
}


// MARK: - LAB
// http://en.wikipedia.org/wiki/Lab_color_space#CIELAB-CIEXYZ_conversions

func XYZToLAB(_ xyzVector: GLKVector3, _ tristimulus: GLKVector3) -> GLKVector3 {
func XYZToLAB(_ xyzVector: simd_float3, _ tristimulus: simd_float3) -> simd_float3 {
func f(_ t: Float) -> Float {
if (t > powf(6.0 / 29.0, 3.0)) {
return powf(t, 1.0 / 3.0)
Expand All @@ -103,10 +110,10 @@ func XYZToLAB(_ xyzVector: GLKVector3, _ tristimulus: GLKVector3) -> GLKVector3
let a = 500 * (fx - fy)
let b = 200 * (fy - fz)

return GLKVector3Make(l, a, b)
return simd_float3(l, a, b)
}

func LABToXYZ(_ labVector: GLKVector3, _ tristimulus: GLKVector3) -> GLKVector3 {
func LABToXYZ(_ labVector: simd_float3, _ tristimulus: simd_float3) -> simd_float3 {
func f(_ t: Float) -> Float {
if (t > (6.0 / 29.0)) {
return powf(t, 3.0)
Expand All @@ -120,23 +127,23 @@ func LABToXYZ(_ labVector: GLKVector3, _ tristimulus: GLKVector3) -> GLKVector3
let x = tristimulus.x * f(c + ((1.0 / 500.0) * labVector.y))
let z = tristimulus.z * f(c - ((1.0 / 200.0) * labVector.z))

return GLKVector3Make(x, y, z)
return simd_float3(x, y, z)
}

// MARK: - Public

// From http://www.easyrgb.com/index.php?X=MATH&H=15#text15
let D65Tristimulus = GLKVector3Make(5.047, 100.0, 108.883)
let D65Tristimulus = simd_float3(5.047, 100.0, 108.883)

func IN_RGBToLAB(_ gVector: GLKVector3) -> GLKVector3 {
func IN_RGBToLAB(_ gVector: simd_float3) -> simd_float3 {
let srgbVector = RGBToSRGB(gVector)
let lSrgbVector = SRGBToLinearSRGB(srgbVector)
let xyzVector = LinearSRGBToXYZ(lSrgbVector)
let labVector = XYZToLAB(xyzVector, D65Tristimulus)
return labVector
}

func IN_LABToRGB(_ gVector: GLKVector3) -> GLKVector3 {
func IN_LABToRGB(_ gVector: simd_float3) -> simd_float3 {
let xyzVector = LABToXYZ(gVector, D65Tristimulus)
let lSrgbVector = XYZToLinearSRGB(xyzVector)
let srgbVector = LinearSRGBToSRGB(lSrgbVector)
Expand Down
25 changes: 13 additions & 12 deletions DominantColor/Shared/DominantColors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import Foundation
#elseif os(iOS)
import UIKit
#endif
import GLKit

import simd

// MARK: Bitmaps

Expand Down Expand Up @@ -62,13 +63,13 @@ private func enumerateRGBAContext(_ context: CGContext, handler: (Int, Int, RGBA

// MARK: Conversions

private func RGBVectorToCGColor(_ rgbVector: GLKVector3) -> CGColor {
private func RGBVectorToCGColor(_ rgbVector: simd_float3) -> CGColor {
return CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [CGFloat(rgbVector.x), CGFloat(rgbVector.y), CGFloat(rgbVector.z), 1.0])!
}

private extension RGBAPixel {
func toRGBVector() -> GLKVector3 {
return GLKVector3Make(
func toRGBVector() -> simd_float3 {
return simd_float3(
Float(r) / Float(UInt8.max),
Float(g) / Float(UInt8.max),
Float(b) / Float(UInt8.max)
Expand All @@ -78,7 +79,7 @@ private extension RGBAPixel {

// MARK: Clustering

extension GLKVector3 : ClusteredType {}
extension simd_float3 : ClusteredType {}

// MARK: Main

Expand Down Expand Up @@ -136,11 +137,11 @@ public func dominantColorsInImage(
// Get the RGB colors from the bitmap context, ignoring any pixels
// that have alpha transparency.
// Also convert the colors to the LAB color space
var labValues = [GLKVector3]()
var labValues = [simd_float3]()
labValues.reserveCapacity(Int(scaledWidth * scaledHeight))
let RGBToLAB: (RGBAPixel) -> GLKVector3 = {
let f: (RGBAPixel) -> GLKVector3 = { IN_RGBToLAB($0.toRGBVector()) }

let RGBToLAB: (RGBAPixel) -> simd_float3 = {
let f: (RGBAPixel) -> simd_float3 = { IN_RGBToLAB($0.toRGBVector()) }
return memoizeConversions ? memoize(f) : f
}()
enumerateRGBAContext(context) { (_, _, pixel) in
Expand All @@ -151,15 +152,15 @@ public func dominantColorsInImage(
// Cluster the colors using the k-means algorithm
let k = selectKForElements(labValues)
var clusters = kmeans(labValues, k: k, seed: seed, distance: 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) -> (GLKVector3, GLKVector3) -> Float {
private func distanceForAccuracy(_ accuracy: GroupingAccuracy) -> (simd_float3, simd_float3) -> Float {
switch accuracy {
case .low:
return CIE76SquaredColorDifference
Expand Down
Loading

0 comments on commit aeb40a5

Please sign in to comment.