來(lái)源 https://zhuanlan.zhihu.com/p/24425116
給深度學(xué)習(xí)入門(mén)者的Python快速教程 - numpy和Matplotlib篇
的番外篇,因?yàn)閲?yán)格來(lái)說(shuō)不是在講Python而是講在Python下使用OpenCV皆怕。本篇將介紹和深度學(xué)習(xí)數(shù)據(jù)處理階段最相關(guān)的基礎(chǔ)使用而线,并完成4個(gè)有趣實(shí)用的小例子:
- 延時(shí)攝影小程序
- 視頻中截屏采樣的小程序
- 圖片數(shù)據(jù)增加(data augmentation)的小工具
- 物體檢測(cè)框標(biāo)注小工具
其中后兩個(gè)例子的代碼可以在下面地址直接下載:
frombeijingwithlove/dlcv_for_beginners
6.1 OpenCV簡(jiǎn)介
OpenCV是計(jì)算機(jī)視覺(jué)領(lǐng)域應(yīng)用最廣泛的開(kāi)源工具包,基于C/C++汇竭,支持Linux/Windows/MacOS/Android/iOS,并提供了Python,Matlab和Java等語(yǔ)言的接口觉吭,因?yàn)槠湄S富的接口,優(yōu)秀的性能和商業(yè)友好的使用許可仆邓,不管是學(xué)術(shù)界還是業(yè)界中都非常受歡迎鲜滩。OpenCV最早源于Intel公司1998年的一個(gè)研究項(xiàng)目,當(dāng)時(shí)在Intel從事計(jì)算機(jī)視覺(jué)的工程師蓋瑞·布拉德斯基(Gary Bradski)訪問(wèn)一些大學(xué)和研究組時(shí)發(fā)現(xiàn)學(xué)生之間實(shí)現(xiàn)計(jì)算機(jī)視覺(jué)算法用的都是各自實(shí)驗(yàn)室里的內(nèi)部代碼或者庫(kù)节值,這樣新來(lái)實(shí)驗(yàn)室的學(xué)生就能基于前人寫(xiě)的基本函數(shù)快速上手進(jìn)行研究徙硅。于是OpenCV旨在提供一個(gè)用于計(jì)算機(jī)視覺(jué)的科研和商業(yè)應(yīng)用的高性能通用庫(kù)。 第一個(gè)alpha版本的OpenCV于2000年的CVPR上發(fā)布搞疗,在接下來(lái)的5年里嗓蘑,又陸續(xù)發(fā)布了5個(gè)beta版本,2006年發(fā)布了第一個(gè)正式版贴汪。2009年隨著蓋瑞加入了Willow Garage,OpenCV從Willow Garage得到了積極的支持休吠,并發(fā)布了1.1版扳埂。2010年OpenCV發(fā)布了2.0版本,添加了非常完備的C++接口瘤礁,從2.0開(kāi)始的版本非常用戶非常龐大阳懂,至今仍在維護(hù)和更新。2015年OpenCV 3正式發(fā)布柜思,除了架構(gòu)的調(diào)整岩调,還加入了更多算法,更多性能的優(yōu)化和更加簡(jiǎn)潔的API赡盘,另外也加強(qiáng)了對(duì)GPU的支持号枕,現(xiàn)在已經(jīng)在許多研究機(jī)構(gòu)和商業(yè)公司中應(yīng)用開(kāi)來(lái)。
6.1.1 OpenCV的結(jié)構(gòu)
和Python一樣陨享,當(dāng)前的OpenCV也有兩個(gè)大版本葱淳,OpenCV2和OpenCV3。相比OpenCV2抛姑,OpenCV3提供了更強(qiáng)的功能和更多方便的特性赞厕。不過(guò)考慮到和深度學(xué)習(xí)框架的兼容性,以及上手安裝的難度定硝,這部分先以2為主進(jìn)行介紹皿桑。
根據(jù)功能和需求的不同,OpenCV中的函數(shù)接口大體可以分為如下部分:
- core:核心模塊,主要包含了OpenCV中最基本的結(jié)構(gòu)(矩陣诲侮,點(diǎn)線和形狀等)镀虐,以及相關(guān)的基礎(chǔ)運(yùn)算/操作。
- imgproc:圖像處理模塊浆西,包含和圖像相關(guān)的基礎(chǔ)功能(濾波粉私,梯度,改變大小等)近零,以及一些衍生的高級(jí)功能(圖像分割诺核,直方圖,形態(tài)分析和邊緣/直線提取等)久信。
- highgui:提供了用戶界面和文件讀取的基本函數(shù)窖杀,比如圖像顯示窗口的生成和控制,圖像/視頻文件的IO等裙士。
如果不考慮視頻應(yīng)用入客,以上三個(gè)就是最核心和常用的模塊了。針對(duì)視頻和一些特別的視覺(jué)應(yīng)用腿椎,OpenCV也提供了強(qiáng)勁的支持:
- video:用于視頻分析的常用功能桌硫,比如光流法(Optical Flow)和目標(biāo)跟蹤等。
- calib3d:三維重建啃炸,立體視覺(jué)和相機(jī)標(biāo)定等的相關(guān)功能铆隘。
- features2d:二維特征相關(guān)的功能,主要是一些不受專(zhuān)利保護(hù)的南用,商業(yè)友好的特征點(diǎn)檢測(cè)和匹配等功能膀钠,比如ORB特征。
- object:目標(biāo)檢測(cè)模塊裹虫,包含級(jí)聯(lián)分類(lèi)和Latent SVM
- ml:機(jī)器學(xué)習(xí)算法模塊肿嘲,包含一些視覺(jué)中最常用的傳統(tǒng)機(jī)器學(xué)習(xí)算法。
- flann:最近鄰算法庫(kù)筑公,F(xiàn)ast Library for Approximate Nearest Neighbors雳窟,用于在多維空間進(jìn)行聚類(lèi)和檢索,經(jīng)常和關(guān)鍵點(diǎn)匹配搭配使用匣屡。
- gpu:包含了一些gpu加速的接口涩拙,底層的加速是CUDA實(shí)現(xiàn)。
- photo:計(jì)算攝像學(xué)(Computational Photography)相關(guān)的接口耸采,當(dāng)然這只是個(gè)名字兴泥,其實(shí)只有圖像修復(fù)和降噪而已。
- stitching:圖像拼接模塊虾宇,有了它可以自己生成全景照片搓彻。
- nonfree:受到專(zhuān)利保護(hù)的一些算法,其實(shí)就是SIFT和SURF。
- contrib:一些實(shí)驗(yàn)性質(zhì)的算法旭贬,考慮在未來(lái)版本中加入的怔接。
- legacy:字面是遺產(chǎn),意思就是廢棄的一些接口稀轨,保留是考慮到向下兼容扼脐。
- ocl:利用OpenCL并行加速的一些接口。
- superres:超分辨率模塊奋刽,其實(shí)就是BTV-L1(Biliteral Total Variation – L1 regularization)算法
- viz:基礎(chǔ)的3D渲染模塊瓦侮,其實(shí)底層就是著名的3D工具包VTK(Visualization Toolkit)。
從使用的角度來(lái)看佣谐,和OpenCV2相比肚吏,OpenCV3的主要變化是更多的功能和更細(xì)化的模塊劃分。
6.1.2 安裝和使用OpenCV
作為最流行的視覺(jué)包狭魂,在Linux中安裝OpenCV是非常方便的罚攀,大多數(shù)Linux的發(fā)行版都支持包管理器的安裝,比如在Ubuntu 16.04 LTS中雌澄,只需要在終端中輸入:
>> sudo apt install libopencv-dev python-opencv
當(dāng)然也可以通過(guò)官網(wǎng)下載源碼編譯安裝斋泄,第一步先安裝各種依賴:
>> sudo apt install build-essential
>> sudo apt install cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev
>> sudo apt-get install python-dev python-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev libjasper-dev libdc1394-22-dev
然后找一個(gè)clone壓縮包的文件夾,把源碼拿下來(lái):
>> git cloneopencv/opencv
然后進(jìn)入OpenCV文件夾:
>> mkdir release
>> cd release
>> cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local ..
準(zhǔn)備完畢镐牺,直接make并安裝:
>> make
>> sudo make install
Windows下的安裝也很簡(jiǎn)單炫掐,直接去OpenCV官網(wǎng)下載:
執(zhí)行exe安裝后,會(huì)在<安裝目錄>/build/python/2.7下發(fā)現(xiàn)一個(gè)叫cv2.pyd的文件任柜,把這個(gè)文件拷貝到\Lib\site-packages下卒废,就可以了沛厨。Windows下如果只想在Python中體驗(yàn)OpenCV還有個(gè)更簡(jiǎn)單的方法是加州大學(xué)爾灣分校(University of California, Irvine)的Christoph Gohlke制作的Windows下的Python科學(xué)計(jì)算包網(wǎng)頁(yè)宙地,下載對(duì)應(yīng)版本的wheel文件,然后通過(guò)pip安裝:
http://www.lfd.uci.edu/~gohlke/pythonlibs/#opencv
本書(shū)只講Python下OpenCV基本使用逆皮,Python中導(dǎo)入OpenCV非常簡(jiǎn)單:
importcv2
就導(dǎo)入成功了宅粥。
6.2 Python-OpenCV基礎(chǔ)
6.2.1 圖像的表示
前面章節(jié)已經(jīng)提到過(guò)了單通道的灰度圖像在計(jì)算機(jī)中的表示,就是一個(gè)8位無(wú)符號(hào)整形的矩陣电谣。在OpenCV的C++代碼中秽梅,表示圖像有個(gè)專(zhuān)門(mén)的結(jié)構(gòu)叫做cv::Mat,不過(guò)在Python-OpenCV中剿牺,因?yàn)橐呀?jīng)有了numpy這種強(qiáng)大的基礎(chǔ)工具企垦,所以這個(gè)矩陣就用numpy的array表示。如果是多通道情況晒来,最常見(jiàn)的就是紅綠藍(lán)(RGB)三通道钞诡,則第一個(gè)維度是高度,第二個(gè)維度是高度,第三個(gè)維度是通道荧降,比如圖6-1a是一幅3×3圖像在計(jì)算機(jī)中表示的例子:
圖6-1 RGB圖像在計(jì)算機(jī)中表示的例子
圖6-1中接箫,右上角的矩陣?yán)锩總€(gè)元素都是一個(gè)3維數(shù)組,分別代表這個(gè)像素上的三個(gè)通道的值朵诫。最常見(jiàn)的RGB通道中辛友,第一個(gè)元素就是紅色(Red)的值,第二個(gè)元素是綠色(Green)的值剪返,第三個(gè)元素是藍(lán)色(Blue)废累,最終得到的圖像如6-1a所示。RGB是最常見(jiàn)的情況随夸,然而在OpenCV中九默,默認(rèn)的圖像的表示確實(shí)反過(guò)來(lái)的,也就是BGR宾毒,得到的圖像是6-1b驼修。可以看到诈铛,前兩行的顏色順序都交換了乙各,最后一行是三個(gè)通道等值的灰度圖,所以沒(méi)有影響幢竹。至于OpenCV為什么不是人民群眾喜聞樂(lè)見(jiàn)的RGB耳峦,這是歷史遺留問(wèn)題,在OpenCV剛開(kāi)始研發(fā)的年代焕毫,BGR是相機(jī)設(shè)備廠商的主流表示方法蹲坷,雖然后來(lái)RGB成了主流和默認(rèn),但是這個(gè)底層的順序卻保留下來(lái)了邑飒,事實(shí)上Windows下的最常見(jiàn)格式之一bmp循签,底層字節(jié)的存儲(chǔ)順序還是BGR。OpenCV的這個(gè)特殊之處還是需要注意的疙咸,比如在Python中县匠,圖像都是用numpy的array表示,但是同樣的array在OpenCV中的顯示效果和matplotlib中的顯示效果就會(huì)不一樣撒轮。下面的簡(jiǎn)單代碼就可以生成兩種表示方式下乞旦,圖6-1中矩陣的對(duì)應(yīng)的圖像,生成圖像后题山,放大看就能體會(huì)到區(qū)別:
importnumpyasnpimportcv2importmatplotlib.pyplotasplt# 圖6-1中的矩陣img=np.array([[[255,0,0],[0,255,0],[0,0,255]],[[255,255,0],[255,0,255],[0,255,255]],[[255,255,255],[128,128,128],[0,0,0]],],dtype=np.uint8)# 用matplotlib存儲(chǔ)plt.imsave('img_pyplot.jpg',img)# 用OpenCV存儲(chǔ)cv2.imwrite('img_cv2.jpg',img)
不管是RGB還是BGR兰粉,都是高度×寬度×通道數(shù),H×W×C的表達(dá)方式顶瞳,而在深度學(xué)習(xí)中玖姑,因?yàn)橐獙?duì)不同通道應(yīng)用卷積崖蜜,所以用的是另一種方式:C×H×W,就是把每個(gè)通道都單獨(dú)表達(dá)成一個(gè)二維矩陣客峭,如圖6-1c所示豫领。
6.2.2 基本圖像處理
存取圖像
讀圖像用cv2.imread(),可以按照不同模式讀取舔琅,一般最常用到的是讀取單通道灰度圖等恐,或者直接默認(rèn)讀取多通道。存圖像用cv2.imwrite()备蚓,注意存的時(shí)候是沒(méi)有單通道這一說(shuō)的课蔬,根據(jù)保存文件名的后綴和當(dāng)前的array維度,OpenCV自動(dòng)判斷存的通道郊尝,另外壓縮格式還可以指定存儲(chǔ)質(zhì)量二跋,來(lái)看代碼例子:
importcv2# 讀取一張400x600分辨率的圖像color_img=cv2.imread('test_400x600.jpg')print(color_img.shape)# 直接讀取單通道gray_img=cv2.imread('test_400x600.jpg',cv2.IMREAD_GRAYSCALE)print(gray_img.shape)# 把單通道圖片保存后,再讀取流昏,仍然是3通道扎即,相當(dāng)于把單通道值復(fù)制到3個(gè)通道保存cv2.imwrite('test_grayscale.jpg',gray_img)reload_grayscale=cv2.imread('test_grayscale.jpg')print(reload_grayscale.shape)# cv2.IMWRITE_JPEG_QUALITY指定jpg質(zhì)量,范圍0到100况凉,默認(rèn)95谚鄙,越高畫(huà)質(zhì)越好,文件越大cv2.imwrite('test_imwrite.jpg',color_img,(cv2.IMWRITE_JPEG_QUALITY,80))# cv2.IMWRITE_PNG_COMPRESSION指定png質(zhì)量刁绒,范圍0到9闷营,默認(rèn)3,越高文件越小知市,畫(huà)質(zhì)越差cv2.imwrite('test_imwrite.png',color_img,(cv2.IMWRITE_PNG_COMPRESSION,5))
縮放傻盟,裁剪和補(bǔ)邊
縮放通過(guò)cv2.resize()實(shí)現(xiàn),裁剪則是利用array自身的下標(biāo)截取實(shí)現(xiàn)嫂丙,此外OpenCV還可以給圖像補(bǔ)邊娘赴,這樣能對(duì)一幅圖像的形狀和感興趣區(qū)域?qū)崿F(xiàn)各種操作。下面的例子中讀取一幅400×600分辨率的圖片奢入,并執(zhí)行一些基礎(chǔ)的操作:
importcv2# 讀取一張四川大錄古藏寨的照片img=cv2.imread('tiger_tibet_village.jpg')# 縮放成200x200的方形圖像img_200x200=cv2.resize(img,(200,200))# 不直接指定縮放后大小筝闹,通過(guò)fx和fy指定縮放比例媳叨,0.5則長(zhǎng)寬都為原來(lái)一半# 等效于img_200x300 = cv2.resize(img, (300, 200))腥光,注意指定大小的格式是(寬度,高度)# 插值方法默認(rèn)是cv2.INTER_LINEAR,這里指定為最近鄰插值img_200x300=cv2.resize(img,(0,0),fx=0.5,fy=0.5,interpolation=cv2.INTER_NEAREST)# 在上張圖片的基礎(chǔ)上糊秆,上下各貼50像素的黑邊武福,生成300x300的圖像img_300x300=cv2.copyMakeBorder(img,50,50,0,0,cv2.BORDER_CONSTANT,value=(0,0,0))# 對(duì)照片中樹(shù)的部分進(jìn)行剪裁patch_tree=img[20:150,-180:-50]cv2.imwrite('cropped_tree.jpg',patch_tree)cv2.imwrite('resized_200x200.jpg',img_200x200)cv2.imwrite('resized_200x300.jpg',img_200x300)cv2.imwrite('bordered_300x300.jpg',img_300x300)
這些處理的效果見(jiàn)圖6-2。
色調(diào)痘番,明暗捉片,直方圖和Gamma曲線
除了區(qū)域平痰,圖像本身的屬性操作也非常多,比如可以通過(guò)HSV空間對(duì)色調(diào)和明暗進(jìn)行調(diào)節(jié)伍纫。HSV空間是由美國(guó)的圖形學(xué)專(zhuān)家A. R. Smith提出的一種顏色空間宗雇,HSV分別是色調(diào)(Hue),飽和度(Saturation)和明度(Value)莹规。在HSV空間中進(jìn)行調(diào)節(jié)就避免了直接在RGB空間中調(diào)節(jié)是還需要考慮三個(gè)通道的相關(guān)性赔蒲。OpenCV中H的取值是[0, 180),其他兩個(gè)通道的取值都是[0, 256)良漱,下面例子接著上面例子代碼舞虱,通過(guò)HSV空間對(duì)圖像進(jìn)行調(diào)整:
# 通過(guò)cv2.cvtColor把圖像從BGR轉(zhuǎn)換到HSVimg_hsv=cv2.cvtColor(img,cv2.COLOR_BGR2HSV)# H空間中,綠色比黃色的值高一點(diǎn)母市,所以給每個(gè)像素+15矾兜,黃色的樹(shù)葉就會(huì)變綠turn_green_hsv=img_hsv.copy()turn_green_hsv[:,:,0]=(turn_green_hsv[:,:,0]+15)%180turn_green_img=cv2.cvtColor(turn_green_hsv,cv2.COLOR_HSV2BGR)cv2.imwrite('turn_green.jpg',turn_green_img)# 減小飽和度會(huì)讓圖像損失鮮艷,變得更灰colorless_hsv=img_hsv.copy()colorless_hsv[:,:,1]=0.5*colorless_hsv[:,:,1]colorless_img=cv2.cvtColor(colorless_hsv,cv2.COLOR_HSV2BGR)cv2.imwrite('colorless.jpg',colorless_img)# 減小明度為原來(lái)一半darker_hsv=img_hsv.copy()darker_hsv[:,:,2]=0.5*darker_hsv[:,:,2]darker_img=cv2.cvtColor(darker_hsv,cv2.COLOR_HSV2BGR)cv2.imwrite('darker.jpg',darker_img)
無(wú)論是HSV還是RGB患久,我們都較難一眼就對(duì)像素中值的分布有細(xì)致的了解椅寺,這時(shí)候就需要直方圖。如果直方圖中的成分過(guò)于靠近0或者255蒋失,可能就出現(xiàn)了暗部細(xì)節(jié)不足或者亮部細(xì)節(jié)丟失的情況配并。比如圖6-2中,背景里的暗部細(xì)節(jié)是非常弱的高镐。這個(gè)時(shí)候溉旋,一個(gè)常用方法是考慮用Gamma變換來(lái)提升暗部細(xì)節(jié)。Gamma變換是矯正相機(jī)直接成像和人眼感受圖像差別的一種常用手段嫉髓,簡(jiǎn)單來(lái)說(shuō)就是通過(guò)非線性變換讓圖像從對(duì)曝光強(qiáng)度的線性響應(yīng)變得更接近人眼感受到的響應(yīng)观腊。具體的定義和實(shí)現(xiàn),還是接著上面代碼中讀取的圖片算行,執(zhí)行計(jì)算直方圖和Gamma變換的代碼如下:
importnumpyasnp# 分通道計(jì)算每個(gè)通道的直方圖hist_b=cv2.calcHist([img],[0],None,[256],[0,256])hist_g=cv2.calcHist([img],[1],None,[256],[0,256])hist_r=cv2.calcHist([img],[2],None,[256],[0,256])# 定義Gamma矯正的函數(shù)defgamma_trans(img,gamma):# 具體做法是先歸一化到1梧油,然后gamma作為指數(shù)值求出新的像素值再還原gamma_table=[np.power(x/255.0,gamma)*255.0forxinrange(256)]gamma_table=np.round(np.array(gamma_table)).astype(np.uint8)# 實(shí)現(xiàn)這個(gè)映射用的是OpenCV的查表函數(shù)returncv2.LUT(img,gamma_table)# 執(zhí)行Gamma矯正,小于1的值讓暗部細(xì)節(jié)大量提升州邢,同時(shí)亮部細(xì)節(jié)少量提升img_corrected=gamma_trans(img,0.5)cv2.imwrite('gamma_corrected.jpg',img_corrected)# 分通道計(jì)算Gamma矯正后的直方圖hist_b_corrected=cv2.calcHist([img_corrected],[0],None,[256],[0,256])hist_g_corrected=cv2.calcHist([img_corrected],[1],None,[256],[0,256])hist_r_corrected=cv2.calcHist([img_corrected],[2],None,[256],[0,256])# 將直方圖進(jìn)行可視化importmatplotlib.pyplotaspltfrommpl_toolkits.mplot3dimportAxes3Dfig=plt.figure()pix_hists=[[hist_b,hist_g,hist_r],[hist_b_corrected,hist_g_corrected,hist_r_corrected]]pix_vals=range(256)forsub_plt,pix_histinzip([121,122],pix_hists):ax=fig.add_subplot(sub_plt,projection='3d')forc,z,channel_histinzip(['b','g','r'],[20,10,0],pix_hist):cs=[c]*256ax.bar(pix_vals,channel_hist,zs=z,zdir='y',color=cs,alpha=0.618,edgecolor='none',lw=0)ax.set_xlabel('Pixel Values')ax.set_xlim([0,256])ax.set_ylabel('Channels')ax.set_zlabel('Counts')plt.show()
上面三段代碼的結(jié)果統(tǒng)一放在下圖中:
可以看到儡陨,Gamma變換后的暗部細(xì)節(jié)比起原圖清楚了很多,并且從直方圖來(lái)看量淌,像素值也從集中在0附近變得散開(kāi)了一些骗村。
6.2.3 圖像的仿射變換
圖像的仿射變換涉及到圖像的形狀位置角度的變化,是深度學(xué)習(xí)預(yù)處理中常到的功能呀枢,在此簡(jiǎn)單回顧一下胚股。仿射變換具體到圖像中的應(yīng)用,主要是對(duì)圖像的縮放裙秋,旋轉(zhuǎn)琅拌,剪切缨伊,翻轉(zhuǎn)和平移的組合。在OpenCV中进宝,仿射變換的矩陣是一個(gè)2×3的矩陣刻坊,其中左邊的2×2子矩陣是線性變換矩陣,右邊的2×1的兩項(xiàng)是平移項(xiàng):
對(duì)于圖像上的任一位置(x,y)党晋,仿射變換執(zhí)行的是如下的操作:
需要注意的是紧唱,對(duì)于圖像而言,寬度方向是x隶校,高度方向是y漏益,坐標(biāo)的順序和圖像像素對(duì)應(yīng)下標(biāo)一致。所以原點(diǎn)的位置不是左下角而是右上角深胳,y的方向也不是向上绰疤,而是向下。在OpenCV中實(shí)現(xiàn)仿射變換是通過(guò)仿射變換矩陣和cv2.warpAffine()這個(gè)函數(shù)舞终,還是通過(guò)代碼來(lái)理解一下轻庆,例子中圖片的分辨率為600×400:
importcv2importnumpyasnp# 讀取一張斯里蘭卡拍攝的大象照片img=cv2.imread('lanka_safari.jpg')# 沿著橫縱軸放大1.6倍,然后平移(-150,-240)敛劝,最后沿原圖大小截取余爆,等效于裁剪并放大M_crop_elephant=np.array([[1.6,0,-150],[0,1.6,-240]],dtype=np.float32)img_elephant=cv2.warpAffine(img,M_crop_elephant,(400,600))cv2.imwrite('lanka_elephant.jpg',img_elephant)# x軸的剪切變換,角度15°theta=15*np.pi/180M_shear=np.array([[1,np.tan(theta),0],[0,1,0]],dtype=np.float32)img_sheared=cv2.warpAffine(img,M_shear,(400,600))cv2.imwrite('lanka_safari_sheared.jpg',img_sheared)# 順時(shí)針旋轉(zhuǎn)夸盟,角度15°M_rotate=np.array([[np.cos(theta),-np.sin(theta),0],[np.sin(theta),np.cos(theta),0]],dtype=np.float32)img_rotated=cv2.warpAffine(img,M_rotate,(400,600))cv2.imwrite('lanka_safari_rotated.jpg',img_rotated)# 某種變換蛾方,具體旋轉(zhuǎn)+縮放+旋轉(zhuǎn)組合可以通過(guò)SVD分解理解M=np.array([[1,1.5,-400],[0.5,2,-100]],dtype=np.float32)img_transformed=cv2.warpAffine(img,M,(400,600))cv2.imwrite('lanka_safari_transformed.jpg',img_transformed)
代碼實(shí)現(xiàn)的操作示意在下圖中:
6.2.4 基本繪圖
OpenCV提供了各種繪圖的函數(shù),可以在畫(huà)面上繪制線段上陕,圓桩砰,矩形和多邊形等,還可以在圖像上指定位置打印文字释簿,比如下面例子:
importnumpyasnpimportcv2# 定義一塊寬600亚隅,高400的畫(huà)布,初始化為白色canvas=np.zeros((400,600,3),dtype=np.uint8)+255# 畫(huà)一條縱向的正中央的黑色分界線cv2.line(canvas,(300,0),(300,399),(0,0,0),2)# 畫(huà)一條右半部份畫(huà)面以150為界的橫向分界線cv2.line(canvas,(300,149),(599,149),(0,0,0),2)# 左半部分的右下角畫(huà)個(gè)紅色的圓cv2.circle(canvas,(200,300),75,(0,0,255),5)# 左半部分的左下角畫(huà)個(gè)藍(lán)色的矩形cv2.rectangle(canvas,(20,240),(100,360),(255,0,0),thickness=3)# 定義兩個(gè)三角形庶溶,并執(zhí)行內(nèi)部綠色填充triangles=np.array([[(200,240),(145,333),(255,333)],[(60,180),(20,237),(100,237)]])cv2.fillPoly(canvas,triangles,(0,255,0))# 畫(huà)一個(gè)黃色五角星# 第一步通過(guò)旋轉(zhuǎn)角度的辦法求出五個(gè)頂點(diǎn)phi=4*np.pi/5rotations=[[[np.cos(i*phi),-np.sin(i*phi)],[i*np.sin(phi),np.cos(i*phi)]]foriinrange(1,5)]pentagram=np.array([[[[0,-1]]+[np.dot(m,(0,-1))forminrotations]]],dtype=np.float)# 定義縮放倍數(shù)和平移向量把五角星畫(huà)在左半部分畫(huà)面的上方pentagram=np.round(pentagram*80+np.array([160,120])).astype(np.int)# 將5個(gè)頂點(diǎn)作為多邊形頂點(diǎn)連線煮纵,得到五角星cv2.polylines(canvas,pentagram,True,(0,255,255),9)# 按像素為間隔從左至右在畫(huà)面右半部份的上方畫(huà)出HSV空間的色調(diào)連續(xù)變化forxinrange(302,600):color_pixel=np.array([[[round(180*float(x-302)/298),255,255]]],dtype=np.uint8)line_color=[int(c)forcincv2.cvtColor(color_pixel,cv2.COLOR_HSV2BGR)[0][0]]cv2.line(canvas,(x,0),(x,147),line_color)# 如果定義圓的線寬大于半斤,則等效于畫(huà)圓點(diǎn)偏螺,隨機(jī)在畫(huà)面右下角的框內(nèi)生成坐標(biāo)np.random.seed(42)n_pts=30pts_x=np.random.randint(310,590,n_pts)pts_y=np.random.randint(160,390,n_pts)pts=zip(pts_x,pts_y)# 畫(huà)出每個(gè)點(diǎn)行疏,顏色隨機(jī)forptinpts:pt_color=[int(c)forcinnp.random.randint(0,255,3)]cv2.circle(canvas,pt,3,pt_color,5)# 在左半部分最上方打印文字cv2.putText(canvas,'Python-OpenCV Drawing Example',(5,15),cv2.FONT_HERSHEY_SIMPLEX,0.5,(0,0,0),1)cv2.imshow('Example of basic drawing functions',canvas)cv2.waitKey()
執(zhí)行這段代碼得到如下的圖像:
6.2.4 視頻功能
視頻中最常用的就是從視頻設(shè)備采集圖片或者視頻,或者讀取視頻文件并從中采樣砖茸。所以比較重要的也是兩個(gè)模塊隘擎,一個(gè)是VideoCapture殴穴,用于獲取相機(jī)設(shè)備并捕獲圖像和視頻凉夯,或是從文件中捕獲货葬。還有一個(gè)VideoWriter,用于生成視頻劲够。還是來(lái)看例子理解這兩個(gè)功能的用法震桶,首先是一個(gè)制作延時(shí)攝影視頻的小例子:
importcv2importtimeinterval=60# 捕獲圖像的間隔,單位:秒num_frames=500# 捕獲圖像的總幀數(shù)out_fps=24# 輸出文件的幀率# VideoCapture(0)表示打開(kāi)默認(rèn)的相機(jī)cap=cv2.VideoCapture(0)# 獲取捕獲的分辨率size=(int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))# 設(shè)置要保存視頻的編碼征绎,分辨率和幀率video=cv2.VideoWriter("time_lapse.avi",cv2.VideoWriter_fourcc('M','P','4','2'),out_fps,size)# 對(duì)于一些低畫(huà)質(zhì)的攝像頭蹲姐,前面的幀可能不穩(wěn)定,略過(guò)foriinrange(42):cap.read()# 開(kāi)始捕獲人柿,通過(guò)read()函數(shù)獲取捕獲的幀try:foriinrange(num_frames):_,frame=cap.read()video.write(frame)# 如果希望把每一幀也存成文件柴墩,比如制作GIF,則取消下面的注釋# filename = '{:0>6d}.png'.format(i)# cv2.imwrite(filename, frame)print('Frame {} is captured.'.format(i))time.sleep(interval)exceptKeyboardInterrupt:# 提前停止捕獲print('Stopped! {}/{} frames captured!'.format(i,num_frames))# 釋放資源并寫(xiě)入視頻文件video.release()cap.release()
這個(gè)例子實(shí)現(xiàn)了延時(shí)攝影的功能凫岖,把程序打開(kāi)并將攝像頭對(duì)準(zhǔn)一些緩慢變化的畫(huà)面江咳,比如桌上緩慢蒸發(fā)的水,或者正在生長(zhǎng)的小草哥放,就能制作出有趣的延時(shí)攝影作品歼指。比如下面這個(gè)鏈接中的圖片就是用這段程序生成的:
http://images.cnitblog.com/blog2015/609274/201503/251904209276278.gif
程序的結(jié)構(gòu)非常清晰簡(jiǎn)單,注釋里也寫(xiě)清楚了每一步甥雕,所以流程就不解釋了踩身。需要提一下的有兩點(diǎn):一個(gè)是VideoWriter中的一個(gè)函數(shù)cv2.VideoWriter_fourcc()。這個(gè)函數(shù)指定了視頻編碼的格式社露,比如例子中用的是MP42挟阻,也就是MPEG-4,更多編碼方式可以在下面的地址查詢:
還有一個(gè)是KeyboardInterrupt峭弟,這是一個(gè)常用的異常赁濒,用來(lái)獲取用戶Ctrl+C的中止,捕獲這個(gè)異常后直接結(jié)束循環(huán)并釋放VideoCapture和VideoWriter的資源孟害,使已經(jīng)捕獲好的部分視頻可以順利生成拒炎。
從視頻中截取幀也是處理視頻時(shí)常見(jiàn)的任務(wù),下面代碼實(shí)現(xiàn)的是遍歷一個(gè)指定文件夾下的所有視頻并按照指定的間隔進(jìn)行截屏并保存:
importcv2importosimportsys# 第一個(gè)輸入?yún)?shù)是包含視頻片段的路徑input_path=sys.argv[1]# 第二個(gè)輸入?yún)?shù)是設(shè)定每隔多少幀截取一幀frame_interval=int(sys.argv[2])# 列出文件夾下所有的視頻文件filenames=os.listdir(input_path)# 獲取文件夾名稱(chēng)video_prefix=input_path.split(os.sep)[-1]# 建立一個(gè)新的文件夾挨务,名稱(chēng)為原文件夾名稱(chēng)后加上_framesframe_path='{}_frames'.format(input_path)ifnotos.path.exists(frame_path):os.mkdir(frame_path)# 初始化一個(gè)VideoCapture對(duì)象cap=cv2.VideoCapture()# 遍歷所有文件forfilenameinfilenames:filepath=os.sep.join([input_path,filename])# VideoCapture::open函數(shù)可以從文件獲取視頻cap.open(filepath)# 獲取視頻幀數(shù)n_frames=int(cap.get(cv2.CAP_PROP_FRAME_COUNT))# 同樣為了避免視頻頭幾幀質(zhì)量低下击你,黑屏或者無(wú)關(guān)等f(wàn)oriinrange(42):cap.read()foriinrange(n_frames):ret,frame=cap.read()# 每隔frame_interval幀進(jìn)行一次截屏操作ifi%frame_interval==0:imagename='{}_{}_{:0>6d}.jpg'.format(video_prefix,filename.split('.')[0],i)imagepath=os.sep.join([frame_path,imagename])print('exported {}!'.format(imagepath))cv2.imwrite(imagepath,frame)# 執(zhí)行結(jié)束釋放資源cap.release()
6.3 用OpenCV實(shí)現(xiàn)數(shù)據(jù)增加小工具
到目前我們已經(jīng)熟悉了numpy中的隨機(jī)模塊,多進(jìn)程調(diào)用和OpenCV的基本操作谎柄,基于這些基礎(chǔ)丁侄,本節(jié)將從思路到代碼一步步實(shí)現(xiàn)一個(gè)最基本的數(shù)據(jù)增加小工具。
第三章和第四章都提到過(guò)數(shù)據(jù)增加(data augmentation)朝巫,作為一種深度學(xué)習(xí)中的常用手段鸿摇,數(shù)據(jù)增加對(duì)模型的泛化性和準(zhǔn)確性都有幫助。數(shù)據(jù)增加的具體使用方式一般有兩種劈猿,一種是實(shí)時(shí)增加拙吉,比如在Caffe中加入數(shù)據(jù)擾動(dòng)層潮孽,每次圖像都先經(jīng)過(guò)擾動(dòng)操作,再去訓(xùn)練筷黔,這樣訓(xùn)練經(jīng)過(guò)幾代(epoch)之后往史,就等效于數(shù)據(jù)增加。還有一種是更加直接簡(jiǎn)單一些的佛舱,就是在訓(xùn)練之前就通過(guò)圖像處理手段對(duì)數(shù)據(jù)樣本進(jìn)行擾動(dòng)和增加椎例,也就是本節(jié)要實(shí)現(xiàn)的。
這個(gè)例子中將包含三種基本類(lèi)型的擾動(dòng):隨機(jī)裁剪请祖,隨機(jī)旋轉(zhuǎn)和隨機(jī)顏色/明暗订歪。
6.3.1 隨機(jī)裁剪
AlexNet中已經(jīng)講過(guò)了隨機(jī)裁剪的基本思路,我們的小例子中打算更進(jìn)一步:在裁剪的時(shí)候考慮圖像寬高比的擾動(dòng)肆捕。在絕大多數(shù)用于分類(lèi)的圖片中陌粹,樣本進(jìn)入網(wǎng)絡(luò)前都是要變?yōu)榻y(tǒng)一大小,所以寬高比擾動(dòng)相當(dāng)于對(duì)物體的橫向和縱向進(jìn)行了縮放福压,這樣除了物體的位置擾動(dòng)掏秩,又多出了一項(xiàng)擾動(dòng)。只要變化范圍控制合適荆姆,目標(biāo)物體始終在畫(huà)面內(nèi)蒙幻,這種擾動(dòng)是有助于提升泛化性能的。實(shí)現(xiàn)這種裁剪的思路如下圖所示:
圖中最左邊是一幅需要剪裁的畫(huà)面胆筒,首先根據(jù)這幅畫(huà)面我們可以算出一個(gè)寬高比w/h邮破。然后設(shè)定一個(gè)小的擾動(dòng)范圍δ和要裁剪的畫(huà)面占原畫(huà)面的比例β,從-
到
之間按均勻采樣仆救,獲取一個(gè)隨機(jī)數(shù)
作為裁剪后畫(huà)面的寬高比擾動(dòng)的比例抒和,則裁剪后畫(huà)面的寬和高分別為:
想象一下先把這個(gè)寬為w’,高為h’的區(qū)域置于原畫(huà)面的右下角彤蔽,則這個(gè)區(qū)域的左上角和原畫(huà)面的左上角框出的小區(qū)域摧莽,如圖中的虛線框所示,就是裁剪后區(qū)域左上角可以取值的范圍顿痪。所以在這個(gè)區(qū)域內(nèi)隨機(jī)采一點(diǎn)作為裁剪區(qū)域的左上角镊辕,就實(shí)現(xiàn)了如圖中位置隨機(jī),且寬高比也隨機(jī)的裁剪蚁袭。
6.3.2 隨機(jī)旋轉(zhuǎn)
前面講到過(guò)的旋轉(zhuǎn)比起來(lái)征懈,做數(shù)據(jù)增加時(shí),一般希望旋轉(zhuǎn)是沿著畫(huà)面的中心揩悄。這樣除了要知道旋轉(zhuǎn)角度卖哎,還得計(jì)算平移的量才能讓仿射變換的效果等效于旋轉(zhuǎn)軸在畫(huà)面中心照藻,好在OpenCV中有現(xiàn)成的函數(shù)cv2.getRotationMatrix2D()可以使用。這個(gè)函數(shù)的第一個(gè)參數(shù)是旋轉(zhuǎn)中心只恨,第二個(gè)參數(shù)是逆時(shí)針旋轉(zhuǎn)角度咱圆,第三個(gè)參數(shù)是縮放倍數(shù)跺涤,對(duì)于只是旋轉(zhuǎn)的情況下這個(gè)值是1,返回值就是做仿射變換的矩陣。
直接用這個(gè)函數(shù)并接著使用cv2.warpAffine()會(huì)有一個(gè)潛在的問(wèn)題窝革,就是旋轉(zhuǎn)之后會(huì)出現(xiàn)黑邊厢拭。如果要旋轉(zhuǎn)后的畫(huà)面不包含黑邊泡一,就得沿著原來(lái)畫(huà)面的輪廓做個(gè)內(nèi)接矩形,該矩形的寬高比和原畫(huà)面相同瞳脓,如下圖所示:
在圖中塑娇,可以看到,限制內(nèi)接矩形大小的主要是原畫(huà)面更靠近中心的那條邊劫侧,也就是圖中比較長(zhǎng)的一條邊AB埋酬。因此我們只要沿著中心O和內(nèi)接矩形的頂點(diǎn)方向的直線哨啃,求出和AB的交點(diǎn)P,就得到了內(nèi)接矩形的大小写妥。先來(lái)看長(zhǎng)邊的方程拳球,考慮之前畫(huà)面和橫軸相交的點(diǎn),經(jīng)過(guò)角度-θ旋轉(zhuǎn)后珍特,到了圖中的Q點(diǎn)所在:
因?yàn)殚L(zhǎng)邊所在直線過(guò)Q點(diǎn)祝峻,且斜率為1/tan(θ),所以有:
這時(shí)候考慮OP這條直線:
把這個(gè)公式帶入再前邊一個(gè)公式扎筒,求解可以得到:
注意到在這個(gè)問(wèn)題中莱找,每個(gè)象限和相鄰象限都是軸對(duì)稱(chēng)的,而且旋轉(zhuǎn)角度對(duì)剪裁寬度和長(zhǎng)度的影響是周期(T=π)變化砸琅,再加上我們關(guān)心的其實(shí)并不是四個(gè)點(diǎn)的位置宋距,而是旋轉(zhuǎn)后要截取的矩形的寬w’和高h(yuǎn)’轴踱,所以復(fù)雜的分區(qū)間情況也簡(jiǎn)化了症脂,首先對(duì)于旋轉(zhuǎn)角度,因?yàn)橹芷跒棣幸В远伎梢曰?到π之間诱篷,然后因?yàn)閷?duì)稱(chēng)性,進(jìn)一步有:
于是對(duì)于0到π/2之間的θ雳灵,有:
當(dāng)然需要注意的是棕所,對(duì)于寬高比非常大或者非常小的圖片,旋轉(zhuǎn)后如果裁剪往往得到的畫(huà)面是非常小的一部分悯辙,甚至不包含目標(biāo)物體。所以是否需要旋轉(zhuǎn),以及是否需要裁剪屋吨,如果裁剪角度多少合適弧圆,都要視情況而定。
6.3.3 隨機(jī)顏色和明暗
比起AlexNet論文里在PCA之后的主成分上做擾動(dòng)的方法拢蛋,本書(shū)用來(lái)實(shí)現(xiàn)隨機(jī)的顏色以及明暗的方法相對(duì)簡(jiǎn)單很多桦他,就是給HSV空間的每個(gè)通道,分別加上一個(gè)微小的擾動(dòng)谆棱。其中對(duì)于色調(diào)快压,從-
到
之間按均勻采樣,獲取一個(gè)隨機(jī)數(shù)
作為要擾動(dòng)的值垃瞧,然后新的像素值x’為原始像素值x +
蔫劣;對(duì)于其他兩個(gè)空間則是新像素值x’為原始像素值x的(1+
)倍,從而實(shí)現(xiàn)色調(diào)个从,飽和度和明暗度的擾動(dòng)脉幢。
因?yàn)槊靼刀炔⒉粫?huì)對(duì)圖像的直方圖相對(duì)分布產(chǎn)生大的影響,所以在HSV擾動(dòng)基礎(chǔ)上,考慮再加入一個(gè)Gamma擾動(dòng)鸵隧,方法是設(shè)定一個(gè)大于1的Gamma值的上限γ绸罗,因?yàn)檫@個(gè)值通常會(huì)和1是一個(gè)量級(jí),再用均勻采樣的近似未必合適豆瘫,所以從-logγ到logγ之間均勻采樣一個(gè)值α珊蟀,然后用
作為Gamma值進(jìn)行變換。
6.3.4 多進(jìn)程調(diào)用加速處理
做數(shù)據(jù)增加時(shí)如果樣本量本身就不小外驱,則處理起來(lái)可能會(huì)很耗費(fèi)時(shí)間育灸,所以可以考慮利用多進(jìn)程并行處理。比如我們的例子中昵宇,設(shè)定使用場(chǎng)景是輸入一個(gè)文件夾路徑磅崭,該文件夾下包含了所有原始的數(shù)據(jù)樣本。用戶指定輸出的文件夾和打算增加圖片的總量瓦哎。執(zhí)行程序的時(shí)候砸喻,通過(guò)os.listdir()獲取所有文件的路徑,然后按照上一章講過(guò)的多進(jìn)程平均劃分樣本的辦法蒋譬,把文件盡可能均勻地分給不同進(jìn)程割岛,進(jìn)行處理。
6.3.5 代碼:圖片數(shù)據(jù)增加小工具
按照前面4個(gè)部分的思路和方法犯助,這節(jié)來(lái)實(shí)現(xiàn)這么一個(gè)圖片數(shù)據(jù)增加小工具癣漆,首先對(duì)于一些基礎(chǔ)的操作,我們定義在一個(gè)叫做image_augmentation.py的文件里:
importnumpyasnpimportcv2'''定義裁剪函數(shù)剂买,四個(gè)參數(shù)分別是:左上角橫坐標(biāo)x0左上角縱坐標(biāo)y0裁剪寬度w裁剪高度h'''crop_image=lambdaimg,x0,y0,w,h:img[y0:y0+h,x0:x0+w]'''隨機(jī)裁剪area_ratio為裁剪畫(huà)面占原畫(huà)面的比例hw_vari是擾動(dòng)占原高寬比的比例范圍'''defrandom_crop(img,area_ratio,hw_vari):h,w=img.shape[:2]hw_delta=np.random.uniform(-hw_vari,hw_vari)hw_mult=1+hw_delta# 下標(biāo)進(jìn)行裁剪惠爽,寬高必須是正整數(shù)w_crop=int(round(w*np.sqrt(area_ratio*hw_mult)))# 裁剪寬度不可超過(guò)原圖可裁剪寬度ifw_crop>w:w_crop=wh_crop=int(round(h*np.sqrt(area_ratio/hw_mult)))ifh_crop>h:h_crop=h# 隨機(jī)生成左上角的位置x0=np.random.randint(0,w-w_crop+1)y0=np.random.randint(0,h-h_crop+1)returncrop_image(img,x0,y0,w_crop,h_crop)'''定義旋轉(zhuǎn)函數(shù):angle是逆時(shí)針旋轉(zhuǎn)的角度crop是個(gè)布爾值,表明是否要裁剪去除黑邊'''defrotate_image(img,angle,crop):h,w=img.shape[:2]# 旋轉(zhuǎn)角度的周期是360°angle%=360# 用OpenCV內(nèi)置函數(shù)計(jì)算仿射矩陣M_rotate=cv2.getRotationMatrix2D((w/2,h/2),angle,1)# 得到旋轉(zhuǎn)后的圖像img_rotated=cv2.warpAffine(img,M_rotate,(w,h))# 如果需要裁剪去除黑邊ifcrop:# 對(duì)于裁剪角度的等效周期是180°angle_crop=angle%180# 并且關(guān)于90°對(duì)稱(chēng)ifangle_crop>90:angle_crop=180-angle_crop# 轉(zhuǎn)化角度為弧度theta=angle_crop*np.pi/180.0# 計(jì)算高寬比hw_ratio=float(h)/float(w)# 計(jì)算裁剪邊長(zhǎng)系數(shù)的分子項(xiàng)tan_theta=np.tan(theta)numerator=np.cos(theta)+np.sin(theta)*tan_theta# 計(jì)算分母項(xiàng)中和寬高比相關(guān)的項(xiàng)r=hw_ratioifh>welse1/hw_ratio# 計(jì)算分母項(xiàng)denominator=r*tan_theta+1# 計(jì)算最終的邊長(zhǎng)系數(shù)crop_mult=numerator/denominator# 得到裁剪區(qū)域w_crop=int(round(crop_mult*w))h_crop=int(round(crop_mult*h))x0=int((w-w_crop)/2)y0=int((h-h_crop)/2)img_rotated=crop_image(img_rotated,x0,y0,w_crop,h_crop)returnimg_rotated'''隨機(jī)旋轉(zhuǎn)angle_vari是旋轉(zhuǎn)角度的范圍[-angle_vari, angle_vari)p_crop是要進(jìn)行去黑邊裁剪的比例'''defrandom_rotate(img,angle_vari,p_crop):angle=np.random.uniform(-angle_vari,angle_vari)crop=Falseifnp.random.random()>p_cropelseTruereturnrotate_image(img,angle,crop)'''定義hsv變換函數(shù):hue_delta是色調(diào)變化比例sat_delta是飽和度變化比例val_delta是明度變化比例'''defhsv_transform(img,hue_delta,sat_mult,val_mult):img_hsv=cv2.cvtColor(img,cv2.COLOR_BGR2HSV).astype(np.float)img_hsv[:,:,0]=(img_hsv[:,:,0]+hue_delta)%180img_hsv[:,:,1]*=sat_multimg_hsv[:,:,2]*=val_multimg_hsv[img_hsv>255]=255returncv2.cvtColor(np.round(img_hsv).astype(np.uint8),cv2.COLOR_HSV2BGR)'''隨機(jī)hsv變換hue_vari是色調(diào)變化比例的范圍sat_vari是飽和度變化比例的范圍val_vari是明度變化比例的范圍'''defrandom_hsv_transform(img,hue_vari,sat_vari,val_vari):hue_delta=np.random.randint(-hue_vari,hue_vari)sat_mult=1+np.random.uniform(-sat_vari,sat_vari)val_mult=1+np.random.uniform(-val_vari,val_vari)returnhsv_transform(img,hue_delta,sat_mult,val_mult)'''定義gamma變換函數(shù):gamma就是Gamma'''defgamma_transform(img,gamma):gamma_table=[np.power(x/255.0,gamma)*255.0forxinrange(256)]gamma_table=np.round(np.array(gamma_table)).astype(np.uint8)returncv2.LUT(img,gamma_table)'''隨機(jī)gamma變換gamma_vari是Gamma變化的范圍[1/gamma_vari, gamma_vari)'''defrandom_gamma_transform(img,gamma_vari):log_gamma_vari=np.log(gamma_vari)alpha=np.random.uniform(-log_gamma_vari,log_gamma_vari)gamma=np.exp(alpha)returngamma_transform(img,gamma)
調(diào)用這些函數(shù)需要通過(guò)一個(gè)主程序瞬哼。這個(gè)主程序里首先定義三個(gè)子模塊婚肆,定義一個(gè)函數(shù)parse_arg()通過(guò)Python的argparse模塊定義了各種輸入?yún)?shù)和默認(rèn)值。需要注意的是這里用argparse來(lái)輸入所有參數(shù)是因?yàn)閰?shù)總量并不是特別多倒槐,如果增加了更多的擾動(dòng)方法旬痹,更合適的參數(shù)輸入方式可能是通過(guò)一個(gè)配置文件。然后定義一個(gè)生成待處理圖像列表的函數(shù)generate_image_list()讨越,根據(jù)輸入中要增加圖片的數(shù)量和并行進(jìn)程的數(shù)目盡可能均勻地為每個(gè)進(jìn)程生成了需要處理的任務(wù)列表两残。執(zhí)行隨機(jī)擾動(dòng)的代碼定義在augment_images()中,這個(gè)函數(shù)是每個(gè)進(jìn)程內(nèi)進(jìn)行實(shí)際處理的函數(shù)把跨,執(zhí)行順序是鏡像
裁剪
旋轉(zhuǎn)
HSV
Gamma人弓。需要注意的是鏡像
裁剪,因?yàn)橹皇莻€(gè)演示例子着逐,這未必是一個(gè)合適的順序崔赌。最后定義一個(gè)main函數(shù)進(jìn)行調(diào)用意蛀,代碼如下:
importosimportargparseimportrandomimportmathfrommultiprocessingimportProcessfrommultiprocessingimportcpu_countimportcv2# 導(dǎo)入image_augmentation.py為一個(gè)可調(diào)用模塊importimage_augmentationasia# 利用Python的argparse模塊讀取輸入輸出和各種擾動(dòng)參數(shù)defparse_args():parser=argparse.ArgumentParser(description='A Simple Image Data Augmentation Tool',formatter_class=argparse.ArgumentDefaultsHelpFormatter)parser.add_argument('input_dir',help='Directory containing images')parser.add_argument('output_dir',help='Directory for augmented images')parser.add_argument('num',help='Number of images to be augmented',type=int)parser.add_argument('--num_procs',help='Number of processes for paralleled augmentation',type=int,default=cpu_count())parser.add_argument('--p_mirror',help='Ratio to mirror an image',type=float,default=0.5)parser.add_argument('--p_crop',help='Ratio to randomly crop an image',type=float,default=1.0)parser.add_argument('--crop_size',help='The ratio of cropped image size to original image size, in area',type=float,default=0.8)parser.add_argument('--crop_hw_vari',help='Variation of h/w ratio',type=float,default=0.1)parser.add_argument('--p_rotate',help='Ratio to randomly rotate an image',type=float,default=1.0)parser.add_argument('--p_rotate_crop',help='Ratio to crop out the empty part in a rotated image',type=float,default=1.0)parser.add_argument('--rotate_angle_vari',help='Variation range of rotate angle',type=float,default=10.0)parser.add_argument('--p_hsv',help='Ratio to randomly change gamma of an image',type=float,default=1.0)parser.add_argument('--hue_vari',help='Variation of hue',type=int,default=10)parser.add_argument('--sat_vari',help='Variation of saturation',type=float,default=0.1)parser.add_argument('--val_vari',help='Variation of value',type=float,default=0.1)parser.add_argument('--p_gamma',help='Ratio to randomly change gamma of an image',type=float,default=1.0)parser.add_argument('--gamma_vari',help='Variation of gamma',type=float,default=2.0)args=parser.parse_args()args.input_dir=args.input_dir.rstrip('/')args.output_dir=args.output_dir.rstrip('/')returnargs'''根據(jù)進(jìn)程數(shù)和要增加的目標(biāo)圖片數(shù),生成每個(gè)進(jìn)程要處理的文件列表和每個(gè)文件要增加的數(shù)目'''defgenerate_image_list(args):# 獲取所有文件名和文件總數(shù)filenames=os.listdir(args.input_dir)num_imgs=len(filenames)# 計(jì)算平均處理的數(shù)目并向下取整num_ave_aug=int(math.floor(args.num/num_imgs))# 剩下的部分不足平均分配到每一個(gè)文件健芭,所以做成一個(gè)隨機(jī)幸運(yùn)列表# 對(duì)于幸運(yùn)的文件就多增加一個(gè)县钥,湊夠指定的數(shù)目rem=args.num-num_ave_aug*num_imgslucky_seq=[True]*rem+[False]*(num_imgs-rem)random.shuffle(lucky_seq)# 根據(jù)平均分配和幸運(yùn)表策略,# 生成每個(gè)文件的全路徑和對(duì)應(yīng)要增加的數(shù)目并放到一個(gè)list里img_list=[(os.sep.join([args.input_dir,filename]),num_ave_aug+1ifluckyelsenum_ave_aug)forfilename,luckyinzip(filenames,lucky_seq)]# 文件可能大小不一慈迈,處理時(shí)間也不一樣若贮,# 所以隨機(jī)打亂,盡可能保證處理時(shí)間均勻random.shuffle(img_list)# 生成每個(gè)進(jìn)程的文件列表痒留,# 盡可能均勻地劃分每個(gè)進(jìn)程要處理的數(shù)目length=float(num_imgs)/float(args.num_procs)indices=[int(round(i*length))foriinrange(args.num_procs+1)]return[img_list[indices[i]:indices[i+1]]foriinrange(args.num_procs)]# 每個(gè)進(jìn)程內(nèi)調(diào)用圖像處理函數(shù)進(jìn)行擾動(dòng)的函數(shù)defaugment_images(filelist,args):# 遍歷所有列表內(nèi)的文件forfilepath,ninfilelist:img=cv2.imread(filepath)filename=filepath.split(os.sep)[-1]dot_pos=filename.rfind('.')# 獲取文件名和后綴名imgname=filename[:dot_pos]ext=filename[dot_pos:]print('Augmenting {} ...'.format(filename))foriinrange(n):img_varied=img.copy()# 擾動(dòng)后文件名的前綴varied_imgname='{}_{:0>3d}_'.format(imgname,i)# 按照比例隨機(jī)對(duì)圖像進(jìn)行鏡像ifrandom.random()
為了排版方便谴麦,并沒(méi)有很遵守Python的規(guī)范(PEP8)。注意到除了前面提的三種類(lèi)型的變化伸头,還增加了鏡像變化匾效,這主要是因?yàn)檫@種變換太簡(jiǎn)單了,順手就寫(xiě)上了恤磷。還有默認(rèn)進(jìn)程數(shù)用的是cpu_count()函數(shù)面哼,這個(gè)獲取的是cpu的核數(shù)。把這段代碼保存為run_augmentation.py碗殷,然后在命令行輸入:
>> python run_augmentation.py -h
或者
>> python run_augmentation.py --help
就能看到腳本的使用方法精绎,每個(gè)參數(shù)的含義速缨,還有默認(rèn)值锌妻。接下里來(lái)執(zhí)行一個(gè)圖片增加任務(wù):
>> python run_augmentation.py imagenet_samples more_samples 1000 --rotate_angle_vari 180 --p_rotate_crop 0.5
其中imagenet_samples為一些從imagenet圖片url中隨機(jī)下載的一些圖片,--rotate_angle_vari設(shè)為180方便測(cè)試全方向的旋轉(zhuǎn)旬牲,--p_rotate_crop設(shè)置為0.5仿粹,讓旋轉(zhuǎn)裁剪對(duì)一半圖片生效。擾動(dòng)增加后的1000張圖片在more_samples文件夾下原茅,得到的部分結(jié)果如下:
6.4 用OpenCV實(shí)現(xiàn)數(shù)據(jù)標(biāo)注小工具
除了對(duì)圖像的處理吭历,OpenCV的圖形用戶界面(GraphicalUserInterface,GUI)和繪圖等相關(guān)功能也是很有用的功能,無(wú)論是可視化擂橘,圖像調(diào)試還是我們這節(jié)要實(shí)現(xiàn)的標(biāo)注任務(wù)晌区,都可以有所幫助。這節(jié)先介紹OpenCV窗口的最基本使用和交互通贞,然后基于這些基礎(chǔ)和之前的知識(shí)實(shí)現(xiàn)一個(gè)用于物體檢測(cè)任務(wù)標(biāo)注的小工具朗若。
6.4.1 OpenCV窗口循環(huán)
OpenCV顯示一幅圖片的函數(shù)是cv2.imshow(),第一個(gè)參數(shù)是顯示圖片的窗口名稱(chēng)昌罩,第二個(gè)參數(shù)是圖片的array哭懈。不過(guò)如果直接執(zhí)行這個(gè)函數(shù)的話,什么都不會(huì)發(fā)生茎用,因?yàn)檫@個(gè)函數(shù)得配合cv2.waitKey()一起使用遣总。cv2.waitKey()指定當(dāng)前的窗口顯示要持續(xù)的毫秒數(shù)睬罗,比如cv2.waitKey(1000)就是顯示一秒,然后窗口就關(guān)閉了旭斥。比較特殊的是cv2.waitKey(0)容达,并不是顯示0毫秒的意思,而是一直顯示垂券,直到有鍵盤(pán)上的按鍵被按下董饰,或者鼠標(biāo)點(diǎn)擊了窗口的小叉子才關(guān)閉。cv2.waitKey()的默認(rèn)參數(shù)就是0圆米,所以對(duì)于圖像展示的場(chǎng)景卒暂,cv2.waitKey()或者cv2.waitKey(0)是最常用的:
importcv2img=cv2.imread('Aitutaki.png')cv2.imshow('Honeymoon Island',img)cv2.waitKey()
執(zhí)行這段代碼得到如下窗口:
cv2.waitKey()參數(shù)不為零的時(shí)候則可以和循環(huán)結(jié)合產(chǎn)生動(dòng)態(tài)畫(huà)面,比如在6.2.4的延時(shí)小例子中娄帖,我們把延時(shí)攝影保存下來(lái)的所有圖像放到一個(gè)叫做frames的文件夾下也祠。下面代碼從frames的文件夾下讀取所有圖片并以24的幀率在窗口中顯示成動(dòng)畫(huà):
importosfromitertoolsimportcycleimportcv2# 列出frames文件夾下的所有圖片filenames=os.listdir('frames')# 通過(guò)itertools.cycle生成一個(gè)無(wú)限循環(huán)的迭代器,每次迭代都輸出下一張圖像對(duì)象img_iter=cycle([cv2.imread(os.sep.join(['frames',x]))forxinfilenames])key=0whilekey&0xFF!=27:cv2.imshow('Animation',next(img_iter))key=cv2.waitKey(42)
在這個(gè)例子中我們采用了Python的itertools模塊中的cycle函數(shù)近速,這個(gè)函數(shù)可以把一個(gè)可遍歷結(jié)構(gòu)編程一個(gè)無(wú)限循環(huán)的迭代器诈嘿。另外從這個(gè)例子中我們還發(fā)現(xiàn),cv2.waitKey()返回的就是鍵盤(pán)上出發(fā)的按鍵削葱。對(duì)于字母就是ascii碼奖亚,特殊按鍵比如上下左右等,則對(duì)應(yīng)特殊的值析砸,其實(shí)這就是鍵盤(pán)事件的最基本用法昔字。
6.4.2 鼠標(biāo)和鍵盤(pán)事件
因?yàn)镚UI總是交互的,所以鼠標(biāo)和鍵盤(pán)事件基本使用必不可少首繁,上節(jié)已經(jīng)提到了cv2.waitKey()就是獲取鍵盤(pán)消息的最基本方法作郭。比如下面這段循環(huán)代碼就能夠獲取鍵盤(pán)上按下的按鍵,并在終端輸出:
whilekey!=27:cv2.imshow('Honeymoon Island',img)key=cv2.waitKey()# 如果獲取的鍵值小于256則作為ascii碼輸出對(duì)應(yīng)字符弦疮,否則直接輸出值msg='{} is pressed'.format(chr(key)ifkey<256elsekey)print(msg)
通過(guò)這個(gè)程序我們能獲取一些常用特殊按鍵的值夹攒,比如在筆者用的機(jī)器上,四個(gè)方向的按鍵和刪除鍵對(duì)應(yīng)的值如下:
- 上(↑):65362
- 下(↓):65364
- 左(←):65361
- 右(→):65363
- 刪除(Delete):65535
需要注意的是在不同的操作系統(tǒng)里這些值可能是不一樣的胁塞。鼠標(biāo)事件比起鍵盤(pán)事件稍微復(fù)雜一點(diǎn)點(diǎn)咏尝,需要定義一個(gè)回調(diào)函數(shù),然后把回調(diào)函數(shù)和一個(gè)指定名稱(chēng)的窗口綁定啸罢,這樣只要鼠標(biāo)位于畫(huà)面區(qū)域內(nèi)的事件就都能捕捉到编检。把下面這段代碼插入到上段代碼的while之前,就能獲取當(dāng)前鼠標(biāo)的位置和動(dòng)作并輸出:
# 定義鼠標(biāo)事件回調(diào)函數(shù)defon_mouse(event,x,y,flags,param):# 鼠標(biāo)左鍵按下伺糠,抬起蒙谓,雙擊ifevent==cv2.EVENT_LBUTTONDOWN:print('Left button down at ({}, {})'.format(x,y))elifevent==cv2.EVENT_LBUTTONUP:print('Left button up at ({}, {})'.format(x,y))elifevent==cv2.EVENT_LBUTTONDBLCLK:print('Left button double clicked at ({}, {})'.format(x,y))# 鼠標(biāo)右鍵按下,抬起训桶,雙擊elifevent==cv2.EVENT_RBUTTONDOWN:print('Right button down at ({}, {})'.format(x,y))elifevent==cv2.EVENT_RBUTTONUP:print('Right button up at ({}, {})'.format(x,y))elifevent==cv2.EVENT_RBUTTONDBLCLK:print('Right button double clicked at ({}, {})'.format(x,y))# 鼠標(biāo)中/滾輪鍵(如果有的話)按下累驮,抬起酣倾,雙擊elifevent==cv2.EVENT_MBUTTONDOWN:print('Middle button down at ({}, {})'.format(x,y))elifevent==cv2.EVENT_MBUTTONUP:print('Middle button up at ({}, {})'.format(x,y))elifevent==cv2.EVENT_MBUTTONDBLCLK:print('Middle button double clicked at ({}, {})'.format(x,y))# 鼠標(biāo)移動(dòng)elifevent==cv2.EVENT_MOUSEMOVE:print('Moving at ({}, {})'.format(x,y))# 為指定的窗口綁定自定義的回調(diào)函數(shù)cv2.namedWindow('Honeymoon Island')cv2.setMouseCallback('Honeymoon Island',on_mouse)
6.4.3 代碼:物體檢測(cè)標(biāo)注的小工具
基于上面兩小節(jié)的基本使用,就能和OpenCV的基本繪圖功能就能實(shí)現(xiàn)一個(gè)超級(jí)簡(jiǎn)單的物體框標(biāo)注小工具了谤专≡晡基本思路是對(duì)要標(biāo)注的圖像建立一個(gè)窗口循環(huán),然后每次循環(huán)的時(shí)候?qū)D像進(jìn)行一次拷貝置侍。鼠標(biāo)在畫(huà)面上畫(huà)框的操作映之,以及已經(jīng)畫(huà)好的框的相關(guān)信息在全局變量中保存,并且在每個(gè)循環(huán)中根據(jù)這些信息蜡坊,在拷貝的圖像上再畫(huà)一遍杠输,然后顯示這份拷貝的圖像。
基于這種實(shí)現(xiàn)思路秕衙,使用上我們采用一個(gè)盡量簡(jiǎn)化的設(shè)計(jì):
- 輸入是一個(gè)文件夾蠢甲,下面包含了所有要標(biāo)注物體框的圖片。如果圖片中標(biāo)注了物體据忘,則生成一個(gè)相同名稱(chēng)加額外后綴名的文件保存標(biāo)注信息鹦牛。
- 標(biāo)注的方式是按下鼠標(biāo)左鍵選擇物體框的左上角,松開(kāi)鼠標(biāo)左鍵選擇物體框的右下角勇吊,鼠標(biāo)右鍵刪除上一個(gè)標(biāo)注好的物體框曼追。所有待標(biāo)注物體的類(lèi)別,和標(biāo)注框顏色由用戶自定義汉规,如果沒(méi)有定義則默認(rèn)只標(biāo)注一種物體礼殊,定義該物體名稱(chēng)叫“Object”。
- 方向鍵的←和→用來(lái)遍歷圖片鲫忍,↑和↓用來(lái)選擇當(dāng)前要標(biāo)注的物體膏燕,Delete鍵刪除一張圖片和對(duì)應(yīng)的標(biāo)注信息。
每張圖片的標(biāo)注信息悟民,以及自定義標(biāo)注物體和顏色的信息,用一個(gè)元組表示篷就,第一個(gè)元素是物體名字射亏,第二個(gè)元素是代表BGR顏色的tuple或者是代表標(biāo)注框坐標(biāo)的元組。對(duì)于這種并不復(fù)雜復(fù)雜的數(shù)據(jù)結(jié)構(gòu)竭业,我們直接利用Python的repr()函數(shù)智润,把數(shù)據(jù)結(jié)構(gòu)保存成機(jī)器可讀的字符串放到文件里,讀取的時(shí)候用eval()函數(shù)就能直接獲得數(shù)據(jù)未辆。這樣的方便之處在于不需要單獨(dú)寫(xiě)個(gè)格式解析器窟绷。如果需要可以在此基礎(chǔ)上再編寫(xiě)一個(gè)轉(zhuǎn)換工具就能夠轉(zhuǎn)換成常見(jiàn)的Pascal VOC的標(biāo)注格式或是其他的自定義格式。
在這些思路和設(shè)計(jì)下咐柜,我們定義標(biāo)注信息文件的格式的例子如下:
('Hill', ((221, 163), (741, 291)))('Horse', ((465, 430), (613, 570)))
元組中第一項(xiàng)是物體名稱(chēng)兼蜈,第二項(xiàng)是標(biāo)注框左上角和右下角的坐標(biāo)攘残。這里之所以不把標(biāo)注信息的數(shù)據(jù)直接用pickle保存,是因?yàn)閿?shù)據(jù)本身不會(huì)很復(fù)雜为狸,直接保存還有更好的可讀性歼郭。自定義標(biāo)注物體和對(duì)應(yīng)標(biāo)注框顏色的格式也類(lèi)似,不過(guò)更簡(jiǎn)單些辐棒,因?yàn)槔ㄌ?hào)可以不寫(xiě)病曾,具體如下:
'Horse', (255, 255, 0)'Hill', (0, 255, 255)'DiaoSi', (0, 0, 255)
第一項(xiàng)是物體名稱(chēng),第二項(xiàng)是物體框的顏色漾根。使用的時(shí)候把自己定義好的內(nèi)容放到一個(gè)文本里泰涂,然后保存成和待標(biāo)注文件夾同名,后綴名為labels的文件辐怕。比如我們?cè)谝粋€(gè)叫samples的文件夾下放上一些草原的照片负敏,然后自定義一個(gè)samples.labels的文本文件。把上段代碼的內(nèi)容放進(jìn)去秘蛇,就定義了小山頭的框?yàn)辄S色其做,駿馬的框?yàn)榍嗌约凹t色的屌絲赁还⊙梗基于以上,標(biāo)注小工具的代碼如下:
importosimportcv2# tkinter是Python內(nèi)置的簡(jiǎn)單GUI庫(kù)艘策,實(shí)現(xiàn)一些比如打開(kāi)文件夾蹈胡,確認(rèn)刪除等操作十分方便fromtkFileDialogimportaskdirectoryfromtkMessageBoximportaskyesno# 定義標(biāo)注窗口的默認(rèn)名稱(chēng)WINDOW_NAME='Simple Bounding Box Labeling Tool'# 定義畫(huà)面刷新的大概幀率(是否能達(dá)到取決于電腦性能)FPS=24# 定義支持的圖像格式SUPPOTED_FORMATS=['jpg','jpeg','png']# 定義默認(rèn)物體框的名字為Object,顏色藍(lán)色朋蔫,當(dāng)沒(méi)有用戶自定義物體時(shí)用默認(rèn)物體DEFAULT_COLOR={'Object':(255,0,0)}# 定義灰色罚渐,用于信息顯示的背景和未定義物體框的顯示COLOR_GRAY=(192,192,192)# 在圖像下方多出BAR_HEIGHT這么多像素的區(qū)域用于顯示文件名和當(dāng)前標(biāo)注物體等信息BAR_HEIGHT=16# 上下左右,ESC及刪除鍵對(duì)應(yīng)的cv.waitKey()的返回值# 注意這個(gè)值根據(jù)操作系統(tǒng)不同有不同驯妄,可以通過(guò)6.4.2中的代碼獲取KEY_UP=65362KEY_DOWN=65364KEY_LEFT=65361KEY_RIGHT=65363KEY_ESC=27KEY_DELETE=65535# 空鍵用于默認(rèn)循環(huán)KEY_EMPTY=0get_bbox_name='{}.bbox'.format# 定義物體框標(biāo)注工具類(lèi)classSimpleBBoxLabeling:def__init__(self,data_dir,fps=FPS,window_name=None):self._data_dir=data_dirself.fps=fpsself.window_name=window_nameifwindow_nameelseWINDOW_NAME#pt0是正在畫(huà)的左上角坐標(biāo)荷并,pt1是鼠標(biāo)所在坐標(biāo)self._pt0=Noneself._pt1=None# 表明當(dāng)前是否正在畫(huà)框的狀態(tài)標(biāo)記self._drawing=False# 當(dāng)前標(biāo)注物體的名稱(chēng)self._cur_label=None# 當(dāng)前圖像對(duì)應(yīng)的所有已標(biāo)注框self._bboxes=[]# 如果有用戶自定義的標(biāo)注信息則讀取,否則用默認(rèn)的物體和顏色label_path='{}.labels'.format(self._data_dir)self.label_colors=DEFAULT_COLORifnotos.path.exists(label_path)elseself.load_labels(label_path)# 獲取已經(jīng)標(biāo)注的文件列表和還未標(biāo)注的文件列表imagefiles=[xforxinos.listdir(self._data_dir)ifx[x.rfind('.')+1:].lower()inSUPPOTED_FORMATS]labeled=[xforxinimagefilesifos.path.exists(get_bbox_name(x))]to_be_labeled=[xforxinimagefilesifxnotinlabeled]# 每次打開(kāi)一個(gè)文件夾青扔,都自動(dòng)從還未標(biāo)注的第一張開(kāi)始self._filelist=labeled+to_be_labeledself._index=len(labeled)ifself._index>len(self._filelist)-1:self._index=len(self._filelist)-1# 鼠標(biāo)回調(diào)函數(shù)def_mouse_ops(self,event,x,y,flags,param):# 按下左鍵時(shí)源织,坐標(biāo)為左上角,同時(shí)表明開(kāi)始畫(huà)框微猖,改變drawing標(biāo)記為T(mén)rueifevent==cv2.EVENT_LBUTTONDOWN:self._drawing=Trueself._pt0=(x,y)# 左鍵抬起谈息,表明當(dāng)前框畫(huà)完了,坐標(biāo)記為右下角凛剥,并保存侠仇,同時(shí)改變drawing標(biāo)記為Falseelifevent==cv2.EVENT_LBUTTONUP:self._drawing=Falseself._pt1=(x,y)self._bboxes.append((self._cur_label,(self._pt0,self._pt1)))# 實(shí)時(shí)更新右下角坐標(biāo)方便畫(huà)框elifevent==cv2.EVENT_MOUSEMOVE:self._pt1=(x,y)# 鼠標(biāo)右鍵刪除最近畫(huà)好的框elifevent==cv2.EVENT_RBUTTONUP:ifself._bboxes:self._bboxes.pop()# 清除所有標(biāo)注框和當(dāng)前狀態(tài)def_clean_bbox(self):self._pt0=Noneself._pt1=Noneself._drawing=Falseself._bboxes=[]# 畫(huà)標(biāo)注框和當(dāng)前信息的函數(shù)def_draw_bbox(self,img):# 在圖像下方多出BAR_HEIGHT這么多像素的區(qū)域用于顯示文件名和當(dāng)前標(biāo)注物體等信息h,w=img.shape[:2]canvas=cv2.copyMakeBorder(img,0,BAR_HEIGHT,0,0,cv2.BORDER_CONSTANT,value=COLOR_GRAY)# 正在標(biāo)注的物體信息,如果鼠標(biāo)左鍵已經(jīng)按下犁珠,則顯示兩個(gè)點(diǎn)坐標(biāo)逻炊,否則顯示當(dāng)前待標(biāo)注物體的名稱(chēng)label_msg='{}: {}, {}'.format(self._cur_label,self._pt0,self._pt1)\ifself._drawing\else'Current label: {}'.format(self._cur_label)# 顯示當(dāng)前文件名互亮,文件個(gè)數(shù)信息msg='{}/{}: {} | {}'.format(self._index+1,len(self._filelist),self._filelist[self._index],label_msg)cv2.putText(canvas,msg,(1,h+12),cv2.FONT_HERSHEY_SIMPLEX,0.5,(0,0,0),1)# 畫(huà)出已經(jīng)標(biāo)好的框和對(duì)應(yīng)名字forlabel,(bpt0,bpt1)inself._bboxes:label_color=self.label_colors[label]iflabelinself.label_colorselseCOLOR_GRAYcv2.rectangle(canvas,bpt0,bpt1,label_color,thickness=2)cv2.putText(canvas,label,(bpt0[0]+3,bpt0[1]+15),cv2.FONT_HERSHEY_SIMPLEX,0.5,label_color,2)# 畫(huà)正在標(biāo)注的框和對(duì)應(yīng)名字ifself._drawing:label_color=self.label_colors[self._cur_label]ifself._cur_labelinself.label_colorselseCOLOR_GRAYifself._pt1[0]>=self._pt0[0]andself._pt1[1]>=self._pt0[1]:cv2.rectangle(canvas,self._pt0,self._pt1,label_color,thickness=2)cv2.putText(canvas,self._cur_label,(self._pt0[0]+3,self._pt0[1]+15),cv2.FONT_HERSHEY_SIMPLEX,0.5,label_color,2)returncanvas# 利用repr()導(dǎo)出標(biāo)注框數(shù)據(jù)到文件@staticmethoddefexport_bbox(filepath,bboxes):ifbboxes:withopen(filepath,'w')asf:forbboxinbboxes:line=repr(bbox)+'\n'f.write(line)elifos.path.exists(filepath):os.remove(filepath)# 利用eval()讀取標(biāo)注框字符串到數(shù)據(jù)@staticmethoddefload_bbox(filepath):bboxes=[]withopen(filepath,'r')asf:line=f.readline().rstrip()whileline:bboxes.append(eval(line))line=f.readline().rstrip()returnbboxes# 利用eval()讀取物體及對(duì)應(yīng)顏色信息到數(shù)據(jù)@staticmethoddefload_labels(filepath):label_colors={}withopen(filepath,'r')asf:line=f.readline().rstrip()whileline:label,color=eval(line)label_colors[label]=colorline=f.readline().rstrip()returnlabel_colors# 讀取圖像文件和對(duì)應(yīng)標(biāo)注框信息(如果有的話)@staticmethoddefload_sample(filepath):img=cv2.imread(filepath)bbox_filepath=get_bbox_name(filepath)bboxes=[]ifos.path.exists(bbox_filepath):bboxes=SimpleBBoxLabeling.load_bbox(bbox_filepath)returnimg,bboxes# 導(dǎo)出當(dāng)前標(biāo)注框信息并清空def_export_n_clean_bbox(self):bbox_filepath=os.sep.join([self._data_dir,get_bbox_name(self._filelist[self._index])])self.export_bbox(bbox_filepath,self._bboxes)self._clean_bbox()# 刪除當(dāng)前樣本和對(duì)應(yīng)的標(biāo)注框信息def_delete_current_sample(self):filename=self._filelist[self._index]filepath=os.sep.join([self._data_dir,filename])ifos.path.exists(filepath):os.remove(filepath)filepath=get_bbox_name(filepath)ifos.path.exists(filepath):os.remove(filepath)self._filelist.pop(self._index)print('{} is deleted!'.format(filename))# 開(kāi)始OpenCV窗口循環(huán)的方法,定義了程序的主邏輯defstart(self):# 之前標(biāo)注的文件名嗅骄,用于程序判斷是否需要執(zhí)行一次圖像讀取last_filename=''# 標(biāo)注物體在列表中的下標(biāo)label_index=0# 所有標(biāo)注物體名稱(chēng)的列表labels=self.label_colors.keys()# 待標(biāo)注物體的種類(lèi)數(shù)n_labels=len(labels)# 定義窗口和鼠標(biāo)回調(diào)cv2.namedWindow(self.window_name)cv2.setMouseCallback(self.window_name,self._mouse_ops)key=KEY_EMPTY# 定義每次循環(huán)的持續(xù)時(shí)間delay=int(1000/FPS)# 只要沒(méi)有按下Esc鍵胳挎,就持續(xù)循環(huán)whilekey!=KEY_ESC:# 上下鍵用于選擇當(dāng)前標(biāo)注物體ifkey==KEY_UP:iflabel_index==0:passelse:label_index-=1elifkey==KEY_DOWN:iflabel_index==n_labels-1:passelse:label_index+=1# 左右鍵切換當(dāng)前標(biāo)注的圖片elifkey==KEY_LEFT:# 已經(jīng)到了第一張圖片的話就不需要清空上一張ifself._index>0:self._export_n_clean_bbox()self._index-=1ifself._index<0:self._index=0elifkey==KEY_RIGHT:# 已經(jīng)到了最后一張圖片的話就不需要清空上一張ifself._indexlen(self._filelist)-1:self._index=len(self._filelist)-1# 刪除當(dāng)前圖片和對(duì)應(yīng)標(biāo)注信息elifkey==KEY_DELETE:ifaskyesno('Delete Sample','Are you sure?'):self._delete_current_sample()key=KEY_EMPTYcontinue# 如果鍵盤(pán)操作執(zhí)行了換圖片,則重新讀取溺森,更新圖片filename=self._filelist[self._index]iffilename!=last_filename:filepath=os.sep.join([self._data_dir,filename])img,self._bboxes=self.load_sample(filepath)# 更新當(dāng)前標(biāo)注物體名稱(chēng)self._cur_label=labels[label_index]# 把標(biāo)注和相關(guān)信息畫(huà)在圖片上并顯示指定的時(shí)間canvas=self._draw_bbox(img)cv2.imshow(self.window_name,canvas)key=cv2.waitKey(delay)# 當(dāng)前文件名就是下次循環(huán)的老文件名last_filename=filenameprint('Finished!')cv2.destroyAllWindows()# 如果退出程序慕爬,需要對(duì)當(dāng)前進(jìn)行保存self.export_bbox(os.sep.join([self._data_dir,get_bbox_name(filename)]),self._bboxes)print('Labels updated!')if__name__=='__main__':dir_with_images=askdirectory(title='Where are the images?')labeling_task=SimpleBBoxLabeling(dir_with_images)labeling_task.start()
需要注意的是幾個(gè)比較通用且獨(dú)立的方法前加上了一句@staticmethod,表明是個(gè)靜態(tài)方法屏积。執(zhí)行這個(gè)程序医窿,并選擇samples文件夾,標(biāo)注時(shí)的畫(huà)面如下圖:
「真誠(chéng)贊賞炊林,手留余香」