├── README.md ├── testdata ├── soccerball.jpg ├── soccerball (copy).jpg ├── soccerball (cropped).jpg ├── soccerball (modifications).jpg ├── soccerball (perspective).jpg └── soccerball (scaled down).jpg ├── .gitignore ├── imageprocess.go ├── matrixutil.go ├── LICENSE ├── phash.go ├── matrixutil_test.go ├── dct.go └── phash_test.go /README.md: -------------------------------------------------------------------------------- 1 | # phash 2 | A simple Go Library to calculate a phash string for a JPEG image. 3 | -------------------------------------------------------------------------------- /testdata/soccerball.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlogit/phash/HEAD/testdata/soccerball.jpg -------------------------------------------------------------------------------- /testdata/soccerball (copy).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlogit/phash/HEAD/testdata/soccerball (copy).jpg -------------------------------------------------------------------------------- /testdata/soccerball (cropped).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlogit/phash/HEAD/testdata/soccerball (cropped).jpg -------------------------------------------------------------------------------- /testdata/soccerball (modifications).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlogit/phash/HEAD/testdata/soccerball (modifications).jpg -------------------------------------------------------------------------------- /testdata/soccerball (perspective).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlogit/phash/HEAD/testdata/soccerball (perspective).jpg -------------------------------------------------------------------------------- /testdata/soccerball (scaled down).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlogit/phash/HEAD/testdata/soccerball (scaled down).jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /imageprocess.go: -------------------------------------------------------------------------------- 1 | package phash 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | func getImageMatrix(img image.Image) [][]float64 { 8 | xSize := img.Bounds().Max.X 9 | ySize := img.Bounds().Max.Y 10 | 11 | vals := make([][]float64, xSize) 12 | 13 | for x := 0; x < xSize; x++ { 14 | vals[x] = make([]float64, ySize) 15 | for y := 0; y < ySize; y++ { 16 | vals[x][y] = getXYValue(img, x, y) 17 | } 18 | } 19 | 20 | return vals 21 | } 22 | 23 | func getXYValue(img image.Image, x int, y int) float64 { 24 | _, _, b, _ := img.At(x, y).RGBA() 25 | return float64(b) 26 | } 27 | -------------------------------------------------------------------------------- /matrixutil.go: -------------------------------------------------------------------------------- 1 | package phash 2 | 3 | func reduceMatrix(dctMatrix [][]float64, size int) [][]float64 { 4 | newMatrix := make([][]float64, size) 5 | for x := 0; x < size; x++ { 6 | newMatrix[x] = make([]float64, size) 7 | for y := 0; y < size; y++ { 8 | newMatrix[x][y] = dctMatrix[x][y] 9 | } 10 | } 11 | 12 | return newMatrix 13 | } 14 | 15 | 16 | func calculateMeanValue(dctMatrix [][]float64) float64 { 17 | var total float64 18 | var xSize = len(dctMatrix) 19 | var ySize = len(dctMatrix[0]) 20 | 21 | for x := 0; x < xSize; x++ { 22 | for y := 0; y < ySize; y++ { 23 | total += dctMatrix[x][y] 24 | } 25 | } 26 | 27 | total -= dctMatrix[0][0] 28 | 29 | avg := total / float64((xSize * ySize) - 1) 30 | 31 | return avg 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 carlogit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /phash.go: -------------------------------------------------------------------------------- 1 | // Package phash computes a phash string for a JPEG image and retrieves 2 | // the hamming distance between two phash strings. 3 | package phash 4 | 5 | import ( 6 | "io" 7 | 8 | "github.com/disintegration/imaging" 9 | ) 10 | 11 | // GetHash returns a phash string for a JPEG image 12 | func GetHash(reader io.Reader) (string, error) { 13 | image, err := imaging.Decode(reader) 14 | 15 | if err != nil { 16 | return "", err 17 | } 18 | 19 | image = imaging.Resize(image, 32, 32, imaging.Lanczos) 20 | image = imaging.Grayscale(image) 21 | 22 | imageMatrixData := getImageMatrix(image) 23 | dctMatrix := getDCTMatrix(imageMatrixData) 24 | 25 | smallDctMatrix := reduceMatrix(dctMatrix, 8) 26 | dctMeanValue := calculateMeanValue(smallDctMatrix) 27 | return buildHash(smallDctMatrix, dctMeanValue), nil 28 | } 29 | 30 | // GetDistance returns the hamming distance between two phashes 31 | func GetDistance(hash1, hash2 string) int { 32 | distance := 0 33 | for i := 0; i < len(hash1); i++ { 34 | if hash1[i] != hash2[i] { 35 | distance++ 36 | } 37 | } 38 | 39 | return distance 40 | } 41 | 42 | func buildHash(dctMatrix [][]float64, dctMeanValue float64) string { 43 | var hash string 44 | var xSize = len(dctMatrix) 45 | var ySize = len(dctMatrix[0]) 46 | 47 | for x := 0; x < xSize; x++ { 48 | for y := 0; y < ySize; y++ { 49 | if dctMatrix[x][y] > dctMeanValue { 50 | hash += "1" 51 | } else { 52 | hash += "0" 53 | } 54 | } 55 | } 56 | 57 | return hash 58 | } 59 | -------------------------------------------------------------------------------- /matrixutil_test.go: -------------------------------------------------------------------------------- 1 | package phash 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestReduceMatrix(t *testing.T) { 8 | matrix := make([][]float64, 9) 9 | for i:=0; i < 9; i++ { 10 | matrix[i] = make([]float64, 9) 11 | for j:=0; j < 9; j++ { 12 | matrix[i][j] = float64(i * 10 + j) 13 | } 14 | } 15 | 16 | 17 | newMatrix := reduceMatrix(matrix, 2) 18 | 19 | if len(newMatrix) != 2 { 20 | t.Errorf("number of rows => %d, want %d", len(newMatrix), 2) 21 | } 22 | 23 | if len(newMatrix[0]) != 2 { 24 | t.Errorf("number of columns for row 1 => %d, want %d", len(newMatrix[0]), 2) 25 | } 26 | 27 | if len(newMatrix[1]) != 2 { 28 | t.Errorf("number of columns for row 2 => %d, want %d", len(newMatrix[1]), 2) 29 | } 30 | 31 | if newMatrix[0][0] != 0 { 32 | t.Errorf("value for [%d, %d] => %d, want %d", newMatrix[0][0], 0) 33 | } 34 | 35 | if newMatrix[0][1] != 1 { 36 | t.Errorf("value for [%d, %d] => %d, want %d", newMatrix[0][1], 1) 37 | } 38 | if newMatrix[1][0] != 10 { 39 | t.Errorf("value for [%d, %d] => %d, want %d", newMatrix[1][0], 10) 40 | } 41 | if newMatrix[1][1] != 11 { 42 | t.Errorf("value for [%d, %d] => %d, want %d", newMatrix[1][1], 11) 43 | } 44 | } 45 | 46 | func TestCalculateMeanValue(t *testing.T) { 47 | matrix := make([][]float64, 3) 48 | matrix[0] = make([]float64, 3) 49 | matrix[1] = make([]float64, 3) 50 | matrix[2] = make([]float64, 3) 51 | 52 | matrix[0][0] = 10 53 | matrix[0][1] = 20 54 | matrix[0][2] = 30 55 | 56 | matrix[1][0] = 5 57 | matrix[1][1] = 25 58 | matrix[1][2] = 45 59 | 60 | matrix[2][0] = 23 61 | matrix[2][1] = 34 62 | matrix[2][2] = 66 63 | 64 | meanValue := calculateMeanValue(matrix) 65 | 66 | if meanValue != 31 { 67 | t.Errorf("mean value for matrix => %d, want %d", meanValue, 31) 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /dct.go: -------------------------------------------------------------------------------- 1 | package phash 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | type dctPoint struct { 8 | xMax, yMax int 9 | xScales, yScales [2]float64 10 | } 11 | 12 | func (point *dctPoint) initializeScaleFactors() { 13 | point.xScales = [2]float64{ 1.0 / math.Sqrt(float64(point.xMax)), math.Sqrt(2.0 / float64(point.xMax))} 14 | point.yScales = [2]float64 { 1.0 / math.Sqrt(float64(point.yMax)), math.Sqrt(2.0 / float64(point.yMax))} 15 | } 16 | 17 | func (point *dctPoint) calculateValue(imageData [][]float64, x, y int) float64 { 18 | sum := float64(0.0) 19 | for i := 0; i < point.xMax; i++ { 20 | for j := 0; j < point.yMax; j++ { 21 | imageValue := float64(imageData[i][j]) 22 | firstCosine := math.Cos(float64((1 + (2 * i)) * x) * math.Pi / float64(2 * point.xMax)) 23 | secondCosine := math.Cos(float64((1 + (2 * j)) * y) * math.Pi / float64(2 * point.yMax)) 24 | sum += (imageValue * firstCosine * secondCosine) 25 | } 26 | } 27 | return sum * point.getScaleFactor(x, y) 28 | } 29 | 30 | func (point *dctPoint) getScaleFactor(x, y int) float64 { 31 | xScaleFactor := point.xScales[1] 32 | if x == 0 { 33 | xScaleFactor = point.xScales[0] 34 | } 35 | yScaleFactor := point.yScales[1] 36 | if y == 0 { 37 | yScaleFactor = point.yScales[0] 38 | } 39 | 40 | return xScaleFactor * yScaleFactor 41 | } 42 | 43 | // getDCTMatrix Generates a DCT matrix from a given matrix. 44 | // This is done using the Discrete Cosine Transformation (DCT) type-II algorithm. 45 | func getDCTMatrix(matrix [][]float64) [][]float64 { 46 | xMax := len(matrix) 47 | yMax := len(matrix[0]) 48 | 49 | dctPoint := &dctPoint{xMax:xMax, yMax:yMax} 50 | dctPoint.initializeScaleFactors() 51 | dctMatrix := make([][]float64, xMax) 52 | for x := 0; x < xMax; x++ { 53 | dctMatrix[x] = make([]float64, yMax) 54 | for y := 0; y < yMax; y++ { 55 | dctMatrix[x][y] = dctPoint.calculateValue(matrix, x, y) 56 | } 57 | } 58 | 59 | return dctMatrix 60 | } 61 | -------------------------------------------------------------------------------- /phash_test.go: -------------------------------------------------------------------------------- 1 | package phash 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestGetHash(t *testing.T) { 9 | file1 := openFile("testdata/soccerball.jpg") 10 | defer file1.Close() 11 | 12 | file2 := openFile("testdata/soccerball (copy).jpg") 13 | defer file2.Close() 14 | 15 | phash1, _ := GetHash(file1) 16 | phash2, _ := GetHash(file2) 17 | 18 | expectedHash := "1101100110001110001101010011100000011010000000010111110111101000" 19 | if phash1 != expectedHash { 20 | t.Errorf("phash => %s, want %s", phash1, expectedHash) 21 | } 22 | 23 | if phash1 != phash2 { 24 | t.Errorf("phashes for files %s and %s must be the same, but they are different", file1.Name(), file2.Name()) 25 | } 26 | } 27 | 28 | func TestSimilarImages(t *testing.T) { 29 | file1 := openFile("testdata/soccerball.jpg") 30 | defer file1.Close() 31 | 32 | file2 := openFile("testdata/soccerball (scaled down).jpg") 33 | defer file2.Close() 34 | 35 | file3 := openFile("testdata/soccerball (cropped).jpg") 36 | defer file3.Close() 37 | 38 | file4 := openFile("testdata/soccerball (modifications).jpg") 39 | defer file4.Close() 40 | 41 | file5 := openFile("testdata/soccerball (perspective).jpg") 42 | defer file5.Close() 43 | 44 | phash1, _ := GetHash(file1) 45 | phash2, _ := GetHash(file2) 46 | phash3, _ := GetHash(file3) 47 | phash4, _ := GetHash(file4) 48 | phash5, _ := GetHash(file5) 49 | 50 | distance := GetDistance(phash1, phash2) 51 | verifyDistanceInRange(t, file2.Name(), distance, 0, 1) 52 | 53 | distance = GetDistance(phash1, phash3) 54 | verifyDistanceInRange(t, file3.Name(), distance, 0, 1) 55 | 56 | distance = GetDistance(phash1, phash4) 57 | verifyDistanceInRange(t, file4.Name(), distance, 1, 3) 58 | 59 | distance = GetDistance(phash1, phash5) 60 | verifyDistanceInRange(t, file5.Name(), distance, 2, 5) 61 | } 62 | 63 | func TestGetDistance(t *testing.T) { 64 | var distancetests = []struct { 65 | hash1 string 66 | hash2 string 67 | distance int 68 | }{ 69 | {"0010011100100010000001000101001000101110100101110", "0010011100100010000001000101001000101110100101110", 0}, 70 | {"0010011100100000000001000101001000101110100101110", "0010011100100010000001000101001000101110100101111", 2}, 71 | {"1111111111111111111111111111111111111111111111111", "0000000000000000000000000000000000000000000000000", 49}, 72 | } 73 | 74 | for _, distancetest := range distancetests { 75 | distance := GetDistance(distancetest.hash1, distancetest.hash2) 76 | if distance != distancetest.distance { 77 | t.Errorf("distance between %s and %s => %d, want %d", distancetest.hash1, distancetest.hash2, distance, distancetest.distance) 78 | } 79 | } 80 | } 81 | 82 | func openFile(filePath string) *os.File { 83 | file, err := os.Open(filePath) 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | return file 89 | } 90 | 91 | func verifyDistanceInRange(t *testing.T, comparedImageName string, distance, minDistance, maxDistance int) { 92 | if distance < minDistance || distance > maxDistance { 93 | t.Errorf("distance with %s => %d, want value between %d and %d", comparedImageName, distance, minDistance, maxDistance) 94 | } 95 | } 96 | 97 | --------------------------------------------------------------------------------