Test rework, bugfixes and more tests
|
@ -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))
|
||||
}
|
||||
|
|
28
colorconverson_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
72
colorcrop_test.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
BIN
testimages/01.png
Normal file
After Width: | Height: | Size: 137 B |
BIN
testimages/02.png
Normal file
After Width: | Height: | Size: 140 B |
BIN
testimages/03.png
Normal file
After Width: | Height: | Size: 140 B |
BIN
testimages/04.png
Normal file
After Width: | Height: | Size: 138 B |
BIN
testimages/05.png
Normal file
After Width: | Height: | Size: 138 B |
BIN
testimages/06.png
Normal file
After Width: | Height: | Size: 139 B |
BIN
testimages/07.png
Normal file
After Width: | Height: | Size: 138 B |
BIN
testimages/08.png
Normal file
After Width: | Height: | Size: 138 B |
BIN
testimages/09.png
Normal file
After Width: | Height: | Size: 137 B |
BIN
testimages/10.png
Normal file
After Width: | Height: | Size: 139 B |
BIN
testimages/11.png
Normal file
After Width: | Height: | Size: 137 B |
BIN
testimages/12.png
Normal file
After Width: | Height: | Size: 136 B |
BIN
testimages/13.png
Normal file
After Width: | Height: | Size: 135 B |
BIN
testimages/14.png
Normal file
After Width: | Height: | Size: 136 B |
BIN
testimages/15.png
Normal file
After Width: | Height: | Size: 134 B |
BIN
testimages/16.png
Normal file
After Width: | Height: | Size: 131 B |