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
This commit is contained in:
parent
cb5a8237c4
commit
aa1fe89e96
4
go.mod
4
go.mod
@ -5,11 +5,11 @@ go 1.21
|
|||||||
toolchain go1.22.0
|
toolchain go1.22.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anthonynsimon/bild v0.13.0
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/gen2brain/avif v0.3.1
|
github.com/gen2brain/avif v0.3.1
|
||||||
github.com/spakin/netpbm v1.3.0
|
github.com/spakin/netpbm v1.3.0
|
||||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
|
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 (
|
require (
|
||||||
|
37
go.sum
37
go.sum
@ -1,46 +1,17 @@
|
|||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8=
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
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/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
|
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
|
||||||
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
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 h1:womS2LKvhS/dSR3zIKUxtJW+riGlY48akGWqc+YgHtE=
|
||||||
github.com/gen2brain/avif v0.3.1/go.mod h1:s9sI2zo2cF6EdyRVCtnIfwL/Qb3k0TkOIEsz6ovK1ms=
|
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 h1:eDX7VvrkN5sHXW0luZXRA4AKDlLmu0E5sNxJ7VSTwxc=
|
||||||
github.com/spakin/netpbm v1.3.0/go.mod h1:Q+ep6vNv1G44qSWp0wt3Y9o1m/QXjmaXZIFC0PMVpq0=
|
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 h1:QtSfd6KLc41DIMpDYlJdoMc6k7QTN246DM2+n2Y/Dx8=
|
||||||
github.com/tetratelabs/wazero v1.7.1/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
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 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
|
||||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
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-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||||
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
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/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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=
|
|
||||||
|
195
hashcompute.go
195
hashcompute.go
@ -8,23 +8,59 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"gitea.narnian.us/lordwelch/goimagehash/etcs"
|
"gitea.narnian.us/lordwelch/goimagehash/etcs"
|
||||||
"gitea.narnian.us/lordwelch/goimagehash/transforms"
|
"gitea.narnian.us/lordwelch/goimagehash/transforms"
|
||||||
"github.com/anthonynsimon/bild/transform"
|
"github.com/disintegration/imaging"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ToGray(img image.Image) *image.Gray {
|
var bufPool = sync.Pool{
|
||||||
gray := image.NewGray(image.Rect(0, 0, img.Bounds().Dx(), img.Bounds().Dy()))
|
New: func() any {
|
||||||
gray.Pix = transforms.Rgb2Gray(img)
|
// 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
|
return gray
|
||||||
}
|
}
|
||||||
|
|
||||||
func resize(img image.Image, w, h int) *image.Gray {
|
func Resize(img image.Image, w, h int, gray *image.Gray) *image.Gray {
|
||||||
resized := transform.Resize(img, w, h, transform.Lanczos)
|
resized := imaging.Resize(img, w, h, imaging.Lanczos)
|
||||||
r_gray := image.NewGray(image.Rect(0, 0, resized.Bounds().Dx(), resized.Bounds().Dy()))
|
if gray == nil || len(gray.Pix) != w*h {
|
||||||
draw.Draw(r_gray, resized.Bounds(), resized, resized.Bounds().Min, draw.Src)
|
gray = image.NewGray(image.Rect(0, 0, resized.Bounds().Dx(), resized.Bounds().Dy()))
|
||||||
return r_gray
|
}
|
||||||
|
draw.Draw(gray, resized.Bounds(), resized, resized.Bounds().Min, draw.Src)
|
||||||
|
return gray
|
||||||
}
|
}
|
||||||
|
|
||||||
// AverageHash function returns a hash computation of average hash.
|
// AverageHash function returns a hash computation of average hash.
|
||||||
@ -34,10 +70,19 @@ func AverageHash(img image.Image) (*ImageHash, error) {
|
|||||||
if img == nil {
|
if img == nil {
|
||||||
return nil, errors.New("image object can not be nil")
|
return nil, errors.New("image object can not be nil")
|
||||||
}
|
}
|
||||||
|
w, h := 8, 8
|
||||||
|
c := w * h
|
||||||
ahash := NewImageHash(0, AHash)
|
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)
|
avg := etcs.MeanOfPixels(gray.Pix)
|
||||||
|
|
||||||
for idx, p := range 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)
|
ahash.leftShiftSet(len(gray.Pix) - idx - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bufPool.Put(&gray.Pix)
|
||||||
return ahash, nil
|
return ahash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,11 +101,19 @@ func DifferenceHash(img image.Image) (*ImageHash, error) {
|
|||||||
if img == nil {
|
if img == nil {
|
||||||
return nil, errors.New("image object can not be nil")
|
return nil, errors.New("image object can not be nil")
|
||||||
}
|
}
|
||||||
|
w, h := 9, 8
|
||||||
|
c := w * h
|
||||||
dhash := NewImageHash(0, DHash)
|
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
|
idx := 0
|
||||||
for y := 0; y < gray.Bounds().Dy(); y++ {
|
for y := 0; y < gray.Bounds().Dy(); y++ {
|
||||||
for x := 0; x < gray.Bounds().Dx()-1; x++ {
|
for x := 0; x < gray.Bounds().Dx()-1; x++ {
|
||||||
@ -70,6 +123,7 @@ func DifferenceHash(img image.Image) (*ImageHash, error) {
|
|||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bufPool.Put(&gray.Pix)
|
||||||
|
|
||||||
return dhash, nil
|
return dhash, nil
|
||||||
}
|
}
|
||||||
@ -82,12 +136,25 @@ func PerceptionHash(img image.Image) (*ImageHash, error) {
|
|||||||
return nil, errors.New("image object can not be nil")
|
return nil, errors.New("image object can not be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w, h := 32, 32
|
||||||
|
c := w * h
|
||||||
|
|
||||||
phash := NewImageHash(0, PHash)
|
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))
|
gray32 := make([]float64, len(gray.Pix))
|
||||||
for i, p := range gray.Pix {
|
for i, p := range gray.Pix {
|
||||||
gray32[i] = float64(p)
|
gray32[i] = float64(p)
|
||||||
}
|
}
|
||||||
|
bufPool.Put(&gray.Pix)
|
||||||
flattens := transforms.DCT2DFast32(&gray32)
|
flattens := transforms.DCT2DFast32(&gray32)
|
||||||
|
|
||||||
median := etcs.MedianOfPixelsFast64(flattens[:])
|
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
|
// 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)
|
// Support 64bits ahash (width=8, height=8) and 256bits ahash (width=16, height=16)
|
||||||
func ExtAverageHash(img image.Image, width, height int) (*ExtImageHash, error) {
|
// func ExtAverageHash(img image.Image, width, height int) (*ExtImageHash, error) {
|
||||||
if img == nil {
|
// if img == nil {
|
||||||
return nil, errors.New("image object can not be nil")
|
// return nil, errors.New("image object can not be nil")
|
||||||
}
|
// }
|
||||||
var ahash []uint64
|
// var ahash []uint64
|
||||||
imgSize := width * height
|
// imgSize := width * height
|
||||||
|
|
||||||
gray := resize(ToGray(img), width, height)
|
// gray := resize(ToGray(img), width, height)
|
||||||
avg := etcs.MeanOfPixels(gray.Pix)
|
// avg := etcs.MeanOfPixels(gray.Pix)
|
||||||
|
|
||||||
lenOfUnit := 64
|
// lenOfUnit := 64
|
||||||
if imgSize%lenOfUnit == 0 {
|
// if imgSize%lenOfUnit == 0 {
|
||||||
ahash = make([]uint64, imgSize/lenOfUnit)
|
// ahash = make([]uint64, imgSize/lenOfUnit)
|
||||||
} else {
|
// } else {
|
||||||
ahash = make([]uint64, imgSize/lenOfUnit+1)
|
// ahash = make([]uint64, imgSize/lenOfUnit+1)
|
||||||
}
|
// }
|
||||||
for idx, p := range gray.Pix {
|
// for idx, p := range gray.Pix {
|
||||||
indexOfArray := idx / lenOfUnit
|
// indexOfArray := idx / lenOfUnit
|
||||||
indexOfBit := lenOfUnit - idx%lenOfUnit - 1
|
// indexOfBit := lenOfUnit - idx%lenOfUnit - 1
|
||||||
if p > avg {
|
// if p > avg {
|
||||||
ahash[indexOfArray] |= 1 << uint(indexOfBit)
|
// ahash[indexOfArray] |= 1 << uint(indexOfBit)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return NewExtImageHash(ahash, AHash, imgSize), nil
|
// return NewExtImageHash(ahash, AHash, imgSize), nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// ExtDifferenceHash function returns dhash of which the size can be set larger than uint64
|
// 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)
|
// Support 64bits dhash (width=8, height=8) and 256bits dhash (width=16, height=16)
|
||||||
func ExtDifferenceHash(img image.Image, width, height int) (*ExtImageHash, error) {
|
// func ExtDifferenceHash(img image.Image, width, height int) (*ExtImageHash, error) {
|
||||||
if img == nil {
|
// if img == nil {
|
||||||
return nil, errors.New("image object can not be nil")
|
// return nil, errors.New("image object can not be nil")
|
||||||
}
|
// }
|
||||||
|
|
||||||
var dhash []uint64
|
// var dhash []uint64
|
||||||
imgSize := width * height
|
// imgSize := width * height
|
||||||
|
|
||||||
gray := resize(ToGray(img), width+1, height)
|
// gray := resize(ToGray(img), width+1, height)
|
||||||
|
|
||||||
lenOfUnit := 64
|
// lenOfUnit := 64
|
||||||
if imgSize%lenOfUnit == 0 {
|
// if imgSize%lenOfUnit == 0 {
|
||||||
dhash = make([]uint64, imgSize/lenOfUnit)
|
// dhash = make([]uint64, imgSize/lenOfUnit)
|
||||||
} else {
|
// } else {
|
||||||
dhash = make([]uint64, imgSize/lenOfUnit+1)
|
// dhash = make([]uint64, imgSize/lenOfUnit+1)
|
||||||
}
|
// }
|
||||||
idx := 0
|
// idx := 0
|
||||||
for y := 0; y < gray.Bounds().Dy(); y++ {
|
// for y := 0; y < gray.Bounds().Dy(); y++ {
|
||||||
for x := 0; x < gray.Bounds().Dx()-1; x++ {
|
// for x := 0; x < gray.Bounds().Dx()-1; x++ {
|
||||||
if gray.Pix[gray.PixOffset(x, y)] < gray.Pix[gray.PixOffset(x+1, y)] {
|
// if gray.Pix[gray.PixOffset(x, y)] < gray.Pix[gray.PixOffset(x+1, y)] {
|
||||||
indexOfArray := idx / lenOfUnit
|
// indexOfArray := idx / lenOfUnit
|
||||||
indexOfBit := lenOfUnit - idx%lenOfUnit - 1
|
// indexOfBit := lenOfUnit - idx%lenOfUnit - 1
|
||||||
dhash[indexOfArray] |= 1 << uint(indexOfBit)
|
// dhash[indexOfArray] |= 1 << uint(indexOfBit)
|
||||||
}
|
// }
|
||||||
idx++
|
// idx++
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return NewExtImageHash(dhash, DHash, imgSize), nil
|
// return NewExtImageHash(dhash, DHash, imgSize), nil
|
||||||
}
|
// }
|
||||||
|
@ -152,39 +152,39 @@ func TestExtendHashCompute(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNilExtendHashCompute(t *testing.T) {
|
// func TestNilExtendHashCompute(t *testing.T) {
|
||||||
hash, err := ExtAverageHash(nil, 8, 8)
|
// hash, err := ExtAverageHash(nil, 8, 8)
|
||||||
if err == nil {
|
// if err == nil {
|
||||||
t.Errorf("Error should be got.")
|
// t.Errorf("Error should be got.")
|
||||||
}
|
// }
|
||||||
if hash != nil {
|
// if hash != nil {
|
||||||
t.Errorf("Nil hash should be got. but got %v", hash)
|
// t.Errorf("Nil hash should be got. but got %v", hash)
|
||||||
}
|
// }
|
||||||
|
|
||||||
hash, err = ExtDifferenceHash(nil, 8, 8)
|
// hash, err = ExtDifferenceHash(nil, 8, 8)
|
||||||
if err == nil {
|
// if err == nil {
|
||||||
t.Errorf("Error should be got.")
|
// t.Errorf("Error should be got.")
|
||||||
}
|
// }
|
||||||
if hash != nil {
|
// if hash != nil {
|
||||||
t.Errorf("Nil hash should be got. but got %v", hash)
|
// t.Errorf("Nil hash should be got. but got %v", hash)
|
||||||
}
|
// }
|
||||||
|
|
||||||
hash, err = ExtPerceptionHash(nil, 8, 8)
|
// hash, err = ExtPerceptionHash(nil, 8, 8)
|
||||||
if err == nil {
|
// if err == nil {
|
||||||
t.Errorf("Error should be got.")
|
// t.Errorf("Error should be got.")
|
||||||
}
|
// }
|
||||||
if hash != nil {
|
// if hash != nil {
|
||||||
t.Errorf("Nil hash should be got. but got %v", hash)
|
// t.Errorf("Nil hash should be got. but got %v", hash)
|
||||||
}
|
// }
|
||||||
|
|
||||||
hash, err = ExtPerceptionHash(nil, 8, 9)
|
// hash, err = ExtPerceptionHash(nil, 8, 9)
|
||||||
if err == nil {
|
// if err == nil {
|
||||||
t.Errorf("Error should be got.")
|
// t.Errorf("Error should be got.")
|
||||||
}
|
// }
|
||||||
if hash != nil {
|
// if hash != nil {
|
||||||
t.Errorf("Nil hash should be got. but got %v", hash)
|
// t.Errorf("Nil hash should be got. but got %v", hash)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
func BenchmarkDistanceIdentical(b *testing.B) {
|
func BenchmarkDistanceIdentical(b *testing.B) {
|
||||||
h1 := &ImageHash{hash: 0xe48ae53c05e502f7}
|
h1 := &ImageHash{hash: 0xe48ae53c05e502f7}
|
||||||
@ -204,129 +204,129 @@ func BenchmarkDistanceDifferent(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtImageHashCompute(t *testing.T) {
|
// func TestExtImageHashCompute(t *testing.T) {
|
||||||
for _, tt := range []struct {
|
// for _, tt := range []struct {
|
||||||
img1 string
|
// img1 string
|
||||||
img2 string
|
// img2 string
|
||||||
width int
|
// width int
|
||||||
height int
|
// height int
|
||||||
method func(img image.Image, width, height int) (*ExtImageHash, error)
|
// method func(img image.Image, width, height int) (*ExtImageHash, error)
|
||||||
name string
|
// name string
|
||||||
distance int
|
// distance int
|
||||||
}{
|
// }{
|
||||||
{"_examples/sample1.jpg", "_examples/sample1.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0},
|
// {"_examples/sample1.jpg", "_examples/sample1.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0},
|
||||||
{"_examples/sample2.jpg", "_examples/sample2.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/sample3.jpg", "_examples/sample3.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0},
|
||||||
{"_examples/sample4.jpg", "_examples/sample4.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/sample2.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 40},
|
||||||
{"_examples/sample1.jpg", "_examples/sample3.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0},
|
// {"_examples/sample1.jpg", "_examples/sample3.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 0},
|
||||||
{"_examples/sample1.jpg", "_examples/sample4.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 34},
|
// {"_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/sample3.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 40},
|
||||||
{"_examples/sample2.jpg", "_examples/sample4.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 6},
|
// {"_examples/sample2.jpg", "_examples/sample4.jpg", 8, 8, ExtAverageHash, "ExtAverageHash", 6},
|
||||||
{"_examples/sample1.jpg", "_examples/sample1.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 0},
|
// {"_examples/sample1.jpg", "_examples/sample1.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 0},
|
||||||
{"_examples/sample2.jpg", "_examples/sample2.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/sample3.jpg", "_examples/sample3.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 0},
|
||||||
{"_examples/sample4.jpg", "_examples/sample4.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/sample2.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 143},
|
||||||
{"_examples/sample1.jpg", "_examples/sample3.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 3},
|
// {"_examples/sample1.jpg", "_examples/sample3.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 3},
|
||||||
{"_examples/sample1.jpg", "_examples/sample4.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 148},
|
// {"_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/sample3.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 146},
|
||||||
{"_examples/sample2.jpg", "_examples/sample4.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 31},
|
// {"_examples/sample2.jpg", "_examples/sample4.jpg", 16, 16, ExtAverageHash, "ExtAverageHash", 31},
|
||||||
{"_examples/sample1.jpg", "_examples/sample1.jpg", 17, 17, ExtAverageHash, "ExtAverageHash", 0},
|
// {"_examples/sample1.jpg", "_examples/sample1.jpg", 17, 17, ExtAverageHash, "ExtAverageHash", 0},
|
||||||
{"_examples/sample2.jpg", "_examples/sample2.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/sample3.jpg", "_examples/sample3.jpg", 17, 17, ExtAverageHash, "ExtAverageHash", 0},
|
||||||
{"_examples/sample4.jpg", "_examples/sample4.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/sample1.jpg", "_examples/sample1.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 0},
|
||||||
{"_examples/sample2.jpg", "_examples/sample2.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/sample3.jpg", "_examples/sample3.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 0},
|
||||||
{"_examples/sample4.jpg", "_examples/sample4.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/sample2.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 32},
|
||||||
{"_examples/sample1.jpg", "_examples/sample3.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 2},
|
// {"_examples/sample1.jpg", "_examples/sample3.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 2},
|
||||||
{"_examples/sample1.jpg", "_examples/sample4.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 30},
|
// {"_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/sample3.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 34},
|
||||||
{"_examples/sample2.jpg", "_examples/sample4.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 20},
|
// {"_examples/sample2.jpg", "_examples/sample4.jpg", 8, 4, ExtPerceptionHash, "ExtPerceptionHash", 20},
|
||||||
{"_examples/sample1.jpg", "_examples/sample1.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 0},
|
// {"_examples/sample1.jpg", "_examples/sample1.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 0},
|
||||||
{"_examples/sample2.jpg", "_examples/sample2.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/sample3.jpg", "_examples/sample3.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 0},
|
||||||
{"_examples/sample4.jpg", "_examples/sample4.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/sample2.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 124},
|
||||||
{"_examples/sample1.jpg", "_examples/sample3.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 14},
|
// {"_examples/sample1.jpg", "_examples/sample3.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 14},
|
||||||
{"_examples/sample1.jpg", "_examples/sample4.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 122},
|
// {"_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/sample3.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 120},
|
||||||
{"_examples/sample2.jpg", "_examples/sample4.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 102},
|
// {"_examples/sample2.jpg", "_examples/sample4.jpg", 16, 4, ExtPerceptionHash, "ExtPerceptionHash", 102},
|
||||||
{"_examples/sample1.jpg", "_examples/sample1.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 0},
|
// {"_examples/sample1.jpg", "_examples/sample1.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 0},
|
||||||
{"_examples/sample2.jpg", "_examples/sample2.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/sample3.jpg", "_examples/sample3.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 0},
|
||||||
{"_examples/sample4.jpg", "_examples/sample4.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/sample2.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 40},
|
||||||
{"_examples/sample1.jpg", "_examples/sample3.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 2},
|
// {"_examples/sample1.jpg", "_examples/sample3.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 2},
|
||||||
{"_examples/sample1.jpg", "_examples/sample4.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 38},
|
// {"_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/sample3.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 42},
|
||||||
{"_examples/sample2.jpg", "_examples/sample4.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 20},
|
// {"_examples/sample2.jpg", "_examples/sample4.jpg", 8, 8, ExtDifferenceHash, "ExtDifferenceHash", 20},
|
||||||
{"_examples/sample1.jpg", "_examples/sample1.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 0},
|
// {"_examples/sample1.jpg", "_examples/sample1.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 0},
|
||||||
{"_examples/sample2.jpg", "_examples/sample2.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/sample3.jpg", "_examples/sample3.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 0},
|
||||||
{"_examples/sample4.jpg", "_examples/sample4.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/sample2.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 137},
|
||||||
{"_examples/sample1.jpg", "_examples/sample3.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 13},
|
// {"_examples/sample1.jpg", "_examples/sample3.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 13},
|
||||||
{"_examples/sample1.jpg", "_examples/sample4.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 124},
|
// {"_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/sample3.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 140},
|
||||||
{"_examples/sample2.jpg", "_examples/sample4.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 109},
|
// {"_examples/sample2.jpg", "_examples/sample4.jpg", 16, 16, ExtDifferenceHash, "ExtDifferenceHash", 109},
|
||||||
{"_examples/sample1.jpg", "_examples/sample1.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0},
|
// {"_examples/sample1.jpg", "_examples/sample1.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0},
|
||||||
{"_examples/sample2.jpg", "_examples/sample2.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/sample3.jpg", "_examples/sample3.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0},
|
||||||
{"_examples/sample4.jpg", "_examples/sample4.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0},
|
// {"_examples/sample4.jpg", "_examples/sample4.jpg", 17, 17, ExtDifferenceHash, "ExtDifferenceHash", 0},
|
||||||
} {
|
// } {
|
||||||
file1, err := os.Open(tt.img1)
|
// file1, err := os.Open(tt.img1)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Errorf("%s", err)
|
// t.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
defer file1.Close()
|
// defer file1.Close()
|
||||||
|
|
||||||
file2, err := os.Open(tt.img2)
|
// file2, err := os.Open(tt.img2)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Errorf("%s", err)
|
// t.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
defer file2.Close()
|
// defer file2.Close()
|
||||||
|
|
||||||
img1, err := jpeg.Decode(file1)
|
// img1, err := jpeg.Decode(file1)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Errorf("%s", err)
|
// t.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
img2, err := jpeg.Decode(file2)
|
// img2, err := jpeg.Decode(file2)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Errorf("%s", err)
|
// t.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
hash1, err := tt.method(img1, tt.width, tt.height)
|
// hash1, err := tt.method(img1, tt.width, tt.height)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Errorf("%s", err)
|
// t.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
hash2, err := tt.method(img2, tt.width, tt.height)
|
// hash2, err := tt.method(img2, tt.width, tt.height)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Errorf("%s", err)
|
// t.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
dis1, err := hash1.Distance(hash2)
|
// dis1, err := hash1.Distance(hash2)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Errorf("%s", err)
|
// t.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
dis2, err := hash2.Distance(hash1)
|
// dis2, err := hash2.Distance(hash1)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Errorf("%s", err)
|
// t.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
if dis1 != dis2 {
|
// if dis1 != dis2 {
|
||||||
t.Errorf("Distance should be identical %v vs %v", dis1, dis2)
|
// t.Errorf("Distance should be identical %v vs %v", dis1, dis2)
|
||||||
}
|
// }
|
||||||
|
|
||||||
if dis1 != tt.distance {
|
// 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)
|
// 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) {
|
func BenchmarkExtImageHashDistanceDifferent(b *testing.B) {
|
||||||
h1 := &ExtImageHash{hash: []uint64{0xe48ae53c05e502f7}}
|
h1 := &ExtImageHash{hash: []uint64{0xe48ae53c05e502f7}}
|
||||||
@ -358,38 +358,38 @@ func BenchmarkPerceptionHash(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkAverageHash(b *testing.B) {
|
// func BenchmarkAverageHash(b *testing.B) {
|
||||||
file1, err := os.Open("_examples/sample3.jpg")
|
// file1, err := os.Open("_examples/sample3.jpg")
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
b.Errorf("%s", err)
|
// b.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
defer file1.Close()
|
// defer file1.Close()
|
||||||
img1, err := jpeg.Decode(file1)
|
// img1, err := jpeg.Decode(file1)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
b.Errorf("%s", err)
|
// b.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
for i := 0; i < b.N; i++ {
|
// for i := 0; i < b.N; i++ {
|
||||||
_, err := ExtAverageHash(img1, 8, 8)
|
// _, err := ExtAverageHash(img1, 8, 8)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
b.Errorf("%s", err)
|
// b.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
func BenchmarkDifferenceHash(b *testing.B) {
|
// func BenchmarkDifferenceHash(b *testing.B) {
|
||||||
file1, err := os.Open("_examples/sample3.jpg")
|
// file1, err := os.Open("_examples/sample3.jpg")
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
b.Errorf("%s", err)
|
// b.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
defer file1.Close()
|
// defer file1.Close()
|
||||||
img1, err := jpeg.Decode(file1)
|
// img1, err := jpeg.Decode(file1)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
b.Errorf("%s", err)
|
// b.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
for i := 0; i < b.N; i++ {
|
// for i := 0; i < b.N; i++ {
|
||||||
_, err := ExtDifferenceHash(img1, 8, 8)
|
// _, err := ExtDifferenceHash(img1, 8, 8)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
b.Errorf("%s", err)
|
// b.Errorf("%s", err)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
@ -78,7 +78,7 @@ func TestSerialization(t *testing.T) {
|
|||||||
AverageHash, PerceptionHash, DifferenceHash,
|
AverageHash, PerceptionHash, DifferenceHash,
|
||||||
}
|
}
|
||||||
extMethods := []func(img image.Image, width int, height int) (*ExtImageHash, error){
|
extMethods := []func(img image.Image, width int, height int) (*ExtImageHash, error){
|
||||||
ExtAverageHash /* ExtPerceptionHash, */, ExtDifferenceHash,
|
// ExtAverageHash ExtPerceptionHash, , ExtDifferenceHash,
|
||||||
}
|
}
|
||||||
examples := []string{
|
examples := []string{
|
||||||
"_examples/sample1.jpg", "_examples/sample2.jpg", "_examples/sample3.jpg", "_examples/sample4.jpg",
|
"_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)
|
hash, err := method(img)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
hex := hash.String(false)
|
hex := hash.String()
|
||||||
// len(kind) == 1, len(":") == 1, len(hash) == 16
|
// len(kind) == 1, len(":") == 1, len(hash) == 16
|
||||||
if len(hex) != 18 {
|
if len(hex) != 18 {
|
||||||
t.Errorf("Got invalid hex string '%v'; %v of '%v'", hex, methodStr, ex)
|
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")
|
extImageHash, err = ExtImageHashFromString("k:g")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDifferentBitSizeHash(t *testing.T) {
|
// func TestDifferentBitSizeHash(t *testing.T) {
|
||||||
checkErr := func(err error) {
|
// checkErr := func(err error) {
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Errorf("%v", err)
|
// t.Errorf("%v", err)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
file, err := os.Open("_examples/sample1.jpg")
|
// file, err := os.Open("_examples/sample1.jpg")
|
||||||
checkErr(err)
|
// checkErr(err)
|
||||||
defer file.Close()
|
// defer file.Close()
|
||||||
|
|
||||||
img, _, err := image.Decode(file)
|
// img, _, err := image.Decode(file)
|
||||||
checkErr(err)
|
// checkErr(err)
|
||||||
|
|
||||||
hash1, _ := ExtAverageHash(img, 32, 32)
|
// hash1, _ := ExtAverageHash(img, 32, 32)
|
||||||
hash2, _ := ExtDifferenceHash(img, 32, 32)
|
// hash2, _ := ExtDifferenceHash(img, 32, 32)
|
||||||
_, err = hash1.Distance(hash2)
|
// _, err = hash1.Distance(hash2)
|
||||||
if err == nil {
|
// if err == nil {
|
||||||
t.Errorf("Should got error with different kinds of hashes")
|
// t.Errorf("Should got error with different kinds of hashes")
|
||||||
}
|
// }
|
||||||
hash3, _ := ExtAverageHash(img, 31, 31)
|
// hash3, _ := ExtAverageHash(img, 31, 31)
|
||||||
_, err = hash1.Distance(hash3)
|
// _, err = hash1.Distance(hash3)
|
||||||
if err == nil {
|
// if err == nil {
|
||||||
t.Errorf("Should got error with different bits of hashes")
|
// t.Errorf("Should got error with different bits of hashes")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
func TestDumpAndLoad(t *testing.T) {
|
func TestDumpAndLoad(t *testing.T) {
|
||||||
checkErr := func(err error) {
|
checkErr := func(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -237,7 +237,7 @@ func TestDumpAndLoad(t *testing.T) {
|
|||||||
|
|
||||||
// test for ExtIExtImageHash
|
// test for ExtIExtImageHash
|
||||||
extMethods := []func(img image.Image, width, height int) (*ExtImageHash, error){
|
extMethods := []func(img image.Image, width, height int) (*ExtImageHash, error){
|
||||||
ExtAverageHash, ExtPerceptionHash, ExtDifferenceHash,
|
// ExtAverageHash, ExtPerceptionHash, ExtDifferenceHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
sizeList := []int{8, 16}
|
sizeList := []int{8, 16}
|
||||||
|
@ -7,8 +7,16 @@ license_files = LICENSE
|
|||||||
classifiers =
|
classifiers =
|
||||||
License :: OSI Approved :: BSD License
|
License :: OSI Approved :: BSD License
|
||||||
|
|
||||||
|
[options]
|
||||||
|
packages = hashImage
|
||||||
|
|
||||||
|
[tox:tox]
|
||||||
|
minversion = 4.0.0
|
||||||
|
basepython = python3.9
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
description = Test go imagehash
|
description = Test go imagehash
|
||||||
|
skip_install = true
|
||||||
deps =
|
deps =
|
||||||
pytest>=7
|
pytest>=7
|
||||||
imagehash
|
imagehash
|
||||||
|
@ -24,16 +24,17 @@ def pytest_report_teststatus(report, config):
|
|||||||
ahash = props.get('ahash:', -1)
|
ahash = props.get('ahash:', -1)
|
||||||
dhash = props.get('dhash:', -1)
|
dhash = props.get('dhash:', -1)
|
||||||
phash = props.get('phash:', -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':
|
if report.outcome == 'failed':
|
||||||
return 'failed', 'E', ('EXACT', {'red': True})
|
return 'failed', 'F', ('FAIL', {'red': True})
|
||||||
return 'exact', 'E', ('EXACT', {'green': True})
|
return 'exact', 'E', ('EXACT', {'green': True})
|
||||||
if ahash >= 10 or dhash >= 10 or phash >= 10:
|
if (ahash <= 4 and dhash <= 4 and phash <= 4) and 0 in (ahash, dhash, phash):
|
||||||
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:
|
|
||||||
return 'compatible', 'C', ('COMPATIBLE', {'green': True})
|
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):
|
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 {}')",
|
'--hasher', default='go run ./hashImage -file {}', help="The commandline image hasher to test ('go run ./hashImage -file {}')",
|
||||||
)
|
)
|
||||||
parser.addoption(
|
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(
|
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 pytest_generate_tests(metafunc):
|
def get_images(path: pathlib.Path, recurse: bool, /, limit: str = '') -> list[str]:
|
||||||
if 'file' in metafunc.fixturenames:
|
|
||||||
file = metafunc.config.getoption('--image-dir')
|
|
||||||
files = []
|
files = []
|
||||||
cwd = pathlib.Path('').absolute()
|
cwd = pathlib.Path('').absolute()
|
||||||
for p in pathlib.Path(file).iterdir():
|
for p in pathlib.Path(path).iterdir():
|
||||||
if not p.is_file():
|
if p.is_dir():
|
||||||
|
if recurse:
|
||||||
|
files.extend(get_images(p, recurse, limit=limit))
|
||||||
|
continue
|
||||||
|
if limit and limit not in str(p):
|
||||||
continue
|
continue
|
||||||
p = p.absolute()
|
p = p.absolute()
|
||||||
if p.is_relative_to(cwd):
|
if p.is_relative_to(cwd):
|
||||||
files.append(str(p.relative_to(cwd)))
|
files.append(str(p.relative_to(cwd)))
|
||||||
else:
|
else:
|
||||||
files.append(str(p.absolute()))
|
files.append(str(p.absolute()))
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_generate_tests(metafunc):
|
||||||
|
if 'file' in metafunc.fixturenames:
|
||||||
|
file = metafunc.config.getoption('--image-dir')
|
||||||
|
files = []
|
||||||
|
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)
|
metafunc.parametrize('file', files)
|
||||||
|
@ -23,7 +23,7 @@ def hamming_distance(h1, h2) -> int:
|
|||||||
n = n1 ^ n2
|
n = n1 ^ n2
|
||||||
|
|
||||||
# count up the 1's in the binary string
|
# 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):
|
def test_hash(request, file, record_property):
|
||||||
@ -31,6 +31,7 @@ def test_hash(request, file, record_property):
|
|||||||
try:
|
try:
|
||||||
python = hashImage.main(['-file', file]) + '\n'
|
python = hashImage.main(['-file', file]) + '\n'
|
||||||
except Exception:
|
except Exception:
|
||||||
|
if request.config.getoption('--skip-invalid-image'):
|
||||||
pytest.skip('python imagehash failed')
|
pytest.skip('python imagehash failed')
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -44,10 +45,12 @@ def test_hash(request, file, record_property):
|
|||||||
external = subprocess.run(cmd_list, shell=None, capture_output=True)
|
external = subprocess.run(cmd_list, shell=None, capture_output=True)
|
||||||
external_stdout = str(external.stdout, encoding='utf-8')
|
external_stdout = str(external.stdout, encoding='utf-8')
|
||||||
if external.returncode != 0:
|
if external.returncode != 0:
|
||||||
|
if request.config.getoption('--skip-invalid-image'):
|
||||||
pytest.skip(str(external.stderr))
|
pytest.skip(str(external.stderr))
|
||||||
return
|
return
|
||||||
|
|
||||||
external_stdout_h = python_h = ''
|
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(' ')
|
p_, e_ = p.split(' '), e.split(' ')
|
||||||
h = hamming_distance(p_[1], e_[1])
|
h = hamming_distance(p_[1], e_[1])
|
||||||
record_property(p_[0], h)
|
record_property(p_[0], h)
|
||||||
|
@ -9,30 +9,54 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var FastYCBCR = true
|
||||||
|
|
||||||
func toGray(r, g, b uint8) uint8 {
|
func toGray(r, g, b uint8) uint8 {
|
||||||
// 19595 + 38470 + 7471 equals 65536.
|
// 19595 + 38470 + 7471 equals 65536.
|
||||||
return uint8((19595*uint32(r) + 38470*uint32(g) + 7471*uint32(b) + 1<<15) >> 16)
|
return uint8((19595*uint32(r) + 38470*uint32(g) + 7471*uint32(b) + 1<<15) >> 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rgb2Gray function converts RGB to a gray scale array.
|
// 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()
|
colorImg.Bounds().Dx()
|
||||||
bounds := colorImg.Bounds()
|
bounds := colorImg.Bounds()
|
||||||
w, h := bounds.Dx(), bounds.Dy()
|
w, h := bounds.Dx(), bounds.Dy()
|
||||||
pixels := make([]uint8, h*w)
|
if pixels == nil {
|
||||||
for i := range pixels {
|
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
|
x, y := i%w, i/w
|
||||||
|
yi = img.YOffset(x, y)
|
||||||
|
if FastYCBCR {
|
||||||
|
pixels[i] = img.Y[yi]
|
||||||
|
} else {
|
||||||
|
ci = img.COffset(x, y)
|
||||||
|
|
||||||
|
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
|
var R, G, B uint32
|
||||||
switch v := colorImg.At(x, y).(type) {
|
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
|
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
|
R, G, B = uint32(v.R)<<8, uint32(v.G)<<8, uint32(v.B)<<8
|
||||||
default:
|
default:
|
||||||
R, G, B, _ = colorImg.At(x, y).RGBA()
|
R, G, B, _ = v.RGBA()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pillow only operates on 8bit data, operating on higher bit data produces rounding differences
|
// 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))
|
pixels[i] = toGray(uint8(R>>8), uint8(G>>8), uint8(B>>8))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return pixels
|
return pixels
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user