{"_id":"@cley_faye/shash-2d","_rev":"3-9171507a8591c9d6f9b77b36c698e868","name":"@cley_faye/shash-2d","dist-tags":{"latest":"1.1.1"},"versions":{"1.0.0":{"name":"@cley_faye/shash-2d","version":"1.0.0","keywords":["hash","image","fuzzy","perceptual","similarity"],"author":{"name":"Gabriel Paul \"Cley Faye\" Risterucci"},"license":"MIT","_id":"@cley_faye/shash-2d@1.0.0","maintainers":[{"name":"cley_faye","email":"cleyfaye@gmail.com"}],"bin":{"shash-compute":"lib/bin/shash-compute.js","shash-difference":"lib/bin/shash-difference.js"},"dist":{"shasum":"d201142c193194432a9ce10c30bff14d84a84db4","tarball":"https://registry.npmjs.org/@cley_faye/shash-2d/-/shash-2d-1.0.0.tgz","fileCount":29,"integrity":"sha512-ZNveckUf/yAJuKLV8qSbHeTXp0AkbNQQ4DipLNLmQrMWLU31fPxMpuOBAsaU/4XlCu1fa4ObaJuiQlN86Rj+8w==","signatures":[{"sig":"MEUCIEbwORZIOaMvvoySku+EDBWPbLSMDlrsXfNNg2h9hEqgAiEAw0LPkGVNFKtm1RZtp9bYJZQq52SThBzbdS+P8gmXVfQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":74694},"main":"./lib/index.js","type":"module","tslib":{"knip":{"ignoreDependencies":["@babel/plugin-syntax-import-attributes"]},"exports":{"enabled":true,"private":["services/**/*.ts","utils/**/*.ts","data/**/*.ts","bin/**/*.ts"]},"typedoc":false,"buildWeb":true,"testPattern":"none"},"types":"./lib/index.d.ts","exports":{".":"./lib/index.js","./index.js":{"node":"./lib/index.js","browser":"./web/index.js","default":"./lib/index.js","react-native":"./web/index.js"}},"gitHead":"24477b031882358668f89ea849a9ce21583baf61","scripts":{"build":"tslib build","check":"tslib check","clean":"tslib clean","prepack":"npm run build"},"_npmUser":{"name":"cley_faye","email":"cleyfaye@gmail.com"},"repository":{"url":"git@ukyo.cleyfaye.net:ProjectInferno/shash-2d.git","type":"git"},"_npmVersion":"11.18.0","description":"Compute similarity hashes on pictures","directories":{},"_nodeVersion":"24.17.0","allowScripts":{"canvas@3.2.3":true,"unrs-resolver":false,"@swc/core@1.15.41":true},"dependencies":{"canvas":"^3.2.3","@keeex/log":"^1.9.0","@cwasm/webp":"^0.1.5","@keeex/utils":"^7.6.2","@cley_faye/argv":"^1.1.1","@keeex/utils-node":"^6.9.2"},"_hasShrinkwrap":false,"devDependencies":{"knip":"^6.17.1","eslint":"^9.39.4","globals":"^17.6.0","prettier":"^3.8.4","@swc/core":"^1.15.41","@babel/cli":"^7.29.7","@eslint/js":"^9.39.4","typescript":"^6.0.3","@babel/core":"^7.29.7","@types/node":"^24.13.2","@keeex/tslib":"^2.15.1","@babel/preset-env":"^7.29.7","typescript-eslint":"^8.61.1","eslint-plugin-tsdoc":"^0.5.2","@keeex/eslint-config":"^2.6.3","eslint-plugin-promise":"^7.3.0","eslint-plugin-import-x":"^4.16.2","babel-plugin-import-redirect":"^1.1.1","eslint-import-resolver-typescript":"^4.4.5","@babel/plugin-syntax-import-attributes":"^7.29.7"},"_npmOperationalInternal":{"tmp":"tmp/shash-2d_1.0.0_1783952341279_0.6856759943730075","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@cley_faye/shash-2d","version":"1.1.0","keywords":["hash","image","fuzzy","perceptual","similarity"],"author":{"name":"Gabriel Paul \"Cley Faye\" Risterucci"},"license":"MIT","_id":"@cley_faye/shash-2d@1.1.0","maintainers":[{"name":"cley_faye","email":"cleyfaye@gmail.com"}],"bin":{"shash-compute":"lib/bin/shash-compute.js","shash-difference":"lib/bin/shash-difference.js"},"dist":{"shasum":"72af52f12c5572ff6cfe148f7406f7825cea88d8","tarball":"https://registry.npmjs.org/@cley_faye/shash-2d/-/shash-2d-1.1.0.tgz","fileCount":29,"integrity":"sha512-04FJPVyh+pG2CEEcH0nTZjV1h2T1jsv2n9xblRcYLaCv3T9AbKChv+k8U5tbLz+FqRZ8ydHkrUrug0QvrkqAVg==","signatures":[{"sig":"MEQCIBoZ6cbtwQpgwIZTlh6xCDlt9+eX7qG00gyN8a1R/VXnAiBskbLsaVSz59Utdz2NSTEUsDIubzB3ljCMMbs7fxp/aQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":75182},"main":"./lib/index.js","type":"module","tslib":{"knip":{"ignoreDependencies":["@babel/plugin-syntax-import-attributes"]},"exports":{"enabled":true,"private":["services/**/*.ts","utils/**/*.ts","data/**/*.ts","bin/**/*.ts"]},"typedoc":false,"buildWeb":true,"testPattern":"none"},"types":"./lib/index.d.ts","exports":{".":"./lib/index.js","./index.js":{"node":"./lib/index.js","browser":"./web/index.js","default":"./lib/index.js","react-native":"./web/index.js"}},"gitHead":"64f776d1068ce482c4f06864178f2855f254a02d","scripts":{"build":"tslib build","check":"tslib check","clean":"tslib clean","prepack":"npm run build"},"_npmUser":{"name":"cley_faye","email":"cleyfaye@gmail.com"},"repository":{"url":"git@ukyo.cleyfaye.net:ProjectInferno/shash-2d.git","type":"git"},"_npmVersion":"11.18.0","description":"Compute similarity hashes on pictures","directories":{},"_nodeVersion":"24.17.0","allowScripts":{"canvas@3.2.3":true,"unrs-resolver":false,"@swc/core@1.15.41":true},"dependencies":{"canvas":"^3.2.3","@keeex/log":"^1.9.0","@cwasm/webp":"^0.1.5","@keeex/utils":"^7.6.2","@cley_faye/argv":"^1.1.1","@keeex/utils-node":"^6.9.2"},"_hasShrinkwrap":false,"devDependencies":{"knip":"^6.17.1","eslint":"^9.39.4","globals":"^17.6.0","prettier":"^3.8.4","@swc/core":"^1.15.41","@babel/cli":"^7.29.7","@eslint/js":"^9.39.4","typescript":"^6.0.3","@babel/core":"^7.29.7","@types/node":"^24.13.2","@keeex/tslib":"^2.15.1","@babel/preset-env":"^7.29.7","typescript-eslint":"^8.61.1","eslint-plugin-tsdoc":"^0.5.2","@keeex/eslint-config":"^2.6.3","eslint-plugin-promise":"^7.3.0","eslint-plugin-import-x":"^4.16.2","babel-plugin-import-redirect":"^1.1.1","eslint-import-resolver-typescript":"^4.4.5","@babel/plugin-syntax-import-attributes":"^7.29.7"},"_npmOperationalInternal":{"tmp":"tmp/shash-2d_1.1.0_1783978834306_0.08063244113379509","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"author":{"name":"Gabriel Paul \"Cley Faye\" Risterucci"},"bin":{"shash-compute":"lib/bin/shash-compute.js","shash-difference":"lib/bin/shash-difference.js"},"description":"Compute similarity hashes on pictures","devDependencies":{"@babel/cli":"^7.29.7","@babel/core":"^7.29.7","@babel/plugin-syntax-import-attributes":"^7.29.7","@babel/preset-env":"^7.29.7","@eslint/js":"^9.39.4","@keeex/eslint-config":"^2.6.3","@keeex/tslib":"^2.15.1","@swc/core":"^1.15.41","@types/node":"^24.13.2","babel-plugin-import-redirect":"^1.1.1","eslint":"^9.39.4","eslint-import-resolver-typescript":"^4.4.5","eslint-plugin-import-x":"^4.16.2","eslint-plugin-promise":"^7.3.0","eslint-plugin-tsdoc":"^0.5.2","globals":"^17.6.0","knip":"^6.17.1","prettier":"^3.8.4","typescript":"^6.0.3","typescript-eslint":"^8.61.1"},"keywords":["hash","image","fuzzy","perceptual","similarity"],"license":"MIT","main":"./lib/index.js","name":"@cley_faye/shash-2d","repository":{"type":"git","url":"git@ukyo.cleyfaye.net:ProjectInferno/shash-2d.git"},"scripts":{"build":"tslib build","check":"tslib check","clean":"tslib clean","prepack":"npm run build"},"tslib":{"buildWeb":true,"exports":{"enabled":true,"private":["services/**/*.ts","utils/**/*.ts","data/**/*.ts","bin/**/*.ts"]},"knip":{"ignoreDependencies":["@babel/plugin-syntax-import-attributes"]},"testPattern":"none","typedoc":false},"type":"module","version":"1.1.1","dependencies":{"@cley_faye/argv":"^1.1.1","@cwasm/webp":"^0.1.5","@keeex/log":"^1.9.0","@keeex/utils":"^7.6.2","@keeex/utils-node":"^6.9.2","canvas":"^3.2.3"},"exports":{".":"./lib/index.js","./index.js":{"node":"./lib/index.js","browser":"./web/index.js","react-native":"./web/index.js","default":"./lib/index.js"}},"allowScripts":{"@swc/core@1.15.41":true,"canvas@3.2.3":true,"unrs-resolver":false},"gitHead":"8e69707cf021d182e981081661a90de9603b3e96","types":"./lib/index.d.ts","_id":"@cley_faye/shash-2d@1.1.1","_nodeVersion":"24.17.0","_npmVersion":"11.18.0","dist":{"integrity":"sha512-8AfCHDfqxPAErxWmc+3h79Kzw8zGxrLgJJEzvWkomh5hGIaAwiXhfGlA6oefxwRdQhayp/UzOKY4Ys+ZvcXMeg==","shasum":"957a76b9c766e2feb199d87a1be2ee4798f99867","tarball":"https://registry.npmjs.org/@cley_faye/shash-2d/-/shash-2d-1.1.1.tgz","fileCount":29,"unpackedSize":75796,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIEQWr5iwtPJWNh4t/a+HHe9hi/04df7E8RrXNSd1KXiyAiEAm1lF6kjAAfxXxolwTcSemkS7/HgpxV6B9C2NFcfvIy8="}]},"_npmUser":{"name":"cley_faye","email":"cleyfaye@gmail.com"},"directories":{},"maintainers":[{"name":"cley_faye","email":"cleyfaye@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/shash-2d_1.1.1_1784052865701_0.7297756581811048"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-13T14:19:01.130Z","modified":"2026-07-14T18:14:25.955Z","1.0.0":"2026-07-13T14:19:01.435Z","1.1.0":"2026-07-13T21:40:34.441Z","1.1.1":"2026-07-14T18:14:25.860Z"},"author":{"name":"Gabriel Paul \"Cley Faye\" Risterucci"},"license":"MIT","keywords":["hash","image","fuzzy","perceptual","similarity"],"repository":{"type":"git","url":"git@ukyo.cleyfaye.net:ProjectInferno/shash-2d.git"},"description":"Compute similarity hashes on pictures","maintainers":[{"name":"cley_faye","email":"cleyfaye@gmail.com"}],"readme":"# `@cley_faye/shash-2d`\n\nPerceptual hash function to find similar-looking images.\nThe idea is basically the stuff described at <https://www.npmjs.com/package/@stabilityprotocol.com/phash> .\nI just wanted to play a bit with it and see if some amount of pre-processing would help.\n\nMain difference is pre-processing, and trying to accomodate heavily distorted source image.\nAnd slightly higher resolution, because why not.\n\n## Basic idea\n\n1. Convert image to 8 bit greyscale\n2. Determine the scale-down dimension\n\n   - The main target is 128x128\n   - If the input image ratio is extremely distorted (>4 or <0.25) divide the largest dimension by 2 and keep that ratio of full squares.\n\n3. Scale down to the computed dimension from the previous step using bilinear interpolation.\n4. Measure the average value for each quarters of the image\n\n   - if the two right quarters are higher than the two left quarters, flip image horizontally (so that brightest is on the left)\n   - if the two bottom quarters are higher than the two top quarters, flip image vertically (so that brightest in at the top)\n\n5. For each input square (128x128):\n\n   - Compute a 2D DCT (first DCT on each rows, then DCT on resulting columns)\n   - Extract the most significant part of the computed DCT (8x8 top-left corner)\n\n6. Compute Mean DCT (if more than one)\n\n   - create a merged 8x8 DCT value where all output coefficient are the mean of all input DCT at that coefficient's position\n\n7. Create the shash output:\n\n   - Compute the median value of all coefficients in the final/single DCT grid, excluding the top-left most coefficient (it seems to represent an average value for the input, but have no relation to the actual signal variations)\n   - create a bitfield by processing each rows, outputing 1 when over the threshold, 0 otherwise. The first bit is set to 1 if the original image was heavily skewed in the third step, 0 otherwise.\n\n### Initial scaling\n\nThe original idea strictly kept the same ratio for ALL images, shrinking them down to 64x64.\nWhile this does account for distortion of images in both axis, it would significantly lower the level of details for very large/small ratio on source images, which are a thing.\nSince we will then shrink the image down to a set of most important coefficients anyway, it might be a good idea to keep some level of details for the most skewed sources.\n\nThis is completely done on intuition of course; I'm not a signal processing expert. Not even a novice.\n\n### First hash bit\n\nWith the initial computation, we would take the top-left coefficient and output something depending on its relation to the computed median.\nAccording to my very cursory glance of the output of a DCT, keeping this information in the hash is not really useful, since we want to not distinguish too much between general brightness changes.\nSo, I'll take this as an opportunity to have a bit indicate if the image have a large aspect ratio.\n\n## Hash size\n\nA 8x8 coefficient list is 64 coefficients.\nEach coefficient is compared to the median of all coefficients, and one bit it output for each.\nWe skip the first one, but replace it with a ratio-inbalance bit.\n\nThe output is still a 64 bits value.\n\n## Checking for similarities\n\nRegistering an image consists of running the algorithm described above, then storing the fingerprint somewhere.\n\nComparing two images for similarity would begin with computing the hash of the candidate image, then comparing the hamming distance between available hashes.\nLet's say that <15% difference (10 bits) is likely to be similar images.\nLess than 5% is very likely to be similar images.\n\nA 64 bits hash is 8 bytes.\nTo try to improve comparison speed, we could implement bit difference lookup tables working on bytes.\nAn array of array of numbers could be used to very quickly retrieve the number of bit of difference between two bytes.\nAnd since we have a maximum threshold, we can bail out once that threshold is reached.\n\n## Lookup optimization\n\nWhen storing the hash with each image, a pre-optimization could be to store the number of set bits alongside the hash.\nAny value with a difference larger than the threshold is obviously not a match.\n\n## Usage\n\n### CLI\n\nInstall globally (`npm install -g @cley_faye/shash-2d`), then run the following command to compute image hashes:\n\n```shell\nshash-compute <image>\n```\n\nYou can run the script with `--help` to get more options.\n\n### Library\n\nInstall the library (`npm install @cley_faye/shash-2d`).\n\nIt basically provides two exports:\n\n```typescript\nimport {prepare, distance} from \"@cley_faye/shash-2d\";\n\nconst context = prepare();\nconst img1Hash = await context.fromPath(\"img1.jpg\");\nconst img2Hash = await context.fromPath(\"img2.jpg\");\n\nconsole.log(\"Distance between img1 and img2:\", distance(img1Hash, img2Hash));\n```\n\nThe `prepare()` function can take arguments to customize the size and resolution of the shash.\nThe context can compute image shash from local FS (NodeJs only), from `Uint8Array` instances, or from `Blob`/`File`.\nThe `distance()` function accept a maximum distance parameters to quickly bailout if too much differences are detected.\n\n#### Comparison\n\nTo improve performance on large dataset, the following lower level function to compare bit distance byte by byte are provided:\n\n- `bitDiff()` compare two bytes and return the number of different bits between them\n- `byteRanges()` return an object with all byte values closer than X bit difference for each byte value\n","readmeFilename":"README.md"}