Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored Perceptionhash for performance #54

Merged
merged 5 commits into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
- [Dominik Honnef](https://github.com/dominikh) dominik@honnef.co
- [Dong-hee Na](https://github.com/corona10/) donghee.na92@gmail.com
- [Gustavo Brunoro](https://github.com/brunoro/) git@hitnail.net
- [Alex Higashino](https://github.com/TokyoWolFrog/) TokyoWolFrog@mayxyou.com
- [Alex Higashino](https://github.com/TokyoWolFrog/) TokyoWolFrog@mayxyou.com
- [Evan Oberholster](https://github.com/evanoberholster/) eroberholster@gmail.com
10 changes: 10 additions & 0 deletions etcs/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ func MedianOfPixels(pixels []float64) float64 {
return v
}

// MedianOfPixelsFast64 function returns a median value of pixels.
// It uses quick selection algorithm.
func MedianOfPixelsFast64(pixels []float64) float64 {
tmp := [64]float64{}
copy(tmp[:], pixels)
l := len(tmp)
pos := l / 2
return quickSelectMedian(tmp[:], 0, l-1, pos)
}

func quickSelectMedian(sequence []float64, low int, hi int, k int) float64 {
if low == hi {
return sequence[k]
Expand Down
39 changes: 39 additions & 0 deletions hashcompute.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package goimagehash
import (
"errors"
"image"
"sync"

"github.com/corona10/goimagehash/etcs"
"github.com/corona10/goimagehash/transforms"
Expand Down Expand Up @@ -84,6 +85,44 @@ func PerceptionHash(img image.Image) (*ImageHash, error) {
return phash, nil
}

var pixelPool64 = sync.Pool{
New: func() interface{} {
p := make([]float64, 4096)
return &p
},
}

// PerceptionHashFast function returns a hash computation of phash.
// Uses static DCT tables for improved performance.
// Implementation follows
// http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html
func PerceptionHashFast(img image.Image) (*ImageHash, error) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func PerceptionHashFast(img image.Image) (*ImageHash, error) {
func PerceptionHash(img image.Image) (*ImageHash, error) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made these changes in new update.

if img == nil {
return nil, errors.New("image object can not be nil")
}

phash := NewImageHash(0, PHash)
resized := resize.Resize(64, 64, img, resize.Bilinear)

pixels := pixelPool64.Get().(*[]float64)

transforms.Rgb2GrayFast(resized, pixels)
transforms.DCT2DFast64(pixels)
flattens := transforms.FlattenPixelsFast64(*pixels, 8, 8)

pixelPool64.Put(pixels)

median := etcs.MedianOfPixelsFast64(flattens)

for idx, p := range flattens {
if p > median {
phash.leftShiftSet(64 - idx - 1) // leftShiftSet
}
}

return phash, nil
}

// ExtPerceptionHash function returns phash of which the size can be set larger than uint64
// Some variable name refer to https://github.com/JohannesBuchner/imagehash/blob/master/imagehash/__init__.py
// Support 64bits phash (width=8, height=8) and 256bits phash (width=16, height=16)
Expand Down
103 changes: 103 additions & 0 deletions hashcompute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import (
"image/jpeg"
"os"
"testing"

"github.com/corona10/goimagehash/etcs"
"github.com/corona10/goimagehash/transforms"
"github.com/nfnt/resize"
corona10 marked this conversation as resolved.
Show resolved Hide resolved
)

func TestHashCompute(t *testing.T) {
Expand Down Expand Up @@ -46,6 +50,15 @@ func TestHashCompute(t *testing.T) {
{"_examples/sample1.jpg", "_examples/sample4.jpg", PerceptionHash, "PerceptionHash", 30},
{"_examples/sample2.jpg", "_examples/sample3.jpg", PerceptionHash, "PerceptionHash", 34},
{"_examples/sample2.jpg", "_examples/sample4.jpg", PerceptionHash, "PerceptionHash", 20},
{"_examples/sample1.jpg", "_examples/sample1.jpg", PerceptionHashFast, "PerceptionHashFast", 0},
{"_examples/sample2.jpg", "_examples/sample2.jpg", PerceptionHashFast, "PerceptionHashFast", 0},
{"_examples/sample3.jpg", "_examples/sample3.jpg", PerceptionHashFast, "PerceptionHashFast", 0},
{"_examples/sample4.jpg", "_examples/sample4.jpg", PerceptionHashFast, "PerceptionHashFast", 0},
{"_examples/sample1.jpg", "_examples/sample2.jpg", PerceptionHashFast, "PerceptionHashFast", 32},
{"_examples/sample1.jpg", "_examples/sample3.jpg", PerceptionHashFast, "PerceptionHashFast", 2},
{"_examples/sample1.jpg", "_examples/sample4.jpg", PerceptionHashFast, "PerceptionHashFast", 30},
{"_examples/sample2.jpg", "_examples/sample3.jpg", PerceptionHashFast, "PerceptionHashFast", 34},
{"_examples/sample2.jpg", "_examples/sample4.jpg", PerceptionHashFast, "PerceptionHashFast", 20},
} {
file1, err := os.Open(tt.img1)
if err != nil {
Expand Down Expand Up @@ -98,6 +111,78 @@ func TestHashCompute(t *testing.T) {
}
}

func TestHashComputeFast(t *testing.T) {
for _, tt := range []struct {
img1 string
}{
{"_examples/sample1.jpg"},
{"_examples/sample2.jpg"},
{"_examples/sample3.jpg"},
{"_examples/sample4.jpg"},
} {
file1, err := os.Open(tt.img1)
if err != nil {
t.Errorf("%s", err)
}
defer file1.Close()

img1, err := jpeg.Decode(file1)
if err != nil {
t.Errorf("%s", err)
}

resized := resize.Resize(64, 64, img1, resize.Bilinear)

hash1 := NewImageHash(0, PHash)
pixels := transforms.Rgb2Gray(resized)
dct := transforms.DCT2D(pixels, 64, 64)
flattens := transforms.FlattenPixels(dct, 8, 8)
median1 := etcs.MedianOfPixels(flattens)

for idx, p := range flattens {
if p > median1 {
hash1.leftShiftSet(len(flattens) - idx - 1)
}
}

p := make([]float64, 4096)
pixelsFast := &p

hash2 := NewImageHash(0, PHash)
transforms.Rgb2GrayFast(resized, pixelsFast)
transforms.DCT2DFast64(pixelsFast)
flattensFast := transforms.FlattenPixelsFast64(*pixelsFast, 8, 8)
median2 := etcs.MedianOfPixels(flattensFast)

for idx, p := range flattensFast {
if p > median2 {
hash2.leftShiftSet(len(flattensFast) - idx - 1)
}
}

if median1 != median2 {
t.Errorf("Medians should be identical %v vs %v", median1, median2)
}

dis1, err := hash1.Distance(hash2)
if err != nil {
t.Errorf("%s", err)
}

dis2, err := hash2.Distance(hash1)
if err != nil {
t.Errorf("%s", err)
}

if dis1 != dis2 {
t.Errorf("Distance should be identical %v vs %v", dis1, dis2)
}
if dis2 != 0 {
t.Errorf("Distance should be identical %v vs %v, with distance of %d", hash1.ToString(), hash2.ToString(), dis1)
}
}
}

evanoberholster marked this conversation as resolved.
Show resolved Hide resolved
func TestNilHashCompute(t *testing.T) {
hash, err := AverageHash(nil)
if err == nil {
Expand Down Expand Up @@ -330,6 +415,24 @@ func BenchmarkPerceptionHash(b *testing.B) {
}
}

func BenchmarkPerceptionHashFast(b *testing.B) {
file1, err := os.Open("_examples/sample3.jpg")
if err != nil {
b.Errorf("%s", err)
}
defer file1.Close()
img1, err := jpeg.Decode(file1)
if err != nil {
b.Errorf("%s", err)
}
for i := 0; i < b.N; i++ {
_, err := PerceptionHashFast(img1)
if err != nil {
b.Errorf("%s", err)
}
}
}

func BenchmarkAverageHash(b *testing.B) {
file1, err := os.Open("_examples/sample3.jpg")
if err != nil {
Expand Down
46 changes: 46 additions & 0 deletions transforms/dct.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,49 @@ func DCT2D(input [][]float64, w int, h int) [][]float64 {
wg.Wait()
return output
}

// DCT2DFast64 function returns a result of DCT2D by using the seperable property.
// Fast uses static DCT tables for improved performance.
func DCT2DFast64(input *[]float64) {
if len(*input) != 4096 {
panic("incorrect input size")
}

for i := 0; i < 64; i++ { // height
DCT1DFast64((*input)[i*64 : (i*64)+64])
//forwardTransformFast((*input)[i*64:(i*64)+64], temp[:], 64)
}

for i := 0; i < 64; i++ { // width
row := [64]float64{}
for j := 0; j < 64; j++ {
row[j] = (*input)[i+((j)*64)]
}
DCT1DFast64(row[:])
for j := 0; j < len(row); j++ {
(*input)[i+(j*64)] = row[j]
}
}
}

func forwardTransformFast(input, temp []float64, Len int) {
if Len == 1 {
return
}

halfLen := Len / 2
t := dctTables[halfLen>>1]
for i := 0; i < halfLen; i++ {
x, y := input[i], input[Len-1-i]
temp[i] = x + y
temp[i+halfLen] = (x - y) / t[i]
}
forwardTransformFast(temp, input, halfLen)
forwardTransformFast(temp[halfLen:], input, halfLen)
for i := 0; i < halfLen-1; i++ {
input[i*2+0] = temp[i]
input[i*2+1] = temp[i+halfLen] + temp[i+halfLen+1]
}

input[Len-2], input[Len-1] = temp[halfLen-1], temp[Len-1]
}
60 changes: 60 additions & 0 deletions transforms/pixels.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,55 @@ func Rgb2Gray(colorImg image.Image) [][]float64 {
return pixels
}

// Rgb2GrayFast function converts RGB to a gray scale array.
func Rgb2GrayFast(colorImg image.Image, pixels *[]float64) {
bounds := colorImg.Bounds()
w, h := bounds.Max.X-bounds.Min.X, bounds.Max.Y-bounds.Min.Y
if w != h {
return
}
switch c := colorImg.(type) {
case *image.YCbCr:
rgb2GrayYCbCR(c, *pixels, w)
case *image.RGBA:
rgb2GrayRGBA(c, *pixels, w)
default:
rgb2GrayDefault(c, *pixels, w)
}
}

// pixel2Gray converts a pixel to grayscale value base on luminosity
func pixel2Gray(r, g, b, a uint32) float64 {
return 0.299*float64(r/257) + 0.587*float64(g/257) + 0.114*float64(b/256)
}

// rgb2GrayDefault uses the image.Image interface
func rgb2GrayDefault(colorImg image.Image, pixels []float64, s int) {
for i := 0; i < s; i++ {
for j := 0; j < s; j++ {
pixels[j+(i*s)] = pixel2Gray(colorImg.At(j, i).RGBA())
}
}
}

// rgb2GrayYCbCR uses *image.YCbCr which is signifiantly faster than the image.Image interface.
func rgb2GrayYCbCR(colorImg *image.YCbCr, pixels []float64, s int) {
for i := 0; i < s; i++ {
for j := 0; j < s; j++ {
pixels[j+(i*s)] = pixel2Gray(colorImg.YCbCrAt(j, i).RGBA())
}
}
}

// rgb2GrayYCbCR uses *image.RGBA which is signifiantly faster than the image.Image interface.
func rgb2GrayRGBA(colorImg *image.RGBA, pixels []float64, s int) {
for i := 0; i < s; i++ {
for j := 0; j < s; j++ {
pixels[(i*s)+j] = pixel2Gray(colorImg.At(j, i).RGBA())
}
}
}

// FlattenPixels function flattens 2d array into 1d array.
func FlattenPixels(pixels [][]float64, x int, y int) []float64 {
flattens := make([]float64, x*y)
Expand All @@ -37,3 +86,14 @@ func FlattenPixels(pixels [][]float64, x int, y int) []float64 {
}
return flattens
}

// FlattenPixelsFast64 function flattens 2d array into 1d array.
func FlattenPixelsFast64(pixels []float64, x int, y int) []float64 {
flattens := [64]float64{}
for i := 0; i < y; i++ {
for j := 0; j < x; j++ {
flattens[y*i+j] = pixels[(i*64)+j]
}
}
return flattens[:]
}
Loading