├── depixlib ├── py.typed ├── __init__.py ├── Rectangle.py ├── LoadedImage.py ├── genpixed.py ├── depix.py └── functions.py ├── setup.py ├── docs └── img │ ├── search_issue.png │ ├── example_output_multiline.png │ ├── example_output_multiword.png │ ├── example_output_randomchars.png │ ├── example_output_singleword.png │ ├── linear_box_filter_example.png │ ├── Recovering_prototype_latest.png │ └── output_depixelizedExample_linear.png ├── images ├── testimages │ ├── testimage1.png │ ├── testimage2.png │ ├── testimage3.png │ ├── testimage1_pixels.png │ ├── testimage2_pixels.png │ ├── testimage3_pixels.png │ ├── sublime_screenshot.png │ └── sublime_screenshot_pixels_gimp.png └── searchimages │ ├── debruin_sublime_Linux_small.png │ ├── debruinseq_notepad_Windows7_close.png │ ├── debruinseq_notepad_Windows10_close.png │ ├── debruinseq_notepad_Windows10_spaced.png │ ├── debruinseq_notepad_Windows10_closeAndSpaced.png │ └── debruinseq.txt ├── LICENSE ├── setup.cfg ├── .gitignore └── README.md /depixlib/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /depixlib/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.0" 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /docs/img/search_issue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/docs/img/search_issue.png -------------------------------------------------------------------------------- /images/testimages/testimage1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/testimages/testimage1.png -------------------------------------------------------------------------------- /images/testimages/testimage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/testimages/testimage2.png -------------------------------------------------------------------------------- /images/testimages/testimage3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/testimages/testimage3.png -------------------------------------------------------------------------------- /docs/img/example_output_multiline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/docs/img/example_output_multiline.png -------------------------------------------------------------------------------- /docs/img/example_output_multiword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/docs/img/example_output_multiword.png -------------------------------------------------------------------------------- /docs/img/example_output_randomchars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/docs/img/example_output_randomchars.png -------------------------------------------------------------------------------- /docs/img/example_output_singleword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/docs/img/example_output_singleword.png -------------------------------------------------------------------------------- /docs/img/linear_box_filter_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/docs/img/linear_box_filter_example.png -------------------------------------------------------------------------------- /images/testimages/testimage1_pixels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/testimages/testimage1_pixels.png -------------------------------------------------------------------------------- /images/testimages/testimage2_pixels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/testimages/testimage2_pixels.png -------------------------------------------------------------------------------- /images/testimages/testimage3_pixels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/testimages/testimage3_pixels.png -------------------------------------------------------------------------------- /docs/img/Recovering_prototype_latest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/docs/img/Recovering_prototype_latest.png -------------------------------------------------------------------------------- /images/testimages/sublime_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/testimages/sublime_screenshot.png -------------------------------------------------------------------------------- /docs/img/output_depixelizedExample_linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/docs/img/output_depixelizedExample_linear.png -------------------------------------------------------------------------------- /images/searchimages/debruin_sublime_Linux_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/searchimages/debruin_sublime_Linux_small.png -------------------------------------------------------------------------------- /images/testimages/sublime_screenshot_pixels_gimp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/testimages/sublime_screenshot_pixels_gimp.png -------------------------------------------------------------------------------- /images/searchimages/debruinseq_notepad_Windows7_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/searchimages/debruinseq_notepad_Windows7_close.png -------------------------------------------------------------------------------- /images/searchimages/debruinseq_notepad_Windows10_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/searchimages/debruinseq_notepad_Windows10_close.png -------------------------------------------------------------------------------- /images/searchimages/debruinseq_notepad_Windows10_spaced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/searchimages/debruinseq_notepad_Windows10_spaced.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This work is licensed under a Creative Commons Attribution 4.0 International License. To view a copy of this license, visit https://creativecommons.org/licenses/by/4.0/. -------------------------------------------------------------------------------- /images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/Depix/main/images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = depix 3 | version = attr: depixlib.__version__ 4 | description = Recovers passwords from pixelized screenshots 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | license = CC BY 4.0 8 | url = https://github.com/beurtschipper/Depix 9 | classifiers = 10 | Development Status :: 5 - Production/Stable 11 | Environment :: Console 12 | Topic :: Scientific/Engineering :: Image Processing 13 | Topic :: Scientific/Engineering :: Image Recognition 14 | Topic :: Security 15 | Topic :: Utilities 16 | Programming Language :: Python :: 3 17 | Programming Language :: Python :: 3.7 18 | Programming Language :: Python :: 3.8 19 | Programming Language :: Python :: 3.9 20 | Programming Language :: Python :: 3.10 21 | 22 | [options] 23 | packages = find: 24 | install_requires = 25 | Pillow 26 | types-Pillow 27 | python_requires = >= 3.7 28 | 29 | [options.entry_points] 30 | console_scripts = 31 | depix = depixlib.depix:main 32 | genpixed = depixlib.genpixed:main 33 | 34 | -------------------------------------------------------------------------------- /depixlib/Rectangle.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class Rectangle: 5 | def __init__( 6 | self, startCoordinates: tuple[int, int], endCoordinates: tuple[int, int] 7 | ) -> None: 8 | self.startCoordinates = startCoordinates 9 | self.endCoordinates = endCoordinates 10 | 11 | self.x = self.startCoordinates[0] 12 | self.y = self.startCoordinates[1] 13 | 14 | self.width = self.endCoordinates[0] - self.x 15 | self.height = self.endCoordinates[1] - self.y 16 | 17 | 18 | class ColorRectange(Rectangle): 19 | def __init__( 20 | self, 21 | color: tuple[int, int, int], 22 | startCoordinates: tuple[int, int], 23 | endCoordinates: tuple[int, int], 24 | ) -> None: 25 | 26 | super(ColorRectange, self).__init__(startCoordinates, endCoordinates) 27 | self.color = color 28 | 29 | 30 | class RectangleMatch: 31 | def __init__(self, x: int, y: int, data: list[tuple[int, int, int]]) -> None: 32 | self.x = x 33 | self.y = y 34 | self.data = data 35 | -------------------------------------------------------------------------------- /depixlib/LoadedImage.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import cast 4 | 5 | from PIL import Image 6 | 7 | 8 | class LoadedImage: 9 | def __init__(self, path: str) -> None: 10 | self.path = path 11 | self.loadedImage = Image.open(self.path) 12 | self.width = self.loadedImage.size[0] 13 | self.height = self.loadedImage.size[1] 14 | self.imageData = self.__loadImageData() 15 | 16 | def getCopyOfLoadedPILImage(self) -> Image.Image: 17 | return self.loadedImage.copy() 18 | 19 | def __loadImageData(self) -> list[list[tuple[int, int, int]]]: 20 | """Load data from image with getdata() because of the speed increase over consecutive calls to getpixel""" 21 | _imageData = [[y for y in range(self.height)] for x in range(self.width)] 22 | 23 | rawData = self.loadedImage.getdata() 24 | rawDataCount = 0 25 | 26 | # because getdata returns the image as one big list 27 | for y in range(self.height): 28 | for x in range(self.width): 29 | 30 | _imageData[x][y] = rawData[rawDataCount][0:3] 31 | rawDataCount += 1 32 | return cast(list[list[tuple[int, int, int]]], _imageData) 33 | -------------------------------------------------------------------------------- /depixlib/genpixed.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import os 4 | import shutil 5 | 6 | from . import __version__ 7 | from .depix import DepixHelpFormatter 8 | from .LoadedImage import LoadedImage 9 | 10 | 11 | def check_file(s: str) -> str: 12 | if os.path.isfile(s): 13 | return s 14 | else: 15 | raise argparse.ArgumentTypeError("%s is not a file." % repr(s)) 16 | 17 | 18 | def parse_args() -> argparse.Namespace: 19 | logging.basicConfig(level=logging.INFO) 20 | 21 | parser = argparse.ArgumentParser( 22 | description="This command generates pixelized image from a given image.", 23 | formatter_class=( 24 | lambda prog: DepixHelpFormatter( 25 | prog, 26 | **{ 27 | "width": shutil.get_terminal_size(fallback=(120, 50)).columns, 28 | "max_help_position": 40, 29 | }, 30 | ) 31 | ), 32 | ) 33 | parser.add_argument( 34 | "-i", 35 | "--image", 36 | help="path to image to pixelize", 37 | required=True, 38 | type=check_file, 39 | metavar="PATH", 40 | ) 41 | parser.add_argument( 42 | "-o", 43 | "--outputimage", 44 | help="path to output image", 45 | default="output.png", 46 | metavar="PATH", 47 | ) 48 | parser.add_argument( 49 | "-V", "--version", action="version", version=f"%(prog)s {__version__}" 50 | ) 51 | return parser.parse_args() 52 | 53 | 54 | def main() -> None: 55 | args = parse_args() 56 | 57 | imagePath = args.image 58 | 59 | image = LoadedImage(imagePath) 60 | outputImage = image.getCopyOfLoadedPILImage() 61 | 62 | blockSize = 5 63 | blockPixelCount = blockSize * blockSize 64 | 65 | for x in range(0, image.width, blockSize): 66 | for y in range(0, image.height, blockSize): 67 | 68 | r = g = b = 0 69 | 70 | maxX = min(x + blockSize, image.width) 71 | maxY = min(y + blockSize, image.height) 72 | 73 | for xx in range(x, maxX): 74 | for yy in range(y, maxY): 75 | 76 | currentPixel = image.imageData[xx][yy] 77 | r += currentPixel[0] 78 | g += currentPixel[1] 79 | b += currentPixel[2] 80 | 81 | averageR = int(r / blockPixelCount) 82 | averageG = int(g / blockPixelCount) 83 | averageB = int(b / blockPixelCount) 84 | averageColor = (averageR, averageG, averageB) 85 | 86 | for xx in range(x, maxX): 87 | for yy in range(y, maxY): 88 | 89 | outputImage.putpixel((xx, yy), averageColor) 90 | 91 | outputImage.save(args.outputimage) 92 | 93 | 94 | if __name__ == "__main__": 95 | main() 96 | 97 | # Generated: 98 | # 676c81 99 | # Gimp: 100 | # 878a9e 101 | 102 | # diff: 2104861 103 | 104 | # Generated: 105 | # 889475 106 | # Gimp: 107 | # a7b194 108 | 109 | # diff: 2039071 110 | 111 | # ? 112 | -------------------------------------------------------------------------------- /images/searchimages/debruinseq.txt: -------------------------------------------------------------------------------- 1 | 00102030405060708090a0b0c0d0e0f0g0h0i0j0k0l0m0n0o0p0q0r0s0t0u0v0w0x0y0z0A0B0C0D0E0F0G0H0I0J0K0L0M0N0O0P0Q0R0S0T0U0V0W0X0Y0Z112131415161718191a1b1c1d1e1f1g1h1i1j1k1l1m1n1o1p1q1r1s1t1u1v1w1x1y1z1A1B1C1D1E1F1G1H1I1J1K1L1M1N1O1P1Q1R1S1T1U1V1W1X1Y1Z2232425262728292a2b2c2d2e2f2g2h2i2j2k2l2m2n2o2p2q2r2s2t2u2v2w2x2y2z2A2B2C2D2E2F2G2H2I2J2K2L2M2N2O2P2Q2R2S2T2U2V2W2X2Y2Z33435363738393a3b3c3d3e3f3g3h3i3j3k3l3m3n3o3p3q3r3s3t3u3v3w3x3y3z3A3B3C3D3E3F3G3H3I3J3K3L3M3N3O3P3Q3R3S3T3U3V3W3X3Y3Z445464748494a4b4c4d4e4f4g4h4i4j4k4l4m4n4o4p4q4r4s4t4u4v4w4x4y4z4A4B4C4D4E4F4G4H4I4J4K4L4M4N4O4P4Q4R4S4T4U4V4W4X4Y4Z5565758595a5b5c5d5e5f5g5h5i5j5k5l5m5n5o5p5q5r5s5t5u5v5w5x5y5z5A5B5C5D5E5F5G5H5I5J5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5Y5Z66768696a6b6c6d6e6f6g6h6i6j6k6l6m6n6o6p6q6r6s6t6u6v6w6x6y6z6A6B6C6D6E6F6G6H6I6J6K6L6M6N6O6P6Q6R6S6T6U6V6W6X6Y6Z778797a7b7c7d7e7f7g7h7i7j7k7l7m7n7o7p7q7r7s7t7u7v7w7x7y7z7A7B7C7D7E7F7G7H7I7J7K7L7M7N7O7P7Q7R7S7T7U7V7W7X7Y7Z8898a8b8c8d8e8f8g8h8i8j8k8l8m8n8o8p8q8r8s8t8u8v8w8x8y8z8A8B8C8D8E8F8G8H8I8J8K8L8M8N8O8P8Q8R8S8T8U8V8W8X8Y8Z99a9b9c9d9e9f9g9h9i9j9k9l9m9n9o9p9q9r9s9t9u9v9w9x9y9z9A9B9C9D9E9F9G9H9I9J9K9L9M9N9O9P9Q9R9S9T9U9V9W9X9Y9ZaabacadaeafagahaiajakalamanaoapaqarasatauavawaxayazaAaBaCaDaEaFaGaHaIaJaKaLaMaNaOaPaQaRaSaTaUaVaWaXaYaZbbcbdbebfbgbhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJbKbLbMbNbObPbQbRbSbTbUbVbWbXbYbZccdcecfcgchcicjckclcmcncocpcqcrcsctcucvcwcxcyczcAcBcCcDcEcFcGcHcIcJcKcLcMcNcOcPcQcRcScTcUcVcWcXcYcZddedfdgdhdidjdkdldmdndodpdqdrdsdtdudvdwdxdydzdAdBdCdDdEdFdGdHdIdJdKdLdMdNdOdPdQdRdSdTdUdVdWdXdYdZeefegeheiejekelemeneoepeqereseteuevewexeyezeAeBeCeDeEeFeGeHeIeJeKeLeMeNeOePeQeReSeTeUeVeWeXeYeZffgfhfifjfkflfmfnfofpfqfrfsftfufvfwfxfyfzfAfBfCfDfEfFfGfHfIfJfKfLfMfNfOfPfQfRfSfTfUfVfWfXfYfZgghgigjgkglgmgngogpgqgrgsgtgugvgwgxgygzgAgBgCgDgEgFgGgHgIgJgKgLgMgNgOgPgQgRgSgTgUgVgWgXgYgZhhihjhkhlhmhnhohphqhrhshthuhvhwhxhyhzhAhBhChDhEhFhGhHhIhJhKhLhMhNhOhPhQhRhShThUhVhWhXhYhZiijikiliminioipiqirisitiuiviwixiyiziAiBiCiDiEiFiGiHiIiJiKiLiMiNiOiPiQiRiSiTiUiViWiXiYiZjjkjljmjnjojpjqjrjsjtjujvjwjxjyjzjAjBjCjDjEjFjGjHjIjJjKjLjMjNjOjPjQjRjSjTjUjVjWjXjYjZkklkmknkokpkqkrksktkukvkwkxkykzkAkBkCkDkEkFkGkHkIkJkKkLkMkNkOkPkQkRkSkTkUkVkWkXkYkZllmlnlolplqlrlsltlulvlwlxlylzlAlBlClDlElFlGlHlIlJlKlLlMlNlOlPlQlRlSlTlUlVlWlXlYlZmmnmompmqmrmsmtmumvmwmxmymzmAmBmCmDmEmFmGmHmImJmKmLmMmNmOmPmQmRmSmTmUmVmWmXmYmZnnonpnqnrnsntnunvnwnxnynznAnBnCnDnEnFnGnHnInJnKnLnMnNnOnPnQnRnSnTnUnVnWnXnYnZoopoqorosotouovowoxoyozoAoBoCoDoEoFoGoHoIoJoKoLoMoNoOoPoQoRoSoToUoVoWoXoYoZppqprpsptpupvpwpxpypzpApBpCpDpEpFpGpHpIpJpKpLpMpNpOpPpQpRpSpTpUpVpWpXpYpZqqrqsqtquqvqwqxqyqzqAqBqCqDqEqFqGqHqIqJqKqLqMqNqOqPqQqRqSqTqUqVqWqXqYqZrrsrtrurvrwrxryrzrArBrCrDrErFrGrHrIrJrKrLrMrNrOrPrQrRrSrTrUrVrWrXrYrZsstsusvswsxsyszsAsBsCsDsEsFsGsHsIsJsKsLsMsNsOsPsQsRsSsTsUsVsWsXsYsZttutvtwtxtytztAtBtCtDtEtFtGtHtItJtKtLtMtNtOtPtQtRtStTtUtVtWtXtYtZuuvuwuxuyuzuAuBuCuDuEuFuGuHuIuJuKuLuMuNuOuPuQuRuSuTuUuVuWuXuYuZvvwvxvyvzvAvBvCvDvEvFvGvHvIvJvKvLvMvNvOvPvQvRvSvTvUvVvWvXvYvZwwxwywzwAwBwCwDwEwFwGwHwIwJwKwLwMwNwOwPwQwRwSwTwUwVwWwXwYwZxxyxzxAxBxCxDxExFxGxHxIxJxKxLxMxNxOxPxQxRxSxTxUxVxWxXxYxZyyzyAyByCyDyEyFyGyHyIyJyKyLyMyNyOyPyQyRySyTyUyVyWyXyYyZzzAzBzCzDzEzFzGzHzIzJzKzLzMzNzOzPzQzRzSzTzUzVzWzXzYzZAABACADAEAFAGAHAIAJAKALAMANAOAPAQARASATAUAVAWAXAYAZBBCBDBEBFBGBHBIBJBKBLBMBNBOBPBQBRBSBTBUBVBWBXBYBZCCDCECFCGCHCICJCKCLCMCNCOCPCQCRCSCTCUCVCWCXCYCZDDEDFDGDHDIDJDKDLDMDNDODPDQDRDSDTDUDVDWDXDYDZEEFEGEHEIEJEKELEMENEOEPEQERESETEUEVEWEXEYEZFFGFHFIFJFKFLFMFNFOFPFQFRFSFTFUFVFWFXFYFZGGHGIGJGKGLGMGNGOGPGQGRGSGTGUGVGWGXGYGZHHIHJHKHLHMHNHOHPHQHRHSHTHUHVHWHXHYHZIIJIKILIMINIOIPIQIRISITIUIVIWIXIYIZJJKJLJMJNJOJPJQJRJSJTJUJVJWJXJYJZKKLKMKNKOKPKQKRKSKTKUKVKWKXKYKZLLMLNLOLPLQLRLSLTLULVLWLXLYLZMMNMOMPMQMRMSMTMUMVMWMXMYMZNNONPNQNRNSNTNUNVNWNXNYNZOOPOQOROSOTOUOVOWOXOYOZPPQPRPSPTPUPVPWPXPYPZQQRQSQTQUQVQWQXQYQZRRSRTRURVRWRXRYRZSSTSUSVSWSXSYSZTTUTVTWTXTYTZUUVUWUXUYUZVVWVXVYVZWWXWYWZXXYXZYYZZ0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Depix 2 | 3 | Depix is a tool for recovering passwords from pixelized screenshots. 4 | 5 | This implementation works on pixelized images that were created with a linear box filter. 6 | In [this article](https://www.linkedin.com/pulse/recovering-passwords-from-pixelized-screenshots-sipke-mellema) I cover background information on pixelization and similar research. 7 | 8 | ## Example 9 | 10 | ![image](docs/img/Recovering_prototype_latest.png) 11 | 12 | ## Installation 13 | 14 | * Install the dependencies: 15 | 16 | ```sh 17 | pip install git+https://github.com/beurtschipper/Depix 18 | ``` 19 | 20 | * Run Depix: 21 | 22 | ```sh 23 | depix \ 24 | -p /path/to/your/input/image.png \ 25 | -s images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png \ 26 | -o /path/to/your/output.png 27 | ``` 28 | 29 | ## Example usage 30 | 31 | * Depixelize example image created with Notepad and pixelized with Greenshot. Greenshot averages by averaging the gamma-encoded 0-255 values, which is Depix's default mode. 32 | 33 | ```sh 34 | depix \ 35 | -p images/testimages/testimage3_pixels.png \ 36 | -s images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png 37 | ``` 38 | 39 | Result: ![image](docs/img/example_output_multiword.png) 40 | 41 | * Depixelize example image created with Sublime and pixelized with Gimp, where averaging is done in linear sRGB. The backgroundcolor option filters out the background color of the editor. 42 | 43 | ```sh 44 | depix \ 45 | -p images/testimages/sublime_screenshot_pixels_gimp.png \ 46 | -s images/searchimages/debruin_sublime_Linux_small.png \ 47 | --backgroundcolor 40,41,35 \ 48 | --averagetype linear 49 | ``` 50 | 51 | Result: ![image](docs/img/output_depixelizedExample_linear.png) 52 | 53 | * (Optional) You can create pixelized image by using `genpixed`. 54 | 55 | ```sh 56 | genpixed -i /path/to/image.png -o pixed_output.png 57 | ``` 58 | 59 | * For a detailed explanation, please try to run `$ depix -h` and `genpixed`. 60 | 61 | ## About 62 | 63 | ### Making a Search Image 64 | 65 | * Cut out the pixelated blocks from the screenshot as a single rectangle. 66 | * Paste a [De Bruijn sequence](https://en.wikipedia.org/wiki/De_Bruijn_sequence) with expected characters in an editor with the same font settings as your input image (Same text size, similar font, same colors). 67 | * Make a screenshot of the sequence. 68 | * Move that screenshot into a folder like `images/searchimages/`. 69 | * Run Depix with the `-s` flag set to the location of this screenshot. 70 | 71 | ### Algorithm 72 | 73 | The algorithm uses the fact that the linear box filter processes every block separately. For every block it pixelizes all blocks in the search image to check for direct matches. 74 | 75 | For most pixelized images Depix manages to find single-match results. It assumes these are correct. The matches of surrounding multi-match blocks are then compared to be geometrically at the same distance as in the pixelized image. Matches are also treated as correct. This process is repeated a couple of times. 76 | 77 | After correct blocks have no more geometrical matches, it will output all correct blocks directly. For multi-match blocks, it outputs the average of all matches. 78 | The algorithm uses the fact that the linear box filter processes every block separately. For every block it pixelizes all blocks in the search image to check for direct matches. 79 | 80 | ### Known limitations 81 | 82 | * The algorithm matches by integer block-boundaries. As a result, it has the underlying assumption that for all characters rendered (both in the de Brujin sequence and the pixelated image), the text positioning is done at pixel level. However, some modern text rasterizers position text [at sub-pixel accuracies](http://agg.sourceforge.net/antigrain.com/research/font_rasterization/). 83 | * ~~The algorithm currently performs pixel averaging in the image's gamma-corrected RGB space. As a result, it cannot reconstruct images pixelated using linear RGB.~~ 84 | 85 | ### Future development 86 | 87 | * Implement more filter functions 88 | 89 | Create more averaging filters that work like some popular editors do. 90 | 91 | * Create a new tool that utilizes HMMs 92 | 93 | After creating this program, someone pointed me to a [research document](https://www.researchgate.net/publication/305423573_On_the_Ineffectiveness_of_Mosaicing_and_Blurring_as_Tools_for_Document_Redaction) from 2016 where a group of researchers managed to create a similar tool. Their tool has better precision and works across many different fonts. 94 | While their original source code is not public, an open-source implementation exists at [DepixHMM](https://github.com/JonasSchatz/DepixHMM). 95 | 96 | Edit 16 Feb '22: [Dan Petro](https://bishopfox.com/authors/dan-petro) created the tool UnRedacter ([write-up](https://bishopfox.com/blog/unredacter-tool-never-pixelation), [source](https://github.com/BishopFox/unredacter)) to crack a [challenge](https://labs.jumpsec.com/can-depix-deobfuscate-your-data/) that was created as a response to Depix! 97 | 98 | Still, anyone who is passionate about this type of depixelization is encouraged to implement their own HMM-based version and share it. 99 | -------------------------------------------------------------------------------- /depixlib/depix.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import logging 5 | import os 6 | import shutil 7 | import textwrap 8 | from typing import cast 9 | 10 | from . import __version__ 11 | from .functions import ( 12 | dropEmptyRectangleMatches, 13 | findGeometricMatchesForSingleResults, 14 | findRectangleMatches, 15 | findRectangleSizeOccurences, 16 | findSameColorSubRectangles, 17 | removeMootColorRectangles, 18 | splitSingleMatchAndMultipleMatches, 19 | writeAverageMatchToImage, 20 | writeFirstMatchToImage, 21 | ) 22 | from .LoadedImage import LoadedImage 23 | from .Rectangle import Rectangle 24 | 25 | 26 | class DepixHelpFormatter( 27 | argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter 28 | ): 29 | pass 30 | 31 | 32 | def check_file(s: str) -> str: 33 | if os.path.isfile(s): 34 | return s 35 | else: 36 | raise argparse.ArgumentTypeError("%s is not a file." % repr(s)) 37 | 38 | 39 | def check_color(s: str | None) -> tuple[int, int, int] | None: 40 | if s is None: 41 | return None 42 | ss = s.split(",") 43 | if len(ss) != 3: 44 | raise argparse.ArgumentTypeError("Given colors must be formatted as 'r,g,b'.") 45 | else: 46 | try: 47 | return cast(tuple[int, int, int], tuple([int(i) for i in ss])) 48 | except ValueError: 49 | raise argparse.ArgumentTypeError( 50 | "Maybe %s is not ',,'." % repr(s) 51 | ) 52 | 53 | 54 | def parse_args() -> argparse.Namespace: 55 | logging.basicConfig(format="%(asctime)s - %(message)s", level=logging.INFO) 56 | 57 | usage = textwrap.dedent(""" 58 | note: 59 | The pixelated rectangle must be cut out to only include the pixelated rectangles. 60 | The pattern search image is generally a screenshot of a De Bruijn sequence of expected characters, 61 | made on a machine with the same editor and text size as the original screenshot that was pixelated. 62 | """) 63 | 64 | parser = argparse.ArgumentParser( 65 | description="This command recovers passwords from pixelized screenshots.", 66 | formatter_class=( 67 | lambda prog: DepixHelpFormatter( 68 | prog, 69 | **{ 70 | "width": shutil.get_terminal_size(fallback=(120, 50)).columns, 71 | "max_help_position": 40, 72 | }, 73 | ) 74 | ), 75 | epilog=usage 76 | ) 77 | parser.add_argument( 78 | "-p", 79 | "--pixelimage", 80 | help="path to image with pixelated rectangle", 81 | required=True, 82 | default=argparse.SUPPRESS, 83 | type=check_file, 84 | metavar="PATH" 85 | ) 86 | parser.add_argument( 87 | "-s", 88 | "--searchimage", 89 | help="path to image with patterns to search", 90 | required=True, 91 | default=argparse.SUPPRESS, 92 | type=check_file, 93 | metavar="PATH", 94 | ) 95 | parser.add_argument( 96 | "-a", 97 | "--averagetype", 98 | help="type of RGB average to use", 99 | default="gammacorrected", 100 | choices=["gammacorrected", "linear"], 101 | metavar="TYPE", 102 | ) 103 | parser.add_argument( 104 | "-b", 105 | "--backgroundcolor", 106 | help="original editor background color in format r,g,b", 107 | default=None, 108 | type=check_color, 109 | metavar="RGB" 110 | ) 111 | parser.add_argument( 112 | "-o", 113 | "--outputimage", 114 | help="path to output image", 115 | default="output.png", 116 | metavar="PATH", 117 | ) 118 | parser.add_argument( 119 | "-V", "--version", action="version", version=f"%(prog)s {__version__}" 120 | ) 121 | return parser.parse_args() 122 | 123 | 124 | def main() -> None: 125 | args = parse_args() 126 | 127 | pixelatedImagePath = args.pixelimage 128 | searchImagePath = args.searchimage 129 | editorBackgroundColor: tuple[int, int, int] | None = args.backgroundcolor 130 | averageType = args.averagetype 131 | 132 | logging.info("Loading pixelated image from %s" % pixelatedImagePath) 133 | pixelatedImage = LoadedImage(pixelatedImagePath) 134 | unpixelatedOutputImage = pixelatedImage.getCopyOfLoadedPILImage() 135 | 136 | logging.info("Loading search image from %s" % searchImagePath) 137 | searchImage = LoadedImage(searchImagePath) 138 | 139 | logging.info("Finding color rectangles from pixelated space") 140 | # fill coordinates here if not cut out 141 | pixelatedRectange = Rectangle( 142 | (0, 0), (pixelatedImage.width - 1, pixelatedImage.height - 1) 143 | ) 144 | 145 | pixelatedSubRectanges = findSameColorSubRectangles( 146 | pixelatedImage, pixelatedRectange 147 | ) 148 | logging.info("Found %s same color rectangles" % len(pixelatedSubRectanges)) 149 | 150 | pixelatedSubRectanges = removeMootColorRectangles( 151 | pixelatedSubRectanges, editorBackgroundColor 152 | ) 153 | logging.info("%s rectangles left after moot filter" % len(pixelatedSubRectanges)) 154 | 155 | rectangeSizeOccurences = findRectangleSizeOccurences(pixelatedSubRectanges) 156 | logging.info("Found %s different rectangle sizes" % len(rectangeSizeOccurences)) 157 | if len(rectangeSizeOccurences) > max( 158 | 10, pixelatedRectange.width * pixelatedRectange.height * 0.01 159 | ): 160 | logging.warning( 161 | "Too many variants on block size. Re-pixelating the image might help." 162 | ) 163 | 164 | logging.info("Finding matches in search image") 165 | rectangleMatches = findRectangleMatches( 166 | rectangeSizeOccurences, pixelatedSubRectanges, searchImage, averageType 167 | ) 168 | 169 | logging.info("Removing blocks with no matches") 170 | pixelatedSubRectanges = dropEmptyRectangleMatches( 171 | rectangleMatches, pixelatedSubRectanges 172 | ) 173 | 174 | logging.info("Splitting single matches and multiple matches") 175 | singleResults, pixelatedSubRectanges = splitSingleMatchAndMultipleMatches( 176 | pixelatedSubRectanges, rectangleMatches 177 | ) 178 | 179 | logging.info( 180 | "[%s straight matches | %s multiple matches]" 181 | % (len(singleResults), len(pixelatedSubRectanges)) 182 | ) 183 | 184 | logging.info("Trying geometrical matches on single-match squares") 185 | singleResults, pixelatedSubRectanges = findGeometricMatchesForSingleResults( 186 | singleResults, pixelatedSubRectanges, rectangleMatches 187 | ) 188 | 189 | logging.info( 190 | "[%s straight matches | %s multiple matches]" 191 | % (len(singleResults), len(pixelatedSubRectanges)) 192 | ) 193 | 194 | logging.info("Trying another pass on geometrical matches") 195 | singleResults, pixelatedSubRectanges = findGeometricMatchesForSingleResults( 196 | singleResults, pixelatedSubRectanges, rectangleMatches 197 | ) 198 | 199 | logging.info( 200 | "[%s straight matches | %s multiple matches]" 201 | % (len(singleResults), len(pixelatedSubRectanges)) 202 | ) 203 | 204 | logging.info("Writing single match results to output") 205 | writeFirstMatchToImage( 206 | singleResults, rectangleMatches, searchImage, unpixelatedOutputImage 207 | ) 208 | 209 | logging.info("Writing average results for multiple matches to output") 210 | writeAverageMatchToImage( 211 | pixelatedSubRectanges, rectangleMatches, searchImage, unpixelatedOutputImage 212 | ) 213 | 214 | # writeRandomMatchesToImage(pixelatedSubRectanges, rectangleMatches, searchImage, unpixelatedOutputImage) 215 | 216 | logging.info("Saving output image to: %s" % args.outputimage) 217 | unpixelatedOutputImage.save(args.outputimage) 218 | 219 | 220 | if __name__ == "__main__": 221 | main() 222 | -------------------------------------------------------------------------------- /depixlib/functions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from random import choice 5 | from typing import cast 6 | 7 | from PIL import Image 8 | 9 | from .LoadedImage import LoadedImage 10 | from .Rectangle import ColorRectange, Rectangle, RectangleMatch 11 | 12 | logging.basicConfig(level=logging.INFO) 13 | 14 | 15 | def findSameColorRectangle( 16 | pixelatedImage: LoadedImage, 17 | startCoordinates: tuple[int, int], 18 | maxCoordinates: tuple[int, int], 19 | ) -> ColorRectange: 20 | if pixelatedImage.imageData is None: 21 | raise ValueError("imageData of pixelatedImage is not set.") 22 | startx, starty = startCoordinates 23 | color = pixelatedImage.imageData[startx][starty] 24 | 25 | width = 0 26 | height = 0 27 | maxx, maxy = maxCoordinates 28 | 29 | # finds width and height quick 30 | for x in range(startx, maxx): 31 | if pixelatedImage.imageData[x][starty] == color: 32 | width += 1 33 | else: 34 | break 35 | 36 | for y in range(starty, maxy): 37 | if pixelatedImage.imageData[startx][y] == color: 38 | height += 1 39 | else: 40 | break 41 | 42 | # checks if real rectange with same color. prefers vertical rectangles 43 | for testx in range(startx, startx + width): 44 | for testy in range(starty, starty + height): 45 | 46 | if pixelatedImage.imageData[testx][testy] != color: 47 | # logging.info("Found rectangle error") 48 | return ColorRectange(color, (startx, starty), (testx, testy)) 49 | 50 | return ColorRectange(color, (startx, starty), (startx + width, starty + height)) 51 | 52 | 53 | def findSameColorSubRectangles( 54 | pixelatedImage: LoadedImage, rectangle: Rectangle 55 | ) -> list[ColorRectange]: 56 | sameColorRectanges = [] 57 | 58 | x = rectangle.x 59 | maxx = rectangle.x + rectangle.width + 1 60 | maxy = rectangle.y + rectangle.height + 1 61 | 62 | while x < maxx: 63 | y = rectangle.y 64 | 65 | while y < maxy: 66 | 67 | sameColorRectange = findSameColorRectangle( 68 | pixelatedImage, (x, y), (maxx, maxy) 69 | ) 70 | if not sameColorRectange: 71 | continue 72 | # logging.info( 73 | # "Found rectangle at (%s, %s) with size (%s,%s) and color %s" 74 | # % ( 75 | # x, 76 | # y, 77 | # sameColorRectange.width, 78 | # sameColorRectange.height, 79 | # sameColorRectange.color, 80 | # ) 81 | # ) 82 | sameColorRectanges.append(sameColorRectange) 83 | 84 | y += sameColorRectange.height 85 | 86 | x += sameColorRectange.width 87 | 88 | return sameColorRectanges 89 | 90 | 91 | def removeMootColorRectangles( 92 | colorRectanges: list[ColorRectange], 93 | editorBackgroundColor: tuple[int, int, int] | None, 94 | ) -> list[ColorRectange]: 95 | pixelatedSubRectanges = [] 96 | 97 | mootColors = [(0, 0, 0), (255, 255, 255)] 98 | if editorBackgroundColor is not None: 99 | mootColors.append(editorBackgroundColor) 100 | 101 | for colorRectange in colorRectanges: 102 | if colorRectange.color not in mootColors: 103 | pixelatedSubRectanges.append(colorRectange) 104 | 105 | return pixelatedSubRectanges 106 | 107 | 108 | def findRectangleSizeOccurences( 109 | colorRectanges: list[ColorRectange], 110 | ) -> dict[tuple[int, int], int]: 111 | rectangeSizeOccurences: dict[tuple[int, int], int] = {} 112 | 113 | for colorRectange in colorRectanges: 114 | size = (colorRectange.width, colorRectange.height) 115 | if size in rectangeSizeOccurences: 116 | rectangeSizeOccurences[size] += 1 117 | else: 118 | rectangeSizeOccurences[size] = 1 119 | 120 | return rectangeSizeOccurences 121 | 122 | 123 | # Thanks to Artoria2e5, see 124 | # https://github.com/beurtschipper/Depix/pull/45 125 | def srgb2lin(s: float) -> float: 126 | if s <= 0.0404482362771082: 127 | lin = s / 12.92 128 | else: 129 | lin = ((s + 0.055) / 1.055) ** 2.4 130 | return lin 131 | 132 | 133 | def lin2srgb(lin: float) -> float: 134 | if lin > 0.0031308: 135 | s = 1.055 * lin ** (1.0 / 2.4) - 0.055 136 | else: 137 | s = 12.92 * lin 138 | return float(s) 139 | 140 | 141 | # return a dictionary, with sub-rectangle coordinates as key and RectangleMatch as value 142 | def findRectangleMatches( 143 | rectangeSizeOccurences: dict[tuple[int, int], int], 144 | pixelatedSubRectanges: list[ColorRectange], 145 | searchImage: LoadedImage, 146 | averageType: str = "gammacorrected", 147 | ) -> dict[tuple[int, int], list[RectangleMatch]]: 148 | r: int | float 149 | rr: int | float 150 | g: int | float 151 | gg: int | float 152 | b: int | float 153 | bb: int | float 154 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]] = {} 155 | 156 | for rectangeSizeOccurence in rectangeSizeOccurences: 157 | 158 | rectangleSize = rectangeSizeOccurence 159 | rectangleWidth = rectangleSize[0] 160 | rectangleHeight = rectangleSize[1] 161 | pixelsInRectangle = rectangleWidth * rectangleHeight 162 | 163 | # filter out the desired rectangle size 164 | matchingRectangles = [] 165 | for colorRectange in pixelatedSubRectanges: 166 | 167 | if (colorRectange.width, colorRectange.height) == rectangleSize: 168 | matchingRectangles.append(colorRectange) 169 | 170 | logging.info( 171 | "Scanning {} blocks with size {}".format( 172 | len(matchingRectangles), rectangleSize 173 | ) 174 | ) 175 | for x in range(searchImage.width - rectangleWidth): 176 | for y in range(searchImage.height - rectangleHeight): 177 | 178 | r = g = b = 0.0 179 | matchData = [] 180 | 181 | for xx in range(rectangleWidth): 182 | 183 | for yy in range(rectangleHeight): 184 | 185 | newPixel = searchImage.imageData[x + xx][y + yy] 186 | matchData.append(newPixel) 187 | 188 | if averageType == "gammacorrected": 189 | rr, gg, bb = newPixel 190 | 191 | if averageType == "linear": 192 | newPixelLinear = tuple( 193 | srgb2lin(float(v / 255)) for v in newPixel 194 | ) 195 | rr, gg, bb = newPixelLinear 196 | 197 | r += rr 198 | g += gg 199 | b += bb 200 | 201 | if averageType == "gammacorrected": 202 | averageColor = ( 203 | int(r / pixelsInRectangle), 204 | int(g / pixelsInRectangle), 205 | int(b / pixelsInRectangle), 206 | ) 207 | 208 | elif averageType == "linear": 209 | averageColor = cast( 210 | tuple[int, int, int], 211 | tuple( 212 | int(round(lin2srgb(v / pixelsInRectangle) * 255)) 213 | for v in (r, g, b) 214 | ), 215 | ) 216 | 217 | for matchingRectangle in matchingRectangles: 218 | 219 | if ( 220 | matchingRectangle.x, 221 | matchingRectangle.y, 222 | ) not in rectangleMatches: 223 | rectangleMatches[ 224 | (matchingRectangle.x, matchingRectangle.y) 225 | ] = [] 226 | 227 | if matchingRectangle.color == averageColor: 228 | newRectangleMatch = RectangleMatch(x, y, matchData) 229 | rectangleMatches[ 230 | (matchingRectangle.x, matchingRectangle.y) 231 | ].append(newRectangleMatch) 232 | 233 | if x % 64 == 0: 234 | logging.info( 235 | "Scanning in searchImage: {}/{}".format( 236 | x, searchImage.width - rectangleWidth 237 | ) 238 | ) 239 | 240 | return rectangleMatches 241 | 242 | 243 | def dropEmptyRectangleMatches( 244 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]], 245 | pixelatedSubRectanges: list[ColorRectange], 246 | ) -> list[ColorRectange]: 247 | newPixelatedSubRectanges = [] 248 | for pixelatedSubRectange in pixelatedSubRectanges: 249 | if len(rectangleMatches[(pixelatedSubRectange.x, pixelatedSubRectange.y)]) > 0: 250 | newPixelatedSubRectanges.append(pixelatedSubRectange) 251 | 252 | return newPixelatedSubRectanges 253 | 254 | 255 | def splitSingleMatchAndMultipleMatches( 256 | pixelatedSubRectanges: list[ColorRectange], 257 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]], 258 | ) -> tuple[list[ColorRectange], list[ColorRectange]]: 259 | newPixelatedSubRectanges = [] 260 | singleResults = [] 261 | for colorRectange in pixelatedSubRectanges: 262 | 263 | firstMatchData = rectangleMatches[(colorRectange.x, colorRectange.y)][0].data 264 | singleMatch = True # only one data matches 265 | 266 | for match in rectangleMatches[(colorRectange.x, colorRectange.y)]: 267 | 268 | if firstMatchData != match.data: 269 | singleMatch = False 270 | break 271 | 272 | if singleMatch: 273 | singleResults.append(colorRectange) 274 | else: 275 | newPixelatedSubRectanges.append(colorRectange) 276 | 277 | return singleResults, newPixelatedSubRectanges 278 | 279 | 280 | def isNeighbor(pixelA: ColorRectange, pixelB: ColorRectange) -> bool: 281 | return ( 282 | (pixelA.x - pixelB.x) in [pixelB.width, 0, -pixelA.width] 283 | and (pixelA.y - pixelB.y) in [pixelB.height, 0, -pixelA.height] 284 | and pixelA != pixelB 285 | ) 286 | 287 | 288 | def findGeometricMatchesForSingleResults( 289 | singleResults: list[ColorRectange], 290 | pixelatedSubRectanges: list[ColorRectange], 291 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]], 292 | ) -> tuple[list[ColorRectange], list[ColorRectange]]: 293 | 294 | newPixelatedSubRectanges = pixelatedSubRectanges[:] 295 | newSingleResults = singleResults[:] 296 | matchCount: dict[ColorRectange, int] = {} 297 | dataSeen = set() 298 | 299 | for singleResult in singleResults: 300 | for pixelatedSubRectange in pixelatedSubRectanges: 301 | if not isNeighbor(singleResult, pixelatedSubRectange): 302 | continue 303 | if ( 304 | pixelatedSubRectange in matchCount 305 | and matchCount[pixelatedSubRectange] > 1 306 | ): 307 | break 308 | 309 | # use relative position to determine its neighbors 310 | for singleResultMatch in rectangleMatches[(singleResult.x, singleResult.y)]: 311 | for compareMatch in rectangleMatches[ 312 | (pixelatedSubRectange.x, pixelatedSubRectange.y) 313 | ]: 314 | 315 | xDistance = singleResult.x - pixelatedSubRectange.x 316 | yDistance = singleResult.y - pixelatedSubRectange.y 317 | xDistanceMatches = singleResultMatch.x - compareMatch.x 318 | yDistanceMatches = singleResultMatch.y - compareMatch.y 319 | 320 | if xDistance == xDistanceMatches and yDistance == yDistanceMatches: 321 | if ( 322 | repr((compareMatch.data, singleResultMatch.data)) 323 | not in dataSeen 324 | ): 325 | 326 | dataSeen.add( 327 | repr((compareMatch.data, singleResultMatch.data)) 328 | ) 329 | 330 | if pixelatedSubRectange not in matchCount: 331 | matchCount[pixelatedSubRectange] = 1 332 | else: 333 | matchCount[pixelatedSubRectange] += 1 334 | 335 | for pixelatedSubRectange in matchCount: 336 | if matchCount[pixelatedSubRectange] == 1: 337 | newSingleResults.append(pixelatedSubRectange) 338 | newPixelatedSubRectanges.remove(pixelatedSubRectange) 339 | 340 | return newSingleResults, newPixelatedSubRectanges 341 | 342 | 343 | def writeFirstMatchToImage( 344 | singleMatchRectangles: list[ColorRectange], 345 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]], 346 | searchImage: LoadedImage, 347 | unpixelatedOutputImage: Image.Image, 348 | ) -> None: 349 | for singleResult in singleMatchRectangles: 350 | singleMatch = rectangleMatches[(singleResult.x, singleResult.y)][0] 351 | 352 | for x in range(singleResult.width): 353 | for y in range(singleResult.height): 354 | 355 | color = searchImage.imageData[singleMatch.x + x][singleMatch.y + y] 356 | unpixelatedOutputImage.putpixel( 357 | (singleResult.x + x, singleResult.y + y), color 358 | ) 359 | 360 | 361 | def writeRandomMatchesToImage( 362 | pixelatedSubRectanges: list[ColorRectange], 363 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]], 364 | searchImage: LoadedImage, 365 | unpixelatedOutputImage: Image.Image, 366 | ) -> None: 367 | for singleResult in pixelatedSubRectanges: 368 | 369 | singleMatch = choice(rectangleMatches[(singleResult.x, singleResult.y)]) 370 | 371 | for x in range(singleResult.width): 372 | for y in range(singleResult.height): 373 | 374 | color = searchImage.imageData[singleMatch.x + x][singleMatch.y + y] 375 | unpixelatedOutputImage.putpixel( 376 | (singleResult.x + x, singleResult.y + y), color 377 | ) 378 | 379 | 380 | def writeAverageMatchToImage( 381 | pixelatedSubRectanges: list[ColorRectange], 382 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]], 383 | searchImage: LoadedImage, 384 | unpixelatedOutputImage: Image.Image, 385 | ) -> None: 386 | for pixelatedSubRectange in pixelatedSubRectanges: 387 | 388 | coordinate = (pixelatedSubRectange.x, pixelatedSubRectange.y) 389 | matches = rectangleMatches[coordinate] 390 | 391 | img = Image.new( 392 | "RGB", 393 | (pixelatedSubRectange.width, pixelatedSubRectange.height), 394 | color="white", 395 | ) 396 | 397 | for match in matches: 398 | 399 | dataCount = 0 400 | for x in range(pixelatedSubRectange.width): 401 | for y in range(pixelatedSubRectange.height): 402 | 403 | pixelData = match.data[dataCount] 404 | dataCount += 1 405 | currentPixel = img.getpixel((x, y))[0:3] 406 | 407 | r = int((pixelData[0] + currentPixel[0]) / 2) 408 | g = int((pixelData[1] + currentPixel[1]) / 2) 409 | b = int((pixelData[2] + currentPixel[2]) / 2) 410 | 411 | averagePixel = (r, g, b) 412 | 413 | img.putpixel((x, y), averagePixel) 414 | 415 | for x in range(pixelatedSubRectange.width): 416 | for y in range(pixelatedSubRectange.height): 417 | 418 | currentPixel = img.getpixel((x, y))[0:3] 419 | unpixelatedOutputImage.putpixel( 420 | (pixelatedSubRectange.x + x, pixelatedSubRectange.y + y), 421 | currentPixel, 422 | ) 423 | --------------------------------------------------------------------------------