pdqhash-go

git clone git://archive.git.mtrnord.blog/MTRNord/pdqhash-go.git
Log | Files | Refs | LICENSE

commit bb38219da9141413d874ac0b26bb82ca17881a52
parent 1df71b59bbf393268c88db5036432a16e12c1a47
Author: MTRNord <mtrnord1@gmail.com>
Date:   Sun, 25 Feb 2024 01:50:13 +0100

Initial conversion of the python version to a golang version

Diffstat:
M.gitignore | 3+++
Acmd/scanner.go | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago.mod | 15+++++++++++++++
Ago.sum | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahelpers/helpers.go | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apdq_hasher.go | 549+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atypes/hash256.go | 237+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 1029 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -19,3 +19,6 @@ # Go workspace file go.work + +scanner +blur-a-little.jpg diff --git a/cmd/scanner.go b/cmd/scanner.go @@ -0,0 +1,91 @@ +package main + +import ( + "flag" + "log" + "os" + "path/filepath" + + pdq "github.com/MTRNord/pdqhash-go" + "github.com/MTRNord/pdqhash-go/types" + "github.com/davidbyttow/govips/v2/vips" + "github.com/h2non/filetype" +) + +// This is an example. It is not meant to be run in prod. + +func processFile(filename string, detailed bool) { + pdqhasher := pdq.NewPDQHasher() + + // Find all image files in the folder + path, _ := filepath.Abs(filename) + items, err := os.ReadDir(filename) + if err != nil { + log.Fatal(err) + } + + numPDQHash := 0 + var prevHash *types.Hash256 + + for _, item := range items { + fullPath := filepath.Join(path, item.Name()) + if !item.IsDir() { + // Check if file is an image + filetypeRef, err := filetype.MatchFile(fullPath) + if err != nil { + log.Fatal(err) + } + if filetypeRef.MIME.Type == "image" { + hashAndQuality := pdqhasher.FromFile(fullPath) + delta := 0 + if numPDQHash == 0 { + delta = 0 + } else { + delta = hashAndQuality.Hash.HammingDistance(prevHash) + } + + if detailed { + log.Printf("hash=%s,norm=%d,delta=%d,quality=%d,filename=%s", hashAndQuality.Hash.String(), hashAndQuality.Hash.HammingNorm(), delta, hashAndQuality.Quality, item.Name()) + } else { + log.Printf("%s,%d,%s", hashAndQuality.Hash.String(), hashAndQuality.Quality, item.Name()) + } + + prevHash = hashAndQuality.Hash + numPDQHash++ + } + } + } +} + +func main() { + var folder string + var detailedOutput bool + + flag.StringVar(&folder, "folder", "", "Folder to scan") + flag.BoolVar(&detailedOutput, "detailed", false, "Detailed output") + + flag.Parse() + + // Check if folder exists and is a folder + fileInfo, err := os.Stat(folder) + if err != nil { + log.Fatal(err) + } + if !fileInfo.IsDir() { + log.Fatalf("'%s' is not a folder", folder) + } + + vips.LoggingSettings(nil, vips.LogLevelMessage) + vips.Startup(&vips.Config{ + ConcurrencyLevel: 0, + MaxCacheFiles: 5, + MaxCacheMem: 50 * 1024 * 1024, + MaxCacheSize: 100, + ReportLeaks: false, + CacheTrace: false, + CollectStats: false, + }) + defer vips.Shutdown() + + processFile(folder, detailedOutput) +} diff --git a/go.mod b/go.mod @@ -0,0 +1,15 @@ +module github.com/MTRNord/pdqhash-go + +go 1.21.7 + +require ( + github.com/davidbyttow/govips/v2 v2.13.0 + github.com/h2non/filetype v1.1.3 + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 +) + +require ( + golang.org/x/image v0.15.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum @@ -0,0 +1,55 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidbyttow/govips/v2 v2.13.0 h1:5MK9ZcXZC5GzUR9Ca8fJwOYqMgll/H096ec0PJP59QM= +github.com/davidbyttow/govips/v2 v2.13.0/go.mod h1:LPTrwWtNa5n4yl9UC52YBOEGdZcY5hDTP4Ms2QWasTw= +github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= +golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= +golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helpers/helpers.go b/helpers/helpers.go @@ -0,0 +1,79 @@ +package helpers + +import "golang.org/x/exp/constraints" + +func Abs[T constraints.Integer](x T) T { + if x < 0 { + return -x + } + return x +} + +func Torben(m [][]float64, numRows, numCols int) float64 { + n := numRows * numCols + midn := (n + 1) / 2 + less := 0 + greater := 0 + equal := 0 + min := m[0][0] + max := m[0][0] + guess := 0.0 + maxltguess := 0.0 + mingtguess := 0.0 + for i := 0; i < numRows; i++ { + for j := 0; j < numCols; j++ { + v := m[i][j] + if v < min { + min = v + } + + if v > max { + max = v + } + } + } + + for { + guess = (min + max) / 2 + less = 0 + greater = 0 + equal = 0 + maxltguess = min + mingtguess = max + + for i := 0; i < numRows; i++ { + for j := 0; j < numCols; j++ { + v := m[i][j] + if v < guess { + less++ + if v > maxltguess { + maxltguess = v + } + } else if v > guess { + greater++ + if v < mingtguess { + mingtguess = v + } + } else { + equal++ + } + } + } + + if less <= midn && greater <= midn { + break + } else if less > greater { + max = maxltguess + } else { + min = mingtguess + } + } + + if less >= midn { + return maxltguess + } else if less+equal >= midn { + return guess + } else { + return mingtguess + } +} diff --git a/pdq_hasher.go b/pdq_hasher.go @@ -0,0 +1,549 @@ +package pdq + +import ( + "log" + "math" + + "github.com/MTRNord/pdqhash-go/helpers" + "github.com/MTRNord/pdqhash-go/types" + "github.com/davidbyttow/govips/v2/vips" + + _ "image/jpeg" + _ "image/png" +) + +// From Wikipedia: standard RGB to luminance (the 'Y' in 'YUV'). +const LUMA_FROM_R_COEFF = 0.299 +const LUMA_FROM_G_COEFF = 0.587 +const LUMA_FROM_B_COEFF = 0.114 + +func DCT_MATRIX_SCALE_FACTOR() float64 { + return math.Sqrt(2.0 / 64.0) +} + +// Wojciech Jarosz 'Fast Image Convolutions' ACM SIGGRAPH 2001: +// X,Y,X,Y passes of 1-D box filters produces a 2D tent filter. +const PDQ_NUM_JAROSZ_XY_PASSES = 2 + +/* +Since PDQ uses 64x64 blocks, 1/64th of the image height/width +respectively is a full block. But since we use two passes, we want half +that window size per pass. Example: 1024x1024 full-resolution input. PDQ +downsamples to 64x64. Each 16x16 block of the input produces a single +downsample pixel. X,Y passes with window size 8 (= 1024/128) average +pixels with 8x8 neighbors. The second X,Y pair of 1D box-filter passes +accumulate data from all 16x16. +*/ +const PDQ_JAROSZ_WINDOW_SIZE_DIVISOR = 128 + +// Flags for which dihedral-transforms are desired to be produced. +const PDQ_DO_DIH_ORIGINAL = 0x01 +const PDQ_DO_DIH_ROTATE_90 = 0x02 +const PDQ_DO_DIH_ROTATE_180 = 0x04 +const PDQ_DO_DIH_ROTATE_270 = 0x08 +const PDQ_DO_DIH_FLIPX = 0x10 +const PDQ_DO_DIH_FLIPY = 0x20 +const PDQ_DO_DIH_FLIP_PLUS1 = 0x40 +const PDQ_DO_DIH_FLIP_MINUS1 = 0x80 +const PDQ_DO_DIH_ALL = 0xFF + +/** + * The only class state is the DCT matrix, so this class may either be + * instantiated once per image, or instantiated once and used for all images; + * the latter will be slightly faster as the DCT matrix will not need to be + * recomputed once per image. + */ +type PDQHasher struct { + DCT_matrix [][]float64 +} + +/** + * Container for multiple-value object: the hash is a 64-character hex + * string and the quality is an integer in the range 0..100. + */ +type HashAndQuality struct { + Hash *types.Hash256 + Quality int +} + +type HashesAndQuality struct { + hash *types.Hash256 + hashRotate90 *types.Hash256 + hashRotate180 *types.Hash256 + hashRotate270 *types.Hash256 + hashFlipX *types.Hash256 + hashFlipY *types.Hash256 + hashFlipPlus1 *types.Hash256 + hashFlipMinus1 *types.Hash256 + quality int +} + +func NewPDQHasher() *PDQHasher { + return &PDQHasher{ + DCT_matrix: ComputeDCTMatrix(), + } +} + +func ComputeDCTMatrix() [][]float64 { + d := make([][]float64, 16) + for i := 0; i < 16; i++ { + di := make([]float64, 64) + for j := 0; j < 64; j++ { + di[j] = DCT_MATRIX_SCALE_FACTOR() * math.Cos((math.Pi/2.0/64.0)*(float64(i)+1.0)*(2.0*float64(j)+1.0)) + } + d[i] = di + } + + return d +} + +func allocateMatrix(numRows, numCols int) [][]float64 { + // Create a slice of slices to represent the matrix + matrix := make([][]float64, numRows) + + // Allocate memory for each row + for i := range matrix { + matrix[i] = make([]float64, numCols) + } + + return matrix +} + +func (p *PDQHasher) FromFile(filename string) HashAndQuality { + params := vips.NewImportParams() + params.AutoRotate.Set(false) + + image, err := vips.LoadImageFromFile(filename, params) + if err != nil { + log.Fatalf("Error opening file: %v", err) + } + + // resizing the image proportionally to max 512px width and max 512px height + err = image.Thumbnail(512, 512, vips.InterestingNone) + if err != nil { + log.Fatalf("Error resizing image: %v", err) + } + numCols := image.Width() + numRows := image.Height() + + buffer1 := make([]float64, numCols*numRows) + buffer2 := make([]float64, numCols*numRows) + buffer64x64 := allocateMatrix(64, 64) + buffer16x64 := allocateMatrix(16, 64) + buffer16x16 := allocateMatrix(16, 16) + + return p.FromImage(image, buffer1, buffer2, buffer64x64, buffer16x64, buffer16x16) +} + +func (p *PDQHasher) FromImage(image *vips.ImageRef, buffer1, buffer2 []float64, buffer64x64, buffer16x64, buffer16x16 [][]float64) HashAndQuality { + numCols := image.Width() + numRows := image.Height() + + p.fillFloatLumaFromBufferImage(image, &buffer1) + + return p.pdqHash256FromFloatLuma(buffer1, buffer2, numRows, numCols, buffer64x64, buffer16x64, buffer16x16) +} + +func (p *PDQHasher) fillFloatLumaFromBufferImage(image *vips.ImageRef, luma *[]float64) { + numCols := image.Width() + numRows := image.Height() + + err := image.ToColorSpace(vips.InterpretationSRGB) + if err != nil { + log.Fatalf("Error converting to RGB: %v", err) + } + goImage, err := image.ToImage(nil) + if err != nil { + log.Fatalf("Error converting to Go image: %v", err) + } + + for i := 0; i < numRows; i++ { + for j := 0; j < numCols; j++ { + colorArray := goImage.At(j, i) + r, g, b, _ := colorArray.RGBA() + (*luma)[i*numCols+j] = LUMA_FROM_R_COEFF*float64(r) + LUMA_FROM_G_COEFF*float64(g) + LUMA_FROM_B_COEFF*float64(b) + } + } +} + +func (p *PDQHasher) pdqHash256FromFloatLuma(fullBuffer1, fullBuffer2 []float64, numRows, numCols int, buffer64x64, buffer16x64, buffer16x16 [][]float64) HashAndQuality { + windowSizeAlongRows := p.computeJaroszWindowSize(numCols) + windowSizeAlongCols := p.computeJaroszWindowSize(numRows) + p.jaroszFilterFloat(&fullBuffer1, &fullBuffer2, numRows, numCols, windowSizeAlongRows, windowSizeAlongCols, PDQ_NUM_JAROSZ_XY_PASSES) + + p.decimateFloat(&fullBuffer1, numRows, numCols, &buffer64x64) + quality := p.computePDQImageDomainQualityMetric(buffer64x64) + p.dct64To16(&buffer64x64, &buffer16x64, &buffer16x16) + hash := p.pdqBuffer16x16ToBits(buffer16x16) + + return HashAndQuality{hash, quality} +} + +func (p *PDQHasher) dihedralFromFile(filename string, dihedralFlags int) HashesAndQuality { + vips.Startup(nil) + defer vips.Shutdown() + image, err := vips.NewImageFromFile(filename) + if err != nil { + log.Fatalf("Error opening file: %v", err) + } + + numRows := image.Height() + numCols := image.Width() + + buffer1 := make([]float64, numCols*numRows) + buffer2 := make([]float64, numCols*numRows) + + buffer64x64 := allocateMatrix(64, 64) + buffer16x64 := allocateMatrix(16, 64) + buffer16x16 := allocateMatrix(16, 16) + buffer16x16Aux := allocateMatrix(16, 16) + + return p.dihedralFromBufferedImage(image, buffer1, buffer2, buffer64x64, buffer16x64, buffer16x16, buffer16x16Aux, dihedralFlags) +} + +func (p *PDQHasher) dihedralFromBufferedImage(image *vips.ImageRef, buffer1, buffer2 []float64, buffer64x64, buffer16x64, buffer16x16, buffer16x16Aux [][]float64, dihedralFlags int) HashesAndQuality { + numRows := image.Height() + numCols := image.Width() + + p.fillFloatLumaFromBufferImage(image, &buffer1) + + return p.pdqHash256esFromFloatLuma(buffer1, buffer2, numRows, numCols, buffer64x64, buffer16x64, buffer16x16, buffer16x16Aux, dihedralFlags) +} + +func (p *PDQHasher) pdqHash256esFromFloatLuma(fullBuffer1, fullBuffer2 []float64, numRows, numCols int, buffer64x64, buffer16x64, buffer16x16, buffer16x16Aux [][]float64, dihedralFlags int) HashesAndQuality { + windowSizeAlongRows := p.computeJaroszWindowSize(numCols) + windowSizeAlongCols := p.computeJaroszWindowSize(numRows) + p.jaroszFilterFloat(&fullBuffer1, &fullBuffer2, numRows, numCols, windowSizeAlongRows, windowSizeAlongCols, PDQ_NUM_JAROSZ_XY_PASSES) + + p.decimateFloat(&fullBuffer1, numRows, numCols, &buffer64x64) + quality := p.computePDQImageDomainQualityMetric(buffer64x64) + p.dct64To16(&buffer64x64, &buffer16x64, &buffer16x16) + + var hash *types.Hash256 + var hashRotate90 *types.Hash256 + var hashRotate180 *types.Hash256 + var hashRotate270 *types.Hash256 + var hashFlipX *types.Hash256 + var hashFlipY *types.Hash256 + var hashFlipPlus1 *types.Hash256 + var hashFlipMinus1 *types.Hash256 + + if dihedralFlags&PDQ_DO_DIH_ORIGINAL != 0 { + hash = p.pdqBuffer16x16ToBits(buffer16x16) + } + + if dihedralFlags&PDQ_DO_DIH_ROTATE_90 != 0 { + p.dct16OriginalToRotate90(&buffer16x16, &buffer16x16Aux) + hashRotate90 = p.pdqBuffer16x16ToBits(buffer16x16Aux) + } + + if dihedralFlags&PDQ_DO_DIH_ROTATE_180 != 0 { + p.dct16OriginalToRotate180(&buffer16x16, &buffer16x16Aux) + hashRotate180 = p.pdqBuffer16x16ToBits(buffer16x16Aux) + } + + if dihedralFlags&PDQ_DO_DIH_ROTATE_270 != 0 { + p.dct16OriginalToRotate270(&buffer16x16, &buffer16x16Aux) + hashRotate270 = p.pdqBuffer16x16ToBits(buffer16x16Aux) + } + + if dihedralFlags&PDQ_DO_DIH_FLIPX != 0 { + p.dct16OriginalToFlipX(&buffer16x16, &buffer16x16Aux) + hashFlipX = p.pdqBuffer16x16ToBits(buffer16x16Aux) + } + + if dihedralFlags&PDQ_DO_DIH_FLIPY != 0 { + p.dct16OriginalToFlipY(&buffer16x16, &buffer16x16Aux) + hashFlipY = p.pdqBuffer16x16ToBits(buffer16x16Aux) + } + + if dihedralFlags&PDQ_DO_DIH_FLIP_PLUS1 != 0 { + p.dct16OriginalToFlipPlus1(&buffer16x16, &buffer16x16Aux) + hashFlipPlus1 = p.pdqBuffer16x16ToBits(buffer16x16Aux) + } + + if dihedralFlags&PDQ_DO_DIH_FLIP_MINUS1 != 0 { + p.dct16OriginalToFlipMinus1(&buffer16x16, &buffer16x16Aux) + hashFlipMinus1 = p.pdqBuffer16x16ToBits(buffer16x16Aux) + } + + return HashesAndQuality{hash, hashRotate90, hashRotate180, hashRotate270, hashFlipX, hashFlipY, hashFlipPlus1, hashFlipMinus1, quality} +} + +// numRows x numCols in row-major order +func (p *PDQHasher) decimateFloat(in *[]float64, inNumRows, inNumCols int, out *[][]float64) { + for i := 0; i < 64; i++ { + ini := int(((float64(i) + 0.5) * float64(inNumRows)) / 64) + for j := 0; j < 64; j++ { + inj := int(((float64(j) + 0.5) * float64(inNumCols)) / 64) + (*out)[i][j] = (*in)[ini*inNumCols+inj] + } + } +} + +/** + * This is all heuristic (see the PDQ hashing doc). Quantization + * matters since we want to count *significant* gradients, not just the + * some of many small ones. The constants are all manually selected, and + * tuned as described in the document. + */ +func (p *PDQHasher) computePDQImageDomainQualityMetric(buffer64x64 [][]float64) int { + gradientSum := 0 + for i := 0; i < 63; i++ { + for j := 0; j < 64; j++ { + u := buffer64x64[i][j] + v := buffer64x64[i+1][j] + d := int(((u - v) * 100) / 255) + gradientSum += int(helpers.Abs(d)) + } + } + for i := 0; i < 64; i++ { + for j := 0; j < 63; j++ { + u := buffer64x64[i][j] + v := buffer64x64[i][j+1] + d := int(((u - v) * 100) / 255) + gradientSum += int(helpers.Abs(d)) + } + } + quality := int(gradientSum / 90) + if quality > 100 { + quality = 100 + } + return quality +} + +/** + * Full 64x64 to 64x64 can be optimized e.g. the Lee algorithm. + * But here we only want slots (1-16)x(1-16) of the full 64x64 output. + * Careful experiments showed that using Lee along all 64 slots in one + * dimension, then Lee along 16 slots in the second, followed by + * extracting slots 1-16 of the output, was actually slower than the + * current implementation which is completely non-clever/non-Lee but + * computes only what is needed. + */ +func (p *PDQHasher) dct64To16(A, T, B *([][]float64)) { + D := p.DCT_matrix + + *T = make([][]float64, 16) + for i := 0; i < 16; i++ { + ti := make([]float64, 64) + + for j := 0; j < 64; j++ { + tij := 0.0 + for k := 0; k < 64; k++ { + tij += D[i][k] * (*A)[k][j] + } + ti[j] = tij + } + (*T)[i] = ti + } + + for i := 0; i < 16; i++ { + for j := 0; j < 16; j++ { + sumk := 0.0 + for k := 0; k < 64; k++ { + sumk += (*T)[i][k] * D[j][k] + } + (*B)[i][j] = sumk + } + } +} + +/* + ------------------------------------- + orig rot90 rot180 rot270 + noxpose xpose noxpose xpose + + + + + - + - + + - + - - - - - + + + + + - + - + - + - + + + + + + + + + + - + - + + - + - - - - - + + + + + - + - + - + - + + + + + + + flipx flipy flipplus flipminus + noxpose noxpose xpose xpose + - - - - - + - + + + + + + - + - + + + + + - + - + + + + + - + - + + - - - - - + - + + + + + + - + - + + + + + - + - + + + + + - + - + + ------------------------------------- +*/ + +func (p *PDQHasher) dct16OriginalToRotate90(A, B *[][]float64) { + for i := 0; i < 16; i++ { + for j := 0; j < 16; j++ { + if (j & 1) != 0 { + (*B)[j][i] = (*A)[i][j] + } else { + (*B)[j][i] = -(*A)[i][j] + } + } + } +} + +func (p *PDQHasher) dct16OriginalToRotate180(A, B *[][]float64) { + for i := 0; i < 16; i++ { + for j := 0; j < 16; j++ { + if ((i + j) & 1) != 0 { + (*B)[i][j] = -(*A)[i][j] + } else { + (*B)[i][j] = (*A)[i][j] + } + } + } +} + +func (p *PDQHasher) dct16OriginalToRotate270(A, B *[][]float64) { + for i := 0; i < 16; i++ { + for j := 0; j < 16; j++ { + if (i & 1) != 0 { + (*B)[j][i] = (*A)[i][j] + } else { + (*B)[j][i] = -(*A)[i][j] + } + } + } +} + +func (p *PDQHasher) dct16OriginalToFlipX(A, B *[][]float64) { + for i := 0; i < 16; i++ { + for j := 0; j < 16; j++ { + if (i & 1) != 0 { + (*B)[i][j] = (*A)[i][j] + } else { + (*B)[i][j] = -(*A)[i][j] + } + } + } +} + +func (p *PDQHasher) dct16OriginalToFlipY(A, B *[][]float64) { + for i := 0; i < 16; i++ { + for j := 0; j < 16; j++ { + if (j & 1) != 0 { + (*B)[i][j] = (*A)[i][j] + } else { + (*B)[i][j] = -(*A)[i][j] + } + } + } +} + +func (p *PDQHasher) dct16OriginalToFlipPlus1(A, B *[][]float64) { + for i := 0; i < 16; i++ { + for j := 0; j < 16; j++ { + (*B)[i][j] = (*A)[i][j] + } + } +} + +func (p *PDQHasher) dct16OriginalToFlipMinus1(A, B *[][]float64) { + for i := 0; i < 16; i++ { + for j := 0; j < 16; j++ { + if ((i + j) & 1) != 0 { + (*B)[j][i] = -(*A)[i][j] + } else { + (*B)[j][i] = (*A)[i][j] + } + } + } +} + +/** + * Each bit of the 16x16 output hash is for whether the given frequency + * component is greater than the median frequency component or not. + */ +func (p *PDQHasher) pdqBuffer16x16ToBits(dctOutput16x16 [][]float64) *types.Hash256 { + hash := types.Hash256{} + dctMedian := helpers.Torben(dctOutput16x16, 16, 16) + for i := 0; i < 16; i++ { + for j := 0; j < 16; j++ { + if dctOutput16x16[i][j] > dctMedian { + hash.SetBit(i*16 + j) + } + } + } + return &hash +} + +// Round up. +func (p *PDQHasher) computeJaroszWindowSize(dimension int) int { + return (dimension + PDQ_JAROSZ_WINDOW_SIZE_DIVISOR - 1) / PDQ_JAROSZ_WINDOW_SIZE_DIVISOR +} + +func (p *PDQHasher) jaroszFilterFloat(buffer1, buffer2 *[]float64, numRows, numCols, windowSizeAlongRows, windowSizeAlongCols, nreps int) { + for i := 0; i < nreps; i++ { + p.boxAlongRowsFloat(buffer1, buffer2, numRows, numCols, windowSizeAlongRows) + p.boxAlongColsFloat(buffer2, buffer1, numRows, numCols, windowSizeAlongCols) + } +} + +func (p *PDQHasher) box1DFloat(invec *[]float64, inStartOffset int, outvec *[]float64, outStartOffset, vectorLength, stride, fullWindowSize int) { + halfWindowSize := int((fullWindowSize + 2) / 2) + phase_1_nreps := int(halfWindowSize - 1) + phase_2_nreps := int(fullWindowSize - halfWindowSize + 1) + phase_3_nreps := int(vectorLength - fullWindowSize) + phase_4_nreps := int(halfWindowSize - 1) + li := 0 // Index of left edge of read window, for subtracts + ri := 0 // Index of right edge of read windows, for adds + oi := 0 // Index of output vector + sum := 0.0 + currentWindowSize := 0 + + // PHASE 1: ACCUMULATE FIRST SUM NO WRITES + i := 0 + for i < phase_1_nreps { + sum += (*invec)[inStartOffset+ri] + currentWindowSize += 1 + ri += stride + i += 1 + } + + // PHASE 2: INITIAL WRITES WITH SMALL WINDOW + i = 0 + for i < phase_2_nreps { + sum += (*invec)[inStartOffset+ri] + currentWindowSize += 1 + (*outvec)[outStartOffset+oi] = sum / float64(currentWindowSize) + ri += stride + oi += stride + i += 1 + } + + // PHASE 3: WRITES WITH FULL WINDOW + i = 0 + for i < phase_3_nreps { + sum += (*invec)[inStartOffset+ri] + sum -= (*invec)[inStartOffset+li] + (*outvec)[outStartOffset+oi] = sum / float64(currentWindowSize) + li += stride + ri += stride + oi += stride + i += 1 + } + + // PHASE 4: FINAL WRITES WITH SMALL WINDOW + i = 0 + for i < phase_4_nreps { + sum -= (*invec)[inStartOffset+li] + currentWindowSize -= 1 + (*outvec)[outStartOffset+oi] = sum / float64(currentWindowSize) + li += stride + oi += stride + i += 1 + } +} + +/** + * input - matrix as numRows x numCols in row-major order + * output - matrix as numRows x numCols in row-major order + */ +func (p *PDQHasher) boxAlongRowsFloat(input, output *[]float64, numRows, numCols, windowSize int) { + for i := 0; i < numRows; i++ { + p.box1DFloat(input, i*numCols, output, i*numCols, numCols, 1, windowSize) + } +} + +func (p *PDQHasher) boxAlongColsFloat(input, output *[]float64, numRows, numCols, windowSize int) { + for i := 0; i < numCols; i++ { + p.box1DFloat(input, i, output, i, numRows, numCols, windowSize) + } +} diff --git a/types/hash256.go b/types/hash256.go @@ -0,0 +1,237 @@ +package types + +import ( + "fmt" + "strconv" + "strings" +) + +// 16 slots of 16 bytes each +// See hashing/pdq/README-MIH.md in upstream repo for why not 8x32 or 32x8, etc. +const HASH256_NUM_SLOTS = 16 +const HASH256_HEX_NUM_NYBBLES = 4 * HASH256_NUM_SLOTS + +type Hash256 struct { + W [HASH256_NUM_SLOTS]int +} + +func (h *Hash256) GetNumWords() int { + return HASH256_NUM_SLOTS +} + +func (h *Hash256) Clone() Hash256 { + rv := Hash256{} + for i := 0; i < HASH256_NUM_SLOTS; i++ { + rv.W[i] = h.W[i] + } + return rv +} + +func (h *Hash256) String() string { + i := HASH256_NUM_SLOTS - 1 + var result []string + + for i >= 0 { + result = append(result, fmt.Sprintf("%04x", h.W[i]&0xFFFF)) + i = i - 1 + } + + return strings.Join(result, "") +} + +func (h *Hash256) ToHexString() string { + return h.String() +} + +func Hash256FromHexString(s string) (*Hash256, error) { + if len(s) != HASH256_HEX_NUM_NYBBLES { + return nil, fmt.Errorf("invalid format: %s", s) + } + + rv := &Hash256{} + i := HASH256_NUM_SLOTS + for x := 0; x < len(s); x += 4 { + i -= 1 + val, err := strconv.ParseUint(s[x:x+4], 16, 16) + if err != nil { + return nil, fmt.Errorf("incorrect format: %s", s) + } + rv.W[i] = int(val) + } + return rv, nil +} + +func (h *Hash256) HammingNorm16(h2 int) int { + return h.BitCount(h2 & 0xFFFF) +} + +func (h *Hash256) BitCount(x int) int { + x -= (x >> 1) & 0x55555555 + x = ((x >> 2) & 0x33333333) + (x & 0x33333333) + x = ((x >> 4) + x) & 0x0F0F0F0F + x += x >> 8 + x += x >> 16 + return x & 0x0000003F +} + +func (h *Hash256) ClearAll() { + for i := 0; i < HASH256_NUM_SLOTS; i++ { + h.W[i] = 0 + } +} + +func (h *Hash256) SetAll() { + for i := 0; i < HASH256_NUM_SLOTS; i++ { + h.W[i] = 0xFFFF + } +} + +func (h *Hash256) HammingNorm() int { + n := 0 + i := 0 + for i < HASH256_NUM_SLOTS { + n += h.HammingNorm16(h.W[i]) + i += 1 + } + return n +} + +func (h *Hash256) HammingDistance(that *Hash256) int { + n := 0 + for i := 0; i < HASH256_NUM_SLOTS; i++ { + n += h.HammingNorm16(h.W[i] ^ that.W[i]) + } + return n +} + +func (h *Hash256) HammingDistanceLE(that *Hash256, d int) bool { + e := 0 + for i := 0; i < HASH256_NUM_SLOTS; i++ { + e += h.HammingNorm16(h.W[i] ^ that.W[i]) + if e > d { + return false + } + } + return true +} + +func (h *Hash256) SetBit(k int) { + h.W[(k&255)>>4] |= 1 << (k & 15) +} + +func (h *Hash256) FlipBit(k int) { + h.W[(k&255)>>4] ^= 1 << (k & 15) +} + +func (h *Hash256) BitwiseXOR(that *Hash256) Hash256 { + rv := Hash256{} + for i := 0; i < HASH256_NUM_SLOTS; i++ { + rv.W[i] = (h.W[i] ^ that.W[i]) + } + return rv +} + +func (h *Hash256) BitwiseAND(that *Hash256) Hash256 { + rv := Hash256{} + for i := 0; i < HASH256_NUM_SLOTS; i++ { + rv.W[i] = (h.W[i] & that.W[i]) + } + return rv +} + +func (h *Hash256) BitwiseOR(that *Hash256) Hash256 { + rv := Hash256{} + for i := 0; i < HASH256_NUM_SLOTS; i++ { + rv.W[i] = (h.W[i] | that.W[i]) + } + return rv +} + +func (h *Hash256) BitwiseNOT() Hash256 { + rv := Hash256{} + for i := 0; i < HASH256_NUM_SLOTS; i++ { + rv.W[i] = ((^h.W[i]) & 0xFFFF) + } + return rv +} + +func (h *Hash256) DumpBits() string { + var str []string + + i := HASH256_NUM_SLOTS - 1 + for i >= 0 { + word := h.W[i] & 0xFFFF + var bits []string + for j := 15; j >= 0; j-- { + if (word & (1 << uint(j))) != 0 { + bits = append(bits, "1") + } else { + bits = append(bits, "0") + } + } + str = append(str, strings.Join(bits, " ")) + i-- + } + return strings.Join(str, "\n") +} + +func (h *Hash256) DumpBitsAcross() string { + var str []string + + i := HASH256_NUM_SLOTS - 1 + for i >= 0 { + word := h.W[i] & 0xFFFF + for j := 15; j >= 0; j-- { + if (word & (1 << uint(j))) != 0 { + str = append(str, "1") + } else { + str = append(str, "0") + } + } + i-- + } + return strings.Join(str, " ") +} + +func (h *Hash256) DumpWords() string { + var words []string + + // Iterate over the reversed list of words + for i := len(h.W) - 1; i >= 0; i-- { + words = append(words, strconv.Itoa(int(h.W[i]))) + } + + // Join the words with commas + return strings.Join(words, ",") +} + +func (h *Hash256) Eq(other *Hash256) bool { + for i := 0; i < HASH256_NUM_SLOTS; i++ { + if h.W[i] != other.W[i] { + return false + } + } + return true +} + +func (h *Hash256) Greater(other *Hash256) bool { + for i := 0; i < HASH256_NUM_SLOTS; i++ { + if h.W[i] > other.W[i] { + return true + } else if h.W[i] < other.W[i] { + return false + } + } + return false +} + +func (h *Hash256) Less(other *Hash256) bool { + for i := 0; i < HASH256_NUM_SLOTS; i++ { + if h.W[i] < other.W[i] { + return true + } else if h.W[i] > other.W[i] { + return false + } + } + return false +}