Implemented linear filter
22
README.md
|
@ -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
|
||||
```
|
||||

|
||||
|
||||
* 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
|
||||
```
|
||||

|
||||
|
||||
## 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
|
||||
|
|
12
depix.py
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 206 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 716 B |
Before Width: | Height: | Size: 684 B |
After Width: | Height: | Size: 3.9 KiB |