大家好,又见面了,我是你们的朋友全栈君。
最近看到各种公众号都在推一个叫Depix的Github项目,用途是能够消除文字马赛克,抱着试试看的态度测试了一下这个项目。
太长不看版:公众号对该项目的效果有一定程度的夸大,但是还是要注意使用各种方法对个人隐私进行保护
项目地址:https://github.com/beurtschipper/Depix
项目自带的Example如下:
这个项目的文档上说,只需要马赛克后的图像,马赛克图像上包含的字符的De Bruijn序列,就可以生成去马赛克的图像。(测试效果如上图所示)
接下来就测试一下。
首先,安装Python3和PIP3。
我这里在linux云端进行的测试,测试的Python环境是Python3,安装过程这里不再赘述。
运行项目需要环境pillow和image,输入命令使用pip进行安装:
pip3 install pillow
pip3 install image
如果下载速度过慢,则需要更改为国内源再测试。
为了检测运行效果,先执行软件自带的例程进行测试:
python3 depix.py -p images/testimages/testimage3_pixels.png -s images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png -o output.png
测试成功的话,同目录下会出现output.png,如果能正常打开并看到如上图所示的Hello from the other side,说明测试通过。
如果报错No Module named xxx,则说明运行环境没搭建好,需要先搜索缺少的module在哪个包里,再使用pip下载缺少的包。
按照项目网站上的说明,要去除文字上的马赛克,需要做如下准备:
详细说明如下。
在这里我们使用记事本截图+某聊天软件自带的马赛克功能。
马赛克的模糊度调低点,保证正好把文字全抹掉。
效果如下所示:
马赛克前:
马赛克后:
文字内容是998877665544。
德布鲁因序列,一般有两个属性:元素和阶。
元素:就是这个序列中有x个不同的字符。
阶:在x个不同的字符中抽出y个进行组合,这个y就是阶
如果某个x元素y阶的德布鲁因序列,把这个序列头尾相接。那么,在x中抽出y个字符组合出来的字符串都能在这个序列里找到,且只能找到一次。
Example: 只有3个元素(abc)2阶的德布鲁因序列为:a a b a c b b c c (用下文的在线生成器生成的序列,会在序列尾部增加一个和序列头部相同的字符) 把序列首尾相接组成一个环,在abc中任意抽出2个字符组成一个字符串,都能在这个序列环里找到,且只能找到一次。
生成网站:https://damip.net/article-de-bruijn-sequence
只要输入我们要生成的序列中包含的字符和长度,就可以生成对应的序列。
在这里按照公众号介绍的算法原理,只需要生成2阶(Code length: 2)的序列就可以了。
(在这里生成的是3阶)
由于我们测试的马赛克字符中只有数字,那么Alphabet一栏只需要输入所有数字 1234567890
就好。
生成的序列如下:
注: x字符y阶的德布鲁因序列长度为: l e n = x y len=x^y len=xy。 即:10字符的3阶德布鲁因序列长度为1000,这里在线生成器为了实现和头尾相接相同的效果增加了y-1字符的长度
得到了德布鲁因序列之后,就需要将序列转换成和待解码文字相同样式(字体大小颜色等,甚至最好使用一样的编辑器和截图工具)的文字图片。
由于待解码图像是记事本上的文字,因此我们只需要在同一个记事本上粘贴上述序列并截图即可。
生成的图像如下:
假设保存的德布鲁因序列图像名字为search.png,待解码图像名字toFind.png。
那么解码命令如下所示:
python3 depix.py -p toFind.png -s search.png -o op.png
解码效果如下:
效果没公众号吹得那么厉害,只看到了第一个字符的一部分。
但是经过测试,多次解码后可以看到更多的数据。
将第一次生成的文件再次进行解码后效果如下:
写一个脚本进行循环解码:
#/bin/bash
for I in {
1..50};do
let op=$I+1
python3 depix.py -p op$I.png -s search.png -o op$op.png
done
echo done.
解码51次后的效果:
看来算法还是有一定效果的。
但是多次解码后,输出的图像和这次的图像相差无几,说明这就是算法的极限了。
首先我们打开主要的脚本文件depix.py。从代码上看,函数需要三个参数:待解码图片、德布鲁因序列图片和输出图片,并把待解码图片、德布鲁因序列图片的路径分别赋给pixelatedImagePath、searchImagePath 两个变量:
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('-o', '--outputimage', help = 'Path to output image', nargs='?', default='output.png') #输出图片,非必须,默认为output.png
args = parser.parse_args()
pixelatedImagePath = args.pixelimage
searchImagePath = args.searchimage
由于已经知道了该方法的原理是通过将德布鲁因序列图片用相同的形式打码,之后再和原图进行比对,找出相同的图块,那么只要我们跟踪这个序列图片做了什么操作,就可以知道这个算法的适用范围。
跟踪searchImagePath这个变量,发现其在下文中作为LoadedImage函数的参数,将图片载入给了变量searchImage,而searchImage这个变量的首次调用是在findRectangleMatches函数中:
logging.info("Loading search image from %s" % searchImagePath)
searchImage = LoadedImage(searchImagePath) # 载入德布鲁因序列图像
# 省略
logging.info("Finding matches in search image")
rectangleMatches = findRectangleMatches(rectangeSizeOccurences, pixelatedSubRectanges, searchImage) #首次调用
这个调用它的函数在depixlib文件夹下的functions.py,定义如下:
def findRectangleMatches(rectangeSizeOccurences, pixelatedSubRectanges, searchImage):
rectangleMatches = {
}
for rectangeSizeOccurence in rectangeSizeOccurences:
rectangleSize = rectangeSizeOccurence
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 = []
for colorRectange in pixelatedSubRectanges:
if (colorRectange.width, colorRectange.height) == rectangleSize:
matchingRectangles.append(colorRectange)
for x in range(searchImage.width - rectangleWidth):
for y in range(searchImage.height - rectangleHeight):
r = g = b = 0
matchData = []
for xx in range(rectangleWidth):
for yy in range(rectangleHeight):
newPixel = searchImage.imageData[x+xx][y+yy]
rr,gg,bb = newPixel
matchData.append(newPixel)
r += rr
g += gg
b += bb
averageColor = (int(r / pixelsInRectangle), int(g / pixelsInRectangle), int(b / pixelsInRectangle))
for matchingRectangle in matchingRectangles:
if (matchingRectangle.x,matchingRectangle.y) not in rectangleMatches:
rectangleMatches[(matchingRectangle.x,matchingRectangle.y)] = []
if matchingRectangle.color == averageColor:
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))
return rectangleMatches
从代码中的xy循环中,我们可以看出该函数将德布鲁因序列图像划分成了rectangleWidth * rectangleHeight 大小的小方块,并对小方块中的颜色取平均值。这就是项目中提到的linear box filter马赛克。
简单说明一下,linear box filter马赛克的打码方式是:
接下来我们来看这个rectangleWidth * rectangleHeight是怎么得出的。
取消下面代码的注释。
logging.info('For rectangle size {}x{}'.format(rectangleWidth, rectangleHeight))
再次运行后,发现这里输出了待解码图片中的每一个马赛克方格的大小,说明在解马赛克的过程中,该脚本会计算出待解码图片中每个马赛克方格的大小,再根据这个大小对德布鲁因序列图片进行马赛克处理。
输出结果:
使用画图计算出的马赛克大小:
至于这个像素大小的计算方式,需要向上追溯到对待解码图片的处理逻辑。
脚本读取了待解码图片后,首先会先查找所有的相同颜色的矩形方块,并将找到的同色矩形方块存入list:
pixelatedImage = LoadedImage(pixelatedImagePath) # 待解码图片读取
#...
pixelatedSubRectanges = findSameColorSubRectangles(pixelatedImage, pixelatedRectange) #同色矩形区域寻找
# ...
def findSameColorSubRectangles(pixelatedImage, rectangle): #函数定义,在functions.py中
sameColorRectanges = []
x = rectangle.x
maxx = rectangle.x + rectangle.width + 1
maxy = rectangle.y + rectangle.height + 1
while x < maxx:
y = rectangle.y
while y < maxy:
sameColorRectange = findSameColorRectangle(pixelatedImage, (x, y), (maxx, maxy))
if sameColorRectange == False:
continue
# logging.info("Found rectangle at (%s, %s) with size (%s,%s) and color %s" % (x, y, sameColorRectange.width,sameColorRectange.height,sameColorRectange.color))
sameColorRectanges.append(sameColorRectange)
y += sameColorRectange.height
x += sameColorRectange.width
return sameColorRectanges
接下来,程序会移除掉待解码图片里的无用色块:
pixelatedSubRectanges = removeMootColorRectangles(pixelatedSubRectanges) #移除无用色块
def removeMootColorRectangles(colorRectanges): #函数定义
pixelatedSubRectanges = []
for colorRectange in colorRectanges:
if colorRectange.color in [(0,0,0),(255,255,255)]: #如果颜色是纯白/纯黑,跳过该色块
continue
pixelatedSubRectanges.append(colorRectange) #将色块附加到pixelatedSubRectanges这个list中
return pixelatedSubRectanges
从这里看出,程序对无用色块(背景色)的判断逻辑是,只要是纯白/纯黑,就会跳过。这里得出两个推论:
接下来,从色块中找出每个方块的大小的出现次数:
def findRectangleSizeOccurences(colorRectanges):
rectangeSizeOccurences = {
}
for colorRectange in colorRectanges:
size = (colorRectange.width, colorRectange.height)
if size in rectangeSizeOccurences:
rectangeSizeOccurences[size] += 1
else:
rectangeSizeOccurences[size] = 1
return rectangeSizeOccurences
这里的方块大小,就是前面findRectangleMatches中对德布鲁因序列处理的方块大小。
接下来的处理逻辑就是对德布鲁因序列图片打码,再对各种色块进行匹配的流程,后续再进一步分析。
后续再次对去马赛克效果进行多次测试,发现该脚本的适用范围是有限的。
从测试结果和算法上来看,这个算法有如下的局限性。
事实上,在多次尝试后发现成功解码的仅有此文中这一次,且文中的例子也只能解出其中的几个数字。
但是不能因此就放松对个人隐私的保护。在发布图像时,建议使用多重马赛克/马赛克+涂抹等方式保护个人信息,进一步增加安全性。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/149162.html原文链接:https://javaforall.cn