2017/8/22

機器人駭客




我們為人人, 人人往往不見得為我們. 同理, 我們不駭人, 但不保證不會被駭, 所以應先了解一下事情到底是如何發生的. AI全面征服人類以前, 若機器人作壞事, 那肯定是人類自己幹的. (看完本篇讀者或許會了解到, 為甚麼各大入口網站的身分驗證機制會改用各種奇怪方式以防機器人的侵略)



進擊的機器人視覺
以中華電信的登入系統為例, 雖然也有設置防機器人的措施, 但其採取隨機字元混合雜訊的圖形驗證方式卻是屬於相對陽春的, 很可能駭客們會看不上眼吧!? (挑戰性




從上圖可以清楚了解到, 人類的視覺是多麼的優秀, 即使字體寫的歪七扭八又摻雜了許多背景雜訊, 人類依然能夠一眼辨識出來. 當然, 在背後每個人類從呀呀學語開始, 可都是其父母經年累月繳了無數學雜費所訓練出來的啊! 因此, 這麼簡單的工作要交給機器人(AI), 可就得花好一番工夫了.

在這網路資訊呈指數爆炸的時代, 無形之中我們都成了知識的遊牧民族, 雖然往往會有許多學習腳步跟不上科技進化速度的憂慮, 然而卻也處處有巨人肩膀可踩的方便. 上述的圖像式字元辨識的問題, 雖說須要一番功夫(若從頭到尾都自己來), 但卻都有現成的資源與工具可解. 當然, 筆者並不是要教大家如何成為駭客, 只希望大家騰個一杯咖啡的時間, 在享受咖啡之餘也順便了解駭客們大概是怎麼辦到的.



Python的影像處理工具
MatplotlibPython將數據圖形化使用最廣泛的套件之一, 但其實它還可以直接拿來做影像處理(事實上不須要外掛影像處理套件), 若搭配Spyder編輯器與IPython Console整體操作介面、執行效能與輸出效果等完全不輸給MATLAB. (Python語言甚至更容易學習與操作, 而且免費)




Matplotlib支援多種檔案格式的讀取, 搭配Urllib甚至還可以直接讀取URL圖檔做處理. 下面程式則示範了如何以urlopen分別讀取JPEGPNG的檔案格式, 並以pyplot直接顯示在IPython的控制台.

from urllib.request import urlopen
from matplotlib import pyplot as plt

url1='http://img06.tooopen.com/images/20170814/tooopen_sy_220422983139.jpg' # background
url2='http://d.lanrentuku.com/down/png/1607/bolt-icons/bolt_flash.png'           # bolt

img1=plt.imread(urlopen(url1),format='jpeg')
img2=plt.imread(urlopen(url2),format='png')

plt.imshow(img1)
plt.show()

plt.imshow(img2)
plt.show()



本例中, 圖像檔img1是一幅以JPEG格式儲存的寬廣草原風景圖, 而圖像檔img2是以PNG格式儲存的迪士尼明星--閃電波特. 事實上透過pyplot讀取進來的資料一律都被轉換為以row,col,layer記錄的矩陣格式, 這也是Python之所以優秀且可供我們之後近一步做影像資料處理非常方便的地方. (事實上都是矩陣運算)

若以object.shape指令細看矩陣的維度, 我們會發現以PNG記錄的原始資料維度比JPEG多了一層Layer(RGB之外又多了一層透明色的mask). 此外, 若近一步檢視PNG像素資料是以float32的格式記錄(而非JPEGunit8), 而這將使得我們無法直接將這兩張影像(矩陣)做相加運算.

img1.shape
Out: (779, 1024, 3)

img2.shape
Out: (256, 256, 4)

type(img1[0,0,0])
Out: numpy.uint8

type(img2[0,0,0])
Out: numpy.float32



這問題不難解決, 只要將原本以0~1浮點數(float32)記錄的PNG像素資料normalize0~255uint8記錄, 並只保留RGB三層影像平面資料(矩陣)即可. mask遮罩(波特影像的輪廓範圍)的影像資料()則單獨出來, 因為在疊回波特影像前我們須先將該範圍內的背景資料扣除.

# # reduce to RGB color layer with uint8
img3=(img2[:,:,0:3]*255).astype('uint8')
mask=(img2[:,:,3]*255).astype('uint8')
plt.imshow(img3)
plt.show()






 




解決兩影像資料(矩陣)像素格式與RGB維度的問題之後, 若我們近一步想要將波特跟草原背景影像結合(在草原奔跑的波特), 則會遭遇到第二個問題: 兩影像資料大小(矩陣維度)不同, 因此無法直接做運算(例如兩矩陣的加權相加)!

此時, 我們需要一些技巧. 假設我們想將波特放在背景圖片座標(ox,oy)的位置, 我們可以先在該座標位置以波特圖片的大小(維度)將部份背景圖片資料取出(roi). 因此, 該部份背景資料維度與波特圖片資料維度相同, 可以直接做矩陣運算. 在此之前, 我們先利用mask資料將範圍背景(roi)內跟波特重疊的部份移除.

# position bolt to the background image
oy=int(img1.shape[0]*2/3-img3.shape[0]/2) # bolt’s position related to the background image
ox=int(img1.shape[1]*1/2-img3.shape[1]/2) # bolt’s position related to the background image
rows,cols,channels=img3.shape # grab the bolt's shape
roi=img1[ox:rows+ox,oy:cols+oy] # region of background image

# mask the background image based on bolt's shape
for row in range(0,rows):
    for col in range(0,cols):
        if mask[row,col]>0:
            roi[row,col,0:3]=255




我們先將波特與局部背景(roi)相加之後, 再放回原背景圖片資料中, 即完成兩張不同影像大小(矩陣維度)的相加. 但這麼做非常偷懶, 因為我們沒先做任何柔邊或其他特效處理, 像素值直接相加的結果顯然不好! (這顯然沒辦法在迪士尼夢工場打工, 畢竟術業有專攻是吧!?)

# merge bolt and the region of background image
dst=roi+img3

# put the merged image (matrix) back
img1[ox:rows+ox,oy:cols+oy]=dst
plt.imshow(img1)






若覺得主角太過渺小, 下面有個技巧可以將矩陣資料做內插法縮放到指定的維度, 例如我們利用Scipy套件把波特的資料維度放大兩倍.

# size up the bolt image
from scipy import misc
scale=2.0
rows,cols,channels=img2.shape # grab the bolt's image shape
img2=misc.imresize(img2,[int(rows*scale),int(cols*scale)])
plt.imshow(img2)
plt.show()



我們重複一次取出範圍影像(roi)、影像遮罩(mask)運算、影像相加並將範圍影像放回等步驟, 可以得到如下圖將下面波特被放大兩倍的影像合成結果.






文字辨識系統 使用Tesseract
Tesseract原本是HP公司所研發的文字辨識系統, 後來交由Google維護, 目前算是相當強大且支援多國語言文字辨識的開源工具. 筆者安裝的是Tesseract 4.0 for Windows版本. Tesseract支援多國的字元辨識, 但預設並不包含支援對中文繁體字體識別的訓練檔案, 請記得勾選Additional language data/Chinese (Traditional), 系統會自行加載該語言的訓練檔案.





Windows7為例, 安裝完成後請記得將Tesseract的安裝路徑加到PATH的環境變數. 可以透過檔案總管, 選擇我的電腦>按滑鼠右鍵>內容>選擇進階設定>進階>環境變數>選擇PATH>編輯, 並將Tesseract的安裝路徑的字串接在最後即可.



我們可以先在命令列中下指令檢視環境是否已包含對中文繁體字元辨識的支援(chi_tra).

> Tesseract –list-langs



只須在命令列中執行如下指令, 辨識完成的內容會轉成文字檔儲存在result.txt. 經筆者自己影像擷取一些文字測試結果來看, 數字與英文字元(例如以圖檔story_eng.jpg儲存)的辨識率還蠻高的. (當然連人類自己都會混淆的地方就不用太追究了)

> Tesseract story_eng.jpg result



也可以透過參數(-l langs)指定其它國家的文字進行識別, 只須在命令列中執行如下指令, 而辨識完成的內容會轉成文字檔儲存在result.txt. 繁體中文字元(例如以圖檔story_chi.jpg儲存)的辨識率還算OK, 印刷字體仍有90%以上成功率.

> Tesseract story_chi.jpg result –l chi_tra




機器人駭客使用Python+Tesseract
在文章開頭提到關於某些網站採取圖形驗證的機制, 接下來讓我們了解一下何以Python程式與Tesseract工具可輕易的擔任機器人角色並執行自動驗證與登入的工作.

在前面範例中我們大致介紹了基於Python程式的影像處理原理, 而下面這段程式碼則是進一步用來破解某些使用隨機字元混合雜訊干擾的圖片驗證機制. 其背後運作的機制為: 讀檔、資料轉成灰階影像、影像二值化、去除雜訊, 最後再呼叫Tesseract作字元辨識的工作.

import subprocess, os, re
from matplotlib import pyplot as plt

# convert to gray
def convert_to_gray(img):
    out=img[:,:,0]*0.299+img[:,:,1]*0.587+img[:,:,2]*0.114
    return out

# binary thresold
def convert_to_binary(img,threshold):
    rows,cols=img.shape
    out=img
    for row in range(0,rows):
        for col in range(0,cols):
            if img[row,col]>threshold:
                out[row,col]=0
            else:
                out[row,col]=255
    return out

# noise mitigation
def mitigate_noise(img):
    rows,cols=img.shape
    out=img
    for row in range(0,rows):
        for col in range(0,cols):
            cnt=0
            for i in range(-3,4):
                for j in range(-3,4):
                    try:
                        if img[row+i,col+j]==0:
                            cnt+=1
                    except IndexError:
                        pass
            if img[row,col]==0:
                if cnt<=5:
                    out[row,col]=255 # remive noise
            else:
                if cnt>22:
                    out[row,col]=0 # enhancement
    return out

#############################################
# original image
ocrPath='C:\\Users\\images\\ocr\\'
jpg=ocrPath+'hinet.jpg' # original image for confirmation
img=plt.imread(jpg)
plt.set_cmap('gray')
plt.imshow(img)
plt.show()

# convert to gray
img2=convert_to_gray(img)
plt.imshow(img2)
plt.show()

# convert to binary
img3=convert_to_binary(img2,100)
plt.imshow(img3)
plt.show()

# imtigate noise
img4=mitigate_noise(img3)
plt.imshow(img4)
plt.show()

# perform OCR
ocrImgFile=ocrPath+'ocr_test.jpg'
plt.imsave(ocrImgFile,img4)
os.chdir(ocrPath)
ocr=subprocess.Popen('tesseract '+ocrImgFile+" result")
ocr.wait()

text=open("result.txt",encoding="utf-8-sig").read()
text=re.sub(r'[.\n ]+',"",text)
print("驗證碼為:"+text)




我們知道Python有許多強大的影像處理套件, 但為讓大家了解底層工作原理該程式並不呼叫它們. 首先, RGB彩色影像轉灰階的過程, 我們透過課本的公式直接做矩陣運算即可, 並把它寫成函式convert_to_gray, 其中

灰階值=R*0.299+G*0.587+B*0.114

接下來函式convert_to_binary逐一掃描影像內容, 若灰階值大於設定的臨界值則設為黑(0), 其餘則設為白(255).

函式mitigate_noise則進一步逐一掃描影像像素(含目標像素)與週邊共7x7範圍內的格子點, 若目標像素為黑(0), 我們做雜訊過濾與消除, 即範圍內的黑色格子點數超過某個臨界值才予以保留, 其它則視為雜訊並將其消除. 反之, 若目標像素為白(255), 我們做影像補強, 即當範圍內的黑色格子點數超過某個臨界值時將目標像素改為黑(0), 以彌補字元因雜訊造成的斷線.




下圖是以Spyder編輯器與IPython控制台來展示執行的結果, 包含每個步驟所產生的中間圖案, 最後透過subprocess交由Tesseract執行字元辨識並將結果(文字檔)讀回來, 其中我們移除一些多餘的非數字字元.




筆者也測了幾次不同的登入圖片, 自動取得驗證碼的執行結果都成功(正確).




可以成功辨識(移除多餘的非數字字元): 5312





可以成功辨識: 8824


最後, 我們只要將所有工作再交由Selenium套件來執行瀏覽器上的各項操作即可達成機器人自動驗證並登入的任務了! (包括所有的URL登入、抓圖、鍵盤輸入、滑鼠敲擊等使用者動作) 但在Google Chrome瀏覽器操作必須先安裝相關的WebDriver驅動程式, 當然這也幫Google間接開了一扇後門, 因此筆者就此停住了!



這不是AI!?
. 若把AI比喻為登陸火星, 我們現在大概知道玩紙飛機將受制於風, 趕緊上網買台帶陀螺儀的遙控飛機來玩, 掌控性更足!

資訊遊民有跟不上知識進化步伐的憂慮, 卻也處處有巨人肩膀可踩的方便. 學會站在巨人的肩膀上看遠一點, 順便看看前方有沒有新的機會是吧?