1. 程式人生 > >【學習筆記】使用python批量讀取並修改xml檔案

【學習筆記】使用python批量讀取並修改xml檔案

在大老闆的安排下最近在某公司實習,實習期間要求實現一個影象識別模組的封裝。無奈基礎太薄弱,只能將任務細分,單獨學習來實現。以此為背景……


本篇目標:通過python批量訪問並修改xml檔案。

目前,存在的問題是,標註好一批圖片後,若改變圖片尺寸,則原始的xml檔案中的bnbbox資料作廢,針對改變尺寸後的圖片還得重新標註。費事費力,在模組封裝任務中也必須解決這個問題。因此,目前急需實現批量修改xml檔案,減少標註工作量。

主要參考部落格:coding思想博主的博文。


1.引入

我所使用的xml檔案為labelImg程式標註的Pascal VOC格式的xml檔案,檔案格式如下:

<annotation>
	<folder>img_oil_leakage</folder>
	<filename>A0000005.jpg</filename>
	<path>/home/kanghao/catkin_ws/src/ros_tf_cv_key/scripts/img_oil_leakage/A0000005.jpg</path>
	<source>
		<database>Unknown</database>
	</source>
	<size>
		<width>640</width>
		<height>480</height>
		<depth>3</depth>
	</size>
	<segmented>0</segmented>
	<object>
		<name>A</name>
		<pose>Unspecified</pose>
		<truncated>0</truncated>
		<difficult>0</difficult>
		<bndbox>
			<xmin>200</xmin>
			<ymin>100</ymin>
			<xmax>409</xmax>
			<ymax>233</ymax>
		</bndbox>
	</object>
</annotation>

其中<folder>標籤表示圖片所在的資料夾,<filename>表示對應圖片名稱;<path>對應圖片地址;<source>這裡記不清了;緊接著就是<size>;是標註圖片時圖片的長寬以及通道數;<segmented>表示是否用於語義分割,不是就寫0;<name>表示標註影象中該類的名稱;<pose>印象中表示拍攝角度?是做左邊還是右邊?(存疑);<truncated>記不清了!;<difficult>表示影象中的目標是否難以識別;<bndbox>重中之重,表示我們標註影象時bounding box的左下角以及右上角的座標值。

在上述標籤中,希望改變的是<path>和<bndbox>。由淺入深,先從修改單一xml檔案說起。

2.修改單一xml檔案

#coding=utf-8
import xml.dom.minidom

###讀取單個xml檔案
dom=xml.dom.minidom.parse('A0000005.xml')

root=dom.documentElement

###獲取標籤對xmin/ymin之間的值
xmin=root.getElementsByTagName('xmin')
ymin=root.getElementsByTagName('ymin')

###原始資訊
print('原始資訊')

xi0 = xmin[0]
print xi0.firstChild.data
# ~ b = unicode.encode(xi0.firstChild.data)

# ~ yi0 = ymin[0]
# ~ print yi0.firstChild.data

###修改標籤對之間的資訊
###疑問?如何將xi0.firstChild.data資料轉為int變數?
###如何讓該式直接可以運算xi0.firstChild.data=xi0.firstChild.data/2
xi0.firstChild.data=200
# ~ xi0.firstChild.data=int(b)/100
yi0.firstChild.data=60

#列印輸出(修改後)
print xi0.firstChild.data
print yi0.firstChild.data

先貼程式碼,python中針對xml檔案使用xml.dom.minidom包。基本按照註釋就可以理解程式碼了。理解程式碼後,我產生了一個想法,那就是我們是否可以在大孩.資料(咳咳,估計惡意機翻一波),就是firstChild.data賦值替換語句中,我們針對不同的圖片是否可以直接讀取它的值,併除一個相同的比例呢?立刻按照想法實踐後發現,firstChild.data返回的資料格式是unicode格式,我們需要int與int格式之間進行運算。

於是,請注意這一句:

b = unicode.encode(xi0.firstChild.data)

我們將純數字的unicode格式轉換為str格式,再使用簡單的int()函式將str格式轉換為int格式。

b = unicode.encode(xi0.firstChild.data)

便實現了我們“一勞永逸”針對單個xml檔案的讀取與修改(修改結果並未儲存在xml檔案中)。

3.遍歷xml檔案

完成了對單一xml檔案的讀取與修改,我們如何遍歷所有的xml檔案呢?首先我們先實現遍歷資料夾中的所有檔案(是的,下面的程式碼會遍歷同一資料夾中所有格式的檔案……好在資料集中xml檔案都是單獨存放)

#coding=utf-8  
import os  
import os.path  
### import xml.dom.minidom  
      
path="/home/×××××/SSD-Tensorflow/VOC2007/Annotations/"  
files=os.listdir(path)  #得到資料夾下所有檔名稱  
  
for xmlFile in files: #遍歷資料夾  
	if not os.path.isdir(xmlFile): #判斷是否是資料夾,不是資料夾才打開  
		print xmlFile  

#遍歷指定路徑下的檔案

程式碼執行效果如下圖所示:

在實現了這個功能後,原博主的思想是將遍歷程式碼和修改單個xml檔案程式碼進行融合,融合後的程式碼為:

#coding:utf-8
import os
import os.path
import xml.dom.minidom

#path="../xml/"
path='/home/kanghao/learning_something/about_xml/xml/'
files=os.listdir(path) #得到資料夾下所有檔名稱
s=[]
for xmlFile in files: #遍歷資料夾
	if not os.path.isdir(xmlFile): #判斷是否是資料夾,不是資料夾才打開
		print xmlFile
		
		#xml讀取操作
		
		#將獲取到的xml檔名送入到dom解析
		#錯誤程式碼:dom=xml.dom.minidom.parse(xmlFile)
		dom=xml.dom.minidom.parse(xmlFile)   ####錯誤出現在這裡
                ###dom=xml.dom.minidom.parse(os.path.join(path,xmlFile)) #拼接地址,將每個具體的.xml檔案帶入
		root=dom.documentElement
		
		###獲取標籤對xmin/ymin之間的值
		xmin=root.getElementsByTagName('xmin')
		ymin=root.getElementsByTagName('ymin')

		###原始資訊
		print('原始資訊')

		xi0 = xmin[0]
		print xi0.firstChild.data
		a= xi0.firstChild.data
		print(type(a))
		yi0 = ymin[0]
		print yi0.firstChild.data

		###修改標籤對之間的資訊
		###疑問?如何將xi0.firstChild.data資料轉為int變數?
		###如何讓該式直接可以運算xi0.firstChild.data=xi0.firstChild.data/2
		###下面這個方法可以將純數字的unicod格式轉換為str格式
		# ~ b = unicode.encode(xi0.firstChild.data)
		# ~ print(b)
		# ~ print(type(b))
		xi0.firstChild.data=100
		yi0.firstChild.data=60

		#列印輸出(修改後)
		print xi0.firstChild.data
		print yi0.firstChild.data
		print '##################'

注意看標註了“#錯誤出現在這裡的”那句語句,對比讀取單個xml的程式碼,我的理解是這裡雖然讀取到了xml檔案,但是傳入

dom=xml.dom.minidom.parse(xmlFile)

這句時僅僅傳入了一個檔名稱,並沒有具體地址所在地。所以會報錯:

報錯圖片來自這裡。因此,原作者使用了python中的地址拼接,即將檔案地址和檔名同時傳入。 也就是

###dom=xml.dom.minidom.parse(os.path.join(path,xmlFile)) #拼接地址,將每個具體的.xml檔案帶入

這一句,在執行程式碼後,輸出結果如下:

至此,我們完成了遍歷資料夾中的xml檔案,讀取標籤值,並將讀取到的標籤值替換為我們需要的值。然而,一個很重要的問題就是,現在只是讀取標籤值,賦新值並列印顯示,可是最後修改的結果並未儲存在xml檔案中。接下來,我們就要將我們修改的結果儲存在xml檔案中。

4.批量讀取xml檔案修改並儲存

現在,我們實現剛才沒有儲存修改結果到xml檔案中的功能,主要程式碼如下:

		#儲存修改到xml檔案中
		with open(os.path.join(path,xmlFile),'w') as fh:
			dom.writexml(fh)
			print('恭喜,寫入xmin/ymin成功!')

整體程式碼為:

#coding:utf-8
import os
import os.path
import xml.dom.minidom

#path="../xml/"
path='/home/kanghao/learning_something/about_xml/xml/'
files=os.listdir(path) #得到資料夾下所有檔名稱
s=[]
for xmlFile in files: #遍歷資料夾
	if not os.path.isdir(xmlFile): #判斷是否是資料夾,不是資料夾才打開
		print xmlFile
		
		#xml讀取操作
		
		#將獲取到的xml檔名送入到dom解析
		#錯誤程式碼:dom=xml.dom.minidom.parse(xmlFile)
		dom=xml.dom.minidom.parse(os.path.join(path,xmlFile))
		root=dom.documentElement
		
		###獲取標籤對xmin/ymin之間的值
		xmin=root.getElementsByTagName('xmin')
		ymin=root.getElementsByTagName('ymin')

		#修改相應標籤的值
		for i in range(len(xmin)):
			print xmin[i].firstChild.data
			a = xmin[i].firstChild.data
			print(type(a))
			xmin[i].firstChild.data=200
			print xmin[i].firstChild.data
			
		for j in range(len(ymin)):
			print ymin[j].firstChild.data
			ymin[j].firstChild.data=100
			print ymin[j].firstChild.data

		#儲存修改到xml檔案中
		with open(os.path.join(path,xmlFile),'w') as fh:
			dom.writexml(fh)
			print('恭喜,寫入xmin/ymin成功!')

程式碼的執行效果如下:

我們讀取每個xml檔案中xmin與ymin的值,並分別賦值200和100,並儲存在xml檔案中。不妨讓我們檢查一下每個xml檔案:

經檢查,資料夾中每個xml檔案對應的資料均已改變。至此,我們實現了批量讀取xml修改並儲存的功能。我們可以發現,遍歷xml檔案並不是按照順序遍歷,彷彿是隨機遍歷?但是感覺對使用沒有影響,這裡先不糾結了。


接下來的想法,是改變圖片尺寸後,x、y的座標對應同比例變化,等按照上述程式碼實驗一下,看看是否可行。