From 3cff9dc95ce0345b2ae510415e7c93f78db8c614 Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Sat, 8 Apr 2017 20:09:00 +1000 Subject: [PATCH 01/16] Add initial implementation of raw DCT. --- math.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 math.go diff --git a/math.go b/math.go new file mode 100644 index 0000000..e3befee --- /dev/null +++ b/math.go @@ -0,0 +1,27 @@ +package simian + +import "math" + +func dct(width int, height int, values []uint8) (result []float32) { + + result = make([]float32, len(values)) + + for u := 0; u < height; u++ { + for v := 0; v < width; v++ { + sum := 0.0 + + for i := 0; i < height; i++ { + for j := 0; j < width; j++ { + + sum += float64(values[i*width+j]) * + math.Cos(((math.Pi*float64(u))/(2*float64(height)))*(2*float64(i)+1)) * + math.Cos(((math.Pi*float64(v))/(2*float64(width)))*(2*float64(j)+1)) + } + } + + result[u*width+v] = float32(sum) + } + } + + return +} From 6ababe47cec74075f3fd0fa275a54ef6344e9eb4 Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Sat, 8 Apr 2017 21:42:23 +1000 Subject: [PATCH 02/16] Tighten up use of types and enforce signed input. --- math.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/math.go b/math.go index e3befee..8d2127c 100644 --- a/math.go +++ b/math.go @@ -2,9 +2,12 @@ package simian import "math" -func dct(width int, height int, values []uint8) (result []float32) { +func DCT(width int, height int, values []int8) (result []int16) { - result = make([]float32, len(values)) + doubleWidth := 2.0 * float64(width) + doubleHeight := 2.0 * float64(height) + + result = make([]int16, len(values)) for u := 0; u < height; u++ { for v := 0; v < width; v++ { @@ -14,12 +17,12 @@ func dct(width int, height int, values []uint8) (result []float32) { for j := 0; j < width; j++ { sum += float64(values[i*width+j]) * - math.Cos(((math.Pi*float64(u))/(2*float64(height)))*(2*float64(i)+1)) * - math.Cos(((math.Pi*float64(v))/(2*float64(width)))*(2*float64(j)+1)) + math.Cos(((math.Pi*float64(u))/doubleHeight)*(2*float64(i)+1)) * + math.Cos(((math.Pi*float64(v))/doubleWidth)*(2*float64(j)+1)) } } - result[u*width+v] = float32(sum) + result[u*width+v] = int16(sum) } } From fada4ef23109a1d647a42d1696139b0d2c87e29b Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Thu, 17 Aug 2017 21:37:44 +1000 Subject: [PATCH 03/16] Implement zig-zag traversal/flattening. --- math.go | 44 ++++++++++++++++++++++++++++++ math_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 math_test.go diff --git a/math.go b/math.go index 8d2127c..2030fd5 100644 --- a/math.go +++ b/math.go @@ -28,3 +28,47 @@ func DCT(width int, height int, values []int8) (result []int16) { return } + +func flattenZigZag(width, height int, values []int16) []int16 { + result := make([]int16, width*height) + + x := 0 + y := 0 + + pAxis := &x + sAxis := &y + pBound := width + sBound := height + + for i := 0; i < len(result); i++ { + result[i] = values[y*width+x] + + if *pAxis+1 < pBound && *sAxis-1 >= 0 { + + // Unobstructed diagonal traversal + *pAxis++ + *sAxis-- + continue + + } else if *pAxis+1 < pBound { + + // Obstructed at the top/left; move right/down + *pAxis++ + + } else { + + // Obstructed at the bottom/right; move right/down + *sAxis++ + } + + // Swap direction (obstructed) + tmpAxis := pAxis + pAxis = sAxis + sAxis = tmpAxis + tmpBound := pBound + pBound = sBound + sBound = tmpBound + } + + return result +} diff --git a/math_test.go b/math_test.go new file mode 100644 index 0000000..9e31cdc --- /dev/null +++ b/math_test.go @@ -0,0 +1,77 @@ +package simian + +import "testing" + +func TestMath(t *testing.T) { + + t.Run("flattenZigZag()", func(t *testing.T) { + + t.Run("produces a zig-zag rearrangement of a 2D matrix", func(t *testing.T) { + + m1 := []int16{ + 0, 1, 5, + 2, 4, 6, + 3, 7, 10, + 8, 9, 11, + } + + result := flattenZigZag(3, 4, m1) + + if expected, actual := len(m1), len(result); expected != actual { + t.Fatalf("Expected result to be of length %d but got %d", expected, actual) + } + for i := 0; i < len(m1); i++ { + if result[i] != int16(i) { + t.Errorf("Expected element %d but got %d", i, result[i]) + } + } + + m2 := []int16{ + 0, 1, 5, 6, 13, + 2, 4, 7, 12, 14, + 3, 8, 11, 15, 18, + 9, 10, 16, 17, 19, + } + + result = flattenZigZag(5, 4, m2) + + if expected, actual := len(m2), len(result); expected != actual { + t.Fatalf("Expected result to be of length %d but got %d", expected, actual) + } + for i := 0; i < len(m2); i++ { + if result[i] != int16(i) { + t.Errorf("Expected element %d but got %d", i, result[i]) + } + } + + m3 := []int16{ + 0, 1, + 2, 3, + } + + result = flattenZigZag(2, 2, m3) + + if expected, actual := len(m3), len(result); expected != actual { + t.Fatalf("Expected result to be of length %d but got %d", expected, actual) + } + for i := 0; i < len(m3); i++ { + if result[i] != int16(i) { + t.Errorf("Expected element %d but got %d", i, result[i]) + } + } + + m4 := []int16{ + 1, + } + + result = flattenZigZag(1, 1, m4) + + if expected, actual := len(m4), len(result); expected != actual { + t.Fatalf("Expected result to be of length %d but got %d", expected, actual) + } + if result[0] != int16(1) { + t.Errorf("Expected element %d but got %d", 1, result[0]) + } + }) + }) +} From fd4d38ef2fcd27772f5847c2f893ac8273e4fef1 Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Wed, 20 Dec 2017 19:46:29 +1100 Subject: [PATCH 04/16] Implement recursive square traversal. --- math.go | 37 ++++++++++++++++++++++++++++++++++++- math_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/math.go b/math.go index 2030fd5..f725d96 100644 --- a/math.go +++ b/math.go @@ -1,6 +1,8 @@ package simian -import "math" +import ( + "math" +) func DCT(width int, height int, values []int8) (result []int16) { @@ -29,6 +31,39 @@ func DCT(width int, height int, values []int8) (result []int16) { return } +func flattenRecursiveSquares(squareMatrix []int16) []int16 { + sideLength := int(math.Sqrt(float64(len(squareMatrix)))) + result := make([]int16, sideLength*sideLength) + + level := 0 + offset := 0 + + for i := 0; i != len(result); { + if offset == level { + + // Sample the last corner of the current square + result[i] = squareMatrix[level*sideLength+level] + i++ + + // Start the next larger square + offset = 0 + level++ + + } else { + + // Sample one from the right and one from the bottom + result[i] = squareMatrix[offset*sideLength+level] + i++ + result[i] = squareMatrix[level*sideLength+offset] + i++ + + offset++ + } + } + + return result +} + func flattenZigZag(width, height int, values []int16) []int16 { result := make([]int16, width*height) diff --git a/math_test.go b/math_test.go index 9e31cdc..3453b4e 100644 --- a/math_test.go +++ b/math_test.go @@ -4,6 +4,37 @@ import "testing" func TestMath(t *testing.T) { + t.Run("flattenRecursiveSquares()", func(t *testing.T) { + + t.Run("produces a recursive square traversal of a square 2D matrix", func(t *testing.T) { + m := []int16{ + 0, 1, 4, + 2, 3, 6, + 5, 7, 8, + } + + result := flattenRecursiveSquares(m) + + if expected, actual := len(m), len(result); expected != actual { + t.Fatalf("Expected result to be of length %d but got %d", expected, actual) + } + + for i := 0; i < len(result); i++ { + if result[i] != int16(i) { + t.Errorf("Expected element %d but got %d", i, result[i]) + } + } + }) + + t.Run("produces an empty result for empty input", func(t *testing.T) { + result := flattenRecursiveSquares([]int16{}) + + if actual := len(result); actual != 0 { + t.Fatalf("Expected zero length result but got %d", actual) + } + }) + }) + t.Run("flattenZigZag()", func(t *testing.T) { t.Run("produces a zig-zag rearrangement of a 2D matrix", func(t *testing.T) { From 4eaef55f7d0b4d7349e384eab075bd177c3c1623 Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Wed, 20 Dec 2017 19:47:16 +1100 Subject: [PATCH 05/16] Remove zigzag traversal, since what we really want is a recursive square traversal. --- math.go | 44 -------------------------------- math_test.go | 71 ---------------------------------------------------- 2 files changed, 115 deletions(-) diff --git a/math.go b/math.go index f725d96..7596eec 100644 --- a/math.go +++ b/math.go @@ -63,47 +63,3 @@ func flattenRecursiveSquares(squareMatrix []int16) []int16 { return result } - -func flattenZigZag(width, height int, values []int16) []int16 { - result := make([]int16, width*height) - - x := 0 - y := 0 - - pAxis := &x - sAxis := &y - pBound := width - sBound := height - - for i := 0; i < len(result); i++ { - result[i] = values[y*width+x] - - if *pAxis+1 < pBound && *sAxis-1 >= 0 { - - // Unobstructed diagonal traversal - *pAxis++ - *sAxis-- - continue - - } else if *pAxis+1 < pBound { - - // Obstructed at the top/left; move right/down - *pAxis++ - - } else { - - // Obstructed at the bottom/right; move right/down - *sAxis++ - } - - // Swap direction (obstructed) - tmpAxis := pAxis - pAxis = sAxis - sAxis = tmpAxis - tmpBound := pBound - pBound = sBound - sBound = tmpBound - } - - return result -} diff --git a/math_test.go b/math_test.go index 3453b4e..c1e33f9 100644 --- a/math_test.go +++ b/math_test.go @@ -34,75 +34,4 @@ func TestMath(t *testing.T) { } }) }) - - t.Run("flattenZigZag()", func(t *testing.T) { - - t.Run("produces a zig-zag rearrangement of a 2D matrix", func(t *testing.T) { - - m1 := []int16{ - 0, 1, 5, - 2, 4, 6, - 3, 7, 10, - 8, 9, 11, - } - - result := flattenZigZag(3, 4, m1) - - if expected, actual := len(m1), len(result); expected != actual { - t.Fatalf("Expected result to be of length %d but got %d", expected, actual) - } - for i := 0; i < len(m1); i++ { - if result[i] != int16(i) { - t.Errorf("Expected element %d but got %d", i, result[i]) - } - } - - m2 := []int16{ - 0, 1, 5, 6, 13, - 2, 4, 7, 12, 14, - 3, 8, 11, 15, 18, - 9, 10, 16, 17, 19, - } - - result = flattenZigZag(5, 4, m2) - - if expected, actual := len(m2), len(result); expected != actual { - t.Fatalf("Expected result to be of length %d but got %d", expected, actual) - } - for i := 0; i < len(m2); i++ { - if result[i] != int16(i) { - t.Errorf("Expected element %d but got %d", i, result[i]) - } - } - - m3 := []int16{ - 0, 1, - 2, 3, - } - - result = flattenZigZag(2, 2, m3) - - if expected, actual := len(m3), len(result); expected != actual { - t.Fatalf("Expected result to be of length %d but got %d", expected, actual) - } - for i := 0; i < len(m3); i++ { - if result[i] != int16(i) { - t.Errorf("Expected element %d but got %d", i, result[i]) - } - } - - m4 := []int16{ - 1, - } - - result = flattenZigZag(1, 1, m4) - - if expected, actual := len(m4), len(result); expected != actual { - t.Fatalf("Expected result to be of length %d but got %d", expected, actual) - } - if result[0] != int16(1) { - t.Errorf("Expected element %d but got %d", 1, result[0]) - } - }) - }) } From 68e5585a7f41fa62f0508db6d9381f97dfb0b2bd Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Wed, 20 Dec 2017 19:49:28 +1100 Subject: [PATCH 06/16] Add test case for 1x1 matrix. --- math_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/math_test.go b/math_test.go index c1e33f9..066ba92 100644 --- a/math_test.go +++ b/math_test.go @@ -26,6 +26,22 @@ func TestMath(t *testing.T) { } }) + t.Run("produces an identity result for a 1x1 matrix", func(t *testing.T) { + m := []int16{ + 7, + } + + result := flattenRecursiveSquares(m) + + if expected, actual := len(m), len(result); expected != actual { + t.Fatalf("Expected result to be of length %d but got %d", expected, actual) + } + + if result[0] != m[0] { + t.Errorf("Expected element %d but got %d", m[0], result[0]) + } + }) + t.Run("produces an empty result for empty input", func(t *testing.T) { result := flattenRecursiveSquares([]int16{}) From 3f2d41d876aa1d078a05449a0c1f642ca305d950 Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Thu, 21 Dec 2017 17:32:57 +1100 Subject: [PATCH 07/16] Add simian-compare and simian-fingerprint commands. --- math.go | 2 +- math_test.go | 8 +-- simian-compare/main.go | 105 +++++++++++++++++++++++++++++++++++++ simian-fingerprint/main.go | 83 +++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 simian-compare/main.go create mode 100644 simian-fingerprint/main.go diff --git a/math.go b/math.go index 7596eec..414c453 100644 --- a/math.go +++ b/math.go @@ -31,7 +31,7 @@ func DCT(width int, height int, values []int8) (result []int16) { return } -func flattenRecursiveSquares(squareMatrix []int16) []int16 { +func FlattenRecursiveSquares(squareMatrix []int16) []int16 { sideLength := int(math.Sqrt(float64(len(squareMatrix)))) result := make([]int16, sideLength*sideLength) diff --git a/math_test.go b/math_test.go index 066ba92..44aef10 100644 --- a/math_test.go +++ b/math_test.go @@ -4,7 +4,7 @@ import "testing" func TestMath(t *testing.T) { - t.Run("flattenRecursiveSquares()", func(t *testing.T) { + t.Run("FlattenRecursiveSquares()", func(t *testing.T) { t.Run("produces a recursive square traversal of a square 2D matrix", func(t *testing.T) { m := []int16{ @@ -13,7 +13,7 @@ func TestMath(t *testing.T) { 5, 7, 8, } - result := flattenRecursiveSquares(m) + result := FlattenRecursiveSquares(m) if expected, actual := len(m), len(result); expected != actual { t.Fatalf("Expected result to be of length %d but got %d", expected, actual) @@ -31,7 +31,7 @@ func TestMath(t *testing.T) { 7, } - result := flattenRecursiveSquares(m) + result := FlattenRecursiveSquares(m) if expected, actual := len(m), len(result); expected != actual { t.Fatalf("Expected result to be of length %d but got %d", expected, actual) @@ -43,7 +43,7 @@ func TestMath(t *testing.T) { }) t.Run("produces an empty result for empty input", func(t *testing.T) { - result := flattenRecursiveSquares([]int16{}) + result := FlattenRecursiveSquares([]int16{}) if actual := len(result); actual != 0 { t.Fatalf("Expected zero length result but got %d", actual) diff --git a/simian-compare/main.go b/simian-compare/main.go new file mode 100644 index 0000000..4f2b8ae --- /dev/null +++ b/simian-compare/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "fmt" + "image" + "image/color" + _ "image/jpeg" + _ "image/png" + "math" + "os" + + "github.com/mandykoh/simian" + "golang.org/x/image/draw" +) + +const fingerprintDCTSideLength = 8 +const samplesPerFingerprint = fingerprintDCTSideLength * fingerprintDCTSideLength + +func makeFingerprint(src image.Image) []int16 { + scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: fingerprintDCTSideLength, Y: fingerprintDCTSideLength}}) + draw.BiLinear.Scale(scaled, scaled.Bounds(), src, src.Bounds(), draw.Src, nil) + + fingerprintSamples := make([]int8, samplesPerFingerprint) + offset := 0 + + for i := scaled.Bounds().Min.Y; i < scaled.Bounds().Max.Y; i++ { + for j := scaled.Bounds().Min.X; j < scaled.Bounds().Max.X; j++ { + r, g, b, _ := scaled.At(j, i).RGBA() + y, _, _ := color.RGBToYCbCr(uint8(r>>8), uint8(g>>8), uint8(b>>8)) + + fingerprintSamples[offset] = int8(y - 128) + offset++ + } + } + + dct := simian.DCT(fingerprintDCTSideLength, fingerprintDCTSideLength, fingerprintSamples) + + for i := 0; i < len(dct); i++ { + if i == 0 { + dct[i] >>= 7 + } else { + dct[i] = dct[i] >> 5 + } + } + + fingerprint := simian.FlattenRecursiveSquares(dct) + + return fingerprint +} + +func makeFingerprintFromImageFile(imageFileName string) ([]int16, error) { + imageFile, err := os.Open(imageFileName) + if err != nil { + return nil, err + } + defer imageFile.Close() + + img, _, err := image.Decode(imageFile) + if err != nil { + return nil, err + } + + return makeFingerprint(img), nil +} + +func main() { + if len(os.Args) < 3 { + fmt.Printf("Usage: simian-compare \n") + return + } + + fingerprint1, err := makeFingerprintFromImageFile(os.Args[1]) + if err != nil { + fmt.Printf("Error: %s\n", err) + return + } + + fingerprint2, err := makeFingerprintFromImageFile(os.Args[2]) + if err != nil { + fmt.Printf("Error: %s\n", err) + return + } + + difference := 0.0 + for i := 0; i < len(fingerprint1); i++ { + difference += math.Abs(float64(fingerprint1[i] - fingerprint2[i])) + } + difference /= samplesPerFingerprint * 12 + + var judgment string + switch { + case difference < 0.05: + judgment = "duplicate" + case difference < 0.1: + judgment = "variation" + case difference < 0.2: + judgment = "similar" + case difference < 0.3: + judgment = "tonally/texturally similar" + default: + judgment = "different" + } + + fmt.Printf("%.4f (%s)\n", difference, judgment) +} diff --git a/simian-fingerprint/main.go b/simian-fingerprint/main.go new file mode 100644 index 0000000..3bae9c4 --- /dev/null +++ b/simian-fingerprint/main.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + "image" + "image/color" + _ "image/jpeg" + _ "image/png" + "os" + + "github.com/mandykoh/simian" + "golang.org/x/image/draw" +) + +const fingerprintDCTSideLength = 8 +const samplesPerFingerprint = fingerprintDCTSideLength * fingerprintDCTSideLength + +func makeFingerprint(src image.Image) []int16 { + scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: fingerprintDCTSideLength, Y: fingerprintDCTSideLength}}) + draw.BiLinear.Scale(scaled, scaled.Bounds(), src, src.Bounds(), draw.Src, nil) + + fingerprintSamples := make([]int8, samplesPerFingerprint) + offset := 0 + + for i := scaled.Bounds().Min.Y; i < scaled.Bounds().Max.Y; i++ { + for j := scaled.Bounds().Min.X; j < scaled.Bounds().Max.X; j++ { + r, g, b, _ := scaled.At(j, i).RGBA() + y, _, _ := color.RGBToYCbCr(uint8(r>>8), uint8(g>>8), uint8(b>>8)) + + fingerprintSamples[offset] = int8(y - 128) + offset++ + } + } + + dct := simian.DCT(fingerprintDCTSideLength, fingerprintDCTSideLength, fingerprintSamples) + + fmt.Printf("DCT:\n") + for i := 0; i < len(dct); i++ { + if i == 0 { + dct[i] >>= 7 + } else { + dct[i] = dct[i] >> 5 + } + + if i > 0 && i%fingerprintDCTSideLength == 0 { + fmt.Println() + } + fmt.Printf(" %5d", dct[i]) + } + fmt.Println() + fmt.Println() + + fingerprint := simian.FlattenRecursiveSquares(dct) + + return fingerprint +} + +func main() { + if len(os.Args) < 2 { + fmt.Printf("Usage: simian-fingerprint \n") + return + } + + imageFile, err := os.Open(os.Args[1]) + if err != nil { + fmt.Printf("Error: %s\n", err) + return + } + defer imageFile.Close() + + img, _, err := image.Decode(imageFile) + if err != nil { + fmt.Printf("Error: %s\n", err) + return + } + + fingerprint := makeFingerprint(img) + + for i := 0; i < len(fingerprint); i++ { + fmt.Printf("%02x", fingerprint[i]+128) + } + fmt.Println() +} From 37b51f97f8b6bbe71ada8a32f94f27f968f7a943 Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Thu, 21 Dec 2017 19:56:24 +1100 Subject: [PATCH 08/16] Update image/draw. --- vendor/vendor.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/vendor.json b/vendor/vendor.json index ba70d97..ddb0bbf 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -15,7 +15,7 @@ "revisionTime": "2017-06-17T12:17:10Z" }, { - "checksumSHA1": "7E3Y1HU/UbsQF/dxMRdjFmx9QDQ=", + "checksumSHA1": "4+1dxGgXahv2izGbBxftVoQjoXI=", "path": "golang.org/x/image/draw", "revision": "83686c547965220f8b5d75e83ddc67d73420a89f", "revisionTime": "2017-01-15T09:09:03Z" From 8b3e80c6f69dfbd3e6bc6051fad4f849565c93cb Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Fri, 22 Dec 2017 10:51:45 +1100 Subject: [PATCH 09/16] Remove superfluous classes. --- diskindexstore.go | 157 -------------------------- index.go | 116 ------------------- indexentry.go | 109 ------------------ indexentry_test.go | 41 ------- indexnode.go | 270 --------------------------------------------- indexnode_test.go | 89 --------------- indexstore.go | 10 -- 7 files changed, 792 deletions(-) delete mode 100644 diskindexstore.go delete mode 100644 index.go delete mode 100644 indexentry.go delete mode 100644 indexentry_test.go delete mode 100644 indexnode.go delete mode 100644 indexnode_test.go delete mode 100644 indexstore.go diff --git a/diskindexstore.go b/diskindexstore.go deleted file mode 100644 index 133c473..0000000 --- a/diskindexstore.go +++ /dev/null @@ -1,157 +0,0 @@ -package simian - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "os" - "path" - - "github.com/mandykoh/keva" -) - -const nodeFingerprintFile = "fingerprint" -const nodeEntriesDir = "entries" -const thumbnailsDir = "thumbnails" - -type DiskIndexStore struct { - rootPath string - nodes *keva.Store -} - -func (s *DiskIndexStore) AddEntry(entry *IndexEntry, node *IndexNode, nodeFingerprint Fingerprint) error { - err := entry.saveThumbnail(s.pathForThumbnail(entry)) - if err != nil { - return err - } - - node.registerEntry(entry) - - fmt.Printf("AddEntry - Saving [%s] %d %d\n", nodeFingerprint.String(), len(node.childFingerprints), len(node.entries)) - return s.nodes.Put(nodeFingerprint.String(), node) -} - -func (s *DiskIndexStore) Close() error { - return s.nodes.Close() -} - -func (s *DiskIndexStore) GetChild(f Fingerprint, parent *IndexNode) (*IndexNode, error) { - var node IndexNode - - err := s.nodes.Get(f.String(), &node) - if err == keva.ErrValueNotFound { - return nil, nil - - } else if err == nil { - err = s.loadThumbnails(&node) - if err != nil { - return nil, err - } - - } else { - return nil, err - } - - return &node, nil -} - -func (s *DiskIndexStore) GetOrCreateChild(f Fingerprint, parent *IndexNode, parentFingerprint Fingerprint) (*IndexNode, error) { - fmt.Printf("GetOrCreateChild() %s\n", f.String()) - - nodeKey := f.String() - - var node IndexNode - err := s.nodes.Get(nodeKey, &node) - - if err == keva.ErrValueNotFound { - fmt.Printf("Creating child\n") - - node = IndexNode{ - childFingerprintsByString: make(map[string]*Fingerprint), - } - - fmt.Printf("GetOrCreateChild - Saving [%s] %d %d\n", nodeKey, len(node.childFingerprints), len(node.entries)) - err = s.nodes.Put(nodeKey, &node) - if err != nil { - return nil, err - } - - parent.registerChild(f) - fmt.Printf("GetOrCreateChild - Parent - Saving [%s] %d %d\n", parentFingerprint.String(), len(parent.childFingerprints), len(parent.entries)) - err = s.nodes.Put(parentFingerprint.String(), parent) - if err != nil { - return nil, err - } - - } else if err == nil { - err = s.loadThumbnails(&node) - if err != nil { - return nil, err - } - - } else { - return nil, err - } - - return &node, nil -} - -func (s *DiskIndexStore) GetRoot() (*IndexNode, error) { - var rootKey = Fingerprint{}.String() - - var root IndexNode - err := s.nodes.Get(rootKey, &root) - - if err == keva.ErrValueNotFound { - fmt.Printf("Root node not found - creating it\n") - root = IndexNode{ - childFingerprintsByString: make(map[string]*Fingerprint), - } - - } else if err == nil { - fmt.Printf("Found root node with %d children and %d entries\n", len(root.childFingerprints), len(root.entries)) - - err = s.loadThumbnails(&root) - if err != nil { - return nil, err - } - - } else { - return nil, err - } - - return &root, nil -} - -func (s *DiskIndexStore) RemoveEntries(node *IndexNode, nodeFingerprint Fingerprint) error { - node.removeEntries() - fmt.Printf("RemoveEntries - Saving [%s] %d %d\n", nodeFingerprint.String(), len(node.childFingerprints), len(node.entries)) - return s.nodes.Put(nodeFingerprint.String(), node) -} - -func (s *DiskIndexStore) loadThumbnails(n *IndexNode) error { - return n.withEachEntry(func(entry *IndexEntry) error { - return entry.loadThumbnail(s.pathForThumbnail(entry)) - }) -} - -func (s *DiskIndexStore) pathForThumbnail(entry *IndexEntry) string { - thumbnailHash := sha256.Sum256(entry.MaxFingerprint.Bytes()) - thumbnailHex := hex.EncodeToString(thumbnailHash[:]) - return path.Join(s.rootPath, thumbnailsDir, thumbnailHex[0:2], thumbnailHex[2:4], thumbnailHex[4:]) -} - -func NewDiskIndexStore(rootPath string) (*DiskIndexStore, error) { - thumbnailsDir := path.Join(rootPath, thumbnailsDir) - os.MkdirAll(thumbnailsDir, os.FileMode(0700)) - - nodeStore, err := keva.NewStore(path.Join(rootPath, "nodes")) - if err != nil { - return nil, err - } - - return &DiskIndexStore{ - rootPath: rootPath, - nodes: nodeStore, - }, nil -} diff --git a/index.go b/index.go deleted file mode 100644 index 83513aa..0000000 --- a/index.go +++ /dev/null @@ -1,116 +0,0 @@ -package simian - -import ( - "fmt" - "image" - "math" - "os" - "sort" -) - -const rootFingerprintSize = 1 - -type Index struct { - Store IndexStore - maxFingerprintSize int - maxEntryDifference float64 -} - -func (i *Index) Add(image image.Image, metadata map[string]interface{}) (key string, err error) { - entry, err := NewIndexEntry(image, i.maxFingerprintSize, metadata) - if err != nil { - return "", nil - } - - root, err := i.Store.GetRoot() - if err != nil { - return "", err - } - - var rootFingerprint Fingerprint - - _, err = root.Add(entry, rootFingerprint, rootFingerprintSize+1, i) - if err != nil { - return "", err - } - - fmt.Printf("Root node has %d children and %d entries\n", len(root.childFingerprints), len(root.entries)) - - return "", nil -} - -func (i *Index) Close() error { - return i.Store.Close() -} - -func (i *Index) FindNearest(image image.Image, maxResults int, maxDifference float64) ([]*IndexEntry, error) { - var dummy map[string]interface{} - - entry, err := NewIndexEntry(image, i.maxFingerprintSize, dummy) - if err != nil { - return nil, nil - } - - root, err := i.Store.GetRoot() - if err != nil { - return nil, err - } - - results, err := root.FindNearest(entry, rootFingerprintSize+1, i, maxResults, math.Max(maxDifference, i.maxEntryDifference)) - if err != nil { - return nil, err - } - sort.Sort(entriesByDifferenceToEntryWith(results, entry)) - - return results, err -} - -func NewIndex(path string, maxFingerprintSize int, maxEntryDifference float64) (*Index, error) { - err := os.MkdirAll(path, 0700) - if err != nil { - return nil, err - } - - indexStore, err := NewDiskIndexStore(path) - if err != nil { - return nil, err - } - - return &Index{ - Store: indexStore, - maxFingerprintSize: maxFingerprintSize, - maxEntryDifference: maxEntryDifference, - }, err -} - -type entriesByDifferenceToEntry struct { - entries []*IndexEntry - differences []float64 -} - -func (sorter *entriesByDifferenceToEntry) Len() int { - return len(sorter.entries) -} - -func (sorter *entriesByDifferenceToEntry) Less(i, j int) bool { - return sorter.differences[i] < sorter.differences[j] -} - -func (sorter *entriesByDifferenceToEntry) Swap(i, j int) { - tmpEntry := sorter.entries[i] - sorter.entries[i] = sorter.entries[j] - sorter.entries[j] = tmpEntry - - tmpDiff := sorter.differences[i] - sorter.differences[i] = sorter.differences[j] - sorter.differences[j] = tmpDiff -} - -func entriesByDifferenceToEntryWith(entries []*IndexEntry, target *IndexEntry) *entriesByDifferenceToEntry { - differences := make([]float64, len(entries), len(entries)) - for i, entry := range entries { - differences[i] = entry.MaxFingerprint.Difference(target.MaxFingerprint) - } - - return &entriesByDifferenceToEntry{entries: entries, differences: differences} -} diff --git a/indexentry.go b/indexentry.go deleted file mode 100644 index 57fe408..0000000 --- a/indexentry.go +++ /dev/null @@ -1,109 +0,0 @@ -package simian - -import ( - "encoding/json" - "image" - "image/png" - "os" - "path/filepath" - - "golang.org/x/image/draw" -) - -const keyBitLength = 256 - -type IndexEntry struct { - Thumbnail image.Image - MaxFingerprint Fingerprint - Attributes map[string]interface{} -} - -func (entry *IndexEntry) FingerprintForSize(size int) Fingerprint { - return NewFingerprint(entry.Thumbnail, size) -} - -func (entry *IndexEntry) MarshalJSON() ([]byte, error) { - return json.Marshal(&indexEntryJSON{ - MaxFingerprint: entry.MaxFingerprint.Bytes(), - Attributes: entry.Attributes, - }) -} - -func (entry *IndexEntry) UnmarshalJSON(b []byte) error { - var value indexEntryJSON - err := json.Unmarshal(b, &value) - if err != nil { - return err - } - - var fingerprint Fingerprint - err = fingerprint.UnmarshalBytes(value.MaxFingerprint) - if err != nil { - return err - } - - entry.MaxFingerprint = fingerprint - entry.Attributes = value.Attributes - - return nil -} - -func (entry *IndexEntry) loadThumbnail(path string) error { - thumbnailFile, err := os.Open(path) - if err != nil { - return err - } - defer thumbnailFile.Close() - - entry.Thumbnail, err = png.Decode(thumbnailFile) - return err -} - -func (entry *IndexEntry) saveThumbnail(path string) error { - thumbnailDir := filepath.Dir(path) - os.MkdirAll(thumbnailDir, os.FileMode(0700)) - - thumbnailOut, err := os.Create(path) - if err != nil { - return err - } - defer thumbnailOut.Close() - - pngEncoder := png.Encoder{} - return pngEncoder.Encode(thumbnailOut, entry.Thumbnail) -} - -func NewIndexEntry(image image.Image, maxFingerprintSize int, attributes map[string]interface{}) (*IndexEntry, error) { - entry := &IndexEntry{ - Thumbnail: makeThumbnail(image, maxFingerprintSize*2), - Attributes: attributes, - } - - entry.MaxFingerprint = entry.FingerprintForSize(maxFingerprintSize) - - return entry, nil -} - -func makeThumbnail(src image.Image, size int) image.Image { - width := float64(src.Bounds().Max.X - src.Bounds().Min.X) - height := float64(src.Bounds().Max.Y - src.Bounds().Min.Y) - target := float64(size) - - if width > height { - width /= height / target - height = target - } else { - height /= width / target - width = target - } - - thumbnail := image.NewNRGBA(image.Rect(0, 0, int(width), int(height))) - draw.BiLinear.Scale(thumbnail, thumbnail.Bounds(), src, src.Bounds(), draw.Src, nil) - - return thumbnail -} - -type indexEntryJSON struct { - MaxFingerprint []byte `json:"maxFingerprint"` - Attributes map[string]interface{} `json:"attributes"` -} diff --git a/indexentry_test.go b/indexentry_test.go deleted file mode 100644 index aae46e3..0000000 --- a/indexentry_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package simian - -import ( - "encoding/json" - "reflect" - "testing" -) - -func TestIndexEntry(t *testing.T) { - - t.Run("JSON serialisation", func(t *testing.T) { - - t.Run("should roundtrip all fields", func(t *testing.T) { - - entry := &IndexEntry{ - MaxFingerprint: Fingerprint{samples: []uint8{0xF0, 0xF0, 0xF0, 0xF0}}, - Attributes: make(map[string]interface{}), - } - entry.Attributes["some key"] = "some value" - entry.Attributes["some other key"] = "some other value" - - jsonBytes, err := json.Marshal(entry) - if err != nil { - t.Fatalf("Error marshalling JSON: %v", err) - } - - var result *IndexEntry - err = json.Unmarshal(jsonBytes, &result) - if err != nil { - t.Fatalf("Error unmarshalling JSON: %v", err) - } - - if distance := result.MaxFingerprint.Distance(entry.MaxFingerprint); distance != 0 { - t.Errorf("Expected no difference in fingerprints but got %d", distance) - } - if !reflect.DeepEqual(entry.Attributes, result.Attributes) { - t.Errorf("Expected attributes to match but got %v", result.Attributes) - } - }) - }) -} diff --git a/indexnode.go b/indexnode.go deleted file mode 100644 index 7b572e7..0000000 --- a/indexnode.go +++ /dev/null @@ -1,270 +0,0 @@ -package simian - -import ( - "encoding/json" - "errors" - "fmt" - "math" - "sort" -) - -var errResultLimitReached = errors.New("result limit reached") - -type IndexNode struct { - childFingerprints []Fingerprint - childFingerprintsByString map[string]*Fingerprint - entries []*IndexEntry -} - -func (node *IndexNode) Add(entry *IndexEntry, nodeFingerprint Fingerprint, childFingerprintSize int, index *Index) (*IndexNode, error) { - - fmt.Printf("Node Add %d\n", childFingerprintSize) - - childFingerprint := entry.FingerprintForSize(childFingerprintSize) - - if len(node.childFingerprints) == 0 { - - // We can go deeper and this new entry is sufficiently different to - // the rest, so split this leaf node by turning entries into children. - fmt.Printf("Max Diff: %f\n", node.maxChildDifferenceTo(entry.MaxFingerprint)) - if childFingerprintSize < index.maxFingerprintSize && node.maxChildDifferenceTo(entry.MaxFingerprint) > index.maxEntryDifference { - fmt.Printf("Pushing %d entries to children\n", len(node.entries)) - node.pushEntriesToChildren(nodeFingerprint, childFingerprintSize, index.Store) - fmt.Printf("Done pushing entries to children\n") - - } else { - fmt.Printf("Adding entry %s\n", nodeFingerprint.String()) - err := index.Store.AddEntry(entry, node, nodeFingerprint) - if err != nil { - return nil, err - } - fmt.Printf("Added entry\n") - return node, nil - } - } - - child, err := index.Store.GetOrCreateChild(childFingerprint, node, nodeFingerprint) - if err != nil { - return nil, err - } - - return child.Add(entry, childFingerprint, childFingerprintSize+1, index) -} - -func (node *IndexNode) FindNearest(entry *IndexEntry, childFingerprintSize int, index *Index, maxResults int, maxDifference float64) ([]*IndexEntry, error) { - results := make([]*IndexEntry, 0, maxResults) - - err := node.gatherNearest(entry, childFingerprintSize, index, maxDifference, &results) - if err != nil && err != errResultLimitReached { - return nil, err - } - - return results, nil -} - -func (node *IndexNode) MarshalJSON() ([]byte, error) { - return json.Marshal(&indexNodeJSON{ - ChildFingerprints: node.childFingerprints, - Entries: node.entries, - }) -} - -func (node *IndexNode) UnmarshalJSON(b []byte) error { - var value indexNodeJSON - err := json.Unmarshal(b, &value) - if err != nil { - return err - } - - node.childFingerprints = value.ChildFingerprints - - node.childFingerprintsByString = make(map[string]*Fingerprint) - for i := 0; i < len(node.childFingerprints); i++ { - f := &node.childFingerprints[i] - node.childFingerprintsByString[f.String()] = f - } - - node.entries = value.Entries - - return nil -} - -func (node *IndexNode) addSimilarEntriesTo(entries *[]*IndexEntry, fingerprint Fingerprint, maxDifference float64) error { - fmt.Printf("addSimilarEntriesTo\n") - - return node.withEachEntry(func(entry *IndexEntry) error { - if len(*entries) >= cap(*entries) { - fmt.Printf("Max results hit\n") - return errResultLimitReached - } - - diff := entry.MaxFingerprint.Difference(fingerprint) - if diff <= maxDifference { - fmt.Printf("Found %d of difference %f\n", len(*entries), diff) - *entries = append(*entries, entry) - } else { - fmt.Printf("Max difference hit at %f\n", diff) - return errResultLimitReached - } - - return nil - }) -} - -func (node *IndexNode) gatherNearest(entry *IndexEntry, childFingerprintSize int, index *Index, maxDifference float64, results *[]*IndexEntry) error { - - fmt.Printf("%d gatherNearest %d\n", childFingerprintSize, len(node.entries)) - - // Check for an exact matching child - childFingerprint := entry.FingerprintForSize(childFingerprintSize) - exactChildFingerprint, exactChildFingerprintExists := node.childFingerprintsByString[childFingerprint.String()] - - var exactChildFingerprintString string - var exactChild *IndexNode - if exactChildFingerprintExists { - exactChildFingerprintString = exactChildFingerprint.String() - - var err error - exactChild, err = index.Store.GetChild(childFingerprint, node) - if err != nil { - return err - } - } - - // One exists - recursively search it - if exactChild != nil { - err := exactChild.gatherNearest(entry, childFingerprintSize+1, index, maxDifference, results) - if err != nil { - return err - } - - err = exactChild.addSimilarEntriesTo(results, entry.MaxFingerprint, maxDifference) - if err != nil { - return err - } - } - - childFingerprints := make([]Fingerprint, len(node.childFingerprints)) - copy(childFingerprints, node.childFingerprints) - - // Need more results - find and sort all children by nearness - sort.Sort(nodesByDifferenceToFingerprintWith(childFingerprints, childFingerprint)) - - // fmt.Printf("Sorting %d children...\n", len(children)) - // for i, child := range children { - // diff := child.fingerprint.Difference(entryFingerprint) - // fmt.Printf("%d sorted child %d of %f (%d %d)\n", childFingerprintSize+1, i, diff, len(child.fingerprint.samples), len(entryFingerprint.samples)) - // } - - // Recursively gather from nearest children - for i, cf := range childFingerprints { - fmt.Printf("Visiting child %d\n", i) - if exactChildFingerprintExists && cf.String() == exactChildFingerprintString { - continue - } - - childNode, err := index.Store.GetChild(cf, node) - if err != nil { - return err - } - - err = childNode.gatherNearest(entry, childFingerprintSize+1, index, maxDifference, results) - if err != nil { - return err - } - - err = childNode.addSimilarEntriesTo(results, entry.MaxFingerprint, maxDifference) - if err != nil { - return err - } - } - - return nil -} - -func (node *IndexNode) maxChildDifferenceTo(f Fingerprint) float64 { - maxDifference := 0.0 - - node.withEachEntry(func(entry *IndexEntry) error { - diff := entry.MaxFingerprint.Difference(f) - maxDifference = math.Max(diff, maxDifference) - return nil - }) - - return maxDifference -} - -func (node *IndexNode) pushEntriesToChildren(nodeFingerprint Fingerprint, childFingerprintSize int, store IndexStore) error { - node.withEachEntry(func(entry *IndexEntry) error { - childFingerprint := entry.FingerprintForSize(childFingerprintSize) - child, err := store.GetOrCreateChild(childFingerprint, node, nodeFingerprint) - if err != nil { - return err - } - fmt.Printf("Pushing entry to child\n") - return store.AddEntry(entry, child, childFingerprint) - }) - - return store.RemoveEntries(node, nodeFingerprint) -} - -func (node *IndexNode) registerChild(childFingerprint Fingerprint) { - node.childFingerprints = append(node.childFingerprints, childFingerprint) - node.childFingerprintsByString[childFingerprint.String()] = &node.childFingerprints[len(node.childFingerprints)-1] -} - -func (node *IndexNode) registerEntry(entry *IndexEntry) { - node.entries = append(node.entries, entry) -} - -func (node *IndexNode) removeEntries() { - node.entries = nil -} - -func (node *IndexNode) withEachEntry(action func(*IndexEntry) error) error { - for _, entry := range node.entries { - err := action(entry) - if err != nil { - return err - } - } - - return nil -} - -type indexNodeJSON struct { - ChildFingerprints []Fingerprint `json:"childFingerprints"` - Entries []*IndexEntry `json:"entries"` -} - -type nodesByDifferenceToFingerprint struct { - nodeFingerprints []Fingerprint - differences []float64 -} - -func (sorter *nodesByDifferenceToFingerprint) Len() int { - return len(sorter.nodeFingerprints) -} - -func (sorter *nodesByDifferenceToFingerprint) Less(i, j int) bool { - return sorter.differences[i] < sorter.differences[j] -} - -func (sorter *nodesByDifferenceToFingerprint) Swap(i, j int) { - tmp := sorter.nodeFingerprints[i] - sorter.nodeFingerprints[i] = sorter.nodeFingerprints[j] - sorter.nodeFingerprints[j] = tmp - - tmpDiff := sorter.differences[i] - sorter.differences[i] = sorter.differences[j] - sorter.differences[j] = tmpDiff -} - -func nodesByDifferenceToFingerprintWith(nodeFingerprints []Fingerprint, f Fingerprint) *nodesByDifferenceToFingerprint { - differences := make([]float64, len(nodeFingerprints), len(nodeFingerprints)) - for i, nf := range nodeFingerprints { - differences[i] = nf.Difference(f) - } - - return &nodesByDifferenceToFingerprint{nodeFingerprints: nodeFingerprints, differences: differences} -} diff --git a/indexnode_test.go b/indexnode_test.go deleted file mode 100644 index 75ee02b..0000000 --- a/indexnode_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package simian - -import ( - "encoding/json" - "testing" -) - -func TestIndexNode(t *testing.T) { - - t.Run("JSON serialisation", func(t *testing.T) { - - t.Run("should roundtrip all fields", func(t *testing.T) { - n := &IndexNode{ - childFingerprintsByString: make(map[string]*Fingerprint), - } - - n.registerChild(Fingerprint{samples: []uint8{0x10, 0x20, 0x30, 0x40}}) - n.registerChild(Fingerprint{samples: []uint8{0x50, 0x60, 0x70, 0x80}}) - - entry1 := &IndexEntry{ - MaxFingerprint: Fingerprint{samples: []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9}}, - Attributes: make(map[string]interface{}), - } - n.registerEntry(entry1) - - entry2 := &IndexEntry{ - MaxFingerprint: Fingerprint{samples: []uint8{10, 11, 12, 13, 14, 15, 16, 17, 18}}, - Attributes: make(map[string]interface{}), - } - n.registerEntry(entry2) - - jsonBytes, err := json.Marshal(n) - if err != nil { - t.Fatalf("Error marshalling JSON: %v", err) - } - - var result *IndexNode - err = json.Unmarshal(jsonBytes, &result) - if err != nil { - t.Fatalf("Error unmarshalling JSON: %v", err) - } - - if actual, expected := len(result.childFingerprints), len(n.childFingerprints); actual != expected { - t.Fatalf("Expected %d child fingerprints but got %d", expected, actual) - } - for i := 0; i < len(result.childFingerprints); i++ { - actual := result.childFingerprints[i].String() - expected := n.childFingerprints[i].String() - - if actual != expected { - t.Errorf("Expected fingerprint '%s' but got '%s'", expected, actual) - } - } - - if actual, expected := len(result.childFingerprintsByString), len(n.childFingerprintsByString); actual != expected { - t.Fatalf("Expected %d child fingerprints mapped by string but got %d", expected, actual) - } - for k, v := range n.childFingerprintsByString { - actual := result.childFingerprintsByString[k].String() - expected := v.String() - - if actual != expected { - t.Errorf("Expected fingerprint '%s' but got '%s'", expected, actual) - } - } - - if actual, expected := len(result.entries), len(n.entries); actual != expected { - t.Fatalf("Expected %d entries but got %d", expected, actual) - } - for i := 0; i < len(result.entries); i++ { - actualBytes, err := json.Marshal(result.entries[i]) - if err != nil { - t.Fatalf("Error marshalling entry: %v", err) - } - actual := string(actualBytes) - - expectedBytes, err := json.Marshal(n.entries[i]) - if err != nil { - t.Fatalf("Error marshalling entry: %v", err) - } - expected := string(expectedBytes) - - if actual != expected { - t.Errorf("Expected entry '%s' but got '%s'", expected, actual) - } - } - }) - }) -} diff --git a/indexstore.go b/indexstore.go deleted file mode 100644 index 7df2d02..0000000 --- a/indexstore.go +++ /dev/null @@ -1,10 +0,0 @@ -package simian - -type IndexStore interface { - AddEntry(entry *IndexEntry, node *IndexNode, nodeFingerprint Fingerprint) error - Close() error - GetChild(f Fingerprint, parent *IndexNode) (*IndexNode, error) - GetOrCreateChild(f Fingerprint, parent *IndexNode, parentFingerprint Fingerprint) (*IndexNode, error) - GetRoot() (*IndexNode, error) - RemoveEntries(node *IndexNode, nodeFingerprint Fingerprint) error -} From 6a0ee2eb4084a473e4980a2e8284c2dea2f4fd3c Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Fri, 22 Dec 2017 11:22:01 +1100 Subject: [PATCH 10/16] Move fingerprint creation into Fingerprint. --- fingerprint.go | 139 ++++++++---------------- fingerprint_test.go | 217 ++++--------------------------------- math.go | 33 ------ math_test.go | 53 --------- simian-compare/main.go | 53 ++------- simian-fingerprint/main.go | 47 +------- 6 files changed, 77 insertions(+), 465 deletions(-) delete mode 100644 math_test.go diff --git a/fingerprint.go b/fingerprint.go index 6e9f08e..61a03e0 100644 --- a/fingerprint.go +++ b/fingerprint.go @@ -1,127 +1,82 @@ package simian import ( - "bytes" - "encoding/hex" + "fmt" "image" "image/color" - "math" "golang.org/x/image/draw" ) -const bitsPerSample = 4 -const sampleBitsMask = (1 << bitsPerSample) - 1 -const samplesPerByte = 8 / bitsPerSample +const fingerprintDCTSideLength = 8 +const SamplesPerFingerprint = fingerprintDCTSideLength * fingerprintDCTSideLength -type Fingerprint struct { - samples []uint8 -} +type Fingerprint [SamplesPerFingerprint]int16 -func (f *Fingerprint) Bytes() []byte { - packed := bytes.Buffer{} - current := byte(0) - bits := uint(8) - i := 0 +func FingerprintFromImage(src image.Image) Fingerprint { + scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: fingerprintDCTSideLength, Y: fingerprintDCTSideLength}}) + draw.BiLinear.Scale(scaled, scaled.Bounds(), src, src.Bounds(), draw.Src, nil) - for ; i < len(f.samples); i++ { - y := f.samples[i] + fingerprintSamples := make([]int8, SamplesPerFingerprint) + offset := 0 - bits -= bitsPerSample - current = (current << bitsPerSample) | (y >> (8 - bitsPerSample)) + for i := scaled.Bounds().Min.Y; i < scaled.Bounds().Max.Y; i++ { + for j := scaled.Bounds().Min.X; j < scaled.Bounds().Max.X; j++ { + r, g, b, _ := scaled.At(j, i).RGBA() + y, _, _ := color.RGBToYCbCr(uint8(r>>8), uint8(g>>8), uint8(b>>8)) - if bits == 0 { - packed.WriteByte(current) - current = 0 - bits = 8 + fingerprintSamples[offset] = int8(y - 128) + offset++ } } - if bits < 8 { - current <<= bits - packed.WriteByte(current) - } + dct := DCT(fingerprintDCTSideLength, fingerprintDCTSideLength, fingerprintSamples) - return packed.Bytes() -} - -func (f *Fingerprint) Difference(to Fingerprint) (diff float64) { - return math.Min(float64(f.Distance(to))/float64(len(to.samples)*255), 1.0) -} - -func (f *Fingerprint) Distance(to Fingerprint) (dist uint64) { - if len(f.samples) != len(to.samples) { - return math.MaxUint64 - } - - for i := 0; i < len(f.samples); i++ { - if f.samples[i] > to.samples[i] { - dist += uint64(f.samples[i] - to.samples[i]) + fmt.Printf("DCT:\n") + for i := 0; i < len(dct); i++ { + if i == 0 { + dct[i] >>= 7 } else { - dist += uint64(to.samples[i] - f.samples[i]) + dct[i] = dct[i] >> 5 } - } - - return dist -} - -func (f *Fingerprint) MarshalText() (text []byte, err error) { - bytes := f.Bytes() - result := make([]byte, hex.EncodedLen(len(bytes))) - hex.Encode(result, bytes) - return result, nil -} - -func (f *Fingerprint) Size() int { - return int(math.Sqrt(float64(len(f.samples)))) -} - -func (f Fingerprint) String() string { - return hex.EncodeToString(f.Bytes()) -} - -func (f *Fingerprint) UnmarshalBytes(fingerprintBytes []byte) error { - sampleCount := int(math.Sqrt(float64(len(fingerprintBytes) * samplesPerByte))) - sampleCount *= sampleCount - f.samples = make([]uint8, sampleCount) - - for i := 0; i < sampleCount; i++ { - b := fingerprintBytes[i/samplesPerByte] - shift := uint(8 - bitsPerSample - (i%samplesPerByte)*bitsPerSample) - bits := b >> shift & sampleBitsMask - f.samples[i] = bits << (8 - bitsPerSample) + if i > 0 && i%fingerprintDCTSideLength == 0 { + fmt.Println() + } + fmt.Printf(" %5d", dct[i]) } + fmt.Println() + fmt.Println() - return nil + return dctToFingerprint(dct) } -func (f *Fingerprint) UnmarshalText(text []byte) error { - hexBytes := make([]byte, hex.DecodedLen(len(text))) - _, err := hex.Decode(hexBytes, text) - if err != nil { - return err - } +func dctToFingerprint(squareMatrix []int16) (f Fingerprint) { + level := 0 + offset := 0 - return f.UnmarshalBytes(hexBytes) -} + for i := 0; i != SamplesPerFingerprint; { + if offset == level { -func NewFingerprint(src image.Image, size int) Fingerprint { - scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: size, Y: size}}) - draw.BiLinear.Scale(scaled, scaled.Bounds(), src, src.Bounds(), draw.Src, nil) + // Sample the last corner of the current square + f[i] = squareMatrix[level*fingerprintDCTSideLength+level] + i++ - fingerprintSamples := make([]uint8, size*size) - offset := 0 + // Start the next larger square + offset = 0 + level++ - for i := scaled.Bounds().Min.Y; i < scaled.Bounds().Max.Y; i++ { - for j := scaled.Bounds().Min.X; j < scaled.Bounds().Max.X; j++ { - r, g, b, _ := scaled.At(j, i).RGBA() - y, _, _ := color.RGBToYCbCr(uint8(r>>8), uint8(g>>8), uint8(b>>8)) + } else { + + // Sample one from the right and one from the bottom + f[i] = squareMatrix[offset*fingerprintDCTSideLength+level] + i++ + f[i] = squareMatrix[level*fingerprintDCTSideLength+offset] + i++ - fingerprintSamples[offset] = y & (sampleBitsMask << (8 - bitsPerSample)) offset++ } } - return Fingerprint{samples: fingerprintSamples} + return } diff --git a/fingerprint_test.go b/fingerprint_test.go index 6ff7211..e9946e8 100644 --- a/fingerprint_test.go +++ b/fingerprint_test.go @@ -1,213 +1,36 @@ package simian import ( - "encoding/hex" - "fmt" - "image" - "image/color" - "math" "testing" ) func TestFingerprint(t *testing.T) { - testImage := func() image.Image { - img := image.NewNRGBA(image.Rectangle{Max: image.Point{X: 256, Y: 256}}) - - for i := img.Bounds().Min.Y; i < img.Bounds().Max.Y; i++ { - for j := img.Bounds().Min.X; j < img.Bounds().Max.X; j++ { - img.Set(j, i, color.RGBA{uint8(i), uint8(j), uint8(i), 255}) + t.Run("dctToFingerprint()", func(t *testing.T) { + + t.Run("produces a recursive square traversal of a square 2D matrix", func(t *testing.T) { + m := []int16{ + 0, 1, 4, 9, 16, 25, 36, 49, + 2, 3, 6, 11, 18, 27, 38, 51, + 5, 7, 8, 13, 20, 29, 40, 53, + 10, 12, 14, 15, 22, 31, 42, 55, + 17, 19, 21, 23, 24, 33, 44, 57, + 26, 28, 30, 32, 34, 35, 46, 59, + 37, 39, 41, 43, 45, 47, 48, 61, + 50, 52, 54, 56, 58, 60, 62, 63, } - } - - return img - } - - t.Run("Bytes() serialises to packed bytes", func(t *testing.T) { - f := Fingerprint{samples: []byte{0x00, 0x00, 0xF0, 0xF0}} - - actualString := fmt.Sprintf("%x", f.Bytes()) - - if actualString != "00ff" { - t.Errorf("Fingerprint '%s' doesn't match expected", actualString) - } - }) - - t.Run("Difference() returns zero for same fingerprint", func(t *testing.T) { - f1 := Fingerprint{samples: []byte{0, 1, 2, 3, 130, 255}} - f2 := Fingerprint{samples: []byte{0, 1, 2, 3, 130, 255}} - - diff := f1.Difference(f2) - - if diff != 0.0 { - t.Errorf("Difference %f doesn't match expected", diff) - } - - diff = f2.Difference(f1) - - if diff != 0.0 { - t.Errorf("Difference %f doesn't match expected", diff) - } - }) - - t.Run("Difference() returns one for completely different fingerprint", func(t *testing.T) { - f1 := Fingerprint{samples: []byte{0, 0, 0, 255, 255, 255}} - f2 := Fingerprint{samples: []byte{255, 255, 255, 0, 0, 0}} - - diff := f1.Difference(f2) - - if diff != 1.0 { - t.Errorf("Difference %f doesn't match expected", diff) - } - - diff = f2.Difference(f1) - - if diff != 1.0 { - t.Errorf("Difference %f doesn't match expected", diff) - } - }) - - t.Run("Difference() returns one for differently sized fingerprint", func(t *testing.T) { - f1 := Fingerprint{samples: []byte{255, 255, 255}} - f2 := Fingerprint{samples: []byte{255, 255, 255, 255}} - - diff := f1.Difference(f2) - - if diff != 1.0 { - t.Errorf("Difference %f doesn't match expected", diff) - } - - diff = f2.Difference(f1) - - if diff != 1.0 { - t.Errorf("Difference %f doesn't match expected", diff) - } - }) - - t.Run("Distance() returns componentwise absolute difference", func(t *testing.T) { - f1 := Fingerprint{samples: []byte{0, 1, 2, 3, 130, 255}} - f2 := Fingerprint{samples: []byte{1, 3, 6, 11, 146, 0}} - - dist := f1.Distance(f2) - - if dist != 286 { - t.Errorf("Distance %d doesn't match expected", dist) - } - - dist = f2.Distance(f1) - - if dist != 286 { - t.Errorf("Distance %d doesn't match expected", dist) - } - }) - t.Run("Distance() returns max value for mismatched length", func(t *testing.T) { - f1 := Fingerprint{samples: []byte{0, 0, 0}} - f2 := Fingerprint{samples: []byte{0, 0, 0, 0}} + result := dctToFingerprint(m) - dist := f1.Distance(f2) - - if dist != math.MaxUint64 { - t.Errorf("Distance %d wasn't max uint64", dist) - } - }) - - t.Run("MarshalText() serialises to packed hex string bytes", func(t *testing.T) { - f := Fingerprint{samples: []byte{0x00, 0x00, 0xFF, 0xFF}} - - actual, err := f.MarshalText() - - if err != nil { - t.Errorf("Error while marshalling: %s", err) - } - if string(actual) != "00ff" { - t.Errorf("Fingerprint '%s' doesn't match expected", actual) - } - }) - - t.Run("Size() returns correct side length", func(t *testing.T) { - img := testImage() - - f := NewFingerprint(img, 3) - size := f.Size() - - if size != 3 { - t.Errorf("Size %d doesn't match expected", size) - } - - f = NewFingerprint(img, 7) - size = f.Size() - - if size != 7 { - t.Errorf("Size %d doesn't match expected", size) - } - - f = Fingerprint{samples: make([]byte, 5*5)} - size = f.Size() - - if size != 5 { - t.Errorf("Size %d doesn't match expected", size) - } - }) - - t.Run("String() serialises to packed hex string", func(t *testing.T) { - f := Fingerprint{samples: []byte{ - 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, - 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, - 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, - 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, - 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, - }} - - actualString := fmt.Sprintf("%s", f) - - if actualString != "fffffffffffffffffffffffff0" { - t.Errorf("Fingerprint '%s' doesn't match expected", actualString) - } - }) - - t.Run("UnmarshalBytes() deserialises from packed bytes", func(t *testing.T) { - b := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0} - - f := Fingerprint{} - f.UnmarshalBytes(b) - - if len(f.samples) != 25 { - t.Fatalf("Fingerprint length %d doesn't match expected", len(f.samples)) - } - for i := 0; i < 25; i++ { - if f.samples[i] != 0xF0 { - t.Errorf("Fingerprint byte '%d' doesn't match expected", f.samples[i]) + if expected, actual := len(m), len(result); expected != actual { + t.Fatalf("Expected result to be of length %d but got %d", expected, actual) } - } - }) - t.Run("UnmarshalText() deserialises from packed hex string bytes", func(t *testing.T) { - text := []byte("fffffffffffffffffffffffff0") - - f := Fingerprint{} - f.UnmarshalText(text) - - if len(f.samples) != 25 { - t.Fatalf("Fingerprint length %d doesn't match expected", len(f.samples)) - } - for i := 0; i < 25; i++ { - if f.samples[i] != 0xF0 { - t.Errorf("Fingerprint byte '%d' doesn't match expected", f.samples[i]) + for i := 0; i < len(result); i++ { + if result[i] != int16(i) { + t.Errorf("Expected element %d but got %d", i, result[i]) + } } - } - }) - - t.Run("NewFingerprint() generates binary representation", func(t *testing.T) { - f := NewFingerprint(testImage(), 3) - - expected, _ := hex.DecodeString("3060805080a070a0c0") - - expectedString := hex.EncodeToString(expected) - actualString := hex.EncodeToString(f.samples) - - if expectedString != actualString { - t.Fatalf("Fingerprint '%s' doesn't match expected '%s'", actualString, expectedString) - } + }) }) } diff --git a/math.go b/math.go index 414c453..c7025e8 100644 --- a/math.go +++ b/math.go @@ -30,36 +30,3 @@ func DCT(width int, height int, values []int8) (result []int16) { return } - -func FlattenRecursiveSquares(squareMatrix []int16) []int16 { - sideLength := int(math.Sqrt(float64(len(squareMatrix)))) - result := make([]int16, sideLength*sideLength) - - level := 0 - offset := 0 - - for i := 0; i != len(result); { - if offset == level { - - // Sample the last corner of the current square - result[i] = squareMatrix[level*sideLength+level] - i++ - - // Start the next larger square - offset = 0 - level++ - - } else { - - // Sample one from the right and one from the bottom - result[i] = squareMatrix[offset*sideLength+level] - i++ - result[i] = squareMatrix[level*sideLength+offset] - i++ - - offset++ - } - } - - return result -} diff --git a/math_test.go b/math_test.go deleted file mode 100644 index 44aef10..0000000 --- a/math_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package simian - -import "testing" - -func TestMath(t *testing.T) { - - t.Run("FlattenRecursiveSquares()", func(t *testing.T) { - - t.Run("produces a recursive square traversal of a square 2D matrix", func(t *testing.T) { - m := []int16{ - 0, 1, 4, - 2, 3, 6, - 5, 7, 8, - } - - result := FlattenRecursiveSquares(m) - - if expected, actual := len(m), len(result); expected != actual { - t.Fatalf("Expected result to be of length %d but got %d", expected, actual) - } - - for i := 0; i < len(result); i++ { - if result[i] != int16(i) { - t.Errorf("Expected element %d but got %d", i, result[i]) - } - } - }) - - t.Run("produces an identity result for a 1x1 matrix", func(t *testing.T) { - m := []int16{ - 7, - } - - result := FlattenRecursiveSquares(m) - - if expected, actual := len(m), len(result); expected != actual { - t.Fatalf("Expected result to be of length %d but got %d", expected, actual) - } - - if result[0] != m[0] { - t.Errorf("Expected element %d but got %d", m[0], result[0]) - } - }) - - t.Run("produces an empty result for empty input", func(t *testing.T) { - result := FlattenRecursiveSquares([]int16{}) - - if actual := len(result); actual != 0 { - t.Fatalf("Expected zero length result but got %d", actual) - } - }) - }) -} diff --git a/simian-compare/main.go b/simian-compare/main.go index 4f2b8ae..82dd51c 100644 --- a/simian-compare/main.go +++ b/simian-compare/main.go @@ -3,64 +3,29 @@ package main import ( "fmt" "image" - "image/color" _ "image/jpeg" _ "image/png" "math" "os" "github.com/mandykoh/simian" - "golang.org/x/image/draw" ) -const fingerprintDCTSideLength = 8 -const samplesPerFingerprint = fingerprintDCTSideLength * fingerprintDCTSideLength - -func makeFingerprint(src image.Image) []int16 { - scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: fingerprintDCTSideLength, Y: fingerprintDCTSideLength}}) - draw.BiLinear.Scale(scaled, scaled.Bounds(), src, src.Bounds(), draw.Src, nil) - - fingerprintSamples := make([]int8, samplesPerFingerprint) - offset := 0 - - for i := scaled.Bounds().Min.Y; i < scaled.Bounds().Max.Y; i++ { - for j := scaled.Bounds().Min.X; j < scaled.Bounds().Max.X; j++ { - r, g, b, _ := scaled.At(j, i).RGBA() - y, _, _ := color.RGBToYCbCr(uint8(r>>8), uint8(g>>8), uint8(b>>8)) - - fingerprintSamples[offset] = int8(y - 128) - offset++ - } - } - - dct := simian.DCT(fingerprintDCTSideLength, fingerprintDCTSideLength, fingerprintSamples) - - for i := 0; i < len(dct); i++ { - if i == 0 { - dct[i] >>= 7 - } else { - dct[i] = dct[i] >> 5 - } - } - - fingerprint := simian.FlattenRecursiveSquares(dct) - - return fingerprint -} - -func makeFingerprintFromImageFile(imageFileName string) ([]int16, error) { - imageFile, err := os.Open(imageFileName) +func makeFingerprintFromImageFile(imageFileName string) (f simian.Fingerprint, err error) { + var imageFile *os.File + imageFile, err = os.Open(imageFileName) if err != nil { - return nil, err + return } defer imageFile.Close() - img, _, err := image.Decode(imageFile) + var img image.Image + img, _, err = image.Decode(imageFile) if err != nil { - return nil, err + return } - return makeFingerprint(img), nil + return simian.FingerprintFromImage(img), nil } func main() { @@ -85,7 +50,7 @@ func main() { for i := 0; i < len(fingerprint1); i++ { difference += math.Abs(float64(fingerprint1[i] - fingerprint2[i])) } - difference /= samplesPerFingerprint * 12 + difference /= float64(len(fingerprint1) * 12) var judgment string switch { diff --git a/simian-fingerprint/main.go b/simian-fingerprint/main.go index 3bae9c4..a8f266d 100644 --- a/simian-fingerprint/main.go +++ b/simian-fingerprint/main.go @@ -3,58 +3,13 @@ package main import ( "fmt" "image" - "image/color" _ "image/jpeg" _ "image/png" "os" "github.com/mandykoh/simian" - "golang.org/x/image/draw" ) -const fingerprintDCTSideLength = 8 -const samplesPerFingerprint = fingerprintDCTSideLength * fingerprintDCTSideLength - -func makeFingerprint(src image.Image) []int16 { - scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: fingerprintDCTSideLength, Y: fingerprintDCTSideLength}}) - draw.BiLinear.Scale(scaled, scaled.Bounds(), src, src.Bounds(), draw.Src, nil) - - fingerprintSamples := make([]int8, samplesPerFingerprint) - offset := 0 - - for i := scaled.Bounds().Min.Y; i < scaled.Bounds().Max.Y; i++ { - for j := scaled.Bounds().Min.X; j < scaled.Bounds().Max.X; j++ { - r, g, b, _ := scaled.At(j, i).RGBA() - y, _, _ := color.RGBToYCbCr(uint8(r>>8), uint8(g>>8), uint8(b>>8)) - - fingerprintSamples[offset] = int8(y - 128) - offset++ - } - } - - dct := simian.DCT(fingerprintDCTSideLength, fingerprintDCTSideLength, fingerprintSamples) - - fmt.Printf("DCT:\n") - for i := 0; i < len(dct); i++ { - if i == 0 { - dct[i] >>= 7 - } else { - dct[i] = dct[i] >> 5 - } - - if i > 0 && i%fingerprintDCTSideLength == 0 { - fmt.Println() - } - fmt.Printf(" %5d", dct[i]) - } - fmt.Println() - fmt.Println() - - fingerprint := simian.FlattenRecursiveSquares(dct) - - return fingerprint -} - func main() { if len(os.Args) < 2 { fmt.Printf("Usage: simian-fingerprint \n") @@ -74,7 +29,7 @@ func main() { return } - fingerprint := makeFingerprint(img) + fingerprint := simian.FingerprintFromImage(img) for i := 0; i < len(fingerprint); i++ { fmt.Printf("%02x", fingerprint[i]+128) From a24226caff46170db4f68852f7dc1ea83038cbf3 Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Fri, 22 Dec 2017 12:16:18 +1100 Subject: [PATCH 11/16] Add tests for fingerprint construction. --- fingerprint.go | 13 ++++++---- fingerprint_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/fingerprint.go b/fingerprint.go index 61a03e0..5d88157 100644 --- a/fingerprint.go +++ b/fingerprint.go @@ -9,6 +9,9 @@ import ( ) const fingerprintDCTSideLength = 8 +const fingerprintACShift = 7 +const fingerprintDCShift = 5 + const SamplesPerFingerprint = fingerprintDCTSideLength * fingerprintDCTSideLength type Fingerprint [SamplesPerFingerprint]int16 @@ -17,7 +20,7 @@ func FingerprintFromImage(src image.Image) Fingerprint { scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: fingerprintDCTSideLength, Y: fingerprintDCTSideLength}}) draw.BiLinear.Scale(scaled, scaled.Bounds(), src, src.Bounds(), draw.Src, nil) - fingerprintSamples := make([]int8, SamplesPerFingerprint) + samples := make([]int8, SamplesPerFingerprint) offset := 0 for i := scaled.Bounds().Min.Y; i < scaled.Bounds().Max.Y; i++ { @@ -25,19 +28,19 @@ func FingerprintFromImage(src image.Image) Fingerprint { r, g, b, _ := scaled.At(j, i).RGBA() y, _, _ := color.RGBToYCbCr(uint8(r>>8), uint8(g>>8), uint8(b>>8)) - fingerprintSamples[offset] = int8(y - 128) + samples[offset] = int8(y - 128) offset++ } } - dct := DCT(fingerprintDCTSideLength, fingerprintDCTSideLength, fingerprintSamples) + dct := DCT(fingerprintDCTSideLength, fingerprintDCTSideLength, samples) fmt.Printf("DCT:\n") for i := 0; i < len(dct); i++ { if i == 0 { - dct[i] >>= 7 + dct[i] >>= fingerprintACShift } else { - dct[i] = dct[i] >> 5 + dct[i] = dct[i] >> fingerprintDCShift } if i > 0 && i%fingerprintDCTSideLength == 0 { diff --git a/fingerprint_test.go b/fingerprint_test.go index e9946e8..d05f031 100644 --- a/fingerprint_test.go +++ b/fingerprint_test.go @@ -1,6 +1,8 @@ package simian import ( + "image" + "image/color" "testing" ) @@ -33,4 +35,63 @@ func TestFingerprint(t *testing.T) { } }) }) + + t.Run("FingerprintFromImage()", func(t *testing.T) { + + t.Run("should product correct fingerprint from DCT of white image", func(t *testing.T) { + img := image.NewNRGBA(image.Rectangle{Max: image.Point{X: 256, Y: 256}}) + for i := img.Bounds().Min.Y; i < img.Bounds().Max.Y; i++ { + for j := img.Bounds().Min.X; j < img.Bounds().Max.X; j++ { + img.Set(j, i, color.RGBA{uint8(255), uint8(255), uint8(255), 255}) + } + } + + f := FingerprintFromImage(img) + + if expected, actual := int16(8064>>fingerprintACShift), f[0]; actual != expected { + t.Errorf("Expected value %d but found %d at position 0", expected, actual) + } + + for i := 1; i < len(f); i++ { + if expected, actual := int16(0), f[i]; actual != expected { + t.Errorf("Expected value %d but found %d at position %d", expected, actual, i) + } + } + }) + + t.Run("should product correct fingerprint from DCT of checkered image", func(t *testing.T) { + img := image.NewNRGBA(image.Rectangle{Max: image.Point{X: fingerprintDCTSideLength, Y: fingerprintDCTSideLength}}) + offset := 0 + for i := img.Bounds().Min.Y; i < img.Bounds().Max.Y; i++ { + for j := img.Bounds().Min.X; j < img.Bounds().Max.X; j++ { + if offset%2 == 0 { + img.Set(j, i, color.RGBA{uint8(255), uint8(255), uint8(255), 255}) + } else { + img.Set(j, i, color.RGBA{uint8(0), uint8(0), uint8(0), 255}) + } + offset++ + } + offset++ + } + + f := FingerprintFromImage(img) + + expected := Fingerprint{ + -1, 0, 0, 4, 0, 0, 0, 0, + 0, 0, 0, 4, 4, 0, 0, 5, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 7, 7, 0, 0, 8, + 8, 0, 0, 12, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 20, 20, 0, 0, 24, + 24, 0, 0, 36, 36, 0, 0, 104, + } + + for i := 0; i < len(expected); i++ { + if expected[i] != f[i] { + t.Errorf("Expected value %d but found %d at position %d", expected[i], f[i], i) + } + } + }) + }) } From 0576471de5df1b7371b280786cf6665e941225bf Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Fri, 22 Dec 2017 13:42:12 +1100 Subject: [PATCH 12/16] Implement comparing fingerprints. --- fingerprint.go | 16 +++++++++++++-- fingerprint_test.go | 40 ++++++++++++++++++++++++++++++++++++-- simian-compare/main.go | 11 +++-------- simian-fingerprint/main.go | 2 +- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/fingerprint.go b/fingerprint.go index 5d88157..0df18a8 100644 --- a/fingerprint.go +++ b/fingerprint.go @@ -4,6 +4,7 @@ import ( "fmt" "image" "image/color" + "math" "golang.org/x/image/draw" ) @@ -16,7 +17,16 @@ const SamplesPerFingerprint = fingerprintDCTSideLength * fingerprintDCTSideLengt type Fingerprint [SamplesPerFingerprint]int16 -func FingerprintFromImage(src image.Image) Fingerprint { +func (f *Fingerprint) Difference(other *Fingerprint) float64 { + result := 0.0 + for i := 0; i < SamplesPerFingerprint; i++ { + result += math.Abs(float64(f[i] - other[i])) + } + + return result / float64(SamplesPerFingerprint*12) +} + +func NewFingerprintFromImage(src image.Image) *Fingerprint { scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: fingerprintDCTSideLength, Y: fingerprintDCTSideLength}}) draw.BiLinear.Scale(scaled, scaled.Bounds(), src, src.Bounds(), draw.Src, nil) @@ -54,7 +64,9 @@ func FingerprintFromImage(src image.Image) Fingerprint { return dctToFingerprint(dct) } -func dctToFingerprint(squareMatrix []int16) (f Fingerprint) { +func dctToFingerprint(squareMatrix []int16) (f *Fingerprint) { + f = &Fingerprint{} + level := 0 offset := 0 diff --git a/fingerprint_test.go b/fingerprint_test.go index d05f031..5722796 100644 --- a/fingerprint_test.go +++ b/fingerprint_test.go @@ -3,11 +3,23 @@ package simian import ( "image" "image/color" + "math/rand" "testing" ) func TestFingerprint(t *testing.T) { + randomImage := func() image.Image { + img := image.NewNRGBA(image.Rectangle{Max: image.Point{X: 256, Y: 256}}) + for i := img.Bounds().Min.Y; i < img.Bounds().Max.Y; i++ { + for j := img.Bounds().Min.X; j < img.Bounds().Max.X; j++ { + img.Set(j, i, color.RGBA{uint8(rand.Int()), uint8(rand.Int()), uint8(rand.Int()), 255}) + } + } + + return img + } + t.Run("dctToFingerprint()", func(t *testing.T) { t.Run("produces a recursive square traversal of a square 2D matrix", func(t *testing.T) { @@ -36,6 +48,30 @@ func TestFingerprint(t *testing.T) { }) }) + t.Run("Difference()", func(t *testing.T) { + + t.Run("returns 0.0 for an exact match", func(t *testing.T) { + f := NewFingerprintFromImage(randomImage()) + + difference := f.Difference(f) + + if difference > 0.00001 { + t.Errorf("Expected no difference but got %f", difference) + } + }) + + t.Run("returns higher than 0.0 for different images", func(t *testing.T) { + f1 := NewFingerprintFromImage(randomImage()) + f2 := NewFingerprintFromImage(randomImage()) + + difference := f1.Difference(f2) + + if difference <= 0.001 { + t.Errorf("Expected some difference but got %f", difference) + } + }) + }) + t.Run("FingerprintFromImage()", func(t *testing.T) { t.Run("should product correct fingerprint from DCT of white image", func(t *testing.T) { @@ -46,7 +82,7 @@ func TestFingerprint(t *testing.T) { } } - f := FingerprintFromImage(img) + f := NewFingerprintFromImage(img) if expected, actual := int16(8064>>fingerprintACShift), f[0]; actual != expected { t.Errorf("Expected value %d but found %d at position 0", expected, actual) @@ -74,7 +110,7 @@ func TestFingerprint(t *testing.T) { offset++ } - f := FingerprintFromImage(img) + f := NewFingerprintFromImage(img) expected := Fingerprint{ -1, 0, 0, 4, 0, 0, 0, 0, diff --git a/simian-compare/main.go b/simian-compare/main.go index 82dd51c..eda4372 100644 --- a/simian-compare/main.go +++ b/simian-compare/main.go @@ -5,13 +5,12 @@ import ( "image" _ "image/jpeg" _ "image/png" - "math" "os" "github.com/mandykoh/simian" ) -func makeFingerprintFromImageFile(imageFileName string) (f simian.Fingerprint, err error) { +func makeFingerprintFromImageFile(imageFileName string) (f *simian.Fingerprint, err error) { var imageFile *os.File imageFile, err = os.Open(imageFileName) if err != nil { @@ -25,7 +24,7 @@ func makeFingerprintFromImageFile(imageFileName string) (f simian.Fingerprint, e return } - return simian.FingerprintFromImage(img), nil + return simian.NewFingerprintFromImage(img), nil } func main() { @@ -46,11 +45,7 @@ func main() { return } - difference := 0.0 - for i := 0; i < len(fingerprint1); i++ { - difference += math.Abs(float64(fingerprint1[i] - fingerprint2[i])) - } - difference /= float64(len(fingerprint1) * 12) + difference := fingerprint1.Difference(fingerprint2) var judgment string switch { diff --git a/simian-fingerprint/main.go b/simian-fingerprint/main.go index a8f266d..7804acf 100644 --- a/simian-fingerprint/main.go +++ b/simian-fingerprint/main.go @@ -29,7 +29,7 @@ func main() { return } - fingerprint := simian.FingerprintFromImage(img) + fingerprint := simian.NewFingerprintFromImage(img) for i := 0; i < len(fingerprint); i++ { fmt.Printf("%02x", fingerprint[i]+128) From 6d145ee9c1808c9fc1636c1d65156f20099866ee Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Fri, 22 Dec 2017 14:29:49 +1100 Subject: [PATCH 13/16] Provide a way to get fingerprint prefixes for each level. --- fingerprint.go | 4 ++++ fingerprint_test.go | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/fingerprint.go b/fingerprint.go index 0df18a8..eef50d9 100644 --- a/fingerprint.go +++ b/fingerprint.go @@ -26,6 +26,10 @@ func (f *Fingerprint) Difference(other *Fingerprint) float64 { return result / float64(SamplesPerFingerprint*12) } +func (f *Fingerprint) Prefix(level int) []int16 { + return f[:level*level] +} + func NewFingerprintFromImage(src image.Image) *Fingerprint { scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: fingerprintDCTSideLength, Y: fingerprintDCTSideLength}}) draw.BiLinear.Scale(scaled, scaled.Bounds(), src, src.Bounds(), draw.Src, nil) diff --git a/fingerprint_test.go b/fingerprint_test.go index 5722796..8bda355 100644 --- a/fingerprint_test.go +++ b/fingerprint_test.go @@ -130,4 +130,27 @@ func TestFingerprint(t *testing.T) { } }) }) + + t.Run("Prefix()", func(t *testing.T) { + + t.Run("returns correct prefix for each level", func(t *testing.T) { + f := NewFingerprintFromImage(randomImage()) + + for level := 0; level < fingerprintDCTSideLength; level++ { + prefix := f.Prefix(level) + expectedPrefix := f[:level*level] + + if expected, actual := len(expectedPrefix), len(prefix); actual != expected { + t.Errorf("Expected length %d but got prefix of length %d", expected, actual) + + } else { + for i := 0; i < len(expectedPrefix); i++ { + if expected, actual := expectedPrefix[i], prefix[i]; actual != expected { + t.Errorf("Expected %d but got prefix value %d", expected, actual) + } + } + } + } + }) + }) } From b003e8a44446aae93c28545878dea1a1b4c50ccb Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Fri, 22 Dec 2017 15:53:25 +1100 Subject: [PATCH 14/16] Scale DC coefficients to the full dynamic range. --- fingerprint.go | 28 +++++++++++++++++++++------- fingerprint_test.go | 12 ++++++------ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/fingerprint.go b/fingerprint.go index eef50d9..7408566 100644 --- a/fingerprint.go +++ b/fingerprint.go @@ -11,7 +11,6 @@ import ( const fingerprintDCTSideLength = 8 const fingerprintACShift = 7 -const fingerprintDCShift = 5 const SamplesPerFingerprint = fingerprintDCTSideLength * fingerprintDCTSideLength @@ -23,7 +22,7 @@ func (f *Fingerprint) Difference(other *Fingerprint) float64 { result += math.Abs(float64(f[i] - other[i])) } - return result / float64(SamplesPerFingerprint*12) + return result / float64(SamplesPerFingerprint*22) } func (f *Fingerprint) Prefix(level int) []int16 { @@ -42,19 +41,34 @@ func NewFingerprintFromImage(src image.Image) *Fingerprint { r, g, b, _ := scaled.At(j, i).RGBA() y, _, _ := color.RGBToYCbCr(uint8(r>>8), uint8(g>>8), uint8(b>>8)) - samples[offset] = int8(y - 128) + val := int8(y - 128) + samples[offset] = val offset++ } } dct := DCT(fingerprintDCTSideLength, fingerprintDCTSideLength, samples) + min := int16(math.MaxInt16) + max := int16(math.MinInt16) + + for i := 1; i < len(dct); i++ { + if dct[i] < min { + min = dct[i] + } + if dct[i] > max { + max = dct[i] + } + } + + scale := 127.0 / float64(max-min) / 2.0 + fmt.Printf("DCT:\n") + + dct[0] >>= fingerprintACShift for i := 0; i < len(dct); i++ { - if i == 0 { - dct[i] >>= fingerprintACShift - } else { - dct[i] = dct[i] >> fingerprintDCShift + if i != 0 { + dct[i] = int16(float64(dct[i]) * scale) } if i > 0 && i%fingerprintDCTSideLength == 0 { diff --git a/fingerprint_test.go b/fingerprint_test.go index 8bda355..697c7e8 100644 --- a/fingerprint_test.go +++ b/fingerprint_test.go @@ -113,14 +113,14 @@ func TestFingerprint(t *testing.T) { f := NewFingerprintFromImage(img) expected := Fingerprint{ - -1, 0, 0, 4, 0, 0, 0, 0, - 0, 0, 0, 4, 4, 0, 0, 5, + -1, 0, 0, 2, 0, 0, 0, 0, + 0, 0, 0, 2, 2, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 7, 7, 0, 0, 8, - 8, 0, 0, 12, 0, 0, 0, 0, + 0, 0, 0, 4, 4, 0, 0, 5, + 5, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 20, 20, 0, 0, 24, - 24, 0, 0, 36, 36, 0, 0, 104, + 0, 0, 0, 12, 12, 0, 0, 14, + 14, 0, 0, 22, 22, 0, 0, 63, } for i := 0; i < len(expected); i++ { From c31ef845e1e85e7176b8feb28205dc72b30b51bc Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Fri, 22 Dec 2017 16:56:30 +1100 Subject: [PATCH 15/16] Add comments. --- fingerprint.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fingerprint.go b/fingerprint.go index 7408566..2f78288 100644 --- a/fingerprint.go +++ b/fingerprint.go @@ -36,6 +36,7 @@ func NewFingerprintFromImage(src image.Image) *Fingerprint { samples := make([]int8, SamplesPerFingerprint) offset := 0 + // Sample from RGBA pixel values for i := scaled.Bounds().Min.Y; i < scaled.Bounds().Max.Y; i++ { for j := scaled.Bounds().Min.X; j < scaled.Bounds().Max.X; j++ { r, g, b, _ := scaled.At(j, i).RGBA() @@ -52,6 +53,7 @@ func NewFingerprintFromImage(src image.Image) *Fingerprint { min := int16(math.MaxInt16) max := int16(math.MinInt16) + // Find the dynamic range for DC coefficients for i := 1; i < len(dct); i++ { if dct[i] < min { min = dct[i] @@ -65,7 +67,10 @@ func NewFingerprintFromImage(src image.Image) *Fingerprint { fmt.Printf("DCT:\n") + // Scale AC coefficient down by fixed amount dct[0] >>= fingerprintACShift + + // Scale DC coefficients down according to dynamic range for i := 0; i < len(dct); i++ { if i != 0 { dct[i] = int16(float64(dct[i]) * scale) From f2cd2e0011f11779eb010415a41ddc3ff78b136e Mon Sep 17 00:00:00 2001 From: Amanda Koh Date: Fri, 22 Dec 2017 17:17:02 +1100 Subject: [PATCH 16/16] Use a constant. --- fingerprint.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fingerprint.go b/fingerprint.go index 2f78288..43cfe2b 100644 --- a/fingerprint.go +++ b/fingerprint.go @@ -11,6 +11,7 @@ import ( const fingerprintDCTSideLength = 8 const fingerprintACShift = 7 +const fingerprintDifferenceScale = 22 const SamplesPerFingerprint = fingerprintDCTSideLength * fingerprintDCTSideLength @@ -22,7 +23,7 @@ func (f *Fingerprint) Difference(other *Fingerprint) float64 { result += math.Abs(float64(f[i] - other[i])) } - return result / float64(SamplesPerFingerprint*22) + return result / float64(SamplesPerFingerprint*fingerprintDifferenceScale) } func (f *Fingerprint) Prefix(level int) []int16 {