pdqhash-go

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

pdq_hasher.go (16245B)


      1 package pdq
      2 
      3 //lint:file-ignore U1000 Ignore all unused code, it's pending tests
      4 
      5 import (
      6 	"log"
      7 
      8 	"math"
      9 
     10 	"github.com/MTRNord/pdqhash-go/helpers"
     11 	"github.com/MTRNord/pdqhash-go/types"
     12 	"github.com/davidbyttow/govips/v2/vips"
     13 
     14 	_ "image/jpeg"
     15 )
     16 
     17 // From Wikipedia: standard RGB to luminance (the 'Y' in 'YUV').
     18 const LUMA_FROM_R_COEFF = 0.299
     19 const LUMA_FROM_G_COEFF = 0.587
     20 const LUMA_FROM_B_COEFF = 0.114
     21 
     22 func DCT_MATRIX_SCALE_FACTOR() float64 {
     23 	return math.Sqrt(2.0 / 64.0)
     24 }
     25 
     26 // Wojciech Jarosz 'Fast Image Convolutions' ACM SIGGRAPH 2001:
     27 // X,Y,X,Y passes of 1-D box filters produces a 2D tent filter.
     28 const PDQ_NUM_JAROSZ_XY_PASSES = 2
     29 
     30 /*
     31 Since PDQ uses 64x64 blocks, 1/64th of the image height/width
     32 respectively is a full block. But since we use two passes, we want half
     33 that window size per pass. Example: 1024x1024 full-resolution input. PDQ
     34 downsamples to 64x64. Each 16x16 block of the input produces a single
     35 downsample pixel.  X,Y passes with window size 8 (= 1024/128) average
     36 pixels with 8x8 neighbors. The second X,Y pair of 1D box-filter passes
     37 accumulate data from all 16x16.
     38 */
     39 const PDQ_JAROSZ_WINDOW_SIZE_DIVISOR = 128
     40 
     41 // Flags for which dihedral-transforms are desired to be produced.
     42 const PDQ_DO_DIH_ORIGINAL = 0x01
     43 const PDQ_DO_DIH_ROTATE_90 = 0x02
     44 const PDQ_DO_DIH_ROTATE_180 = 0x04
     45 const PDQ_DO_DIH_ROTATE_270 = 0x08
     46 const PDQ_DO_DIH_FLIPX = 0x10
     47 const PDQ_DO_DIH_FLIPY = 0x20
     48 const PDQ_DO_DIH_FLIP_PLUS1 = 0x40
     49 const PDQ_DO_DIH_FLIP_MINUS1 = 0x80
     50 const PDQ_DO_DIH_ALL = 0xFF
     51 
     52 /**
     53  * The only class state is the DCT matrix, so this class may either be
     54  * instantiated once per image, or instantiated once and used for all images;
     55  * the latter will be slightly faster as the DCT matrix will not need to be
     56  * recomputed once per image.
     57  */
     58 type PDQHasher struct {
     59 	DCT_matrix [][]float64
     60 }
     61 
     62 /**
     63  * Container for multiple-value object: the hash is a 64-character hex
     64  * string and the quality is an integer in the range 0..100.
     65  */
     66 type HashAndQuality struct {
     67 	Hash    *types.Hash256
     68 	Quality int
     69 }
     70 
     71 type HashesAndQuality struct {
     72 	hash           *types.Hash256
     73 	hashRotate90   *types.Hash256
     74 	hashRotate180  *types.Hash256
     75 	hashRotate270  *types.Hash256
     76 	hashFlipX      *types.Hash256
     77 	hashFlipY      *types.Hash256
     78 	hashFlipPlus1  *types.Hash256
     79 	hashFlipMinus1 *types.Hash256
     80 	quality        int
     81 }
     82 
     83 func NewPDQHasher() *PDQHasher {
     84 	return &PDQHasher{
     85 		DCT_matrix: ComputeDCTMatrix(),
     86 	}
     87 }
     88 
     89 func ComputeDCTMatrix() [][]float64 {
     90 	d := make([][]float64, 16)
     91 	for i := 0; i < len(d); i++ {
     92 		di := make([]float64, 64)
     93 		for j := 0; j < len(di); j++ {
     94 			di[j] = DCT_MATRIX_SCALE_FACTOR() * math.Cos((math.Pi/2.0/64.0)*(float64(i)+1.0)*(2.0*float64(j)+1.0))
     95 		}
     96 		d[i] = di
     97 	}
     98 
     99 	return d
    100 }
    101 
    102 func allocateMatrix(numRows, numCols int) [][]float64 {
    103 	// Create a slice of slices to represent the matrix
    104 	matrix := make([][]float64, numRows)
    105 
    106 	// Allocate memory for each row
    107 	for i := range matrix {
    108 		matrix[i] = make([]float64, numCols)
    109 	}
    110 
    111 	return matrix
    112 }
    113 
    114 func (p *PDQHasher) FromFile(filename string) HashAndQuality {
    115 	params := vips.NewImportParams()
    116 	params.AutoRotate.Set(false)
    117 
    118 	image, err := vips.LoadImageFromFile(filename, params)
    119 	if err != nil {
    120 		log.Fatalf("Error opening file: %v", err)
    121 	}
    122 
    123 	// resizing the image proportionally to max 512px width and max 512px height
    124 	err = image.ThumbnailWithSize(512, 512, vips.InterestingNone, vips.SizeDown)
    125 	if err != nil {
    126 		log.Fatalf("Error resizing image: %v", err)
    127 	}
    128 	numCols := image.Width()
    129 	numRows := image.Height()
    130 
    131 	buffer1 := make([]float64, numCols*numRows)
    132 	buffer2 := make([]float64, numCols*numRows)
    133 	buffer64x64 := allocateMatrix(64, 64)
    134 	buffer16x64 := allocateMatrix(16, 64)
    135 	buffer16x16 := allocateMatrix(16, 16)
    136 
    137 	return p.FromImage(image, buffer1, buffer2, buffer64x64, buffer16x64, buffer16x16)
    138 }
    139 
    140 func (p *PDQHasher) FromImage(image *vips.ImageRef, buffer1, buffer2 []float64, buffer64x64, buffer16x64, buffer16x16 [][]float64) HashAndQuality {
    141 	numCols := image.Width()
    142 	numRows := image.Height()
    143 
    144 	p.fillFloatLumaFromBufferImage(image, &buffer1)
    145 
    146 	return p.pdqHash256FromFloatLuma(buffer1, buffer2, numRows, numCols, buffer64x64, buffer16x64, buffer16x16)
    147 }
    148 
    149 func (p *PDQHasher) fillFloatLumaFromBufferImage(image *vips.ImageRef, luma *[]float64) {
    150 	numCols := image.Width()
    151 	numRows := image.Height()
    152 
    153 	err := image.ToColorSpace(vips.InterpretationSRGB)
    154 	if err != nil {
    155 		log.Fatalf("Error converting to RGB: %v", err)
    156 	}
    157 
    158 	for i := 0; i < numRows; i++ {
    159 		for j := 0; j < numCols; j++ {
    160 			colorArray, err := image.GetPoint(j, i)
    161 			if err != nil {
    162 				log.Fatalf("Error getting pixel: %v", err)
    163 			}
    164 			r := colorArray[0]
    165 			g := colorArray[1]
    166 			b := colorArray[2]
    167 			(*luma)[i*numCols+j] = LUMA_FROM_R_COEFF*float64(r) + LUMA_FROM_G_COEFF*float64(g) + LUMA_FROM_B_COEFF*float64(b)
    168 		}
    169 	}
    170 }
    171 
    172 func (p *PDQHasher) pdqHash256FromFloatLuma(fullBuffer1, fullBuffer2 []float64, numRows, numCols int, buffer64x64, buffer16x64, buffer16x16 [][]float64) HashAndQuality {
    173 	windowSizeAlongRows := p.computeJaroszWindowSize(numCols)
    174 	windowSizeAlongCols := p.computeJaroszWindowSize(numRows)
    175 	p.jaroszFilterFloat(&fullBuffer1, &fullBuffer2, numRows, numCols, windowSizeAlongRows, windowSizeAlongCols, PDQ_NUM_JAROSZ_XY_PASSES)
    176 
    177 	p.decimateFloat(&fullBuffer1, numRows, numCols, &buffer64x64)
    178 	quality := p.computePDQImageDomainQualityMetric(buffer64x64)
    179 	p.dct64To16(&buffer64x64, &buffer16x64, &buffer16x16)
    180 	hash := p.pdqBuffer16x16ToBits(buffer16x16)
    181 
    182 	return HashAndQuality{hash, quality}
    183 }
    184 
    185 func (p *PDQHasher) DihedralFromFile(filename string, dihedralFlags int) HashesAndQuality {
    186 	image, err := vips.NewImageFromFile(filename)
    187 	if err != nil {
    188 		log.Fatalf("Error opening file: %v", err)
    189 	}
    190 
    191 	numRows := image.Height()
    192 	numCols := image.Width()
    193 
    194 	buffer1 := make([]float64, numCols*numRows)
    195 	buffer2 := make([]float64, numCols*numRows)
    196 
    197 	buffer64x64 := allocateMatrix(64, 64)
    198 	buffer16x64 := allocateMatrix(16, 64)
    199 	buffer16x16 := allocateMatrix(16, 16)
    200 	buffer16x16Aux := allocateMatrix(16, 16)
    201 
    202 	return p.dihedralFromBufferedImage(image, buffer1, buffer2, buffer64x64, buffer16x64, buffer16x16, buffer16x16Aux, dihedralFlags)
    203 }
    204 
    205 func (p *PDQHasher) dihedralFromBufferedImage(image *vips.ImageRef, buffer1, buffer2 []float64, buffer64x64, buffer16x64, buffer16x16, buffer16x16Aux [][]float64, dihedralFlags int) HashesAndQuality {
    206 	numRows := image.Height()
    207 	numCols := image.Width()
    208 
    209 	p.fillFloatLumaFromBufferImage(image, &buffer1)
    210 
    211 	return p.pdqHash256esFromFloatLuma(buffer1, buffer2, numRows, numCols, buffer64x64, buffer16x64, buffer16x16, buffer16x16Aux, dihedralFlags)
    212 }
    213 
    214 func (p *PDQHasher) pdqHash256esFromFloatLuma(fullBuffer1, fullBuffer2 []float64, numRows, numCols int, buffer64x64, buffer16x64, buffer16x16, buffer16x16Aux [][]float64, dihedralFlags int) HashesAndQuality {
    215 	windowSizeAlongRows := p.computeJaroszWindowSize(numCols)
    216 	windowSizeAlongCols := p.computeJaroszWindowSize(numRows)
    217 	p.jaroszFilterFloat(&fullBuffer1, &fullBuffer2, numRows, numCols, windowSizeAlongRows, windowSizeAlongCols, PDQ_NUM_JAROSZ_XY_PASSES)
    218 
    219 	p.decimateFloat(&fullBuffer1, numRows, numCols, &buffer64x64)
    220 	quality := p.computePDQImageDomainQualityMetric(buffer64x64)
    221 	p.dct64To16(&buffer64x64, &buffer16x64, &buffer16x16)
    222 
    223 	var hash *types.Hash256
    224 	var hashRotate90 *types.Hash256
    225 	var hashRotate180 *types.Hash256
    226 	var hashRotate270 *types.Hash256
    227 	var hashFlipX *types.Hash256
    228 	var hashFlipY *types.Hash256
    229 	var hashFlipPlus1 *types.Hash256
    230 	var hashFlipMinus1 *types.Hash256
    231 
    232 	if dihedralFlags&PDQ_DO_DIH_ORIGINAL != 0 {
    233 		hash = p.pdqBuffer16x16ToBits(buffer16x16)
    234 	}
    235 
    236 	if dihedralFlags&PDQ_DO_DIH_ROTATE_90 != 0 {
    237 		p.dct16OriginalToRotate90(&buffer16x16, &buffer16x16Aux)
    238 		hashRotate90 = p.pdqBuffer16x16ToBits(buffer16x16Aux)
    239 	}
    240 
    241 	if dihedralFlags&PDQ_DO_DIH_ROTATE_180 != 0 {
    242 		p.dct16OriginalToRotate180(&buffer16x16, &buffer16x16Aux)
    243 		hashRotate180 = p.pdqBuffer16x16ToBits(buffer16x16Aux)
    244 	}
    245 
    246 	if dihedralFlags&PDQ_DO_DIH_ROTATE_270 != 0 {
    247 		p.dct16OriginalToRotate270(&buffer16x16, &buffer16x16Aux)
    248 		hashRotate270 = p.pdqBuffer16x16ToBits(buffer16x16Aux)
    249 	}
    250 
    251 	if dihedralFlags&PDQ_DO_DIH_FLIPX != 0 {
    252 		p.dct16OriginalToFlipX(&buffer16x16, &buffer16x16Aux)
    253 		hashFlipX = p.pdqBuffer16x16ToBits(buffer16x16Aux)
    254 	}
    255 
    256 	if dihedralFlags&PDQ_DO_DIH_FLIPY != 0 {
    257 		p.dct16OriginalToFlipY(&buffer16x16, &buffer16x16Aux)
    258 		hashFlipY = p.pdqBuffer16x16ToBits(buffer16x16Aux)
    259 	}
    260 
    261 	if dihedralFlags&PDQ_DO_DIH_FLIP_PLUS1 != 0 {
    262 		p.dct16OriginalToFlipPlus1(&buffer16x16, &buffer16x16Aux)
    263 		hashFlipPlus1 = p.pdqBuffer16x16ToBits(buffer16x16Aux)
    264 	}
    265 
    266 	if dihedralFlags&PDQ_DO_DIH_FLIP_MINUS1 != 0 {
    267 		p.dct16OriginalToFlipMinus1(&buffer16x16, &buffer16x16Aux)
    268 		hashFlipMinus1 = p.pdqBuffer16x16ToBits(buffer16x16Aux)
    269 	}
    270 
    271 	return HashesAndQuality{hash, hashRotate90, hashRotate180, hashRotate270, hashFlipX, hashFlipY, hashFlipPlus1, hashFlipMinus1, quality}
    272 }
    273 
    274 // numRows x numCols in row-major order
    275 func (p *PDQHasher) decimateFloat(in *[]float64, inNumRows, inNumCols int, out *[][]float64) {
    276 	for i := 0; i < 64; i++ {
    277 		ini := int(((float64(i) + 0.5) * float64(inNumRows)) / 64.0)
    278 		for j := 0; j < 64; j++ {
    279 			inj := int(((float64(j) + 0.5) * float64(inNumCols)) / 64.0)
    280 			(*out)[i][j] = (*in)[ini*inNumCols+inj]
    281 		}
    282 	}
    283 }
    284 
    285 /**
    286  * This is all heuristic (see the PDQ hashing doc). Quantization
    287  * matters since we want to count *significant* gradients, not just the
    288  * some of many small ones. The constants are all manually selected, and
    289  * tuned as described in the document.
    290  */
    291 func (p *PDQHasher) computePDQImageDomainQualityMetric(buffer64x64 [][]float64) int {
    292 	gradientSum := 0
    293 	for i := 0; i < 63; i++ {
    294 		for j := 0; j < 64; j++ {
    295 			u := buffer64x64[i][j]
    296 			v := buffer64x64[i+1][j]
    297 			d := int(((u - v) * 100.0) / 255.0)
    298 			gradientSum += int(helpers.Abs(d))
    299 		}
    300 	}
    301 	for i := 0; i < 64; i++ {
    302 		for j := 0; j < 63; j++ {
    303 			u := buffer64x64[i][j]
    304 			v := buffer64x64[i][j+1]
    305 			d := int(((u - v) * 100.0) / 255.0)
    306 			gradientSum += int(helpers.Abs(d))
    307 		}
    308 	}
    309 	quality := float64(gradientSum) / 90.0
    310 	if quality > 100 {
    311 		quality = 100
    312 	}
    313 	return int(quality)
    314 }
    315 
    316 /**
    317  * Full 64x64 to 64x64 can be optimized e.g. the Lee algorithm.
    318  *    But here we only want slots (1-16)x(1-16) of the full 64x64 output.
    319  *    Careful experiments showed that using Lee along all 64 slots in one
    320  *    dimension, then Lee along 16 slots in the second, followed by
    321  *    extracting slots 1-16 of the output, was actually slower than the
    322  *    current implementation which is completely non-clever/non-Lee but
    323  *    computes only what is needed.
    324  */
    325 func (p *PDQHasher) dct64To16(A, T, B *([][]float64)) {
    326 	D := p.DCT_matrix
    327 
    328 	*T = make([][]float64, 16)
    329 	for i := 0; i < 16; i++ {
    330 		ti := make([]float64, 64)
    331 
    332 		for j := 0; j < 64; j++ {
    333 			tij := float64(0.0)
    334 			for k := 0; k < 64; k++ {
    335 				tij += D[i][k] * (*A)[k][j]
    336 			}
    337 			ti[j] = tij
    338 		}
    339 		(*T)[i] = ti
    340 	}
    341 
    342 	for i := 0; i < 16; i++ {
    343 		for j := 0; j < 16; j++ {
    344 			sumk := float64(0.0)
    345 			for k := 0; k < 64; k++ {
    346 				sumk += (*T)[i][k] * D[j][k]
    347 			}
    348 			(*B)[i][j] = sumk
    349 		}
    350 	}
    351 }
    352 
    353 /*
    354    -------------------------------------
    355    orig      rot90     rot180    rot270
    356    noxpose   xpose     noxpose   xpose
    357    + + + +   - + - +   + - + -   - - - -
    358    + + + +   - + - +   - + - +   + + + +
    359    + + + +   - + - +   + - + -   - - - -
    360    + + + +   - + - +   - + - +   + + + +
    361 
    362    flipx     flipy     flipplus  flipminus
    363    noxpose   noxpose   xpose     xpose
    364    - - - -   - + - +   + + + +   + - + -
    365    + + + +   - + - +   + + + +   - + - +
    366    - - - -   - + - +   + + + +   + - + -
    367    + + + +   - + - +   + + + +   - + - +
    368    -------------------------------------
    369 */
    370 
    371 func (p *PDQHasher) dct16OriginalToRotate90(A, B *[][]float64) {
    372 	for i := 0; i < 16; i++ {
    373 		for j := 0; j < 16; j++ {
    374 			if (j & 1) != 0 {
    375 				(*B)[j][i] = (*A)[i][j]
    376 			} else {
    377 				(*B)[j][i] = -(*A)[i][j]
    378 			}
    379 		}
    380 	}
    381 }
    382 
    383 func (p *PDQHasher) dct16OriginalToRotate180(A, B *[][]float64) {
    384 	for i := 0; i < 16; i++ {
    385 		for j := 0; j < 16; j++ {
    386 			if ((i + j) & 1) != 0 {
    387 				(*B)[i][j] = -(*A)[i][j]
    388 			} else {
    389 				(*B)[i][j] = (*A)[i][j]
    390 			}
    391 		}
    392 	}
    393 }
    394 
    395 func (p *PDQHasher) dct16OriginalToRotate270(A, B *[][]float64) {
    396 	for i := 0; i < 16; i++ {
    397 		for j := 0; j < 16; j++ {
    398 			if (i & 1) != 0 {
    399 				(*B)[j][i] = (*A)[i][j]
    400 			} else {
    401 				(*B)[j][i] = -(*A)[i][j]
    402 			}
    403 		}
    404 	}
    405 }
    406 
    407 func (p *PDQHasher) dct16OriginalToFlipX(A, B *[][]float64) {
    408 	for i := 0; i < 16; i++ {
    409 		for j := 0; j < 16; j++ {
    410 			if (i & 1) != 0 {
    411 				(*B)[i][j] = (*A)[i][j]
    412 			} else {
    413 				(*B)[i][j] = -(*A)[i][j]
    414 			}
    415 		}
    416 	}
    417 }
    418 
    419 func (p *PDQHasher) dct16OriginalToFlipY(A, B *[][]float64) {
    420 	for i := 0; i < 16; i++ {
    421 		for j := 0; j < 16; j++ {
    422 			if (j & 1) != 0 {
    423 				(*B)[i][j] = (*A)[i][j]
    424 			} else {
    425 				(*B)[i][j] = -(*A)[i][j]
    426 			}
    427 		}
    428 	}
    429 }
    430 
    431 func (p *PDQHasher) dct16OriginalToFlipPlus1(A, B *[][]float64) {
    432 	for i := 0; i < 16; i++ {
    433 		for j := 0; j < 16; j++ {
    434 			(*B)[j][i] = (*A)[i][j]
    435 		}
    436 	}
    437 }
    438 
    439 func (p *PDQHasher) dct16OriginalToFlipMinus1(A, B *[][]float64) {
    440 	for i := 0; i < 16; i++ {
    441 		for j := 0; j < 16; j++ {
    442 			if ((i + j) & 1) != 0 {
    443 				(*B)[j][i] = -(*A)[i][j]
    444 			} else {
    445 				(*B)[j][i] = (*A)[i][j]
    446 			}
    447 		}
    448 	}
    449 }
    450 
    451 /**
    452  * Each bit of the 16x16 output hash is for whether the given frequency
    453  * component is greater than the median frequency component or not.
    454  */
    455 func (p *PDQHasher) pdqBuffer16x16ToBits(dctOutput16x16 [][]float64) *types.Hash256 {
    456 	hash := types.Hash256{}
    457 	dctMedian := helpers.Torben(dctOutput16x16, 16, 16)
    458 	for i := 0; i < 16; i++ {
    459 		for j := 0; j < 16; j++ {
    460 			if dctOutput16x16[i][j] > dctMedian {
    461 				hash.SetBit(i*16 + j)
    462 			}
    463 		}
    464 	}
    465 	return &hash
    466 }
    467 
    468 // Round up.
    469 func (p *PDQHasher) computeJaroszWindowSize(dimension int) int {
    470 	result := (float64(dimension) + float64(PDQ_JAROSZ_WINDOW_SIZE_DIVISOR) - 1.0) / float64(PDQ_JAROSZ_WINDOW_SIZE_DIVISOR)
    471 	return int(result)
    472 }
    473 
    474 func (p *PDQHasher) jaroszFilterFloat(buffer1, buffer2 *[]float64, numRows, numCols, windowSizeAlongRows, windowSizeAlongCols, nreps int) {
    475 	for i := 0; i < nreps; i++ {
    476 		p.boxAlongRowsFloat(buffer1, buffer2, numRows, numCols, windowSizeAlongRows)
    477 		p.boxAlongColsFloat(buffer2, buffer1, numRows, numCols, windowSizeAlongCols)
    478 	}
    479 }
    480 
    481 func (p *PDQHasher) box1DFloat(invec *[]float64, inStartOffset int, outvec *[]float64, outStartOffset, vectorLength, stride, fullWindowSize int) {
    482 	halfWindowSize := int(((float64(fullWindowSize) + 2.0) / 2.0))
    483 	phase_1_nreps := int(halfWindowSize - 1)
    484 	phase_2_nreps := int(fullWindowSize - halfWindowSize + 1)
    485 	phase_3_nreps := int(vectorLength - fullWindowSize)
    486 	phase_4_nreps := int(halfWindowSize - 1)
    487 	li := 0 // Index of left edge of read window, for subtracts
    488 	ri := 0 // Index of right edge of read windows, for adds
    489 	oi := 0 // Index of output vector
    490 	sum := float64(0.0)
    491 	currentWindowSize := 0
    492 
    493 	// PHASE 1: ACCUMULATE FIRST SUM NO WRITES
    494 	for i := 0; i < phase_1_nreps; i++ {
    495 		sum += (*invec)[inStartOffset+ri]
    496 		currentWindowSize += 1
    497 		ri += stride
    498 	}
    499 
    500 	// PHASE 2: INITIAL WRITES WITH SMALL WINDOW
    501 	for i := 0; i < phase_2_nreps; i++ {
    502 		sum += (*invec)[inStartOffset+ri]
    503 		currentWindowSize += 1
    504 		(*outvec)[outStartOffset+oi] = sum / float64(currentWindowSize)
    505 		ri += stride
    506 		oi += stride
    507 	}
    508 
    509 	// PHASE 3: WRITES WITH FULL WINDOW
    510 	for i := 0; i < phase_3_nreps; i++ {
    511 		sum += (*invec)[inStartOffset+ri]
    512 		sum -= (*invec)[inStartOffset+li]
    513 		(*outvec)[outStartOffset+oi] = sum / float64(currentWindowSize)
    514 		li += stride
    515 		ri += stride
    516 		oi += stride
    517 	}
    518 
    519 	// PHASE 4: FINAL WRITES WITH SMALL WINDOW
    520 	for i := 0; i < phase_4_nreps; i++ {
    521 		sum -= (*invec)[inStartOffset+li]
    522 		currentWindowSize -= 1
    523 		(*outvec)[outStartOffset+oi] = sum / float64(currentWindowSize)
    524 		li += stride
    525 		oi += stride
    526 	}
    527 }
    528 
    529 /**
    530  * input - matrix as numRows x numCols in row-major order
    531  * output - matrix as numRows x numCols in row-major order
    532  */
    533 func (p *PDQHasher) boxAlongRowsFloat(input, output *[]float64, numRows, numCols, windowSize int) {
    534 	for i := 0; i < numRows; i++ {
    535 		p.box1DFloat(input, i*numCols, output, i*numCols, numCols, 1, windowSize)
    536 	}
    537 }
    538 
    539 func (p *PDQHasher) boxAlongColsFloat(input, output *[]float64, numRows, numCols, windowSize int) {
    540 	for i := 0; i < numCols; i++ {
    541 		p.box1DFloat(input, i, output, i, numRows, numCols, windowSize)
    542 	}
    543 }