From aa1fe89e96b1c469e41861d096878e8c7b6d95d5 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 4 Aug 2024 17:21:58 -0700 Subject: [PATCH] Performance Reduce allocations by re-using uint8 slices where possible Use github.com/disintegration/imaging for resizing Add a module option to skip RGB conversion for YCbCr images --- AUTHORS.md | 2 +- go.mod | 4 +- go.sum | 37 +---- hashcompute.go | 195 +++++++++++++++-------- hashcompute_test.go | 358 +++++++++++++++++++++---------------------- imagehash_test.go | 52 +++---- setup.cfg | 8 + test/conftest.py | 53 ++++--- test/hash_test.py | 11 +- transforms/pixels.go | 50 ++++-- 10 files changed, 430 insertions(+), 340 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 89a716e..eb229bf 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -3,4 +3,4 @@ - [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 -- [Evan Oberholster](https://github.com/evanoberholster/) eroberholster@gmail.com \ No newline at end of file +- [Evan Oberholster](https://github.com/evanoberholster/) eroberholster@gmail.com diff --git a/go.mod b/go.mod index cdecf15..8798cab 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,11 @@ go 1.21 toolchain go1.22.0 require ( - github.com/anthonynsimon/bild v0.13.0 + github.com/disintegration/imaging v1.6.2 github.com/gen2brain/avif v0.3.1 github.com/spakin/netpbm v1.3.0 golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 - golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9 + golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 ) require ( diff --git a/go.sum b/go.sum index d961400..846d6cb 100644 --- a/go.sum +++ b/go.sum @@ -1,46 +1,17 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8= -github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gen2brain/avif v0.3.1 h1:womS2LKvhS/dSR3zIKUxtJW+riGlY48akGWqc+YgHtE= github.com/gen2brain/avif v0.3.1/go.mod h1:s9sI2zo2cF6EdyRVCtnIfwL/Qb3k0TkOIEsz6ovK1ms= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/spakin/netpbm v1.3.0 h1:eDX7VvrkN5sHXW0luZXRA4AKDlLmu0E5sNxJ7VSTwxc= github.com/spakin/netpbm v1.3.0/go.mod h1:Q+ep6vNv1G44qSWp0wt3Y9o1m/QXjmaXZIFC0PMVpq0= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/tetratelabs/wazero v1.7.1 h1:QtSfd6KLc41DIMpDYlJdoMc6k7QTN246DM2+n2Y/Dx8= github.com/tetratelabs/wazero v1.7.1/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= -golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9 h1:uc17S921SPw5F2gJo7slQ3aqvr2RwpL7eb3+DZncu3s= -golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/hashcompute.go b/hashcompute.go index c48a92a..4e09e10 100644 --- a/hashcompute.go +++ b/hashcompute.go @@ -8,23 +8,59 @@ import ( "errors" "image" "image/draw" + "sync" "gitea.narnian.us/lordwelch/goimagehash/etcs" "gitea.narnian.us/lordwelch/goimagehash/transforms" - "github.com/anthonynsimon/bild/transform" + "github.com/disintegration/imaging" ) -func ToGray(img image.Image) *image.Gray { - gray := image.NewGray(image.Rect(0, 0, img.Bounds().Dx(), img.Bounds().Dy())) - gray.Pix = transforms.Rgb2Gray(img) +var bufPool = sync.Pool{ + New: func() any { + // The Pool's New function should generally only return pointer + // types, since a pointer can be put into the return interface + // value without an allocation: + return make([]uint8, 1024) + }, +} + +func getBuf(size, capacity int) []uint8 { + if capacity < size { + capacity = size + } + + buf := *bufPool.Get().(*[]uint8) + if cap(buf) < capacity { + bufPool.Put(&buf) + buf = make([]uint8, capacity) + } + + if len(buf) != size { + buf = (buf)[:size] + } + return buf +} +func ToGray(img image.Image, pix []uint8) *image.Gray { + c := img.Bounds().Dx() * img.Bounds().Dy() + if cap(pix) < c { + pix = make([]uint8, c) + } + pix = pix[:c] + gray := &image.Gray{ + Pix: transforms.Rgb2Gray(img, pix), + Stride: img.Bounds().Dx(), + Rect: img.Bounds(), + } return gray } -func resize(img image.Image, w, h int) *image.Gray { - resized := transform.Resize(img, w, h, transform.Lanczos) - r_gray := image.NewGray(image.Rect(0, 0, resized.Bounds().Dx(), resized.Bounds().Dy())) - draw.Draw(r_gray, resized.Bounds(), resized, resized.Bounds().Min, draw.Src) - return r_gray +func Resize(img image.Image, w, h int, gray *image.Gray) *image.Gray { + resized := imaging.Resize(img, w, h, imaging.Lanczos) + if gray == nil || len(gray.Pix) != w*h { + gray = image.NewGray(image.Rect(0, 0, resized.Bounds().Dx(), resized.Bounds().Dy())) + } + draw.Draw(gray, resized.Bounds(), resized, resized.Bounds().Min, draw.Src) + return gray } // AverageHash function returns a hash computation of average hash. @@ -34,10 +70,19 @@ func AverageHash(img image.Image) (*ImageHash, error) { if img == nil { return nil, errors.New("image object can not be nil") } - + w, h := 8, 8 + c := w * h ahash := NewImageHash(0, AHash) - gray := resize(ToGray(img), 8, 8) + pix := getBuf(img.Bounds().Dx()*img.Bounds().Dy(), 0) + sizedPix := getBuf(c, 0) + + gray := Resize(ToGray(img, pix), w, h, &image.Gray{ + Pix: sizedPix, + Stride: w, + Rect: image.Rect(0, 0, w, h), + }) + bufPool.Put(&pix) avg := etcs.MeanOfPixels(gray.Pix) for idx, p := range gray.Pix { @@ -45,7 +90,7 @@ func AverageHash(img image.Image) (*ImageHash, error) { ahash.leftShiftSet(len(gray.Pix) - idx - 1) } } - + bufPool.Put(&gray.Pix) return ahash, nil } @@ -56,11 +101,19 @@ func DifferenceHash(img image.Image) (*ImageHash, error) { if img == nil { return nil, errors.New("image object can not be nil") } - + w, h := 9, 8 + c := w * h dhash := NewImageHash(0, DHash) - gray := resize(ToGray(img), 9, 8) + pix := getBuf(img.Bounds().Dx()*img.Bounds().Dy(), 0) + sizedPix := getBuf(c, 0) + gray := Resize(ToGray(img, pix), w, h, &image.Gray{ + Pix: sizedPix, + Stride: w, + Rect: image.Rect(0, 0, w, h), + }) + bufPool.Put(&pix) idx := 0 for y := 0; y < gray.Bounds().Dy(); y++ { for x := 0; x < gray.Bounds().Dx()-1; x++ { @@ -70,6 +123,7 @@ func DifferenceHash(img image.Image) (*ImageHash, error) { idx++ } } + bufPool.Put(&gray.Pix) return dhash, nil } @@ -82,12 +136,25 @@ func PerceptionHash(img image.Image) (*ImageHash, error) { return nil, errors.New("image object can not be nil") } + w, h := 32, 32 + c := w * h + phash := NewImageHash(0, PHash) - gray := resize(ToGray(img), 32, 32) + + pix := getBuf(img.Bounds().Dx()*img.Bounds().Dy(), 0) + sizedPix := getBuf(c, 0) + + gray := Resize(ToGray(img, pix), w, h, &image.Gray{ + Pix: sizedPix, + Stride: w, + Rect: image.Rect(0, 0, w, h), + }) + bufPool.Put(&pix) gray32 := make([]float64, len(gray.Pix)) for i, p := range gray.Pix { gray32[i] = float64(p) } + bufPool.Put(&gray.Pix) flattens := transforms.DCT2DFast32(&gray32) median := etcs.MedianOfPixelsFast64(flattens[:]) @@ -157,60 +224,60 @@ func ExtPerceptionHash(img image.Image, hash_size, freq int) (*ExtImageHash, err // ExtAverageHash function returns ahash of which the size can be set larger than uint64 // Support 64bits ahash (width=8, height=8) and 256bits ahash (width=16, height=16) -func ExtAverageHash(img image.Image, width, height int) (*ExtImageHash, error) { - if img == nil { - return nil, errors.New("image object can not be nil") - } - var ahash []uint64 - imgSize := width * height +// func ExtAverageHash(img image.Image, width, height int) (*ExtImageHash, error) { +// if img == nil { +// return nil, errors.New("image object can not be nil") +// } +// var ahash []uint64 +// imgSize := width * height - gray := resize(ToGray(img), width, height) - avg := etcs.MeanOfPixels(gray.Pix) +// gray := resize(ToGray(img), width, height) +// avg := etcs.MeanOfPixels(gray.Pix) - lenOfUnit := 64 - if imgSize%lenOfUnit == 0 { - ahash = make([]uint64, imgSize/lenOfUnit) - } else { - ahash = make([]uint64, imgSize/lenOfUnit+1) - } - for idx, p := range gray.Pix { - indexOfArray := idx / lenOfUnit - indexOfBit := lenOfUnit - idx%lenOfUnit - 1 - if p > avg { - ahash[indexOfArray] |= 1 << uint(indexOfBit) - } - } - return NewExtImageHash(ahash, AHash, imgSize), nil -} +// lenOfUnit := 64 +// if imgSize%lenOfUnit == 0 { +// ahash = make([]uint64, imgSize/lenOfUnit) +// } else { +// ahash = make([]uint64, imgSize/lenOfUnit+1) +// } +// for idx, p := range gray.Pix { +// indexOfArray := idx / lenOfUnit +// indexOfBit := lenOfUnit - idx%lenOfUnit - 1 +// if p > avg { +// ahash[indexOfArray] |= 1 << uint(indexOfBit) +// } +// } +// return NewExtImageHash(ahash, AHash, imgSize), nil +// } // ExtDifferenceHash function returns dhash of which the size can be set larger than uint64 // Support 64bits dhash (width=8, height=8) and 256bits dhash (width=16, height=16) -func ExtDifferenceHash(img image.Image, width, height int) (*ExtImageHash, error) { - if img == nil { - return nil, errors.New("image object can not be nil") - } +// func ExtDifferenceHash(img image.Image, width, height int) (*ExtImageHash, error) { +// if img == nil { +// return nil, errors.New("image object can not be nil") +// } - var dhash []uint64 - imgSize := width * height +// var dhash []uint64 +// imgSize := width * height - gray := resize(ToGray(img), width+1, height) +// gray := resize(ToGray(img), width+1, height) - lenOfUnit := 64 - if imgSize%lenOfUnit == 0 { - dhash = make([]uint64, imgSize/lenOfUnit) - } else { - dhash = make([]uint64, imgSize/lenOfUnit+1) - } - idx := 0 - for y := 0; y < gray.Bounds().Dy(); y++ { - for x := 0; x < gray.Bounds().Dx()-1; x++ { - if gray.Pix[gray.PixOffset(x, y)] < gray.Pix[gray.PixOffset(x+1, y)] { - indexOfArray := idx / lenOfUnit - indexOfBit := lenOfUnit - idx%lenOfUnit - 1 - dhash[indexOfArray] |= 1 << uint(indexOfBit) - } - idx++ - } - } - return NewExtImageHash(dhash, DHash, imgSize), nil -} +// lenOfUnit := 64 +// if imgSize%lenOfUnit == 0 { +// dhash = make([]uint64, imgSize/lenOfUnit) +// } else { +// dhash = make([]uint64, imgSize/lenOfUnit+1) +// } +// idx := 0 +// for y := 0; y < gray.Bounds().Dy(); y++ { +// for x := 0; x < gray.Bounds().Dx()-1; x++ { +// if gray.Pix[gray.PixOffset(x, y)] < gray.Pix[gray.PixOffset(x+1, y)] { +// indexOfArray := idx / lenOfUnit +// indexOfBit := lenOfUnit - idx%lenOfUnit - 1 +// dhash[indexOfArray] |= 1 << uint(indexOfBit) +// } +// idx++ +// } +// } +// return NewExtImageHash(dhash, DHash, imgSize), nil +// } diff --git a/hashcompute_test.go b/hashcompute_test.go index b6725cf..5ae2dec 100644 --- a/hashcompute_test.go +++ b/hashcompute_test.go @@ -152,39 +152,39 @@ func TestExtendHashCompute(t *testing.T) { } } -func TestNilExtendHashCompute(t *testing.T) { - hash, err := ExtAverageHash(nil, 8, 8) - if err == nil { - t.Errorf("Error should be got.") - } - if hash != nil { - t.Errorf("Nil hash should be got. but got %v", hash) - } +// func TestNilExtendHashCompute(t *testing.T) { +// hash, err := ExtAverageHash(nil, 8, 8) +// if err == nil { +// t.Errorf("Error should be got.") +// } +// if hash != nil { +// t.Errorf("Nil hash should be got. but got %v", hash) +// } - hash, err = ExtDifferenceHash(nil, 8, 8) - if err == nil { - t.Errorf("Error should be got.") - } - if hash != nil { - t.Errorf("Nil hash should be got. but got %v", hash) - } +// hash, err = ExtDifferenceHash(nil, 8, 8) +// if err == nil { +// t.Errorf("Error should be got.") +// } +// if hash != nil { +// t.Errorf("Nil hash should be got. but got %v", hash) +// } - hash, err = ExtPerceptionHash(nil, 8, 8) - if err == nil { - t.Errorf("Error should be got.") - } - if hash != nil { - t.Errorf("Nil hash should be got. but got %v", hash) - } +// hash, err = ExtPerceptionHash(nil, 8, 8) +// if err == nil { +// t.Errorf("Error should be got.") +// } +// if hash != nil { +// t.Errorf("Nil hash should be got. but got %v", hash) +// } - hash, err = ExtPerceptionHash(nil, 8, 9) - if err == nil { - t.Errorf("Error should be got.") - } - if hash != nil { - t.Errorf("Nil hash should be got. but got %v", hash) - } -} +// hash, err = ExtPerceptionHash(nil, 8, 9) +// if err == nil { +// t.Errorf("Error should be got.") +// } +// if hash != nil { +// t.Errorf("Nil hash should be got. but got %v", hash) +// } +// } func BenchmarkDistanceIdentical(b *testing.B) { h1 := &ImageHash{hash: 0xe48ae53c05e502f7} @@ -204,129 +204,129 @@ func BenchmarkDistanceDifferent(b *testing.B) { } } -func TestExtImageHashCompute(t *testing.T) { - for _, tt := range []struct { - img1 string - img2 string - width int - height int - method func(img image.Image, width, height int) (*ExtImageHash, error) - name string - distance int - }{ - {"_examples/sample1.jpg", "_examples/sample1.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample2.jpg", "_examples/sample2.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample3.jpg", "_examples/sample3.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample4.jpg", "_examples/sample4.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample1.jpg", "_examples/sample2.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 40}, - {"_examples/sample1.jpg", "_examples/sample3.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample1.jpg", "_examples/sample4.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 34}, - {"_examples/sample2.jpg", "_examples/sample3.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 40}, - {"_examples/sample2.jpg", "_examples/sample4.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 6}, - {"_examples/sample1.jpg", "_examples/sample1.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample2.jpg", "_examples/sample2.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample3.jpg", "_examples/sample3.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample4.jpg", "_examples/sample4.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample1.jpg", "_examples/sample2.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 143}, - {"_examples/sample1.jpg", "_examples/sample3.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 3}, - {"_examples/sample1.jpg", "_examples/sample4.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 148}, - {"_examples/sample2.jpg", "_examples/sample3.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 146}, - {"_examples/sample2.jpg", "_examples/sample4.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 31}, - {"_examples/sample1.jpg", "_examples/sample1.jpg", 17, 17, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample2.jpg", "_examples/sample2.jpg", 17, 17, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample3.jpg", "_examples/sample3.jpg", 17, 17, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample4.jpg", "_examples/sample4.jpg", 17, 17, ExtAverageHash, "ExtAverageHash", 0}, - {"_examples/sample1.jpg", "_examples/sample1.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, - {"_examples/sample2.jpg", "_examples/sample2.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, - {"_examples/sample3.jpg", "_examples/sample3.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, - {"_examples/sample4.jpg", "_examples/sample4.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, - {"_examples/sample1.jpg", "_examples/sample2.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 32}, - {"_examples/sample1.jpg", "_examples/sample3.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 2}, - {"_examples/sample1.jpg", "_examples/sample4.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 30}, - {"_examples/sample2.jpg", "_examples/sample3.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 34}, - {"_examples/sample2.jpg", "_examples/sample4.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 20}, - {"_examples/sample1.jpg", "_examples/sample1.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, - {"_examples/sample2.jpg", "_examples/sample2.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, - {"_examples/sample3.jpg", "_examples/sample3.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, - {"_examples/sample4.jpg", "_examples/sample4.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, - {"_examples/sample1.jpg", "_examples/sample2.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 124}, - {"_examples/sample1.jpg", "_examples/sample3.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 14}, - {"_examples/sample1.jpg", "_examples/sample4.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 122}, - {"_examples/sample2.jpg", "_examples/sample3.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 120}, - {"_examples/sample2.jpg", "_examples/sample4.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 102}, - {"_examples/sample1.jpg", "_examples/sample1.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 0}, - {"_examples/sample2.jpg", "_examples/sample2.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 0}, - {"_examples/sample3.jpg", "_examples/sample3.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 0}, - {"_examples/sample4.jpg", "_examples/sample4.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 0}, - {"_examples/sample1.jpg", "_examples/sample2.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 40}, - {"_examples/sample1.jpg", "_examples/sample3.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 2}, - {"_examples/sample1.jpg", "_examples/sample4.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 38}, - {"_examples/sample2.jpg", "_examples/sample3.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 42}, - {"_examples/sample2.jpg", "_examples/sample4.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 20}, - {"_examples/sample1.jpg", "_examples/sample1.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 0}, - {"_examples/sample2.jpg", "_examples/sample2.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 0}, - {"_examples/sample3.jpg", "_examples/sample3.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 0}, - {"_examples/sample4.jpg", "_examples/sample4.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 0}, - {"_examples/sample1.jpg", "_examples/sample2.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 137}, - {"_examples/sample1.jpg", "_examples/sample3.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 13}, - {"_examples/sample1.jpg", "_examples/sample4.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 124}, - {"_examples/sample2.jpg", "_examples/sample3.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 140}, - {"_examples/sample2.jpg", "_examples/sample4.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 109}, - {"_examples/sample1.jpg", "_examples/sample1.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0}, - {"_examples/sample2.jpg", "_examples/sample2.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0}, - {"_examples/sample3.jpg", "_examples/sample3.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0}, - {"_examples/sample4.jpg", "_examples/sample4.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0}, - } { - file1, err := os.Open(tt.img1) - if err != nil { - t.Errorf("%s", err) - } - defer file1.Close() +// func TestExtImageHashCompute(t *testing.T) { +// for _, tt := range []struct { +// img1 string +// img2 string +// width int +// height int +// method func(img image.Image, width, height int) (*ExtImageHash, error) +// name string +// distance int +// }{ +// {"_examples/sample1.jpg", "_examples/sample1.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample2.jpg", "_examples/sample2.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample3.jpg", "_examples/sample3.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample4.jpg", "_examples/sample4.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample1.jpg", "_examples/sample2.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 40}, +// {"_examples/sample1.jpg", "_examples/sample3.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample1.jpg", "_examples/sample4.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 34}, +// {"_examples/sample2.jpg", "_examples/sample3.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 40}, +// {"_examples/sample2.jpg", "_examples/sample4.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 6}, +// {"_examples/sample1.jpg", "_examples/sample1.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample2.jpg", "_examples/sample2.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample3.jpg", "_examples/sample3.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample4.jpg", "_examples/sample4.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample1.jpg", "_examples/sample2.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 143}, +// {"_examples/sample1.jpg", "_examples/sample3.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 3}, +// {"_examples/sample1.jpg", "_examples/sample4.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 148}, +// {"_examples/sample2.jpg", "_examples/sample3.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 146}, +// {"_examples/sample2.jpg", "_examples/sample4.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 31}, +// {"_examples/sample1.jpg", "_examples/sample1.jpg", 17, 17, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample2.jpg", "_examples/sample2.jpg", 17, 17, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample3.jpg", "_examples/sample3.jpg", 17, 17, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample4.jpg", "_examples/sample4.jpg", 17, 17, ExtAverageHash, "ExtAverageHash", 0}, +// {"_examples/sample1.jpg", "_examples/sample1.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, +// {"_examples/sample2.jpg", "_examples/sample2.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, +// {"_examples/sample3.jpg", "_examples/sample3.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, +// {"_examples/sample4.jpg", "_examples/sample4.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, +// {"_examples/sample1.jpg", "_examples/sample2.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 32}, +// {"_examples/sample1.jpg", "_examples/sample3.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 2}, +// {"_examples/sample1.jpg", "_examples/sample4.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 30}, +// {"_examples/sample2.jpg", "_examples/sample3.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 34}, +// {"_examples/sample2.jpg", "_examples/sample4.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 20}, +// {"_examples/sample1.jpg", "_examples/sample1.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, +// {"_examples/sample2.jpg", "_examples/sample2.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, +// {"_examples/sample3.jpg", "_examples/sample3.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, +// {"_examples/sample4.jpg", "_examples/sample4.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 0}, +// {"_examples/sample1.jpg", "_examples/sample2.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 124}, +// {"_examples/sample1.jpg", "_examples/sample3.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 14}, +// {"_examples/sample1.jpg", "_examples/sample4.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 122}, +// {"_examples/sample2.jpg", "_examples/sample3.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 120}, +// {"_examples/sample2.jpg", "_examples/sample4.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 102}, +// {"_examples/sample1.jpg", "_examples/sample1.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 0}, +// {"_examples/sample2.jpg", "_examples/sample2.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 0}, +// {"_examples/sample3.jpg", "_examples/sample3.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 0}, +// {"_examples/sample4.jpg", "_examples/sample4.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 0}, +// {"_examples/sample1.jpg", "_examples/sample2.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 40}, +// {"_examples/sample1.jpg", "_examples/sample3.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 2}, +// {"_examples/sample1.jpg", "_examples/sample4.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 38}, +// {"_examples/sample2.jpg", "_examples/sample3.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 42}, +// {"_examples/sample2.jpg", "_examples/sample4.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 20}, +// {"_examples/sample1.jpg", "_examples/sample1.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 0}, +// {"_examples/sample2.jpg", "_examples/sample2.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 0}, +// {"_examples/sample3.jpg", "_examples/sample3.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 0}, +// {"_examples/sample4.jpg", "_examples/sample4.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 0}, +// {"_examples/sample1.jpg", "_examples/sample2.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 137}, +// {"_examples/sample1.jpg", "_examples/sample3.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 13}, +// {"_examples/sample1.jpg", "_examples/sample4.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 124}, +// {"_examples/sample2.jpg", "_examples/sample3.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 140}, +// {"_examples/sample2.jpg", "_examples/sample4.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 109}, +// {"_examples/sample1.jpg", "_examples/sample1.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0}, +// {"_examples/sample2.jpg", "_examples/sample2.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0}, +// {"_examples/sample3.jpg", "_examples/sample3.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0}, +// {"_examples/sample4.jpg", "_examples/sample4.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0}, +// } { +// file1, err := os.Open(tt.img1) +// if err != nil { +// t.Errorf("%s", err) +// } +// defer file1.Close() - file2, err := os.Open(tt.img2) - if err != nil { - t.Errorf("%s", err) - } - defer file2.Close() +// file2, err := os.Open(tt.img2) +// if err != nil { +// t.Errorf("%s", err) +// } +// defer file2.Close() - img1, err := jpeg.Decode(file1) - if err != nil { - t.Errorf("%s", err) - } +// img1, err := jpeg.Decode(file1) +// if err != nil { +// t.Errorf("%s", err) +// } - img2, err := jpeg.Decode(file2) - if err != nil { - t.Errorf("%s", err) - } +// img2, err := jpeg.Decode(file2) +// if err != nil { +// t.Errorf("%s", err) +// } - hash1, err := tt.method(img1, tt.width, tt.height) - if err != nil { - t.Errorf("%s", err) - } - hash2, err := tt.method(img2, tt.width, tt.height) - if err != nil { - t.Errorf("%s", err) - } +// hash1, err := tt.method(img1, tt.width, tt.height) +// if err != nil { +// t.Errorf("%s", err) +// } +// hash2, err := tt.method(img2, tt.width, tt.height) +// if err != nil { +// t.Errorf("%s", err) +// } - dis1, err := hash1.Distance(hash2) - if err != nil { - t.Errorf("%s", err) - } +// dis1, err := hash1.Distance(hash2) +// if err != nil { +// t.Errorf("%s", err) +// } - dis2, err := hash2.Distance(hash1) - 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 dis1 != dis2 { +// t.Errorf("Distance should be identical %v vs %v", dis1, dis2) +// } - if dis1 != tt.distance { - t.Errorf("%s: Distance between %v and %v is expected %v but got %v: %v %v", tt.name, tt.img1, tt.img2, tt.distance, dis1, hash1, hash2) - } - } -} +// if dis1 != tt.distance { +// t.Errorf("%s: Distance between %v and %v is expected %v but got %v: %v %v", tt.name, tt.img1, tt.img2, tt.distance, dis1, hash1, hash2) +// } +// } +// } func BenchmarkExtImageHashDistanceDifferent(b *testing.B) { h1 := &ExtImageHash{hash: []uint64{0xe48ae53c05e502f7}} @@ -358,38 +358,38 @@ func BenchmarkPerceptionHash(b *testing.B) { } } -func BenchmarkAverageHash(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 := ExtAverageHash(img1, 8, 8) - if err != nil { - b.Errorf("%s", err) - } - } -} +// func BenchmarkAverageHash(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 := ExtAverageHash(img1, 8, 8) +// if err != nil { +// b.Errorf("%s", err) +// } +// } +// } -func BenchmarkDifferenceHash(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 := ExtDifferenceHash(img1, 8, 8) - if err != nil { - b.Errorf("%s", err) - } - } -} +// func BenchmarkDifferenceHash(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 := ExtDifferenceHash(img1, 8, 8) +// if err != nil { +// b.Errorf("%s", err) +// } +// } +// } diff --git a/imagehash_test.go b/imagehash_test.go index 0e4849c..19f9608 100644 --- a/imagehash_test.go +++ b/imagehash_test.go @@ -78,7 +78,7 @@ func TestSerialization(t *testing.T) { AverageHash, PerceptionHash, DifferenceHash, } extMethods := []func(img image.Image, width int, height int) (*ExtImageHash, error){ - ExtAverageHash /* ExtPerceptionHash, */, ExtDifferenceHash, + // ExtAverageHash ExtPerceptionHash, , ExtDifferenceHash, } examples := []string{ "_examples/sample1.jpg", "_examples/sample2.jpg", "_examples/sample3.jpg", "_examples/sample4.jpg", @@ -99,7 +99,7 @@ func TestSerialization(t *testing.T) { hash, err := method(img) checkErr(err) - hex := hash.String(false) + hex := hash.String() // len(kind) == 1, len(":") == 1, len(hash) == 16 if len(hex) != 18 { t.Errorf("Got invalid hex string '%v'; %v of '%v'", hex, methodStr, ex) @@ -163,31 +163,31 @@ func TestSerialization(t *testing.T) { extImageHash, err = ExtImageHashFromString("k:g") } -func TestDifferentBitSizeHash(t *testing.T) { - checkErr := func(err error) { - if err != nil { - t.Errorf("%v", err) - } - } - file, err := os.Open("_examples/sample1.jpg") - checkErr(err) - defer file.Close() +// func TestDifferentBitSizeHash(t *testing.T) { +// checkErr := func(err error) { +// if err != nil { +// t.Errorf("%v", err) +// } +// } +// file, err := os.Open("_examples/sample1.jpg") +// checkErr(err) +// defer file.Close() - img, _, err := image.Decode(file) - checkErr(err) +// img, _, err := image.Decode(file) +// checkErr(err) - hash1, _ := ExtAverageHash(img, 32, 32) - hash2, _ := ExtDifferenceHash(img, 32, 32) - _, err = hash1.Distance(hash2) - if err == nil { - t.Errorf("Should got error with different kinds of hashes") - } - hash3, _ := ExtAverageHash(img, 31, 31) - _, err = hash1.Distance(hash3) - if err == nil { - t.Errorf("Should got error with different bits of hashes") - } -} +// hash1, _ := ExtAverageHash(img, 32, 32) +// hash2, _ := ExtDifferenceHash(img, 32, 32) +// _, err = hash1.Distance(hash2) +// if err == nil { +// t.Errorf("Should got error with different kinds of hashes") +// } +// hash3, _ := ExtAverageHash(img, 31, 31) +// _, err = hash1.Distance(hash3) +// if err == nil { +// t.Errorf("Should got error with different bits of hashes") +// } +// } func TestDumpAndLoad(t *testing.T) { checkErr := func(err error) { if err != nil { @@ -237,7 +237,7 @@ func TestDumpAndLoad(t *testing.T) { // test for ExtIExtImageHash extMethods := []func(img image.Image, width, height int) (*ExtImageHash, error){ - ExtAverageHash, ExtPerceptionHash, ExtDifferenceHash, + // ExtAverageHash, ExtPerceptionHash, ExtDifferenceHash, } sizeList := []int{8, 16} diff --git a/setup.cfg b/setup.cfg index 2c3623c..0c1422e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,8 +7,16 @@ license_files = LICENSE classifiers = License :: OSI Approved :: BSD License +[options] +packages = hashImage + +[tox:tox] +minversion = 4.0.0 +basepython = python3.9 + [testenv] description = Test go imagehash +skip_install = true deps = pytest>=7 imagehash diff --git a/test/conftest.py b/test/conftest.py index 1fbbead..2949a2f 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -24,16 +24,17 @@ def pytest_report_teststatus(report, config): ahash = props.get('ahash:', -1) dhash = props.get('dhash:', -1) phash = props.get('phash:', -1) - if ahash == dhash == phash == 0: + report.wasxfail = f'reason: {ahash},{dhash},{phash}' + if ahash == dhash == phash == 0 or ahash == dhash == phash == -1: if report.outcome == 'failed': - return 'failed', 'E', ('EXACT', {'red': True}) + return 'failed', 'F', ('FAIL', {'red': True}) return 'exact', 'E', ('EXACT', {'green': True}) - if ahash >= 10 or dhash >= 10 or phash >= 10: - return 'incompatible', 'I', ('INCOMPATIBLE', {'red': True}) - if 4 < ahash < 10 or 4 < dhash < 10 or 4 < phash < 10: - return 'passable', 'P', ('PASSABLE', {'yellow': True}) - if ahash <= 4 or dhash <= 4 or phash <= 4: + if (ahash <= 4 and dhash <= 4 and phash <= 4) and 0 in (ahash, dhash, phash): return 'compatible', 'C', ('COMPATIBLE', {'green': True}) + if ahash <= 8 and dhash <= 8 and phash <= 8: + return 'passable', 'P', ('PASSABLE', {'yellow': True}) + + return 'incompatible', 'I', ('INCOMPATIBLE', {'red': True}) def pytest_terminal_summary(terminalreporter, exitstatus, config): @@ -74,24 +75,40 @@ def pytest_addoption(parser): '--hasher', default='go run ./hashImage -file {}', help="The commandline image hasher to test ('go run ./hashImage -file {}')", ) parser.addoption( - '--image-dir', default='_examples/images', help='The file path to a directory of images to test', + '--image-dir', nargs='+', default='_examples/images', help='The file path to a directory of images to test', ) parser.addoption( - '--fail-skip', action='store_true', help="skip images that can't be decoded", + '--skip-invalid-image', action='store_true', help="skip images that can't be decoded", ) + parser.addoption( + '--recurse', action='store_true', help='recursively load images', + ) + parser.addoption('--only-files', help='Only add files containing the specified string eg "--only-files thumb"') + + +def get_images(path: pathlib.Path, recurse: bool, /, limit: str = '') -> list[str]: + files = [] + cwd = pathlib.Path('').absolute() + for p in pathlib.Path(path).iterdir(): + if p.is_dir(): + if recurse: + files.extend(get_images(p, recurse, limit=limit)) + continue + if limit and limit not in str(p): + continue + p = p.absolute() + if p.is_relative_to(cwd): + files.append(str(p.relative_to(cwd))) + else: + files.append(str(p.absolute())) + return files def pytest_generate_tests(metafunc): if 'file' in metafunc.fixturenames: file = metafunc.config.getoption('--image-dir') files = [] - cwd = pathlib.Path('').absolute() - for p in pathlib.Path(file).iterdir(): - if not p.is_file(): - continue - p = p.absolute() - if p.is_relative_to(cwd): - files.append(str(p.relative_to(cwd))) - else: - files.append(str(p.absolute())) + limit = metafunc.config.getoption('--only-files') + for f in file: + files.extend(get_images(f, metafunc.config.getoption('--recurse'), limit=limit)) metafunc.parametrize('file', files) diff --git a/test/hash_test.py b/test/hash_test.py index 2e53cf0..e05038e 100644 --- a/test/hash_test.py +++ b/test/hash_test.py @@ -23,7 +23,7 @@ def hamming_distance(h1, h2) -> int: n = n1 ^ n2 # count up the 1's in the binary string - return sum(b == '1' for b in bin(n)[2:]) + return bin(n)[2:].count('1') def test_hash(request, file, record_property): @@ -31,7 +31,8 @@ def test_hash(request, file, record_property): try: python = hashImage.main(['-file', file]) + '\n' except Exception: - pytest.skip('python imagehash failed') + if request.config.getoption('--skip-invalid-image'): + pytest.skip('python imagehash failed') return sh = shlex.shlex(hasher, punctuation_chars=True, posix=True) @@ -44,10 +45,12 @@ def test_hash(request, file, record_property): external = subprocess.run(cmd_list, shell=None, capture_output=True) external_stdout = str(external.stdout, encoding='utf-8') if external.returncode != 0: - pytest.skip(str(external.stderr)) + if request.config.getoption('--skip-invalid-image'): + pytest.skip(str(external.stderr)) return + external_stdout_h = python_h = '' - for p, e in zip(python.split('\n'), external_stdout.splitlines()): + for p, e in zip(python.splitlines(), external_stdout.splitlines()): p_, e_ = p.split(' '), e.split(' ') h = hamming_distance(p_[1], e_[1]) record_property(p_[0], h) diff --git a/transforms/pixels.go b/transforms/pixels.go index b9a05d6..88f0292 100644 --- a/transforms/pixels.go +++ b/transforms/pixels.go @@ -9,29 +9,53 @@ import ( "image/color" ) +var FastYCBCR = true + func toGray(r, g, b uint8) uint8 { // 19595 + 38470 + 7471 equals 65536. return uint8((19595*uint32(r) + 38470*uint32(g) + 7471*uint32(b) + 1<<15) >> 16) } // Rgb2Gray function converts RGB to a gray scale array. -func Rgb2Gray(colorImg image.Image) []uint8 { +func Rgb2Gray(colorImg image.Image, pixels []uint8) []uint8 { colorImg.Bounds().Dx() bounds := colorImg.Bounds() w, h := bounds.Dx(), bounds.Dy() - pixels := make([]uint8, h*w) - for i := range pixels { - x, y := i%w, i/w - var R, G, B uint32 - switch v := colorImg.At(x, y).(type) { - case color.NRGBA: // pillow discards alpha during conversion and provides no way to pre-multiply it - R, G, B = uint32(v.R)<<8, uint32(v.G)<<8, uint32(v.B)<<8 - default: - R, G, B, _ = colorImg.At(x, y).RGBA() - } + if pixels == nil { + pixels = make([]uint8, h*w) + } + switch img := colorImg.(type) { + case *image.YCbCr: + var R, G, B uint8 + var yi, ci int + for i := 0; i < h*w; i++ { + x, y := i%w, i/w + yi = img.YOffset(x, y) + if FastYCBCR { + pixels[i] = img.Y[yi] + } else { + ci = img.COffset(x, y) - // Pillow only operates on 8bit data, operating on higher bit data produces rounding differences - pixels[i] = toGray(uint8(R>>8), uint8(G>>8), uint8(B>>8)) + R, G, B = color.YCbCrToRGB(img.Y[yi], img.Cb[ci], img.Cr[ci]) + + // Pillow only operates on 8bit data, operating on higher bit data produces rounding differences + pixels[i] = toGray(R, G, B) + } + } + default: + var R, G, B uint32 + for i := 0; i < h*w; i++ { + x, y := i%w, i/w + switch v := img.At(x, y).(type) { + case color.NRGBA: // pillow discards alpha during conversion and provides no way to pre-multiply it + R, G, B = uint32(v.R)<<8, uint32(v.G)<<8, uint32(v.B)<<8 + default: + R, G, B, _ = v.RGBA() + } + + // Pillow only operates on 8bit data, operating on higher bit data produces rounding differences + pixels[i] = toGray(uint8(R>>8), uint8(G>>8), uint8(B>>8)) + } } return pixels