您好,登录后才能下订单哦!
前言
说到幻影坦克,我就想起红色警戒里的……
幻影坦克(Mirage Tank),《红色警戒2》以及《尤里的复仇》中盟军的一款伪装坦克,盟军王牌坦克之一。是爱因斯坦在德国黑森林中研发的一种坦克。虽然它无法隐形,但它却可以利用先进的光线偏折原理可以伪装成树木(岩石或草丛)来隐藏自己。
在一些MOD中,幻影坦克可以选择变换的树木,这样便可以和背景的树木融合,而不会令人生疑。
额!这是从什么百科ctrl+v过来的吗。我跟你说个P~ UBG
不过话说回来,里面有一句说到和背景融合,这大概就是这种图片的原理所在了。
一些聊天软件或网站总是以白色背景和黑色背景(夜间模式)显示图片,你在默认的白色背景下看到一张图(图A),但是点击放大却变成另一张图(图B)。这是因为查看详情使用的背景是黑色背景。
之前在网上看到用PS制作幻影坦克效果图的方法,了解到几个图层混合模式的公式,也录制过PS动作来自动化操作。但总感觉不够效率,作为极客嘛,当然是要用代码来完成这些事情。
一、准备图片
创建一个文件夹Import,将你要处理的所有图片都放到这个文件夹里
图片的命名方式:
二、Python+PIL代码实现过程
Ⅰ. 初始化
注:脚本文件与 Import文件夹在同一目录
运行,导入模块,定义变量,创建导出目录Export,并将工作目录切换到Import
# -*- coding: utf-8 -*- # python 3.7.2 # 2019/04/21 by sryml. import os import math from timeit import timeit from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor from multiprocessing import cpu_count # import numba as nb import numpy as np from PIL import Image # --- IMPORT_FOLDER = 'Import' EXPORT_FOLDER = 'Export' IMAGE_FILES = [] # ALIGN2_A = 0 ALIGN2_B = 1 ALIGN2_MAX = 'max' NO_MODIFT = 0 STRETCH = 1 CONSTRAINT_RATIO = 2 # --- if __name__ == '__main__': if not os.path.exists(EXPORT_FOLDER): os.makedirs(EXPORT_FOLDER) os.chdir(IMPORT_FOLDER)
Ⅱ. 将所有要处理的图片文件添加到列表
执行all_img2list()
获取当前目录(Import)所有文件,按名字升序排序。将后缀带_d的图B与图A配对一组,白图到原图,原图到黑图的图片也进行相关标记并存到一个列表。每个元组将生成一张幻影坦克图片
def all_img2list(): global IMAGE_FILES IMAGE_FILES= [] Imgs = os.listdir('./') Imgs.sort(key= lambda i: os.path.splitext(i)[0]) for i in Imgs: name = os.path.splitext(i) imgB= name[0]+'_d' + name[1] if imgB in Imgs: Imgs.remove(imgB) img_group= (i,imgB) elif name[0][-6:].lower() == '_black': img_group= (i,'_black') else: img_group= (i,None) IMAGE_FILES.append(img_group)
Ⅲ. 自动化处理,多进程任务分配
执行AutoMTank()
不想让cpu满载运行,进程数量为cpu总核心减1,将列表里所有元组分成N等份集合的列表task_assign(N为进程数量)
def AutoMTank(): cpu = cpu_count()-1 pool = ProcessPoolExecutor(cpu) #max_workers=4 L = IMAGE_FILES F = int(len(L)/cpu) task_assign = [L[n*F:] if (n+1)==cpu else L[n*F:(n+1)*F] for n in range(cpu)] results = list(pool.map(FlashMakeMTank, task_assign)) pool.shutdown() print ('\n%d辆幻影坦克制作完成!' % len(IMAGE_FILES))
每个进程对接到的任务列表进行多线程处理:FlashMakeMTank
因为是图片算法处理,属于计算密集型,线程数量不需要太多。经过测试多线程还是有点效率提升的,线程数就设置为cpu核心数吧。
def FlashMakeMTank(task): pool = ThreadPoolExecutor(cpu_count()) results = list(pool.map(MakeMTank, task)) pool.shutdown()
Ⅳ. 盟军战车工厂
每个线程都将它接到的任务 - 图片组丢给我们的盟军战车工厂:MakeMTank 来生产幻影坦克
开头是打开图A和图B文件对象赋值给imgA和imgB,判断到那些想要白图到原图效果的图片,则在内存中生成一张纯白色的图片对象赋值给imgA。原图到黑图则生成纯黑色图片对象赋值给imgB
别以为这战车工厂看起来这么短,实际上算法都是通过调用函数获得返回结果,解释起来可有点费劲
def MakeMTank(i_group): ratios= [0,0] align= [] if not i_group[1]: imgB= Image.open(i_group[0]) imgA= Image.new('L',imgB.size,(255,)) elif i_group[1]=='_black': imgA= Image.open(i_group[0]) imgB= Image.new('L',imgA.size,(0,)) else: imgA= Image.open(i_group[0]) imgB= Image.open(i_group[1]) ratios= [0.5,-0.5] #明度比值 # ALIGN2_MAX(取最大的宽和最大的高) ALIGN2_A(缩放到图A) ALIGN2_B(缩放到图B) # NO_MODIFT(不修改) STRETCH(拉伸) CONSTRAINT_RATIO(约束比例) align= [ALIGN2_B, CONSTRAINT_RATIO] A_Size,B_Size= imgA.size,imgB.size img_objs= [imgA,imgB] for n,img in enumerate(img_objs): if img.mode== 'RGBA': img= img.convert('RGB') img_array= np.array(img) if img.mode != 'L' and ( [(img_array[:,:,i]==img_array[:,:,2]).all() for i in range(2)]!= [True,True] ): img= Desaturate(img_array) #去色 else: img= img.convert('L') if align and (A_Size!=B_Size): img= ImgAlign(n,img,A_Size,B_Size,align) #图像对齐 if ratios[n]: img= Lightness(img,ratios[n]) #明度 img_objs[n]= img imgA,imgB = img_objs imgA = Invert(imgA) #反相 imgO = LinearDodge(imgA, imgB) #线性减淡(添加) imgR = Divide(imgO, imgB) #划分 imgR_mask = AddMask(imgR, imgO) #添加透明蒙版 name= os.path.splitext(i_group[0])[0] imgR_mask.save('../'+EXPORT_FOLDER+'/' + name+'.png')
图片对象打开完成之后呢,把它们放到一个列表里遍历它进行操作
首先判断到图片模式是否为RGBA,最后的A表示这张图片是带有透明通道的。而我们的幻影坦克原理就是利用的透明通道,怎能让它来胡搅蛮缠呢,速速将它转换为RGB模式
接着将图像对象转为数组,判断这张图片如果不是灰度模式并且还没有去色的情况下,那就要对它进行去色操作了。
去完色的再将它转为灰度模式。
有些人可能对灰度和去色有什么误解,灰度 ≠ 去色,这是重点。虽然它们的结果都是灰色的图片,但是算法不一样,呈现的图片对比度也不一样,直接转成灰度的坦克是没有灵魂的。RGB图片直接转灰度会丢失一些细节,所以要对它进行去色操作。下面的操作都是仿照PS的步骤来处理了
(1) 去色函数:Desaturate
公式:( max(r,g,b) + min(r,g,b) ) / 2
每个像素取其RGB颜色中最大与最小值的均数
这个函数接受一个数组参数
例如某个像素RGB值(233,50,23),计算得出 (233+23) / 2 = 128,这时候此像素点三个通道都是同一个值(128,128,128)
这个算法过程消耗的性能较多,像一张1000*1000的图片就得进行一百万次计算,因此我使用了numba.jit加速。
对图片数组进行操作,使用argsort()将所有像素的RGB值从小到大排序并返回一个索引数组。
uint8类型的值的范围在0~255,若计算出的值不在这范围则会抛出溢出错误,因此使用了int。
我创建了一个灰度图片数组data,将每一个对应像素的均值赋值给它,相当于去色后再转为灰度模式。
最后返回由数组转换成的图片对象
@nb.jit def Desaturate(img_array): idx_array = img_array.argsort() width = img_array.shape[1] height = img_array.shape[0] data = np.zeros((height,width),dtype=np.uint8) for x in range(height): for y in range(width): idx= idx_array[x,y] color_min= img_array[x,y, idx[0]] color_max= img_array[x,y, idx[2]] data[x,y]= round( (int(color_min) + int(color_max)) / 2 ) return Image.fromarray(data)
(2) 图像对齐:ImgAlign
对齐方式(列表类型两个值)
对齐目标 | 缩放图像 | ||
---|---|---|---|
ALIGN2_MAX | 取最大的宽和最大的高 | NO_MODIFT | 不修改(缩小或仅画布) |
ALIGN2_A | 图A | STRETCH | 拉伸 |
ALIGN2_B | 图B | CONSTRAINT_RATIO | 约束比例 |
例如我要把图A对齐到图B且按比例缩放:mode = [ALIGN2_B, CONSTRAINT_RATIO]
这个函数接受5个参数
①当前图片序号(0代表图A,1代表图B)
②当前图片对象
③ - ④图A和图B的尺寸
⑤对齐方式
def ImgAlign(idx,img,A_Size,B_Size,mode): size= img.size old_size= (A_Size,B_Size) if mode[0]== ALIGN2_MAX: total_size= max(A_Size[0], B_Size[0]), max(A_Size[1], B_Size[1]) if size != total_size: if mode[1]== STRETCH: img= img.resize(total_size, Image.ANTIALIAS) else: new_img= Image.new('L',total_size, (255 if idx==0 else 0,)) diff= (total_size[0]-size[0],total_size[1]-size[1]) min_diff= min(diff[0],diff[1]) if min_diff != 0 and mode[1]: idx= diff.index(min_diff) scale= total_size[idx] / size[idx] resize= [total_size[idx], round(size[1-idx]*scale)] if idx: resize.reverse() img= img.resize(resize, Image.ANTIALIAS) new_img.paste(img, [(total_size[i]-img.size[i])//2 for i in range(2)]) img= new_img elif idx != mode[0]: total_size= old_size[mode[0]] if mode[1]== STRETCH: img= img.resize(total_size, Image.ANTIALIAS) else: new_img= Image.new('L',total_size, (255 if idx==0 else 0,)) diff= (total_size[0]-size[0],total_size[1]-size[1]) min_diff= min(diff[0],diff[1]) if (min_diff > 0 and mode[1]) or (min_diff < 0): idx= diff.index(min_diff) scale= total_size[idx] / size[idx] resize= [total_size[idx], round(size[1-idx]*scale)] if idx: resize.reverse() img= img.resize(resize, Image.ANTIALIAS) new_img.paste(img, [(total_size[i]-img.size[i])//2 for i in range(2)]) img= new_img return img
(3) 明度函数:Lightness
公式:255 * ratio + img * (1-ratio)
0 * ratio + img * (1-ratio)
为什么是两条公式呢,可以看到只有 255和 0的区别,一个是提高明度,一个是降低
注意,明度 ≠ 亮度,用亮度做出来的坦克是畸形的。亮度对颜色0和255不会起任何作用,任你怎么加亮度,我白是白,黑仍然是黑。这又涉及到幻影坦克效果的原理了,图A每个像素值必须大于图B对应的像素值,否则将没有透明度效果。
所以,最好的效果就是图A明度提高50%,图B降低50%
这个函数接受2个参数
①图片对象
②明度比值(-1~1)
尽量仿照PS的算法结果,提高明度的值为向下取整,降低明度为向上取整
def Lightness(img,ratio): if ratio>0: return img.point(lambda i: int(i*(1-ratio) + 255*ratio)) return img.point(lambda i: math.ceil(i*(1+ratio)))
实际上这是图层的不透明度混合公式,PS中,明度的实现就是在当前图层的上方创建一个白色或黑色图层,然后调整其透明度即可。所以,
明度调 100% 相当于白色图层的不透明度为100%,显示纯白
明度调 -100% 相当于黑色图层的不透明度为100%,显示纯黑。
看到这里,要暂停一下了。是不是感觉说了这么多都没有提到幻影坦克的详细原理,是的,只有当你理解了PS的不透明度混合公式,你才能理解后面的步骤。
(3-x) 重点!!推导幻影坦克的原理……
这里需要用到PS的几个图层混合模式
不透明度混合公式:Img输出 = Img上 * o + Img下 * (1 - o)
小字母o代表不透明度。想一想,把两张图片导入到PS,上面的图层命名为imgA,下面的图层为imgB。
当imgA的不透明度为100%(o=1)时,根据图层混合公式得到img输出=imgA,也就是完全显示上层图像。
当imgA的不透明度为0%(o=0)时,得到img输出=imgB,完全显示下层图像。
当不透明度为50%,自然就看到了A与B的混合图像。
但是我们要将这两张图给整进一张图里,然后在类似手机QQ这种只有白色背景和黑色背景的环境下,分别显示出imgA和imgB。听起来有点抽象,不要慌,我们来列方程。假设这张最终成果图为imgR
① ImgA = ImgR * o + 255 * (1 - o) | 白色背景下 |
② ImgB = ImgR * o + 0 * (1 - o) | 黑色背景下(点击放大后) |
这时候ImgR充当上图层(Img上)。它有一个固定不透明度o,或者说是它的图层蒙版(ImgO表示ImgR的蒙版),蒙版的像素值为0~255的单通道灰度色值。填充为黑色0相当于图层的不透明度为0%,填充为白色相当于图层不透明度为100%。那么这个固定不透明度 o 实际上就是 ⑨ o = ImgO / 255
而Img下就是聊天软件中的白色背景和黑色背景两种可能了。
现在来解一下方程,由②得:
ImgR = ImgB / o
将⑨ o = ImgO / 255 代入得
③ ImgR = ImgB / ImgO * 255
将③和⑨代入①得:
ImgA = (ImgB / ImgO * 255) * (ImgO / 255) + 255 * (1 - ImgO / 255)
ImgA = ImgB / ImgO * ImgO / 255 * 255 + 255 * (1 - ImgO / 255)
ImgA = ImgB + 2551 - 255(ImgO / 255)ImgA = ImgB + 255 - ImgO
④ ImgO = (255 - ImgA) + ImgB
那么现在,ImgB是我们已知的要在黑色背景下显示的图像,只要拿到ImgO就可以得出成品图ImgR了。
(255 - ImgA) 这个是什么意思,就是PS中的反相操作啦。让我们回到代码操作
(4) 反相函数:Invert
公式:255 - Img
即对每个像素进行 255-像素值
def Invert(img): return img.point(lambda i: 255-i)
反ImgA = Invert(ImgA )
然后这个反相后的ImgA(反ImgA)与ImgB相加,即PS中的线性减淡模式
(5) 线性减淡(添加):LinearDodge
公式:Img上 + Img下
def LinearDodge(imgA, imgB): size = imgA.size imgO = Image.new('L',size,(0,)) pxA= imgA.load() pxB= imgB.load() pxO= imgO.load() for x in range(size[0]): for y in range(size[1]): pxO[x,y] = (pxA[x,y]+pxB[x,y],) return imgO
至此得到 ImgO = LinearDodge(反ImgA, ImgB)
注:之前我们说过ImgA的所有像素值必须大于ImgB。如果小于或等于,那么反相后加自身(或加比自身大的值)就是255了。因为ImgO是成果图ImgR的透明蒙版,ImgO=255意味着不透明度为100%,就没有透明效果了。
接着看方程式子③ ImgR = ImgB / ImgO * 255,这便是PS的一种图层混合模式划分了
(6) 划分:Divide
公式:Img下 / Img上 * 255
几个注意的条件
①若混合色为黑色,基色非黑结果为白色、基色为黑结果为黑色(混合色是Img上,基色是Img下)
②若混合色为白色则结果为基色
③若混合色与基色相同则结果为白色
不妨可以在PS中一试便知真假
def Divide(imgO, imgB): size = imgB.size imgR = Image.new('L',size,(0,)) pxB= imgB.load() pxO= imgO.load() pxR= imgR.load() for x in range(size[0]): for y in range(size[1]): o=pxO[x,y] b=pxB[x,y] if o==0: #如混合色为黑色,基色非黑结果为白色、基色为黑结果为黑色 color= (b and 255 or 0,) elif o==255: #混合色为白色则结果为基色 color=(b,) elif o==b: #混合色与基色相同则结果为白色 color=(255,) else: color=(round((b/o)*255),) pxR[x,y] = color return imgR
调用划分函数ImgR = Divide(ImgO, ImgB),终于,我们得到了梦寐以求的成果图ImgR
但不要忘了它的不透明度,把ImgO添加为它的图层蒙版
(6) 最后:添加透明蒙版并保存
def AddMask(imgR,imgO): img = imgR.convert("RGBA") img.putalpha(imgO) return img
imgR_mask = AddMask(imgR, imgO)
name= os.path.splitext(i_group[0])[0] imgR_mask.save('../'+EXPORT_FOLDER+'/' + name+'.png')
保存在导出文件夹。。。
个人感觉
这个脚本生成的幻影坦克与PS做的相比就犹如真假美猴王一般,说到美猴王,我就想起……
三、完整代码文件
MirageTank.py
# -*- coding: utf-8 -*- # python 3.7.2 # 2019/04/21 by sryml. import os import math from timeit import timeit from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor from multiprocessing import cpu_count # import numba as nb import numpy as np from PIL import Image # --- IMPORT_FOLDER = 'Import' EXPORT_FOLDER = 'Export' IMAGE_FILES = [] # ALIGN2_A = 0 ALIGN2_B = 1 ALIGN2_MAX = 'max' NO_MODIFT = 0 STRETCH = 1 CONSTRAINT_RATIO = 2 # --- ### 图像对齐 def ImgAlign(idx,img,A_Size,B_Size,mode): size= img.size old_size= (A_Size,B_Size) if mode[0]== ALIGN2_MAX: total_size= max(A_Size[0], B_Size[0]), max(A_Size[1], B_Size[1]) if size != total_size: if mode[1]== STRETCH: img= img.resize(total_size, Image.ANTIALIAS) else: new_img= Image.new('L',total_size, (255 if idx==0 else 0,)) diff= (total_size[0]-size[0],total_size[1]-size[1]) min_diff= min(diff[0],diff[1]) if min_diff != 0 and mode[1]: idx= diff.index(min_diff) scale= total_size[idx] / size[idx] resize= [total_size[idx], round(size[1-idx]*scale)] if idx: resize.reverse() img= img.resize(resize, Image.ANTIALIAS) new_img.paste(img, [(total_size[i]-img.size[i])//2 for i in range(2)]) img= new_img elif idx != mode[0]: total_size= old_size[mode[0]] if mode[1]== STRETCH: img= img.resize(total_size, Image.ANTIALIAS) else: new_img= Image.new('L',total_size, (255 if idx==0 else 0,)) diff= (total_size[0]-size[0],total_size[1]-size[1]) min_diff= min(diff[0],diff[1]) if (min_diff > 0 and mode[1]) or (min_diff < 0): idx= diff.index(min_diff) scale= total_size[idx] / size[idx] resize= [total_size[idx], round(size[1-idx]*scale)] if idx: resize.reverse() img= img.resize(resize, Image.ANTIALIAS) new_img.paste(img, [(total_size[i]-img.size[i])//2 for i in range(2)]) img= new_img return img ### 去色 @nb.jit def Desaturate(img_array): idx_array = img_array.argsort() width = img_array.shape[1] height = img_array.shape[0] data = np.zeros((height,width),dtype=np.uint8) for x in range(height): for y in range(width): idx= idx_array[x,y] color_min= img_array[x,y, idx[0]] color_max= img_array[x,y, idx[2]] data[x,y]= round( (int(color_min) + int(color_max)) / 2 ) return Image.fromarray(data) ### 明度 def Lightness(img,ratio): if ratio>0: return img.point(lambda i: int(i*(1-ratio) + 255*ratio)) return img.point(lambda i: math.ceil(i*(1+ratio))) ### 反相 def Invert(img): return img.point(lambda i: 255-i) ### 线性减淡(添加) def LinearDodge(imgA, imgB): size = imgA.size imgO = Image.new('L',size,(0,)) pxA= imgA.load() pxB= imgB.load() pxO= imgO.load() for x in range(size[0]): for y in range(size[1]): pxO[x,y] = (pxA[x,y]+pxB[x,y],) return imgO ### 划分 def Divide(imgO, imgB): size = imgB.size imgR = Image.new('L',size,(0,)) pxB= imgB.load() pxO= imgO.load() pxR= imgR.load() for x in range(size[0]): for y in range(size[1]): o=pxO[x,y] b=pxB[x,y] if o==0: #如混合色为黑色,基色非黑结果为白色、基色为黑结果为黑色 color= (b and 255 or 0,) elif o==255: #混合色为白色则结果为基色 color=(b,) elif o==b: #混合色与基色相同则结果为白色 color=(255,) else: color=(round((b/o)*255),) pxR[x,y] = color return imgR def AddMask(imgR,imgO): img = imgR.convert("RGBA") img.putalpha(imgO) return img #### #### 将所有要处理的图片文件添加到列表 def all_img2list(): global IMAGE_FILES IMAGE_FILES= [] Imgs = os.listdir('./') Imgs.sort(key= lambda i: os.path.splitext(i)[0]) for i in Imgs: name = os.path.splitext(i) imgB= name[0]+'_d' + name[1] if imgB in Imgs: Imgs.remove(imgB) img_group= (i,imgB) elif name[0][-6:].lower() == '_black': img_group= (i,'_black') else: img_group= (i,None) IMAGE_FILES.append(img_group) def MakeMTank(i_group): ratios= [0,0] align= [] if not i_group[1]: imgB= Image.open(i_group[0]) imgA= Image.new('L',imgB.size,(255,)) elif i_group[1]=='_black': imgA= Image.open(i_group[0]) imgB= Image.new('L',imgA.size,(0,)) else: imgA= Image.open(i_group[0]) imgB= Image.open(i_group[1]) ratios= [0.5,-0.5] #明度比值 # ALIGN2_MAX(取最大的宽和最大的高) ALIGN2_A(缩放到图A) ALIGN2_B(缩放到图B) # NO_MODIFT(不修改) STRETCH(拉伸) CONSTRAINT_RATIO(约束比例) align= [ALIGN2_B, CONSTRAINT_RATIO] A_Size,B_Size= imgA.size,imgB.size img_objs= [imgA,imgB] for n,img in enumerate(img_objs): if img.mode== 'RGBA': img= img.convert('RGB') img_array= np.array(img) if img.mode != 'L' and ( [(img_array[:,:,i]==img_array[:,:,2]).all() for i in range(2)]!= [True,True] ): img= Desaturate(img_array) #去色 else: img= img.convert('L') if align and (A_Size!=B_Size): img= ImgAlign(n,img,A_Size,B_Size,align) #图像对齐 if ratios[n]: img= Lightness(img,ratios[n]) #明度 img_objs[n]= img imgA,imgB = img_objs imgA = Invert(imgA) #反相 imgO = LinearDodge(imgA, imgB) #线性减淡(添加) imgR = Divide(imgO, imgB) #划分 imgR_mask = AddMask(imgR, imgO) #添加透明蒙版 name= os.path.splitext(i_group[0])[0] imgR_mask.save('../'+EXPORT_FOLDER+'/' + name+'.png') def FlashMakeMTank(task): pool = ThreadPoolExecutor(cpu_count()) results = list(pool.map(MakeMTank, task)) pool.shutdown() def AutoMTank(): cpu = cpu_count()-1 pool = ProcessPoolExecutor(cpu) #max_workers=4 L = IMAGE_FILES F = int(len(L)/cpu) task_assign = [L[n*F:] if (n+1)==cpu else L[n*F:(n+1)*F] for n in range(cpu)] results = list(pool.map(FlashMakeMTank, task_assign)) pool.shutdown() print ('\n%d辆幻影坦克制作完成!' % len(IMAGE_FILES)) # --- def Fire(): all_img2list() sec = timeit(lambda:AutoMTank(),number=1) print ('Time used: {} sec'.format(sec)) s= input('\n按回车键退出...\n') if __name__ == '__main__': if not os.path.exists(EXPORT_FOLDER): os.makedirs(EXPORT_FOLDER) os.chdir(IMPORT_FOLDER) while True: s= input('>>> 按F进入坦克:') if s.upper()== 'F': print ('少女祈祷中...') Fire() #开炮 break elif not s: break
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对亿速云的支持。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。