├── .gitignore ├── LICENSE ├── README.md ├── depix.py ├── depix_static.py ├── depixlib ├── LoadedImage.py ├── Rectangle.py ├── __init__.py ├── functions.py └── helpers.py ├── docs └── img │ ├── Recovering_prototype_latest.png │ ├── example_output_multiline.png │ ├── example_output_multiword.png │ ├── example_output_randomchars.png │ ├── example_output_singleword.png │ ├── linear_box_filter_example.png │ ├── output_depixelizedExample_linear.png │ └── search_issue.png ├── images ├── searchimages │ ├── debruin_sublime_Linux_small.png │ ├── debruinseq.txt │ ├── debruinseq_notepad_Windows10_close.png │ ├── debruinseq_notepad_Windows10_closeAndSpaced.png │ ├── debruinseq_notepad_Windows10_spaced.png │ └── debruinseq_notepad_Windows7_close.png ├── stars.png └── testimages │ ├── sublime_screenshot.png │ ├── sublime_screenshot_pixels_gimp.png │ ├── testimage1.png │ ├── testimage1_pixels.png │ ├── testimage2.png │ ├── testimage2_pixels.png │ ├── testimage3.png │ └── testimage3_pixels.png ├── tool_gen_pixelated.py └── tool_show_boxes.py /.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 | -------------------------------------------------------------------------------- /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/. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Depix 2 | 3 | Depix is a PoC for a technique to recover plaintext 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.spipm.nl/2030.html) I cover background information on pixelization and similar research. 7 | 8 | ## Example 9 | 10 | ![image](docs/img/Recovering_prototype_latest.png) 11 | 12 | ## Updates 13 | 14 | * 24 dec '24: Made repo private, changed the name and made it public again. It just had a ridiculous amount of stars because of the media hype, which didn't feel right. I made this as a quick PoC for a company back in the day, because someone pixelated part of a password for an account with Domain Admin rights. The hype got running by the catchy image and eventually this repo had 26152 stars. If I ever get this much stars again, I want it to be for a project that I'm that hyped about as well. 15 | ![image](images/stars.png) 16 | * 27 nov '23: Refactored and removed all this pip stuff. I like scripts I can just run. If a package can't be found, just install it. Also added `tool_show_boxes.py` to show how bad the box detector is (you have to really cut out the pixels exactly). Made a TODO to create a version that just cuts out boxes of static size. 17 | 18 | ## Installation 19 | 20 | * Install the dependencies 21 | * Run Depix: 22 | 23 | ```sh 24 | python3 depix.py \ 25 | -p /path/to/your/input/image.png \ 26 | -s images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png \ 27 | -o /path/to/your/output.png 28 | ``` 29 | 30 | ## Example usage 31 | 32 | * 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. 33 | 34 | ```sh 35 | python3 depix.py \ 36 | -p images/testimages/testimage3_pixels.png \ 37 | -s images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png 38 | ``` 39 | 40 | Result: ![image](docs/img/example_output_multiword.png) 41 | 42 | * 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. 43 | 44 | ```sh 45 | python3 depix.py \ 46 | -p images/testimages/sublime_screenshot_pixels_gimp.png \ 47 | -s images/searchimages/debruin_sublime_Linux_small.png \ 48 | --backgroundcolor 40,41,35 \ 49 | --averagetype linear 50 | ``` 51 | 52 | Result: ![image](docs/img/output_depixelizedExample_linear.png) 53 | 54 | * (Optional) You can view if the box detector thingie finds your pixels with `tool_show_boxes.py`. Consider a smaller batch of pixels if this looks all mangled. Example of good looking boxes: 55 | 56 | ```sh 57 | python3 tool_show_boxes.py \ 58 | -p images/testimages/testimage3_pixels.png \ 59 | -s images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png 60 | ``` 61 | 62 | * (Optional) You can create pixelized image by using `tool_gen_pixelated.py`. 63 | 64 | ```sh 65 | python3 tool_gen_pixelated.py -i /path/to/image.png -o pixed_output.png 66 | ``` 67 | 68 | * For a detailed explanation, please try to run `$ python3 depix.py -h` and `tool_gen_pixelated.py`. 69 | 70 | ## About 71 | 72 | ### Making a Search Image 73 | 74 | * Cut out the pixelated blocks from the screenshot as a single rectangle. 75 | * 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). 76 | * Make a screenshot of the sequence. 77 | * Move that screenshot into a folder like `images/searchimages/`. 78 | * Run Depix with the `-s` flag set to the location of this screenshot. 79 | 80 | ### Making a Pixelized Image 81 | 82 | * Cut out the pixelized blocks exactly. See the `testimages` for examples. 83 | * It tries to detect blocks but it doesn't do an amazing job. Play with the `tool_show_boxes.py` script and different cutouts if your blocks aren't properly detected. 84 | 85 | ### Algorithm 86 | 87 | 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. 88 | 89 | For some 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. 90 | 91 | 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. 92 | 93 | ### Known limitations 94 | 95 | * 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/). 96 | * You need to know the font specifications and in some cases the screen settings with which the screenshot was taken. However, if there is enough plaintext in the original image you might be able to use the original as a search image. 97 | * This approach doesn't work if additional image compression is performed, because it messes up the colors of a block. 98 | 99 | ### Future development 100 | 101 | * Implement more filter functions 102 | 103 | Create more averaging filters that work like some popular editors do. 104 | 105 | * Create a new tool that utilizes HMMs 106 | 107 | Still, anyone who is passionate about this type of depixelization is encouraged to implement their own HMM-based version and share it. 108 | 109 | ### Other sources and tools 110 | 111 | 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. While their original source code is not public, an open-source implementation exists at [DepixHMM](https://github.com/JonasSchatz/DepixHMM). 112 | 113 | 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! 114 | 115 | Edit 16 Apr '25: Jeff Geerling created a [challenge](https://www.jeffgeerling.com/blog/2025/its-easier-ever-de-censor-videos) for depixelating pixelated folder content in a moving image. Three people were able to do it. [Here](https://github.com/KoKuToru/de-pixelate_gaV-O6NPWrI) is a repo from KoKuToru showing how to do this with TensorFlow! Amazing! 116 | -------------------------------------------------------------------------------- /depix.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import logging 5 | logging.basicConfig(format="%(asctime)s - %(message)s", level=logging.INFO) 6 | 7 | 8 | from depixlib.helpers import ( 9 | check_file, 10 | check_color 11 | ) 12 | from depixlib.functions import ( 13 | dropEmptyRectangleMatches, 14 | findGeometricMatchesForSingleResults, 15 | findRectangleMatches, 16 | findRectangleSizeOccurences, 17 | findSameColorSubRectangles, 18 | removeMootColorRectangles, 19 | splitSingleMatchAndMultipleMatches, 20 | writeAverageMatchToImage, 21 | writeFirstMatchToImage 22 | ) 23 | from depixlib.LoadedImage import LoadedImage 24 | from depixlib.Rectangle import Rectangle 25 | 26 | 27 | def parse_args() -> argparse.Namespace: 28 | 29 | usage = """ 30 | note: 31 | The pixelated rectangle must be cut out to only include the pixelated rectangles. 32 | The pattern search image is generally a screenshot of a De Bruijn sequence of expected characters, 33 | made on a machine with the same editor and text size as the original screenshot that was pixelated. 34 | """ 35 | 36 | parser = argparse.ArgumentParser( 37 | description="This command recovers passwords from pixelized screenshots.", 38 | epilog=usage 39 | ) 40 | parser.add_argument( 41 | "-p", 42 | "--pixelimage", 43 | help="path to image with pixelated rectangle", 44 | required=True, 45 | default=argparse.SUPPRESS, 46 | type=check_file, 47 | metavar="PATH" 48 | ) 49 | parser.add_argument( 50 | "-s", 51 | "--searchimage", 52 | help="path to image with patterns to search", 53 | required=True, 54 | default=argparse.SUPPRESS, 55 | type=check_file, 56 | metavar="PATH", 57 | ) 58 | parser.add_argument( 59 | "-a", 60 | "--averagetype", 61 | help="type of RGB average to use", 62 | default="gammacorrected", 63 | choices=["gammacorrected", "linear"], 64 | metavar="TYPE", 65 | ) 66 | parser.add_argument( 67 | "-b", 68 | "--backgroundcolor", 69 | help="original editor background color in format r,g,b (color to ignore)", 70 | default=None, 71 | type=check_color, 72 | metavar="RGB" 73 | ) 74 | parser.add_argument( 75 | "-o", 76 | "--outputimage", 77 | help="path to output image", 78 | default="output.png", 79 | metavar="PATH", 80 | ) 81 | return parser.parse_args() 82 | 83 | 84 | def main() -> None: 85 | args = parse_args() 86 | 87 | pixelatedImagePath = args.pixelimage 88 | searchImagePath = args.searchimage 89 | editorBackgroundColor: tuple[int, int, int] | None = args.backgroundcolor 90 | averageType = args.averagetype 91 | 92 | logging.info("Loading pixelated image from %s" % pixelatedImagePath) 93 | pixelatedImage = LoadedImage(pixelatedImagePath) 94 | unpixelatedOutputImage = pixelatedImage.getCopyOfLoadedPILImage() 95 | 96 | logging.info("Loading search image from %s" % searchImagePath) 97 | searchImage = LoadedImage(searchImagePath) 98 | 99 | logging.info("Finding color rectangles from pixelated space") 100 | # fill coordinates here if not cut out 101 | pixelatedRectange = Rectangle( 102 | (0, 0), (pixelatedImage.width - 1, pixelatedImage.height - 1) 103 | ) 104 | 105 | pixelatedSubRectanges = findSameColorSubRectangles( 106 | pixelatedImage, pixelatedRectange 107 | ) 108 | logging.info("Found %s same color rectangles" % len(pixelatedSubRectanges)) 109 | 110 | pixelatedSubRectanges = removeMootColorRectangles( 111 | pixelatedSubRectanges, editorBackgroundColor 112 | ) 113 | logging.info("%s rectangles left after moot filter" % len(pixelatedSubRectanges)) 114 | 115 | rectangeSizeOccurences = findRectangleSizeOccurences(pixelatedSubRectanges) 116 | logging.info("Found %s different rectangle sizes" % len(rectangeSizeOccurences)) 117 | if len(rectangeSizeOccurences) > max( 118 | 10, pixelatedRectange.width * pixelatedRectange.height * 0.01 119 | ): 120 | logging.warning( 121 | "Too many variants on block size. Re-cropping the image might help." 122 | ) 123 | 124 | logging.info("Finding matches in search image") 125 | rectangleMatches = findRectangleMatches( 126 | rectangeSizeOccurences, pixelatedSubRectanges, searchImage, averageType 127 | ) 128 | 129 | logging.info("Removing blocks with no matches") 130 | pixelatedSubRectanges = dropEmptyRectangleMatches( 131 | rectangleMatches, pixelatedSubRectanges 132 | ) 133 | 134 | logging.info("Splitting single matches and multiple matches") 135 | singleResults, pixelatedSubRectanges = splitSingleMatchAndMultipleMatches( 136 | pixelatedSubRectanges, rectangleMatches 137 | ) 138 | 139 | logging.info( 140 | "[%s straight matches | %s multiple matches]" 141 | % (len(singleResults), len(pixelatedSubRectanges)) 142 | ) 143 | 144 | logging.info("Trying geometrical matches on single-match squares") 145 | singleResults, pixelatedSubRectanges = findGeometricMatchesForSingleResults( 146 | singleResults, pixelatedSubRectanges, rectangleMatches 147 | ) 148 | 149 | logging.info( 150 | "[%s straight matches | %s multiple matches]" 151 | % (len(singleResults), len(pixelatedSubRectanges)) 152 | ) 153 | 154 | logging.info("Trying another pass on geometrical matches") 155 | singleResults, pixelatedSubRectanges = findGeometricMatchesForSingleResults( 156 | singleResults, pixelatedSubRectanges, rectangleMatches 157 | ) 158 | 159 | logging.info( 160 | "[%s straight matches | %s multiple matches]" 161 | % (len(singleResults), len(pixelatedSubRectanges)) 162 | ) 163 | 164 | logging.info("Writing single match results to output") 165 | writeFirstMatchToImage( 166 | singleResults, rectangleMatches, searchImage, unpixelatedOutputImage 167 | ) 168 | 169 | logging.info("Writing average results for multiple matches to output") 170 | writeAverageMatchToImage( 171 | pixelatedSubRectanges, rectangleMatches, searchImage, unpixelatedOutputImage 172 | ) 173 | 174 | # writeRandomMatchesToImage(pixelatedSubRectanges, rectangleMatches, searchImage, unpixelatedOutputImage) 175 | 176 | logging.info("Saving output image to: %s" % args.outputimage) 177 | unpixelatedOutputImage.save(args.outputimage) 178 | 179 | 180 | if __name__ == "__main__": 181 | main() 182 | -------------------------------------------------------------------------------- /depix_static.py: -------------------------------------------------------------------------------- 1 | 2 | # todo 3 | # simply cut out x by x boxes and match 4 | -------------------------------------------------------------------------------- /depixlib/LoadedImage.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import cast 4 | from PIL import Image 5 | 6 | 7 | class LoadedImage: 8 | def __init__(self, path: str) -> None: 9 | self.path = path 10 | self.loadedImage = Image.open(self.path) 11 | self.width = self.loadedImage.size[0] 12 | self.height = self.loadedImage.size[1] 13 | self.imageData = self.__loadImageData() 14 | 15 | def getCopyOfLoadedPILImage(self) -> Image.Image: 16 | return self.loadedImage.copy() 17 | 18 | def __loadImageData(self) -> list[list[tuple[int, int, int]]]: 19 | """Load data from image with getdata() because of the speed increase over consecutive calls to getpixel""" 20 | _imageData = [[y for y in range(self.height)] for x in range(self.width)] 21 | 22 | rawData = self.loadedImage.getdata() 23 | rawDataCount = 0 24 | 25 | # because getdata returns the image as one big list 26 | for y in range(self.height): 27 | for x in range(self.width): 28 | 29 | _imageData[x][y] = rawData[rawDataCount][0:3] 30 | rawDataCount += 1 31 | return cast(list[list[tuple[int, int, int]]], _imageData) 32 | -------------------------------------------------------------------------------- /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/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/depixlib/__init__.py -------------------------------------------------------------------------------- /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 depixlib.LoadedImage import LoadedImage 10 | from depixlib.Rectangle import ColorRectange, Rectangle, RectangleMatch 11 | 12 | 13 | def findSameColorRectangle( 14 | pixelatedImage: LoadedImage, 15 | startCoordinates: tuple[int, int], 16 | maxCoordinates: tuple[int, int], 17 | ) -> ColorRectange: 18 | 19 | if pixelatedImage.imageData is None: 20 | raise ValueError("imageData of pixelatedImage is not set.") 21 | 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 | 57 | sameColorRectanges = [] 58 | 59 | x = rectangle.x 60 | maxx = rectangle.x + rectangle.width + 1 61 | maxy = rectangle.y + rectangle.height + 1 62 | 63 | while x < maxx: 64 | y = rectangle.y 65 | 66 | while y < maxy: 67 | 68 | sameColorRectange = findSameColorRectangle( 69 | pixelatedImage, (x, y), (maxx, maxy) 70 | ) 71 | if not sameColorRectange: 72 | continue 73 | # logging.info( 74 | # "Found rectangle at (%s, %s) with size (%s,%s) and color %s" 75 | # % ( 76 | # x, 77 | # y, 78 | # sameColorRectange.width, 79 | # sameColorRectange.height, 80 | # sameColorRectange.color, 81 | # ) 82 | # ) 83 | sameColorRectanges.append(sameColorRectange) 84 | 85 | y += sameColorRectange.height 86 | 87 | x += sameColorRectange.width 88 | 89 | return sameColorRectanges 90 | 91 | 92 | def removeMootColorRectangles( 93 | colorRectanges: list[ColorRectange], 94 | editorBackgroundColor: tuple[int, int, int] | None, 95 | ) -> list[ColorRectange]: 96 | 97 | pixelatedSubRectanges = [] 98 | 99 | mootColors = [(0, 0, 0), (255, 255, 255)] 100 | if editorBackgroundColor is not None: 101 | mootColors.append(editorBackgroundColor) 102 | 103 | for colorRectange in colorRectanges: 104 | if colorRectange.color not in mootColors: 105 | pixelatedSubRectanges.append(colorRectange) 106 | 107 | return pixelatedSubRectanges 108 | 109 | 110 | def findRectangleSizeOccurences( 111 | colorRectanges: list[ColorRectange], 112 | ) -> dict[tuple[int, int], int]: 113 | 114 | rectangeSizeOccurences: dict[tuple[int, int], int] = {} 115 | 116 | for colorRectange in colorRectanges: 117 | size = (colorRectange.width, colorRectange.height) 118 | if size in rectangeSizeOccurences: 119 | rectangeSizeOccurences[size] += 1 120 | else: 121 | rectangeSizeOccurences[size] = 1 122 | 123 | return rectangeSizeOccurences 124 | 125 | 126 | # Thanks to Artoria2e5, see 127 | # https://github.com/beurtschipper/Depix/pull/45 128 | def srgb2lin(s: float) -> float: 129 | 130 | if s <= 0.0404482362771082: 131 | lin = s / 12.92 132 | 133 | else: 134 | lin = ((s + 0.055) / 1.055) ** 2.4 135 | 136 | return lin 137 | 138 | 139 | def lin2srgb(lin: float) -> float: 140 | 141 | if lin > 0.0031308: 142 | s = 1.055 * lin ** (1.0 / 2.4) - 0.055 143 | 144 | else: 145 | s = 12.92 * lin 146 | 147 | return float(s) 148 | 149 | 150 | # return a dictionary, with sub-rectangle coordinates as key and RectangleMatch as value 151 | def findRectangleMatches( 152 | rectangeSizeOccurences: dict[tuple[int, int], int], 153 | pixelatedSubRectanges: list[ColorRectange], 154 | searchImage: LoadedImage, 155 | averageType: str = "gammacorrected", 156 | ) -> dict[tuple[int, int], list[RectangleMatch]]: 157 | r: int | float 158 | rr: int | float 159 | g: int | float 160 | gg: int | float 161 | b: int | float 162 | bb: int | float 163 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]] = {} 164 | 165 | for rectangeSizeOccurence in rectangeSizeOccurences: 166 | 167 | rectangleSize = rectangeSizeOccurence 168 | rectangleWidth = rectangleSize[0] 169 | rectangleHeight = rectangleSize[1] 170 | pixelsInRectangle = rectangleWidth * rectangleHeight 171 | 172 | # filter out the desired rectangle size 173 | matchingRectangles = [] 174 | for colorRectange in pixelatedSubRectanges: 175 | 176 | if (colorRectange.width, colorRectange.height) == rectangleSize: 177 | matchingRectangles.append(colorRectange) 178 | 179 | logging.info( 180 | "Scanning {} blocks with size {}".format( 181 | len(matchingRectangles), rectangleSize 182 | ) 183 | ) 184 | for x in range(searchImage.width - rectangleWidth): 185 | for y in range(searchImage.height - rectangleHeight): 186 | 187 | r = g = b = 0.0 188 | matchData = [] 189 | 190 | for xx in range(rectangleWidth): 191 | 192 | for yy in range(rectangleHeight): 193 | 194 | newPixel = searchImage.imageData[x + xx][y + yy] 195 | matchData.append(newPixel) 196 | 197 | if averageType == "gammacorrected": 198 | rr, gg, bb = newPixel 199 | 200 | if averageType == "linear": 201 | newPixelLinear = tuple( 202 | srgb2lin(float(v / 255)) for v in newPixel 203 | ) 204 | rr, gg, bb = newPixelLinear 205 | 206 | r += rr 207 | g += gg 208 | b += bb 209 | 210 | if averageType == "gammacorrected": 211 | averageColor = ( 212 | int(r / pixelsInRectangle), 213 | int(g / pixelsInRectangle), 214 | int(b / pixelsInRectangle), 215 | ) 216 | 217 | elif averageType == "linear": 218 | averageColor = cast( 219 | tuple[int, int, int], 220 | tuple( 221 | int(round(lin2srgb(v / pixelsInRectangle) * 255)) 222 | for v in (r, g, b) 223 | ), 224 | ) 225 | 226 | for matchingRectangle in matchingRectangles: 227 | 228 | if ( 229 | matchingRectangle.x, 230 | matchingRectangle.y, 231 | ) not in rectangleMatches: 232 | rectangleMatches[ 233 | (matchingRectangle.x, matchingRectangle.y) 234 | ] = [] 235 | 236 | if matchingRectangle.color == averageColor: 237 | newRectangleMatch = RectangleMatch(x, y, matchData) 238 | rectangleMatches[ 239 | (matchingRectangle.x, matchingRectangle.y) 240 | ].append(newRectangleMatch) 241 | 242 | if x % ((searchImage.width - rectangleWidth)/10) == 0: 243 | logging.info( 244 | "Scanning in searchImage: {}/{}".format( 245 | x, searchImage.width - rectangleWidth 246 | ) 247 | ) 248 | 249 | return rectangleMatches 250 | 251 | 252 | def dropEmptyRectangleMatches( 253 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]], 254 | pixelatedSubRectanges: list[ColorRectange], 255 | ) -> list[ColorRectange]: 256 | 257 | newPixelatedSubRectanges = [] 258 | for pixelatedSubRectange in pixelatedSubRectanges: 259 | if len(rectangleMatches[(pixelatedSubRectange.x, pixelatedSubRectange.y)]) > 0: 260 | newPixelatedSubRectanges.append(pixelatedSubRectange) 261 | 262 | return newPixelatedSubRectanges 263 | 264 | 265 | def splitSingleMatchAndMultipleMatches( 266 | pixelatedSubRectanges: list[ColorRectange], 267 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]], 268 | ) -> tuple[list[ColorRectange], list[ColorRectange]]: 269 | 270 | newPixelatedSubRectanges = [] 271 | singleResults = [] 272 | for colorRectange in pixelatedSubRectanges: 273 | 274 | firstMatchData = rectangleMatches[(colorRectange.x, colorRectange.y)][0].data 275 | singleMatch = True # only one data matches 276 | 277 | for match in rectangleMatches[(colorRectange.x, colorRectange.y)]: 278 | 279 | if firstMatchData != match.data: 280 | singleMatch = False 281 | break 282 | 283 | if singleMatch: 284 | singleResults.append(colorRectange) 285 | else: 286 | newPixelatedSubRectanges.append(colorRectange) 287 | 288 | return singleResults, newPixelatedSubRectanges 289 | 290 | 291 | def isNeighbor(pixelA: ColorRectange, pixelB: ColorRectange) -> bool: 292 | return ( 293 | (pixelA.x - pixelB.x) in [pixelB.width, 0, -pixelA.width] 294 | and (pixelA.y - pixelB.y) in [pixelB.height, 0, -pixelA.height] 295 | and pixelA != pixelB 296 | ) 297 | 298 | 299 | def findGeometricMatchesForSingleResults( 300 | singleResults: list[ColorRectange], 301 | pixelatedSubRectanges: list[ColorRectange], 302 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]], 303 | ) -> tuple[list[ColorRectange], list[ColorRectange]]: 304 | 305 | newPixelatedSubRectanges = pixelatedSubRectanges[:] 306 | newSingleResults = singleResults[:] 307 | matchCount: dict[ColorRectange, int] = {} 308 | dataSeen = set() 309 | 310 | for singleResult in singleResults: 311 | for pixelatedSubRectange in pixelatedSubRectanges: 312 | if not isNeighbor(singleResult, pixelatedSubRectange): 313 | continue 314 | if ( 315 | pixelatedSubRectange in matchCount 316 | and matchCount[pixelatedSubRectange] > 1 317 | ): 318 | break 319 | 320 | # use relative position to determine its neighbors 321 | for singleResultMatch in rectangleMatches[(singleResult.x, singleResult.y)]: 322 | for compareMatch in rectangleMatches[ 323 | (pixelatedSubRectange.x, pixelatedSubRectange.y) 324 | ]: 325 | 326 | xDistance = singleResult.x - pixelatedSubRectange.x 327 | yDistance = singleResult.y - pixelatedSubRectange.y 328 | xDistanceMatches = singleResultMatch.x - compareMatch.x 329 | yDistanceMatches = singleResultMatch.y - compareMatch.y 330 | 331 | if xDistance == xDistanceMatches and yDistance == yDistanceMatches: 332 | if ( 333 | repr((compareMatch.data, singleResultMatch.data)) 334 | not in dataSeen 335 | ): 336 | 337 | dataSeen.add( 338 | repr((compareMatch.data, singleResultMatch.data)) 339 | ) 340 | 341 | if pixelatedSubRectange not in matchCount: 342 | matchCount[pixelatedSubRectange] = 1 343 | else: 344 | matchCount[pixelatedSubRectange] += 1 345 | 346 | for pixelatedSubRectange in matchCount: 347 | if matchCount[pixelatedSubRectange] == 1: 348 | newSingleResults.append(pixelatedSubRectange) 349 | newPixelatedSubRectanges.remove(pixelatedSubRectange) 350 | 351 | return newSingleResults, newPixelatedSubRectanges 352 | 353 | 354 | def writeFirstMatchToImage( 355 | singleMatchRectangles: list[ColorRectange], 356 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]], 357 | searchImage: LoadedImage, 358 | unpixelatedOutputImage: Image.Image, 359 | ) -> None: 360 | 361 | for singleResult in singleMatchRectangles: 362 | singleMatch = rectangleMatches[(singleResult.x, singleResult.y)][0] 363 | 364 | for x in range(singleResult.width): 365 | for y in range(singleResult.height): 366 | 367 | color = searchImage.imageData[singleMatch.x + x][singleMatch.y + y] 368 | unpixelatedOutputImage.putpixel( 369 | (singleResult.x + x, singleResult.y + y), color 370 | ) 371 | 372 | 373 | def writeRandomMatchesToImage( 374 | pixelatedSubRectanges: list[ColorRectange], 375 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]], 376 | searchImage: LoadedImage, 377 | unpixelatedOutputImage: Image.Image, 378 | ) -> None: 379 | 380 | for singleResult in pixelatedSubRectanges: 381 | 382 | singleMatch = choice(rectangleMatches[(singleResult.x, singleResult.y)]) 383 | 384 | for x in range(singleResult.width): 385 | for y in range(singleResult.height): 386 | 387 | color = searchImage.imageData[singleMatch.x + x][singleMatch.y + y] 388 | unpixelatedOutputImage.putpixel( 389 | (singleResult.x + x, singleResult.y + y), color 390 | ) 391 | 392 | 393 | def writeAverageMatchToImage( 394 | pixelatedSubRectanges: list[ColorRectange], 395 | rectangleMatches: dict[tuple[int, int], list[RectangleMatch]], 396 | searchImage: LoadedImage, 397 | unpixelatedOutputImage: Image.Image, 398 | ) -> None: 399 | 400 | for pixelatedSubRectange in pixelatedSubRectanges: 401 | 402 | coordinate = (pixelatedSubRectange.x, pixelatedSubRectange.y) 403 | matches = rectangleMatches[coordinate] 404 | 405 | img = Image.new( 406 | "RGB", 407 | (pixelatedSubRectange.width, pixelatedSubRectange.height), 408 | color="white", 409 | ) 410 | 411 | for match in matches: 412 | 413 | dataCount = 0 414 | for x in range(pixelatedSubRectange.width): 415 | for y in range(pixelatedSubRectange.height): 416 | 417 | pixelData = match.data[dataCount] 418 | dataCount += 1 419 | currentPixel = img.getpixel((x, y))[0:3] 420 | 421 | r = int((pixelData[0] + currentPixel[0]) / 2) 422 | g = int((pixelData[1] + currentPixel[1]) / 2) 423 | b = int((pixelData[2] + currentPixel[2]) / 2) 424 | 425 | averagePixel = (r, g, b) 426 | 427 | img.putpixel((x, y), averagePixel) 428 | 429 | for x in range(pixelatedSubRectange.width): 430 | for y in range(pixelatedSubRectange.height): 431 | 432 | currentPixel = img.getpixel((x, y))[0:3] 433 | unpixelatedOutputImage.putpixel( 434 | (pixelatedSubRectange.x + x, pixelatedSubRectange.y + y), 435 | currentPixel, 436 | ) 437 | -------------------------------------------------------------------------------- /depixlib/helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | 4 | from typing import cast 5 | 6 | def check_file(s: str) -> str: 7 | if os.path.isfile(s): 8 | return s 9 | else: 10 | raise argparse.ArgumentTypeError("%s is not a file." % repr(s)) 11 | 12 | 13 | def check_color(s: str | None) -> tuple[int, int, int] | None: 14 | if s is None: 15 | return None 16 | ss = s.split(",") 17 | if len(ss) != 3: 18 | raise argparse.ArgumentTypeError("Given colors must be formatted as 'r,g,b'.") 19 | else: 20 | try: 21 | return cast(tuple[int, int, int], tuple([int(i) for i in ss])) 22 | except ValueError: 23 | raise argparse.ArgumentTypeError( 24 | "Maybe %s is not ',,'." % repr(s) 25 | ) 26 | 27 | -------------------------------------------------------------------------------- /docs/img/Recovering_prototype_latest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/docs/img/Recovering_prototype_latest.png -------------------------------------------------------------------------------- /docs/img/example_output_multiline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/docs/img/example_output_multiline.png -------------------------------------------------------------------------------- /docs/img/example_output_multiword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/docs/img/example_output_multiword.png -------------------------------------------------------------------------------- /docs/img/example_output_randomchars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/docs/img/example_output_randomchars.png -------------------------------------------------------------------------------- /docs/img/example_output_singleword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/docs/img/example_output_singleword.png -------------------------------------------------------------------------------- /docs/img/linear_box_filter_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/docs/img/linear_box_filter_example.png -------------------------------------------------------------------------------- /docs/img/output_depixelizedExample_linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/docs/img/output_depixelizedExample_linear.png -------------------------------------------------------------------------------- /docs/img/search_issue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/docs/img/search_issue.png -------------------------------------------------------------------------------- /images/searchimages/debruin_sublime_Linux_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/searchimages/debruin_sublime_Linux_small.png -------------------------------------------------------------------------------- /images/searchimages/debruinseq.txt: -------------------------------------------------------------------------------- 1 | 00102030405060708090a0b0c0d0e0f0g0h0i0j0k0l0m0n0o0p0q0r0s0t0u0v0w0x0y0z0A0B0C0D0E0F0G0H0I0J0K0L0M0N0O0P0Q0R0S0T0U0V0W0X0Y0Z112131415161718191a1b1c1d1e1f1g1h1i1j1k1l1m1n1o1p1q1r1s1t1u1v1w1x1y1z1A1B1C1D1E1F1G1H1I1J1K1L1M1N1O1P1Q1R1S1T1U1V1W1X1Y1Z2232425262728292a2b2c2d2e2f2g2h2i2j2k2l2m2n2o2p2q2r2s2t2u2v2w2x2y2z2A2B2C2D2E2F2G2H2I2J2K2L2M2N2O2P2Q2R2S2T2U2V2W2X2Y2Z33435363738393a3b3c3d3e3f3g3h3i3j3k3l3m3n3o3p3q3r3s3t3u3v3w3x3y3z3A3B3C3D3E3F3G3H3I3J3K3L3M3N3O3P3Q3R3S3T3U3V3W3X3Y3Z445464748494a4b4c4d4e4f4g4h4i4j4k4l4m4n4o4p4q4r4s4t4u4v4w4x4y4z4A4B4C4D4E4F4G4H4I4J4K4L4M4N4O4P4Q4R4S4T4U4V4W4X4Y4Z5565758595a5b5c5d5e5f5g5h5i5j5k5l5m5n5o5p5q5r5s5t5u5v5w5x5y5z5A5B5C5D5E5F5G5H5I5J5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5Y5Z66768696a6b6c6d6e6f6g6h6i6j6k6l6m6n6o6p6q6r6s6t6u6v6w6x6y6z6A6B6C6D6E6F6G6H6I6J6K6L6M6N6O6P6Q6R6S6T6U6V6W6X6Y6Z778797a7b7c7d7e7f7g7h7i7j7k7l7m7n7o7p7q7r7s7t7u7v7w7x7y7z7A7B7C7D7E7F7G7H7I7J7K7L7M7N7O7P7Q7R7S7T7U7V7W7X7Y7Z8898a8b8c8d8e8f8g8h8i8j8k8l8m8n8o8p8q8r8s8t8u8v8w8x8y8z8A8B8C8D8E8F8G8H8I8J8K8L8M8N8O8P8Q8R8S8T8U8V8W8X8Y8Z99a9b9c9d9e9f9g9h9i9j9k9l9m9n9o9p9q9r9s9t9u9v9w9x9y9z9A9B9C9D9E9F9G9H9I9J9K9L9M9N9O9P9Q9R9S9T9U9V9W9X9Y9ZaabacadaeafagahaiajakalamanaoapaqarasatauavawaxayazaAaBaCaDaEaFaGaHaIaJaKaLaMaNaOaPaQaRaSaTaUaVaWaXaYaZbbcbdbebfbgbhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJbKbLbMbNbObPbQbRbSbTbUbVbWbXbYbZccdcecfcgchcicjckclcmcncocpcqcrcsctcucvcwcxcyczcAcBcCcDcEcFcGcHcIcJcKcLcMcNcOcPcQcRcScTcUcVcWcXcYcZddedfdgdhdidjdkdldmdndodpdqdrdsdtdudvdwdxdydzdAdBdCdDdEdFdGdHdIdJdKdLdMdNdOdPdQdRdSdTdUdVdWdXdYdZeefegeheiejekelemeneoepeqereseteuevewexeyezeAeBeCeDeEeFeGeHeIeJeKeLeMeNeOePeQeReSeTeUeVeWeXeYeZffgfhfifjfkflfmfnfofpfqfrfsftfufvfwfxfyfzfAfBfCfDfEfFfGfHfIfJfKfLfMfNfOfPfQfRfSfTfUfVfWfXfYfZgghgigjgkglgmgngogpgqgrgsgtgugvgwgxgygzgAgBgCgDgEgFgGgHgIgJgKgLgMgNgOgPgQgRgSgTgUgVgWgXgYgZhhihjhkhlhmhnhohphqhrhshthuhvhwhxhyhzhAhBhChDhEhFhGhHhIhJhKhLhMhNhOhPhQhRhShThUhVhWhXhYhZiijikiliminioipiqirisitiuiviwixiyiziAiBiCiDiEiFiGiHiIiJiKiLiMiNiOiPiQiRiSiTiUiViWiXiYiZjjkjljmjnjojpjqjrjsjtjujvjwjxjyjzjAjBjCjDjEjFjGjHjIjJjKjLjMjNjOjPjQjRjSjTjUjVjWjXjYjZkklkmknkokpkqkrksktkukvkwkxkykzkAkBkCkDkEkFkGkHkIkJkKkLkMkNkOkPkQkRkSkTkUkVkWkXkYkZllmlnlolplqlrlsltlulvlwlxlylzlAlBlClDlElFlGlHlIlJlKlLlMlNlOlPlQlRlSlTlUlVlWlXlYlZmmnmompmqmrmsmtmumvmwmxmymzmAmBmCmDmEmFmGmHmImJmKmLmMmNmOmPmQmRmSmTmUmVmWmXmYmZnnonpnqnrnsntnunvnwnxnynznAnBnCnDnEnFnGnHnInJnKnLnMnNnOnPnQnRnSnTnUnVnWnXnYnZoopoqorosotouovowoxoyozoAoBoCoDoEoFoGoHoIoJoKoLoMoNoOoPoQoRoSoToUoVoWoXoYoZppqprpsptpupvpwpxpypzpApBpCpDpEpFpGpHpIpJpKpLpMpNpOpPpQpRpSpTpUpVpWpXpYpZqqrqsqtquqvqwqxqyqzqAqBqCqDqEqFqGqHqIqJqKqLqMqNqOqPqQqRqSqTqUqVqWqXqYqZrrsrtrurvrwrxryrzrArBrCrDrErFrGrHrIrJrKrLrMrNrOrPrQrRrSrTrUrVrWrXrYrZsstsusvswsxsyszsAsBsCsDsEsFsGsHsIsJsKsLsMsNsOsPsQsRsSsTsUsVsWsXsYsZttutvtwtxtytztAtBtCtDtEtFtGtHtItJtKtLtMtNtOtPtQtRtStTtUtVtWtXtYtZuuvuwuxuyuzuAuBuCuDuEuFuGuHuIuJuKuLuMuNuOuPuQuRuSuTuUuVuWuXuYuZvvwvxvyvzvAvBvCvDvEvFvGvHvIvJvKvLvMvNvOvPvQvRvSvTvUvVvWvXvYvZwwxwywzwAwBwCwDwEwFwGwHwIwJwKwLwMwNwOwPwQwRwSwTwUwVwWwXwYwZxxyxzxAxBxCxDxExFxGxHxIxJxKxLxMxNxOxPxQxRxSxTxUxVxWxXxYxZyyzyAyByCyDyEyFyGyHyIyJyKyLyMyNyOyPyQyRySyTyUyVyWyXyYyZzzAzBzCzDzEzFzGzHzIzJzKzLzMzNzOzPzQzRzSzTzUzVzWzXzYzZAABACADAEAFAGAHAIAJAKALAMANAOAPAQARASATAUAVAWAXAYAZBBCBDBEBFBGBHBIBJBKBLBMBNBOBPBQBRBSBTBUBVBWBXBYBZCCDCECFCGCHCICJCKCLCMCNCOCPCQCRCSCTCUCVCWCXCYCZDDEDFDGDHDIDJDKDLDMDNDODPDQDRDSDTDUDVDWDXDYDZEEFEGEHEIEJEKELEMENEOEPEQERESETEUEVEWEXEYEZFFGFHFIFJFKFLFMFNFOFPFQFRFSFTFUFVFWFXFYFZGGHGIGJGKGLGMGNGOGPGQGRGSGTGUGVGWGXGYGZHHIHJHKHLHMHNHOHPHQHRHSHTHUHVHWHXHYHZIIJIKILIMINIOIPIQIRISITIUIVIWIXIYIZJJKJLJMJNJOJPJQJRJSJTJUJVJWJXJYJZKKLKMKNKOKPKQKRKSKTKUKVKWKXKYKZLLMLNLOLPLQLRLSLTLULVLWLXLYLZMMNMOMPMQMRMSMTMUMVMWMXMYMZNNONPNQNRNSNTNUNVNWNXNYNZOOPOQOROSOTOUOVOWOXOYOZPPQPRPSPTPUPVPWPXPYPZQQRQSQTQUQVQWQXQYQZRRSRTRURVRWRXRYRZSSTSUSVSWSXSYSZTTUTVTWTXTYTZUUVUWUXUYUZVVWVXVYVZWWXWYWZXXYXZYYZZ0 -------------------------------------------------------------------------------- /images/searchimages/debruinseq_notepad_Windows10_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/searchimages/debruinseq_notepad_Windows10_close.png -------------------------------------------------------------------------------- /images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png -------------------------------------------------------------------------------- /images/searchimages/debruinseq_notepad_Windows10_spaced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/searchimages/debruinseq_notepad_Windows10_spaced.png -------------------------------------------------------------------------------- /images/searchimages/debruinseq_notepad_Windows7_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/searchimages/debruinseq_notepad_Windows7_close.png -------------------------------------------------------------------------------- /images/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/stars.png -------------------------------------------------------------------------------- /images/testimages/sublime_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/testimages/sublime_screenshot.png -------------------------------------------------------------------------------- /images/testimages/sublime_screenshot_pixels_gimp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/testimages/sublime_screenshot_pixels_gimp.png -------------------------------------------------------------------------------- /images/testimages/testimage1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/testimages/testimage1.png -------------------------------------------------------------------------------- /images/testimages/testimage1_pixels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/testimages/testimage1_pixels.png -------------------------------------------------------------------------------- /images/testimages/testimage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/testimages/testimage2.png -------------------------------------------------------------------------------- /images/testimages/testimage2_pixels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/testimages/testimage2_pixels.png -------------------------------------------------------------------------------- /images/testimages/testimage3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/testimages/testimage3.png -------------------------------------------------------------------------------- /images/testimages/testimage3_pixels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spipm/Depixelization_poc/26c7326b2f620cced2022e4c5a250679f5113012/images/testimages/testimage3_pixels.png -------------------------------------------------------------------------------- /tool_gen_pixelated.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import os 4 | import shutil 5 | 6 | from depix import DepixHelpFormatter 7 | from LoadedImage import LoadedImage 8 | 9 | 10 | def check_file(s: str) -> str: 11 | if os.path.isfile(s): 12 | return s 13 | else: 14 | raise argparse.ArgumentTypeError("%s is not a file." % repr(s)) 15 | 16 | 17 | def parse_args() -> argparse.Namespace: 18 | logging.basicConfig(level=logging.INFO) 19 | 20 | parser = argparse.ArgumentParser( 21 | description="This command generates pixelized image from a given image.", 22 | formatter_class=( 23 | lambda prog: DepixHelpFormatter( 24 | prog, 25 | **{ 26 | "width": shutil.get_terminal_size(fallback=(120, 50)).columns, 27 | "max_help_position": 40, 28 | }, 29 | ) 30 | ), 31 | ) 32 | parser.add_argument( 33 | "-i", 34 | "--image", 35 | help="path to image to pixelize", 36 | required=True, 37 | type=check_file, 38 | metavar="PATH", 39 | ) 40 | parser.add_argument( 41 | "-o", 42 | "--outputimage", 43 | help="path to output image", 44 | default="output.png", 45 | metavar="PATH", 46 | ) 47 | return parser.parse_args() 48 | 49 | 50 | def main() -> None: 51 | args = parse_args() 52 | 53 | imagePath = args.image 54 | 55 | image = LoadedImage(imagePath) 56 | outputImage = image.getCopyOfLoadedPILImage() 57 | 58 | blockSize = 5 59 | blockPixelCount = blockSize * blockSize 60 | 61 | for x in range(0, image.width, blockSize): 62 | for y in range(0, image.height, blockSize): 63 | 64 | r = g = b = 0 65 | 66 | maxX = min(x + blockSize, image.width) 67 | maxY = min(y + blockSize, image.height) 68 | 69 | for xx in range(x, maxX): 70 | for yy in range(y, maxY): 71 | 72 | currentPixel = image.imageData[xx][yy] 73 | r += currentPixel[0] 74 | g += currentPixel[1] 75 | b += currentPixel[2] 76 | 77 | averageR = int(r / blockPixelCount) 78 | averageG = int(g / blockPixelCount) 79 | averageB = int(b / blockPixelCount) 80 | averageColor = (averageR, averageG, averageB) 81 | 82 | for xx in range(x, maxX): 83 | for yy in range(y, maxY): 84 | 85 | outputImage.putpixel((xx, yy), averageColor) 86 | 87 | outputImage.save(args.outputimage) 88 | 89 | 90 | if __name__ == "__main__": 91 | main() 92 | 93 | # Generated: 94 | # 676c81 95 | # Gimp: 96 | # 878a9e 97 | 98 | # diff: 2104861 99 | 100 | # Generated: 101 | # 889475 102 | # Gimp: 103 | # a7b194 104 | 105 | # diff: 2039071 106 | 107 | # ? 108 | -------------------------------------------------------------------------------- /tool_show_boxes.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import logging 5 | logging.basicConfig(format="%(asctime)s - %(message)s", level=logging.INFO) 6 | 7 | from PIL import Image, ImageDraw 8 | 9 | from depixlib.helpers import ( 10 | check_file, 11 | check_color 12 | ) 13 | from depixlib.functions import ( 14 | dropEmptyRectangleMatches, 15 | findGeometricMatchesForSingleResults, 16 | findRectangleMatches, 17 | findRectangleSizeOccurences, 18 | findSameColorSubRectangles, 19 | removeMootColorRectangles, 20 | splitSingleMatchAndMultipleMatches, 21 | writeAverageMatchToImage, 22 | writeFirstMatchToImage 23 | ) 24 | from depixlib.LoadedImage import LoadedImage 25 | from depixlib.Rectangle import Rectangle 26 | 27 | 28 | def parse_args() -> argparse.Namespace: 29 | 30 | usage = """ 31 | note: 32 | The pixelated rectangle must be cut out to only include the pixelated rectangles. 33 | The pattern search image is generally a screenshot of a De Bruijn sequence of expected characters, 34 | made on a machine with the same editor and text size as the original screenshot that was pixelated. 35 | """ 36 | 37 | parser = argparse.ArgumentParser( 38 | description="This command recovers passwords from pixelized screenshots.", 39 | epilog=usage 40 | ) 41 | parser.add_argument( 42 | "-p", 43 | "--pixelimage", 44 | help="path to image with pixelated rectangle", 45 | required=True, 46 | default=argparse.SUPPRESS, 47 | type=check_file, 48 | metavar="PATH" 49 | ) 50 | parser.add_argument( 51 | "-s", 52 | "--searchimage", 53 | help="path to image with patterns to search", 54 | required=True, 55 | default=argparse.SUPPRESS, 56 | type=check_file, 57 | metavar="PATH", 58 | ) 59 | parser.add_argument( 60 | "-a", 61 | "--averagetype", 62 | help="type of RGB average to use", 63 | default="gammacorrected", 64 | choices=["gammacorrected", "linear"], 65 | metavar="TYPE", 66 | ) 67 | parser.add_argument( 68 | "-b", 69 | "--backgroundcolor", 70 | help="original editor background color in format r,g,b (color to ignore)", 71 | default=None, 72 | type=check_color, 73 | metavar="RGB" 74 | ) 75 | parser.add_argument( 76 | "-o", 77 | "--outputimage", 78 | help="path to output image", 79 | default="output.png", 80 | metavar="PATH", 81 | ) 82 | return parser.parse_args() 83 | 84 | 85 | def main() -> None: 86 | args = parse_args() 87 | 88 | pixelatedImagePath = args.pixelimage 89 | searchImagePath = args.searchimage 90 | editorBackgroundColor: tuple[int, int, int] | None = args.backgroundcolor 91 | averageType = args.averagetype 92 | 93 | logging.info("Loading pixelated image from %s" % pixelatedImagePath) 94 | pixelatedImage = LoadedImage(pixelatedImagePath) 95 | unpixelatedOutputImage = pixelatedImage.getCopyOfLoadedPILImage() 96 | 97 | logging.info("Loading search image from %s" % searchImagePath) 98 | searchImage = LoadedImage(searchImagePath) 99 | 100 | logging.info("Finding color rectangles from pixelated space") 101 | # fill coordinates here if not cut out 102 | pixelatedRectange = Rectangle( 103 | (0, 0), (pixelatedImage.width - 1, pixelatedImage.height - 1) 104 | ) 105 | 106 | pixelatedSubRectanges = findSameColorSubRectangles( 107 | pixelatedImage, pixelatedRectange 108 | ) 109 | logging.info("Found %s same color rectangles" % len(pixelatedSubRectanges)) 110 | 111 | pixelatedSubRectanges = removeMootColorRectangles( 112 | pixelatedSubRectanges, editorBackgroundColor 113 | ) 114 | logging.info("%s rectangles left after moot filter" % len(pixelatedSubRectanges)) 115 | 116 | rectangeSizeOccurences = findRectangleSizeOccurences(pixelatedSubRectanges) 117 | logging.info("Found %s different rectangle sizes" % len(rectangeSizeOccurences)) 118 | if len(rectangeSizeOccurences) > max( 119 | 10, pixelatedRectange.width * pixelatedRectange.height * 0.01 120 | ): 121 | logging.warning( 122 | "Too many variants on block size. Re-cropping the image might help." 123 | ) 124 | 125 | logging.info("Finding matches in search image") 126 | rectangleMatches = findRectangleMatches( 127 | rectangeSizeOccurences, pixelatedSubRectanges, searchImage, averageType 128 | ) 129 | 130 | logging.info("Removing blocks with no matches") 131 | pixelatedSubRectanges = dropEmptyRectangleMatches( 132 | rectangleMatches, pixelatedSubRectanges 133 | ) 134 | 135 | # Enhance like in the movies 136 | enhance = 3 137 | 138 | image = Image.open(pixelatedImagePath) 139 | enhancedImage = image.resize((image.width*enhance, image.height*enhance)) 140 | draw = ImageDraw.Draw(enhancedImage) 141 | 142 | # Show crappy box detector output 143 | for box in pixelatedSubRectanges: 144 | 145 | draw.line([ 146 | (box.x*enhance, box.y*enhance), 147 | (((box.x+box.width)*enhance)-enhance, box.y*enhance ), 148 | (((box.x+box.width)*enhance)-enhance, ((box.y+box.height)*enhance)-enhance), 149 | (box.x*enhance, ((box.y+box.height)*enhance) - enhance), 150 | (box.x*enhance, box.y*enhance) 151 | ], width=1, fill="red") 152 | 153 | enhancedImage.show() 154 | 155 | 156 | if __name__ == "__main__": 157 | main() 158 | --------------------------------------------------------------------------------