diff --git a/colorconversion.go b/colorconversion.go index e9d0510..5e68834 100644 --- a/colorconversion.go +++ b/colorconversion.go @@ -5,10 +5,13 @@ import ( "math" ) -func rgbtoXYZ(r, g, b uint32) (x, y, z float64) { - varR := float64(r) / 255 - varG := float64(g) / 255 - varB := float64(b) / 255 +// colorToXYZ returns CIE XYZ representation of color. +// https://en.wikipedia.org/wiki/Color_model#CIE_XYZ_color_space +func colorToXYZ(color color.Color) (x, y, z float64) { + r, g, b, _ := color.RGBA() + varR := float64(r>>8) / 255 + varG := float64(g>>8) / 255 + varB := float64(b>>8) / 255 if varR > 0.04045 { varR = math.Pow((varR+0.055)/1.055, 2.4) @@ -28,16 +31,15 @@ func rgbtoXYZ(r, g, b uint32) (x, y, z float64) { varB = varB / 12.92 } - varR = varR * 100 - varG = varG * 100 - varB = varB * 100 + x = varR*41.24 + varG*35.76 + varB*18.05 + y = varR*21.26 + varG*71.52 + varB*7.22 + z = varR*1.93 + varG*11.92 + varB*95.05 - x = varR*0.4124 + varG*0.3576 + varB*0.1805 - y = varR*0.2126 + varG*0.7152 + varB*0.0722 - z = varR*0.0193 + varG*0.1192 + varB*0.9505 return x, y, z } +// xyztoLAB converts CIE XYZ color space to CIE LAB color space +// https://en.wikipedia.org/wiki/Lab_color_space#CIELAB-CIEXYZ_conversions func xyztoLAB(x, y, z float64) (l, a, b float64) { refX, refY, refZ := 95.047, 100.000, 108.883 // Daylight, sRGB, Adobe-RGB, Observer D65, 2° @@ -68,7 +70,8 @@ func xyztoLAB(x, y, z float64) (l, a, b float64) { return l, a, b } +// colorToLAB returns LAB representation of any color (without aplha) +// https://en.wikipedia.org/wiki/Lab_color_space func colorToLAB(color color.Color) (l, a, b float64) { - cr, cg, cb, _ := color.RGBA() - return xyztoLAB(rgbtoXYZ(cr, cg, cb)) + return xyztoLAB(colorToXYZ(color)) } diff --git a/colorconverson_test.go b/colorconverson_test.go new file mode 100644 index 0000000..8ab4fd3 --- /dev/null +++ b/colorconverson_test.go @@ -0,0 +1,28 @@ +package colorcrop + +import ( + "image/color" + "math" + "testing" +) + +func TestColorToLAB(t *testing.T) { + tests := []struct { + color color.Color + expectedL, expectedA, expectedB float64 + gotL, gotA, gotB float64 + }{ + {color: color.RGBA{0, 0, 0, 255}, expectedL: 0.0, expectedA: 0.0, expectedB: 0.0}, + {color: color.RGBA{0, 0, 255, 255}, expectedL: 32.30258667, expectedA: 79.19666179, expectedB: -107.86368104}, + {color: color.RGBA{0, 255, 0, 255}, expectedL: 87.73703347, expectedA: -86.18463650, expectedB: 83.18116475}, + {color: color.RGBA{255, 0, 0, 255}, expectedL: 53.23288179, expectedA: 80.10930953, expectedB: 67.22006831}, + {color: color.RGBA{255, 255, 255, 255}, expectedL: 100.00000000, expectedA: 0.00526050, expectedB: -0.01040818}, + } + + for _, test := range tests { + test.gotL, test.gotA, test.gotB = colorToLAB(test.color) + if math.Abs(test.gotL-test.expectedL) > epsilon || math.Abs(test.gotA-test.expectedA) > epsilon || math.Abs(test.gotB-test.expectedB) > epsilon { + t.Errorf("%v: expected {%.8f, %.8f, %.8f}, got {%.8f, %.8f, %.8f}", test.color, test.expectedL, test.expectedA, test.expectedB, test.gotL, test.gotA, test.gotB) + } + } +} diff --git a/colorcrop_test.go b/colorcrop_test.go new file mode 100644 index 0000000..c7e0454 --- /dev/null +++ b/colorcrop_test.go @@ -0,0 +1,72 @@ +package colorcrop + +import ( + "image" + "image/color" + "image/png" + "os" + "reflect" + "runtime" + "strings" + "testing" +) + +// epsilon is a maximum permissible error +const epsilon = 0.00000001 + +func TestCropRectanle(t *testing.T) { + type test struct { + filename string + expected image.Rectangle + got image.Rectangle + } + + comparators := []comparator{CmpRGBComponents, CmpEuclidean, CmpCIE76} + thresold := 0.5 + + tests := []test{ + {filename: "01.png", expected: image.Rectangle{image.Point{0, 0}, image.Point{1, 1}}}, + {filename: "02.png", expected: image.Rectangle{image.Point{1, 0}, image.Point{2, 1}}}, + {filename: "03.png", expected: image.Rectangle{image.Point{2, 0}, image.Point{3, 1}}}, + {filename: "04.png", expected: image.Rectangle{image.Point{3, 0}, image.Point{4, 1}}}, + {filename: "05.png", expected: image.Rectangle{image.Point{0, 1}, image.Point{1, 2}}}, + {filename: "06.png", expected: image.Rectangle{image.Point{1, 1}, image.Point{2, 2}}}, + {filename: "07.png", expected: image.Rectangle{image.Point{2, 1}, image.Point{3, 2}}}, + {filename: "08.png", expected: image.Rectangle{image.Point{3, 1}, image.Point{4, 2}}}, + {filename: "09.png", expected: image.Rectangle{image.Point{0, 2}, image.Point{1, 3}}}, + {filename: "10.png", expected: image.Rectangle{image.Point{1, 2}, image.Point{2, 3}}}, + {filename: "11.png", expected: image.Rectangle{image.Point{2, 2}, image.Point{3, 3}}}, + {filename: "12.png", expected: image.Rectangle{image.Point{3, 2}, image.Point{4, 3}}}, + {filename: "13.png", expected: image.Rectangle{image.Point{0, 3}, image.Point{1, 4}}}, + {filename: "14.png", expected: image.Rectangle{image.Point{1, 3}, image.Point{2, 4}}}, + {filename: "15.png", expected: image.Rectangle{image.Point{2, 3}, image.Point{3, 4}}}, + {filename: "16.png", expected: image.Rectangle{image.Point{3, 3}, image.Point{4, 4}}}, + } + + for _, comparator := range comparators { + for _, test := range tests { + file, err := os.Open("testimages/" + test.filename) + if err != nil { + t.Fatal(err) + } + defer file.Close() + image, err := png.Decode(file) + if err != nil { + t.Fatal(err) + } + test.got = cropRectanle(image, color.RGBA{255, 255, 255, 255}, thresold, comparator) + if !reflect.DeepEqual(test.expected, test.got) { + t.Errorf("expected %v, got %v for comparator: %s, file: %s", test.expected, test.got, getFuncName(comparator), test.filename) + } + } + } +} + +func getFuncName(i interface{}) string { + s := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() + p := strings.LastIndex(s, ".") + if p > 0 { + return string([]rune(s)[p+1:]) + } + return s +} diff --git a/comparators.go b/comparators.go index 174bead..7ea9599 100644 --- a/comparators.go +++ b/comparators.go @@ -10,6 +10,7 @@ import ( type comparator func(color.Color, color.Color) float64 // CmpEuclidean returns Euclidean difference of two colors. +// // https://en.wikipedia.org/wiki/Color_difference#Euclidean func CmpEuclidean(color1 color.Color, color2 color.Color) float64 { const maxDiff = 113509.94967402637 // Difference between black and white colors @@ -29,8 +30,8 @@ func CmpRGBComponents(color1 color.Color, color2 color.Color) float64 { r1, g1, b1, _ := color1.RGBA() r2, g2, b2, _ := color2.RGBA() - r1, g1, b1 = r1/256, g1/256, b1/256 - r2, g2, b2 = r2/256, g2/256, b2/256 + r1, g1, b1 = r1>>8, g1>>8, b1>>8 + r2, g2, b2 = r2>>8, g2>>8, b2>>8 return float64((max(r1, r2)-min(r1, r2))+ (max(g1, g2)-min(g1, g2))+ @@ -38,15 +39,13 @@ func CmpRGBComponents(color1 color.Color, color2 color.Color) float64 { } // CmpCIE76 returns difference of two colors defined in CIE76 standart. +// // https://en.wikipedia.org/wiki/Color_difference#CIE76 func CmpCIE76(color1 color.Color, color2 color.Color) float64 { - const maxDiff = 149.95514755026548 // Difference between blue and white colors + const maxDiff = 149.95514755 // Difference between blue and white colors - r1, g1, b1, _ := color1.RGBA() - r2, g2, b2, _ := color2.RGBA() - - cl1, ca1, cb1 := xyztoLAB(rgbtoXYZ(r1/256, g1/256, b1/256)) - cl2, ca2, cb2 := xyztoLAB(rgbtoXYZ(r2/256, g2/256, b2/256)) + cl1, ca1, cb1 := colorToLAB(color1) + cl2, ca2, cb2 := colorToLAB(color2) return math.Sqrt(distance(cl2, cl1)+distance(ca2, ca1)+distance(cb2, cb1)) / maxDiff } @@ -55,6 +54,7 @@ func distance(x, y float64) float64 { return (x - y) * (x - y) } +// min is minimum of two uint32 func min(a, b uint32) uint32 { if a < b { return a @@ -62,6 +62,7 @@ func min(a, b uint32) uint32 { return b } +// max is maximum of two uint32 func max(a, b uint32) uint32 { if a > b { return a diff --git a/comparators_test.go b/comparators_test.go index e8354c6..b61f949 100644 --- a/comparators_test.go +++ b/comparators_test.go @@ -2,40 +2,102 @@ package colorcrop import ( "image/color" - "reflect" - "runtime" + "math" "testing" ) -func TestColorComparators(t *testing.T) { - type In struct { - color1 color.Color - color2 color.Color - } - +func TestLinearComparators(t *testing.T) { comparators := []comparator{CmpEuclidean, CmpRGBComponents} tests := []struct { - in In - out float64 - commentary string + color1 color.Color + color2 color.Color + expected float64 + got float64 }{ - {in: In{color.RGBA{0, 0, 0, 255}, color.RGBA{255, 255, 255, 255}}, - out: 1.00, - commentary: "Difference between black and white colors"}, - {in: In{color.RGBA{255, 255, 255, 255}, color.RGBA{255, 255, 255, 255}}, - out: 0.00, - commentary: "Difference between same colors"}, - {in: In{color.RGBA{255, 255, 255, 0}, color.RGBA{255, 255, 255, 255}}, - out: 0.00, - commentary: "Difference between same colors with different transparency"}, + {color1: color.RGBA{0, 0, 0, 255}, color2: color.RGBA{0, 0, 0, 255}, expected: 0.00}, // same black colors + {color1: color.RGBA{255, 255, 255, 255}, color2: color.RGBA{255, 255, 255, 255}, expected: 0.00}, // same white colors + {color1: color.RGBA{0, 0, 0, 255}, color2: color.RGBA{255, 255, 255, 255}, expected: 1.00}, // different (black and white) colors + {color1: color.RGBA{255, 255, 255, 255}, color2: color.RGBA{0, 0, 0, 255}, expected: 1.00}, // different (white and black) colors + {color1: color.RGBA{255, 255, 255, 0}, color2: color.RGBA{255, 255, 255, 255}, expected: 0.00}, // must ignore alpha channel } for _, comparator := range comparators { for _, test := range tests { - if comparator(test.in.color2, test.in.color1) != test.out { - t.Errorf("%s: %s: expected %.2f, got %.2f", runtime.FuncForPC(reflect.ValueOf(comparator).Pointer()).Name(), test.commentary, test.out, comparator(test.in.color2, test.in.color1)) + test.got = comparator(test.color1, test.color2) + if math.Abs(test.got-test.expected) > epsilon { + t.Errorf("%v %v: expected %.8f, got %.8f", test.color1, test.color2, test.expected, test.got) } } } } + +func TestCmpCIE76(t *testing.T) { + type test struct { + color1 color.Color + color2 color.Color + expected float64 + got float64 + } + + tests := []test{ + {color1: color.RGBA{0, 0, 0, 255}, color2: color.RGBA{0, 0, 0, 255}, expected: 0.00000000 / 149.95514755}, // same black colors + {color1: color.RGBA{255, 255, 255, 255}, color2: color.RGBA{255, 255, 255, 255}, expected: 0.00000000 / 149.95514755}, // same white colors + {color1: color.RGBA{0, 0, 0, 255}, color2: color.RGBA{255, 255, 255, 255}, expected: 100.00000068 / 149.95514755}, // different (black and white) colors + {color1: color.RGBA{255, 255, 255, 255}, color2: color.RGBA{0, 0, 0, 255}, expected: 100.00000068 / 149.95514755}, // different (white and black) colors + {color1: color.RGBA{255, 0, 0, 255}, color2: color.RGBA{255, 255, 255, 255}, expected: 114.55897602 / 149.95514755}, // different (red and white) colors + {color1: color.RGBA{0, 255, 0, 255}, color2: color.RGBA{255, 255, 255, 255}, expected: 120.41559907 / 149.95514755}, // different (green and white) colors + {color1: color.RGBA{0, 0, 255, 255}, color2: color.RGBA{255, 255, 255, 255}, expected: 149.95514755 / 149.95514755}, // different (blue and white) colors + } + + for _, test := range tests { + test.got = CmpCIE76(test.color1, test.color2) + if math.Abs(test.got-test.expected) > epsilon { + t.Errorf("%v %v: expected %.8f, got %.8f", test.color1, test.color2, test.expected, test.got) + } + } +} + +func TestMin(t *testing.T) { + type test struct { + x uint32 + y uint32 + expected uint32 + got uint32 + } + + tests := []test{ + {x: 1, y: 2, expected: 1}, + {x: 3, y: 2, expected: 2}, + {x: 4, y: 4, expected: 4}, + } + + for _, test := range tests { + test.got = min(test.x, test.y) + if test.got != test.expected { + t.Errorf("min(%d, %d): expected %d, got %d", test.x, test.y, test.expected, test.got) + } + } +} + +func TestMax(t *testing.T) { + type test struct { + x uint32 + y uint32 + expected uint32 + got uint32 + } + + tests := []test{ + {x: 1, y: 2, expected: 2}, + {x: 3, y: 2, expected: 3}, + {x: 4, y: 4, expected: 4}, + } + + for _, test := range tests { + test.got = max(test.x, test.y) + if test.got != test.expected { + t.Errorf("max(%d, %d): expected %d, got %d", test.x, test.y, test.expected, test.got) + } + } +} diff --git a/example_test.go b/example_test.go index 92ed267..cb12430 100644 --- a/example_test.go +++ b/example_test.go @@ -1,3 +1,6 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. package colorcrop_test import ( @@ -9,7 +12,7 @@ import ( "github.com/nxshock/colorcrop" ) -func Example() { +func ExampleBasicUsage() { log.SetFlags(0) // Read source image @@ -18,8 +21,34 @@ func Example() { sourceImage, _ := png.Decode(sourceFile) - // Crop image white border with 50% thresold - croppedImage := colorcrop.Crop(sourceImage, color.RGBA{255, 255, 255, 255}, 0.5) + // Crop white border with 50% thresold + croppedImage := colorcrop.Crop( + sourceImage, // for source image + color.RGBA{255, 255, 255, 255}, // crop white border + 0.5) // with 50% thresold + + // Save cropped image + croppedFile, _ := os.Create("cropped.png") + defer croppedFile.Close() + + png.Encode(croppedFile, croppedImage) +} + +func ExampleCustomComparator() { + log.SetFlags(0) + + // Read source image + sourceFile, _ := os.Open("img.png") + defer sourceFile.Close() + + sourceImage, _ := png.Decode(sourceFile) + + // Crop white border with 50% thresold + croppedImage := colorcrop.CropWithComparator( + sourceImage, // for source image + color.RGBA{255, 255, 255, 255}, // crop white border + 0.5, // with 50% thresold + colorcrop.CmpCIE76) // using CIE76 standart for defining color difference // Save cropped image croppedFile, _ := os.Create("cropped.png") diff --git a/testimages/01.png b/testimages/01.png new file mode 100644 index 0000000..6ce94cc Binary files /dev/null and b/testimages/01.png differ diff --git a/testimages/02.png b/testimages/02.png new file mode 100644 index 0000000..e6e1559 Binary files /dev/null and b/testimages/02.png differ diff --git a/testimages/03.png b/testimages/03.png new file mode 100644 index 0000000..a54e10f Binary files /dev/null and b/testimages/03.png differ diff --git a/testimages/04.png b/testimages/04.png new file mode 100644 index 0000000..066cb41 Binary files /dev/null and b/testimages/04.png differ diff --git a/testimages/05.png b/testimages/05.png new file mode 100644 index 0000000..0e1bea9 Binary files /dev/null and b/testimages/05.png differ diff --git a/testimages/06.png b/testimages/06.png new file mode 100644 index 0000000..7e1fc86 Binary files /dev/null and b/testimages/06.png differ diff --git a/testimages/07.png b/testimages/07.png new file mode 100644 index 0000000..f812ae8 Binary files /dev/null and b/testimages/07.png differ diff --git a/testimages/08.png b/testimages/08.png new file mode 100644 index 0000000..2fbbd6f Binary files /dev/null and b/testimages/08.png differ diff --git a/testimages/09.png b/testimages/09.png new file mode 100644 index 0000000..389bcbe Binary files /dev/null and b/testimages/09.png differ diff --git a/testimages/10.png b/testimages/10.png new file mode 100644 index 0000000..da88603 Binary files /dev/null and b/testimages/10.png differ diff --git a/testimages/11.png b/testimages/11.png new file mode 100644 index 0000000..2e43c8b Binary files /dev/null and b/testimages/11.png differ diff --git a/testimages/12.png b/testimages/12.png new file mode 100644 index 0000000..73923d1 Binary files /dev/null and b/testimages/12.png differ diff --git a/testimages/13.png b/testimages/13.png new file mode 100644 index 0000000..a7f9b7d Binary files /dev/null and b/testimages/13.png differ diff --git a/testimages/14.png b/testimages/14.png new file mode 100644 index 0000000..e4dd9ff Binary files /dev/null and b/testimages/14.png differ diff --git a/testimages/15.png b/testimages/15.png new file mode 100644 index 0000000..dfd392a Binary files /dev/null and b/testimages/15.png differ diff --git a/testimages/16.png b/testimages/16.png new file mode 100644 index 0000000..88d678a Binary files /dev/null and b/testimages/16.png differ