Skip to main content

add my copycrop script

ID
a330766
date
2023-05-01 07:39:55+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
dd26743
message
add my copycrop script
changed files
6 files, 181 additions

Changed files

images/README.md (0) → images/README.md (1069)

diff --git a/images/README.md b/images/README.md
new file mode 100644
index 0000000..509dd36
--- /dev/null
+++ b/images/README.md
@@ -0,0 +1,37 @@
+# images
+
+These scripts are all for working with images and other visual material.
+
+## The individual scripts
+
+<dl>
+  <dt>
+    <a href="https://github.com/alexwlchan/scripts/blob/main/images/copycrop">
+      <code>copycrop</code>
+    </a>
+  </dt>
+  <dd>
+    this script will “copy” the crop from one image pair to another.
+    <p>For example, suppose I have a full-screen screenshot and a crop to a small region of the screen:</p>
+    <p>
+      <table>
+        <tr>
+          <td><img src="examples/light_original.png"></td>
+          <td>&amp;</td>
+          <td><img src="examples/light_crop.png"></td>
+        </tr>
+      </table>
+    </p>
+    I can use this tool to extract the equivalent region from a second screenshot:
+    <p>
+      <table>
+        <tr>
+          <td><img src="examples/dark_original.png"></td>
+          <td>&rarr;</td>
+          <td><img src="examples/dark_crop.png"></td>
+        </tr>
+      </table>
+    </p>
+    I often use this when making images for my website, to create identical light mode and dark mode screenshots.
+  </dd>
+</dl>

images/copycrop (0) → images/copycrop (4115)

diff --git a/images/copycrop b/images/copycrop
new file mode 100755
index 0000000..b1b9545
--- /dev/null
+++ b/images/copycrop
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+"""
+This "copies" the crop of one image pair to another.
+
+Suppose we have a pair of images, one which has been cropped from
+the other:
+
+    +----------+
+    |x.x.111.x.|                +-----+
+    |.x.x.x.x.x|      --->      |x.111|
+    |x.x.x.x.x.|                +-----+
+    +----------+
+
+This script will identify where the cropped image came from the
+bigger image, then apply that crop to a second image which has the
+same dimensions:
+
+    +----------+
+    |.y.y222.y.|                +-----+
+    |y.y.y.y.y.|      --->      |y222.|
+    |.y.y.y.y.y|                +-----+
+    +----------+
+
+This is a very rudimentary form of "template matching" [1].  It relies
+on the first pair of images being only a crop, and not modified in any
+other way.  This is the case for images I've made (screenshots), but
+it may fail if the image has been processed in other ways.
+
+See the README for a more visual example.
+
+[1]: https://en.wikipedia.org/wiki/Template_matching
+
+"""
+
+import argparse
+import collections
+import itertools
+import os
+
+from PIL import Image
+from PIL import ImageChops
+
+
+def parse_args():
+    parser = argparse.ArgumentParser(
+        prog="copycrop", description="Copy the crop from one image to another"
+    )
+
+    parser.add_argument("ORIGINAL_IMAGE_1")
+    parser.add_argument("CROPPED_IMAGE_1")
+    parser.add_argument("ORIGINAL_IMAGE_2")
+
+    return parser.parse_args()
+
+
+def find_crop_region(original: Image, cropped: Image):
+    """
+    Given the pair of images, work out where the cropped image appears
+    in the original.
+
+    This returns a 4-tuple (left, top, right, bottom) which can be passed
+    into the Image.crop method, e.g. (990, 332, 1864, 677).
+
+    == How it works ==
+
+    We look at the palette of both images, and in particular colours which
+    appear in both.  Then we compare their relative positions, and use them
+    to "guess" some possible candidates for the crop.
+
+    e.g. if there's a single red pixel in both images, which:
+
+    * appears at (10, 10) in the original image
+    * appears at (1, 1) in the cropped image
+
+    then we can guess this is the same red pixel, and try a crop that
+    starts at (10, 10).
+
+    """
+    original_palette = collections.Counter(original.getdata())
+    cropped_palette = collections.Counter(cropped.getdata())
+
+    least_frequent_colour = min(
+        cropped_palette, key=lambda c: original_palette[c] * cropped_palette[c]
+    )
+
+    original_pixels = original.load()
+
+    matching_original_coordinates = {
+        (x, y)
+        for x in range(original.width)
+        for y in range(original.height)
+        if original_pixels[x, y] == least_frequent_colour
+    }
+
+    cropped_pixels = cropped.load()
+
+    matching_crop_coordinates = {
+        (x, y)
+        for x in range(cropped.width)
+        for y in range(cropped.height)
+        if cropped_pixels[x, y] == least_frequent_colour
+    }
+
+    for ((original_x, original_y), (cropped_x, cropped_y)) in itertools.product(
+        matching_original_coordinates, matching_crop_coordinates
+    ):
+        left = original_x - cropped_x
+        top = original_y - cropped_y
+
+        crop_info = (
+            left,
+            top,
+            left + cropped.width,
+            top + cropped.height,
+        )
+
+        new_crop = original.crop(crop_info)
+
+        diff = ImageChops.difference(cropped, new_crop)
+        if diff.getbbox() is None:
+            return crop_info
+
+    raise RuntimeError("Could not find cropped image inside original image!")
+
+
+if __name__ == "__main__":
+    args = parse_args()
+
+    original_im_1 = Image.open(args.ORIGINAL_IMAGE_1)
+    original_im_2 = Image.open(args.ORIGINAL_IMAGE_2)
+
+    if original_im_1.size != original_im_2.size:
+        raise ValueError("Original images must have the same dimensions!")
+
+    cropped_im_1 = Image.open(args.CROPPED_IMAGE_1)
+
+    crop_region = find_crop_region(original_im_1, cropped_im_1)
+
+    cropped_im_2 = original_im_2.crop(crop_region)
+
+    name, ext = os.path.splitext(args.ORIGINAL_IMAGE_2)
+    out_path = f"{name}.cropped.{ext}"
+    cropped_im_2.save(out_path)
+    print(out_path)

images/examples/dark_crop.png (0) → images/examples/dark_crop.png (100698)

diff --git a/images/examples/dark_crop.png b/images/examples/dark_crop.png
new file mode 100644
index 0000000..74c12ab
Binary files /dev/null and b/images/examples/dark_crop.png differ

images/examples/dark_original.png (0) → images/examples/dark_original.png (693559)

diff --git a/images/examples/dark_original.png b/images/examples/dark_original.png
new file mode 100644
index 0000000..8c50a4a
Binary files /dev/null and b/images/examples/dark_original.png differ

images/examples/light_crop.png (0) → images/examples/light_crop.png (118261)

diff --git a/images/examples/light_crop.png b/images/examples/light_crop.png
new file mode 100644
index 0000000..1c48275
Binary files /dev/null and b/images/examples/light_crop.png differ

images/examples/light_original.png (0) → images/examples/light_original.png (674219)

diff --git a/images/examples/light_original.png b/images/examples/light_original.png
new file mode 100644
index 0000000..774f0a8
Binary files /dev/null and b/images/examples/light_original.png differ