diff --git a/web/favicon.ico b/web/favicon.ico new file mode 100755 index 0000000..cbdc8ac Binary files /dev/null and b/web/favicon.ico differ diff --git a/web/highlight.css b/web/highlight.css new file mode 100644 index 0000000..63f24de --- /dev/null +++ b/web/highlight.css @@ -0,0 +1,109 @@ +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em +} +code.hljs { + padding: 3px 5px +} + +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em +} +code.hljs { + padding: 3px 5px +} +/* end baseline CSS */ +.hljs { + background: color-mix(in srgb, var(--background) 30%, transparent); + color: var(--foreground); +} +/* Base color: saturation 0; */ +.hljs-subst { + /* default */ + +} +/* purposely ignored */ +.hljs-formula, +.hljs-attr, +.hljs-property, +.hljs-params { + +} +.hljs-comment { + /* color: #697070 */ + color: color-mix(in srgb, var(--foreground) 70%, transparent); +} +.hljs-tag, +.hljs-punctuation { + color: #444a +} +.hljs-tag .hljs-name, +.hljs-tag .hljs-attr { + color: #444 +} +.hljs-keyword, +.hljs-attribute, +.hljs-selector-tag, +.hljs-meta .hljs-keyword, +.hljs-doctag, +.hljs-name { + font-weight: bold +} + +.hljs-type, +.hljs-string, +.hljs-number, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion { + /* color: #880000; */ + color: var(--color1); +} +.hljs-title, +.hljs-section { + /* color: #880000; */ + color: var(--color1); + font-weight: bold +} +.hljs-regexp, +.hljs-symbol, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-operator, +.hljs-selector-pseudo { + /* color: #ab5656 */ + color: var(--color2); +} +/* Language color: hue: 90; */ +.hljs-literal { + /* color: #695 */ + color: var(--color3); +} +.hljs-built_in, +.hljs-bullet, +.hljs-code, +.hljs-addition { + /* color: #397300 */ + color: var(--color6); +} +/* Meta color: hue: 200 */ +.hljs-meta { + color: #1f7199 +} +.hljs-meta .hljs-string { + color: #38a +} +/* Misc effects */ +.hljs-emphasis { + font-style: italic +} +.hljs-strong { + font-weight: bold +} \ No newline at end of file diff --git a/web/img/moon1.jpg b/web/img/moon1.jpg new file mode 100644 index 0000000..fadcd18 Binary files /dev/null and b/web/img/moon1.jpg differ diff --git a/web/img/moon2.jpg b/web/img/moon2.jpg new file mode 100644 index 0000000..18e0946 Binary files /dev/null and b/web/img/moon2.jpg differ diff --git a/web/img/sunflower1.jpg b/web/img/sunflower1.jpg new file mode 100644 index 0000000..8808ce6 Binary files /dev/null and b/web/img/sunflower1.jpg differ diff --git a/web/img/sunflower2.jpg b/web/img/sunflower2.jpg new file mode 100644 index 0000000..22fb9c2 Binary files /dev/null and b/web/img/sunflower2.jpg differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..7141330 --- /dev/null +++ b/web/index.html @@ -0,0 +1,212 @@ + + + + + + + + + + +
+
+

Small image descriptors for near-copy detection

+

Introduction

+

+ This is quite a long setup towards playing around with small image fingerprints for similarity matching. + If you don't care about the background, feel free to skip straight to the demo! +

+

+ Many computer vision applications don't process the image directly, but use some kind of representation of the image instead. + Usually this takes the form of some one-way function that calculates a vector of some dimensionality. + In modern machine learning this is called an embedding. + In computer vision we might consider something a feature, a descriptor or even a fingerprint, depending on the application. +

+

+ The basic ideas are the same: We want to say something about the image, and to have the computer do some calculations with this image it is almost always easier to work with some (often smaller) representation of that image. + Even the biggest representations are usually at least an order of magnitude smaller than the original image, which makes running computations on them much faster. +

+

+ Different tasks require larger representations of the image. + Object recognition, image classification, or even 3d image reconstruction require more precise input and thus more data to represent the image. + However, some simpler tasks might be accomplished with much less data. + One of these simpler applications is perceptual hashing. +

+

Image compare

+

+ Let's imagine that we have two images, and we want to check if these two images are the same image. + Well, you might start by simply comparing data byte-for-byte, which works quite well for direct copies. + GNU diffutils even has a nice cmp tool that will do exactly that. + It will look through two files until it finds a byte that does not match between the two: +

+$ cp a.jpg a.copy.jpg
+$ cmp a.jpg a.copy.jpg && echo "Same" || echo "Not same"
+Same
+
+
+

+ This works for our direct copy. + Now lets see how it handles two entirely different images: +

+
+$ cmp a.jpg b.jpg 
+a.jpg b.jpg differ: byte 14, line 1
+
+
+

+ The two different images differ on byte 14, which is the byte in the JPEG header which defines the "density", or pixels per length of an image. + In this case, a.jpg does not have anything set, while b.jpg defines its density as pixels per inch. + What about two images that are the same, but reencoded? +

+
+$ convert c.png c.jpg && convert c.jpg d.png
+$ cmp c.png d.png 
+c.png d.png differ: byte 36, line 3
+
+
+

+ In this case we convert a png image into a jpeg and then back into a png. + This introduces jpeg encoding artefacts into the image, which subtly alter the pixel values such that cmp no longer considers them the same image. +

+

+ This gives us our first hints at why image comparisons are tricky. + Even when both files are jpeg images, the subtle differences in encoding already trip us up before we even get to the actual pixel information. + And even when both files are visually identical (and identically encoded), the subtle differences in pixel values still gets us. + +

+

+ This might all seem a little silly so far. + Of course comparing images in this way does not make sense. + What we are looking for is a method that uses a more complex perception-based model to say if two images are the same. + Even more so, we are looking for a method to not only compare two given images, + but quickly calculate the nearest neighbours of a given image based on their similarity. +

+
+
+

Perceptual hashing

+

Locality-sensitive hashing

+

+ I am going to assume you know what a hashing function is. + Different hashing functions have various properties that make them more or less suited to various purposes, + but one common feature is that they try to avoid collisions by having small changes in input correspond to large changes in the output. + When we want to avoid this behaviour we are doing something called locality-sensitive hashing (LSH). + In locality-sensitive hashing we are more likely to put similar data into the same bucket. +

+

+ + In normal hashing we want to avoid the same output values so we call it a collision (bad!). + For LSH we want to encourage the same output values, so we say we "put it in a bucket" (good!). + +

+

+ So what are the requirements of a good perceptual hash? +

    +
  1. Small (i.e. good dimensionality reduction)
  2. +
  3. Avoid collisions between different images.
  4. +
  5. High similarity between similar images.
  6. +
+ We see that we have multiple objectives to optimize for, but we also see that two of our requirements depend on a subjective measure. +

+

Size

+

+ I have decided to limit myself to fingerprints that are at most 64 bits (or 8 bytes) in size. + Firstly, this allows me to "compete" in a little happy place of small descriptors. + But more importantly, because modern processors are very good at processing 64 bit integers, this should also be a very performant fingerprint. +

+

+ The good news is that this takes care of our first requirement. +

+

Subjective image similarity

+

+ Broadly speaking, we have to answer the question: when are two images the same? +

+

+ Lets start by looking at some examples. +

+ These two pictures are, to a human observer, definitely similar, but also not the same. + The framing is similar, the subject is similar, the colors are different. + So we would like a good perceptual hashing algorithm to generate hashes for these images that are close, but not to put them in the same bucket. +

+

+

+ These are more similar. + We have the same subject, same coloring, very similar brightness and contrast values. + If we look a bit closer we see that the rotation is a bit different in both images, the left one is a bit sharper, and the right image has more "empty space" around the subject. +

+

+ All things considered, these image should probably be classified as very similar by most perceptual hashing algorithms. +

+
+
+

Demo

+

Select an image to run this demo with. Don't worry, nothing will be sent to any server! All calculations are done in the browser.

+

Unanimated images with no transparancy work best, but feel free to experiment.

+ +
+
+
+
+
+

Resize

+

The first step is to size the image down. Since we are aiming for a 64-bit descriptor, the logical target size is 8 by 8 since that will give us 64 pixels to work with.

+
+
+
+
+

+ It is obvious, but worth noting, that this action is destructive. + We are throwing away a lot of information here, especially regarding the finer details. + For now, this is a good thing. +

+

+ After all, we have to describe our image in only 64-bits, so throwing away information was inevitable. + If you squint your eyes the original image can be recognized. Sort of. +

+

DCT

+

What is DCT?

+

Frequency domain. Plaatje. Bla bla.

+
+
+

Mauris semper ipsum libero. Praesent laoreet massa sagittis enim consequat malesuada. Nullam viverra nibh sit amet lacus volutpat sollicitudin. Nullam lacus sem, commodo ut vulputate nec, consectetur in erat. Quisque eget nunc ac felis condimentum ultrices eu sit amet arcu. Praesent imperdiet faucibus aliquam. Curabitur sit amet faucibus erat.

+

Nam pulvinar, mi id sagittis laoreet, risus dolor pellentesque metus, sed mattis nunc erat sed urna. Ut facilisis, velit vel condimentum euismod, ex ante dictum leo, lobortis feugiat eros sem nec velit. Aliquam quis eros lacus. Duis venenatis purus at luctus tempor. Praesent gravida euismod ante, tristique placerat tortor mattis molestie. Duis viverra ex eget lectus tempus consectetur. Aliquam semper, ligula at molestie dapibus, sapien turpis rutrum massa, ac tristique lorem ex eget neque. Donec vel turpis odio.

+

Mauris semper ipsum libero. Praesent laoreet massa sagittis enim consequat malesuada. Nullam viverra nibh sit amet lacus volutpat sollicitudin. Nullam lacus sem, commodo ut vulputate nec, consectetur in erat. Quisque eget nunc ac felis condimentum ultrices eu sit amet arcu. Praesent imperdiet faucibus aliquam. Curabitur sit amet faucibus erat.

+

Mauris semper ipsum libero. Praesent laoreet massa sagittis enim consequat malesuada. Nullam viverra nibh sit amet lacus volutpat sollicitudin. Nullam lacus sem, commodo ut vulputate nec, consectetur in erat. Quisque eget nunc ac felis condimentum ultrices eu sit amet arcu. Praesent imperdiet faucibus aliquam. Curabitur sit amet faucibus erat.

+

Mauris semper ipsum libero. Praesent laoreet massa sagittis enim consequat malesuada. Nullam viverra nibh sit amet lacus volutpat sollicitudin. Nullam lacus sem, commodo ut vulputate nec, consectetur in erat. Quisque eget nunc ac felis condimentum ultrices eu sit amet arcu. Praesent imperdiet faucibus aliquam. Curabitur sit amet faucibus erat.

+

Mauris semper ipsum libero. Praesent laoreet massa sagittis enim consequat malesuada. Nullam viverra nibh sit amet lacus volutpat sollicitudin. Nullam lacus sem, commodo ut vulputate nec, consectetur in erat. Quisque eget nunc ac felis condimentum ultrices eu sit amet arcu. Praesent imperdiet faucibus aliquam. Curabitur sit amet faucibus erat.

+

Nam pulvinar, mi id sagittis laoreet, risus dolor pellentesque metus, sed mattis nunc erat sed urna. Ut facilisis, velit vel condimentum euismod, ex ante dictum leo, lobortis feugiat eros sem nec velit. Aliquam quis eros lacus. Duis venenatis purus at luctus tempor. Praesent gravida euismod ante, tristique placerat tortor mattis molestie. Duis viverra ex eget lectus tempus consectetur. Aliquam semper, ligula at molestie dapibus, sapien turpis rutrum massa, ac tristique lorem ex eget neque. Donec vel turpis odio.

+

Nam pulvinar, mi id sagittis laoreet, risus dolor pellentesque metus, sed mattis nunc erat sed urna. Ut facilisis, velit vel condimentum euismod, ex ante dictum leo, lobortis feugiat eros sem nec velit. Aliquam quis eros lacus. Duis venenatis purus at luctus tempor. Praesent gravida euismod ante, tristique placerat tortor mattis molestie. Duis viverra ex eget lectus tempus consectetur. Aliquam semper, ligula at molestie dapibus, sapien turpis rutrum massa, ac tristique lorem ex eget neque. Donec vel turpis odio.

+

Nam pulvinar, mi id sagittis laoreet, risus dolor pellentesque metus, sed mattis nunc erat sed urna. Ut facilisis, velit vel condimentum euismod, ex ante dictum leo, lobortis feugiat eros sem nec velit. Aliquam quis eros lacus. Duis venenatis purus at luctus tempor. Praesent gravida euismod ante, tristique placerat tortor mattis molestie. Duis viverra ex eget lectus tempus consectetur. Aliquam semper, ligula at molestie dapibus, sapien turpis rutrum massa, ac tristique lorem ex eget neque. Donec vel turpis odio.

+

Nam pulvinar, mi id sagittis laoreet, risus dolor pellentesque metus, sed mattis nunc erat sed urna. Ut facilisis, velit vel condimentum euismod, ex ante dictum leo, lobortis feugiat eros sem nec velit. Aliquam quis eros lacus. Duis venenatis purus at luctus tempor. Praesent gravida euismod ante, tristique placerat tortor mattis molestie. Duis viverra ex eget lectus tempus consectetur. Aliquam semper, ligula at molestie dapibus, sapien turpis rutrum massa, ac tristique lorem ex eget neque. Donec vel turpis odio.

+
+
+ + + + diff --git a/web/resize.js b/web/resize.js new file mode 100644 index 0000000..4991609 --- /dev/null +++ b/web/resize.js @@ -0,0 +1,11 @@ +import init, { dctify, resize } from './pkg/image_similarity.js'; + +async function run() { + await init(); +} +run(); + +onmessage = (e) => { + let resizedBuffer = resize(e.data); + postMessage(resizedBuffer); +}; \ No newline at end of file diff --git a/web/script.js b/web/script.js new file mode 100644 index 0000000..fd776a0 --- /dev/null +++ b/web/script.js @@ -0,0 +1,69 @@ +import init, { dctify, resize } from './pkg/image_similarity.js'; + +async function run() { + await init(); +} +run(); +const resizeWorker = new Worker("resize.js", { type: 'module' }); + +let imageContainers = document.getElementsByClassName("image-original"); +let resizedImageContainer = document.getElementById("image-resize"); +let dctCoefficientContainer = document.getElementById("image-dct"); +let formImage = document.getElementById("dctimage"); + +let originalImage = document.createElement("img"); +let resizedBuffer = null; + +// Uses the resized buffer to get DCT coefficients +function getDCT() { + let buf = new Uint8Array(resizedBuffer) + let dct = dctify(buf); + + for (const c of dct) { + let cdiv = document.createElement("div"); + cdiv.innerHTML = c.toFixed(1); + dctCoefficientContainer.appendChild(cdiv); + } +} + +// Resets all containers, deletes images, etc +// Redefines the event handlers for the base image +function reset() { + //imageContainers.forEach((imageContainer) => { + for (let imageContainer of imageContainers) { + imageContainer.replaceChildren(); + } + resizedImageContainer.replaceChildren(); + dctCoefficientContainer.replaceChildren(); +} + +// Result from the resize worker, means we get resized buffer +resizeWorker.onmessage = function (e) { + console.log(e.data); + resizedBuffer = e.data.buffer; + const resizedImage = document.createElement("img"); + resizedImage.src = URL.createObjectURL( + new Blob([resizedBuffer], { type: 'image/png' }) + ); + resizedImageContainer.appendChild(resizedImage); +} + +// User selected an image from disk. Start the demo. +formImage.addEventListener("change", function() { + reset(); + // Should only get one file from picker + for (const file of formImage.files) { + let originalUrl = URL.createObjectURL(file); + for (let imageContainer of imageContainers) { + let originalImage = document.createElement("img"); + originalImage.src = originalUrl; + imageContainer.appendChild(originalImage); + } + + file.arrayBuffer().then((buf) => { + buf = new Uint8Array(buf); + resizeWorker.postMessage(buf); + }); + } + document.getElementById("demo-cont").classList.add("visible"); +}); \ No newline at end of file diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..20aaa40 --- /dev/null +++ b/web/style.css @@ -0,0 +1,184 @@ +body { + margin: 0; + padding: 0; + color: var(--foreground); + background: url('bg.jpeg'); + background-size: cover; + font-family: sans-serif; + height: 100%; + overflow: hidden; + font-size: 14px; +} + +#wrapper { + overflow-x: hidden; + overflow-y: auto; + width: 100%; + height: 100%; +} + +h1, h2, h3, h4, h5, h6 { + color: var(--color1); + margin-top: 10px; + margin-bottom: 0; +} + +h1 { + margin-top: 0; + font-size: 24px; +} + +h2 { + color: var(--color2); +} + +p { + margin-top: 2px; +} + +a { + color: var(--color2); +} + +strong { + color: var(--color4); +} + +em { + color: var(--color3); +} + +pre { + margin: 0; +} + +pre > code { + border: 1px solid; +} + +article { + max-width: 1024px; + min-height: 128px; + margin: 25px auto; + padding: 20px; + background: color-mix(in srgb, var(--background) 90%, transparent); + box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.9); +} + +code.block { + display: block; +} + +img { + max-width: 100%; + image-rendering: pixelated; +} + +#demo-cont { + visibility: hidden; +} + +#demo-cont.visible { + visibility: visible; +} + +#base .image-original img { + max-width: 256px; + min-width: 256px; +} + +#resize .image-original img { + max-height: 50vh; + min-height: 257px; + max-width: 50vw; + min-width: 257px; +} + +#image-original img { + position: absolute; + max-width: 90vw; + max-height: 90vh; + min-width: 257px; + min-height: 257px; + opacity: 1; + transition: + width 2s linear, + height 2s linear, + filter 3s linear, + opacity 3s ease-in; +} + +#image-original img.resize { + width: 256px !important; + height: 256px !important; +} + +#image-original img.grayscale { + filter: grayscale(1) blur(10px); +} + +#image-original img.fade { + opacity: 0; +} + +#image-resize img { + width: 256px; + height: 256px; + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; +} + +#image-dct { + margin-left: 32px; + display:grid; + grid-template-columns: repeat(8, 32px); +} + +#image-dct > div { + height: 32px; + line-height:32px; + font-size: 11px; + text-align: center; + border: 1px solid var(--color1); + box-sizing: border-box; +} + +.similarity-example { + display: flex; + flex-flow: row nowrap; + align-items: flex-end; +} + +figure { + margin: 10px; + width: 50%; +} + +/* Loader */ + +#loader.show { + border-radius: 50%; + width: 256px; + height: 256px; + font-size: 10px; + position: fixed; + z-index: 99; + top: calc(50vh - 128px); + left: calc(50vw - 128px); + border: 16px solid transparent; + border-top: 16px solid darkred; + transform: translateZ(0); + animation: loader 2s infinite ease-in-out; +} + +@keyframes loader { + 0% { + transform: rotate(0deg); + } + 50% { + transform: rotate(360deg); + } + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file