目錄python
說到幻影坦克,我就想起紅色警惕裏的……ios
幻影坦克(Mirage Tank),《紅色警惕2》以及《尤里的復仇》中盟軍的一款假裝坦克,盟軍王牌坦克之一。是愛因斯坦在德國黑森林中研發的一種坦克。雖然它沒法隱形,但它卻能夠利用先進的光線偏折原理能夠假裝成樹木(岩石或草叢)來隱藏本身。
在一些MOD中,幻影坦克能夠選擇變換的樹木,這樣即可以和背景的樹木融合,而不會使人生疑。算法
額!這是從什麼百科ctrl+v過來的嗎。我跟你說個P~ UBG
不過話說回來,裏面有一句說到和背景融合,這大概就是這種圖片的原理所在了。
一些聊天軟件或網站老是以白色背景和黑色背景(夜間模式)顯示圖片,你在默認的白色背景下看到一張圖(圖A),可是點擊放大卻變成另外一張圖(圖B)。這是由於查看詳情使用的背景是黑色背景。數組
以前在網上看到用PS製做幻影坦克效果圖的方法,瞭解到幾個圖層混合模式的公式,也錄製過PS動做來自動化操做。但總感受不夠效率,做爲極客嘛,固然是要用代碼來完成這些事情。多線程
這個腳本生成的最終效果:
點擊放大查看,這類圖片使用手機QQ瀏覽效果最佳
app
Import
,將你要處理的全部圖片都放到這個文件夾裏_d
1.png
,圖B則爲1_d.png
,與之配對成爲一組便可_d
圖片,會自動生成白色圖片(圖A)_black
注:腳本文件與 Import
文件夾在同一目錄ide
運行,導入模塊,定義變量,建立導出目錄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
來生產幻影坦克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的步驟來處理了
Desaturate
例如某個像素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)
ImgAlign
對齊方式(列表類型兩個值)
對齊目標 | 縮放圖像 | ||
---|---|---|---|
ALIGN2_MAX | 取最大的寬和最大的高 | NO_MODIFT | 不修改(縮小或僅畫布) |
ALIGN2_A | 圖A | STRETCH | 拉伸 |
ALIGN2_B | 圖B | CONSTRAINT_RATIO | 約束比例 |
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
Lightness
這個函數接受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的不透明度混合公式
,你才能理解後面的步驟。
不透明度混合公式: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中的反相操做啦。讓咱們回到代碼操做
Invert
公式:255 - Img
即對每一個像素進行 255-像素值
def Invert(img): return img.point(lambda i: 255-i)
反ImgA = Invert(ImgA )
而後這個反相後的ImgA(反ImgA)與ImgB相加,即PS中的線性減淡模式
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的一種圖層混合模式劃分了
Divide
幾個注意的條件
①若混合色爲黑色,基色非黑結果爲白色、基色爲黑結果爲黑色(混合色是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添加爲它的圖層蒙版
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作的相比就猶如真假美猴王通常,說到美猴王,我就想起……
# -*- 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