Implemented linear filter

This commit is contained in:
beurtschipper 2021-03-21 17:11:30 +01:00
parent 9c357e4cfe
commit e00871fe2f
11 changed files with 73 additions and 11 deletions

View File

@ -24,6 +24,20 @@ python depix.py -p /path/to/your/input/image.png -s images/searchimages/debruins
* It is reccomended that you use a folder in the `images/searchimages/` directory for the `-s` flag in order to achieve best results.
* `-p` and `-o` (Input and output, respectively) can be either relative paths (to the repo's folder) or absolute to your drive. (`/` or `C:\`)
## Example usage
* 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.
```
python3 depix.py -p images/testimages/testimage3_pixels.png -s images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png
```
![image](docs/img/example_output_multiword.png)
* 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.
```
python3 depix.py -p images/testimages/sublime_screenshot_pixels_gimp.png -s images/searchimages/debruin_sublime_Linux_small.png --backgroundcolor 40,41,35 --averagetype linear
```
![image](docs/img/output_depixelizedExample_linear.png)
## About
### Making a Search Image
* Cut out the pixelated blocks from the screenshot as a single rectangle.
@ -42,11 +56,17 @@ The algorithm uses the fact that the linear box filter processes every block sep
### Known limitations
* 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.
* 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/).
* ~~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.~~
### Future development
* Implement more filter functions
Create more averaging filters that work like some popular editors do.
* Create a new tool that utilizes HMMs
After creating this program, someone pointed me to a research document from 2016 where a group of researchers managed to create a similar tool. Their tool has better precision and works across many different fonts. I encourage anyone passionate about this type of depixalization to implement their HMM-based version and share it:
https://www.researchgate.net/publication/305423573_On_the_Ineffectiveness_of_Mosaicing_and_Blurring_as_Tools_for_Document_Redaction

View File

@ -16,11 +16,19 @@ usage = '''
parser = argparse.ArgumentParser(description = usage)
parser.add_argument('-p', '--pixelimage', help = 'Path to image with pixelated rectangle', required=True)
parser.add_argument('-s', '--searchimage', help = 'Path to image with patterns to search', required=True)
parser.add_argument('-a', '--averagetype', help = 'Type of RGB average to use (linear or gammacorrected)',
default='gammacorrected', choices=['gammacorrected', 'linear'])
parser.add_argument('-b', '--backgroundcolor', help = 'Original editor background color in format r,g,b', default=None)
parser.add_argument('-o', '--outputimage', help = 'Path to output image', nargs='?', default='output.png')
args = parser.parse_args()
pixelatedImagePath = args.pixelimage
searchImagePath = args.searchimage
if args.backgroundcolor != None:
editorBackgroundColor = tuple([int(x) for x in args.backgroundcolor.split(",")])
else:
editorBackgroundColor = args.backgroundcolor
averageType = args.averagetype
logging.info("Loading pixelated image from %s" % pixelatedImagePath)
@ -39,7 +47,7 @@ pixelatedRectange = Rectangle((0, 0), (pixelatedImage.width-1, pixelatedImage.he
pixelatedSubRectanges = findSameColorSubRectangles(pixelatedImage, pixelatedRectange)
logging.info("Found %s same color rectangles" % len(pixelatedSubRectanges))
pixelatedSubRectanges = removeMootColorRectangles(pixelatedSubRectanges)
pixelatedSubRectanges = removeMootColorRectangles(pixelatedSubRectanges, editorBackgroundColor)
logging.info("%s rectangles left after moot filter" % len(pixelatedSubRectanges))
rectangeSizeOccurences = findRectangleSizeOccurences(pixelatedSubRectanges)
@ -48,7 +56,7 @@ if len(rectangeSizeOccurences) > max(10, pixelatedRectange.width * pixelatedRect
logging.warning("Too many variants on block size. Re-pixelating the image might help.")
logging.info("Finding matches in search image")
rectangleMatches = findRectangleMatches(rectangeSizeOccurences, pixelatedSubRectanges, searchImage)
rectangleMatches = findRectangleMatches(rectangeSizeOccurences, pixelatedSubRectanges, searchImage, averageType)
logging.info("Removing blocks with no matches")
pixelatedSubRectanges = dropEmptyRectangleMatches(rectangleMatches, pixelatedSubRectanges)

View File

@ -65,13 +65,17 @@ def findSameColorSubRectangles(pixelatedImage, rectangle):
return sameColorRectanges
def removeMootColorRectangles(colorRectanges):
def removeMootColorRectangles(colorRectanges, editorBackgroundColor):
pixelatedSubRectanges = []
mootColors = [(0,0,0), (255,255,255)]
if editorBackgroundColor != None:
mootColors.append(editorBackgroundColor)
for colorRectange in colorRectanges:
if colorRectange.color in [(0,0,0),(255,255,255)]:
if colorRectange.color in mootColors:
continue
pixelatedSubRectanges.append(colorRectange)
@ -95,8 +99,26 @@ def findRectangleSizeOccurences(colorRectanges):
return rectangeSizeOccurences
# Thanks to Artoria2e5, see
# https://github.com/beurtschipper/Depix/pull/45
def srgb2lin(s):
if s <= 0.0404482362771082:
lin = s / 12.92
else:
lin = ((s + 0.055) / 1.055) ** 2.4
return lin
def lin2srgb(lin):
if lin > 0.0031308:
s = 1.055 * lin**(1.0 / 2.4) - 0.055
else:
s = 12.92 * lin
return s
# return a dictionary, with sub-rectangle coordinates as key and RectangleMatch as value
def findRectangleMatches(rectangeSizeOccurences, pixelatedSubRectanges, searchImage):
def findRectangleMatches(rectangeSizeOccurences, pixelatedSubRectanges, searchImage, averageType = 'gammacorrected'):
rectangleMatches = {}
@ -106,7 +128,6 @@ def findRectangleMatches(rectangeSizeOccurences, pixelatedSubRectanges, searchIm
rectangleWidth = rectangleSize[0]
rectangleHeight = rectangleSize[1]
pixelsInRectangle = rectangleWidth*rectangleHeight
# logging.info('For rectangle size {}x{}'.format(rectangleWidth, rectangleHeight))
# filter out the desired rectangle size
matchingRectangles = []
@ -115,6 +136,8 @@ def findRectangleMatches(rectangeSizeOccurences, pixelatedSubRectanges, searchIm
if (colorRectange.width, colorRectange.height) == rectangleSize:
matchingRectangles.append(colorRectange)
logging.info('Scanning {} blocks with size {}'.format(len(matchingRectangles), rectangleSize))
for x in range(searchImage.width - rectangleWidth):
for y in range(searchImage.height - rectangleHeight):
@ -124,15 +147,26 @@ def findRectangleMatches(rectangeSizeOccurences, pixelatedSubRectanges, searchIm
for xx in range(rectangleWidth):
for yy in range(rectangleHeight):
newPixel = searchImage.imageData[x+xx][y+yy]
rr,gg,bb = newPixel
matchData.append(newPixel)
if averageType == 'gammacorrected':
rr,gg,bb = newPixel
if averageType == 'linear':
newPixelLinear = tuple(srgb2lin(v/255) for v in newPixel)
rr,gg,bb = newPixelLinear
r += rr
g += gg
b += bb
averageColor = (int(r / pixelsInRectangle), int(g / pixelsInRectangle), int(b / pixelsInRectangle))
if averageType == 'gammacorrected':
averageColor = (int(r / pixelsInRectangle), int(g / pixelsInRectangle), int(b / pixelsInRectangle))
elif averageType == 'linear':
averageColor = tuple(int(round(lin2srgb(v / pixelsInRectangle)*255)) for v in (r,g,b))
for matchingRectangle in matchingRectangles:
@ -143,8 +177,8 @@ def findRectangleMatches(rectangeSizeOccurences, pixelatedSubRectanges, searchIm
newRectangleMatch = RectangleMatch(x, y, matchData)
rectangleMatches[(matchingRectangle.x,matchingRectangle.y)].append(newRectangleMatch)
# if x % 64 == 0:
# logging.info('Scanning in searchImage: {}/{}'.format(x, searchImage.width - rectangleWidth))
if x % 64 == 0:
logging.info('Scanning in searchImage: {}/{}'.format(x, searchImage.width - rectangleWidth))
return rectangleMatches

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

BIN
output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB