????????最近需要做基于衛星和無人機的農業大棚的旋轉目標檢測,基于YOLO V8 OBB的原因是因為嘗試的第二個模型就是YOLO V8,后面會基于YOLO V9模型做農業大棚的旋轉目標檢測。YOLO V9目前還不能進行旋轉目標的檢測,需要修改代碼
? ? ? ? PS:歡迎大家分享農業大棚數據集,數據制作太花時間了......下面是我制作的農業大棚圖像
?一、下載代碼配置環境
GitHub - ultralytics/ultralytics: NEW - YOLOv8 🚀 in PyTorch > ONNX > OpenVINO > CoreML > TFLite
? ? ? ? 下載解壓縮源碼之后,激活環境進入根目錄配置環境(我已經換源):
pip install pyproject.dependencies
????????我的換源方法是到用戶文件夾(C:\Users\Administrator)下創建一個pip文件夾,然后在pip文件夾里創建一個txt文件,在txt文件里面寫入下面的內容,然后把txt文件后綴改成ini
[global]
index-url = http://pypi.mirrors.ustc.edu.cn/simple
[install]
use-mirrors =true
mirrors =http://pypi.mirrors.ustc.edu.cn/simple/
trusted-host =pypi.mirrors.ustc.edu.cn
二、數據集準備
? ? ? ? 流程:數據集標注——>XML——>DOTA_XML——>DOTA_TXT——>劃分數據集(train和val)——>YOLO格式TXT
(1)LabelImg2標注數據集生成XML標注文件
????????在LabelImg2上標注好數據,LabelImg2標注是五點式,即旋轉框的中心x,y坐標、旋轉框的長度和寬度、旋轉角度
(2)XML標注文件轉DOTA格式標簽文件(TXT)
????????下面將五點式XML文件轉換為八點式XML文件,再將八點式XML文件轉換為YOLO可訓練的TXT格式
?提示:DOTA數據集的TXT格式
x1,y1,x2,y2,x3,<y3,x4,y4,class_index,difficult
# 示例
307 308 330 299 422 541 398 550 dog 0
? ? ? ?OBB檢測方法里面旋轉框的表示方法有好幾種,YOLO V8 OBB使用的是(通過坐標在 0 和 1 之間歸一化的四個角點來指定邊界框):
class_index, x1, y1, x2, y2, x3, y3, x4, y4(需要做歸一化)
# 示例
0 0.332813 0.164062 0.403125 0.15 0.45 0.373437 0.379688 0.389062
注意事項:
? ? ? ? 【1】運行代碼之前將cls_list = ['dog'] ?# 修改為自己的標簽,不修改也不會報錯,只是轉換后的TXT中將沒有任何數據
? ? ? ? 【2】查看ultralytics/data/converter.py腳本中的代碼,圖片數據格式是png還是jpg。如果你的圖像格式與代碼中要求的圖像格式不符就無法生成TXT標簽(PS:我是將jpg轉png再運行代碼的)
? ? ? ? ?(2024.05.22更新)當然也可以直接修改ultralytics/data/converter.py腳本中的代碼,將
if image_path.suffix != ".png":
????????修改為
if image_path.suffix != [".png", ".jpg", ".jpeg"]:
? ? ? ? 修改之后就不用擔心圖像格式了,png、jpg、jpeg中的任何一種都可以
# 文件名稱 :roxml_to_dota.py
# 功能描述 :把rolabelimg標注的xml文件轉換成dota能識別的xml文件,
# 再轉換成dota格式的txt文件
# 把旋轉框 cx,cy,w,h,angle,或者矩形框cx,cy,w,h,轉換成四點坐標x1,y1,x2,y2,x3,y3,x4,y4
import os
import xml.etree.ElementTree as ET
import mathcls_list = ['dog'] # 修改為自己的標簽def edit_xml(xml_file, dotaxml_file):"""修改xml文件:param xml_file:xml文件的路徑:return:"""# dxml_file = open(xml_file,encoding='gbk')# tree = ET.parse(dxml_file).getroot()tree = ET.parse(xml_file)objs = tree.findall('object')for ix, obj in enumerate(objs):x0 = ET.Element("x0") # 創建節點y0 = ET.Element("y0")x1 = ET.Element("x1")y1 = ET.Element("y1")x2 = ET.Element("x2")y2 = ET.Element("y2")x3 = ET.Element("x3")y3 = ET.Element("y3")# obj_type = obj.find('bndbox')# type = obj_type.text# print(xml_file)if (obj.find('robndbox') == None):obj_bnd = obj.find('bndbox')obj_xmin = obj_bnd.find('xmin')obj_ymin = obj_bnd.find('ymin')obj_xmax = obj_bnd.find('xmax')obj_ymax = obj_bnd.find('ymax')# 以防有負值坐標xmin = max(float(obj_xmin.text), 0)ymin = max(float(obj_ymin.text), 0)xmax = max(float(obj_xmax.text), 0)ymax = max(float(obj_ymax.text), 0)obj_bnd.remove(obj_xmin) # 刪除節點obj_bnd.remove(obj_ymin)obj_bnd.remove(obj_xmax)obj_bnd.remove(obj_ymax)x0.text = str(xmin)y0.text = str(ymax)x1.text = str(xmax)y1.text = str(ymax)x2.text = str(xmax)y2.text = str(ymin)x3.text = str(xmin)y3.text = str(ymin)else:obj_bnd = obj.find('robndbox')obj_bnd.tag = 'bndbox' # 修改節點名obj_cx = obj_bnd.find('cx')obj_cy = obj_bnd.find('cy')obj_w = obj_bnd.find('w')obj_h = obj_bnd.find('h')obj_angle = obj_bnd.find('angle')cx = float(obj_cx.text)cy = float(obj_cy.text)w = float(obj_w.text)h = float(obj_h.text)angle = float(obj_angle.text)obj_bnd.remove(obj_cx) # 刪除節點obj_bnd.remove(obj_cy)obj_bnd.remove(obj_w)obj_bnd.remove(obj_h)obj_bnd.remove(obj_angle)x0.text, y0.text = rotatePoint(cx, cy, cx - w / 2, cy - h / 2, -angle)x1.text, y1.text = rotatePoint(cx, cy, cx + w / 2, cy - h / 2, -angle)x2.text, y2.text = rotatePoint(cx, cy, cx + w / 2, cy + h / 2, -angle)x3.text, y3.text = rotatePoint(cx, cy, cx - w / 2, cy + h / 2, -angle)# obj.remove(obj_type) # 刪除節點obj_bnd.append(x0) # 新增節點obj_bnd.append(y0)obj_bnd.append(x1)obj_bnd.append(y1)obj_bnd.append(x2)obj_bnd.append(y2)obj_bnd.append(x3)obj_bnd.append(y3)tree.write(dotaxml_file, method='xml', encoding='utf-8') # 更新xml文件# 轉換成四點坐標
def rotatePoint(xc, yc, xp, yp, theta):xoff = xp - xc;yoff = yp - yc;cosTheta = math.cos(theta)sinTheta = math.sin(theta)pResx = cosTheta * xoff + sinTheta * yoffpResy = - sinTheta * xoff + cosTheta * yoffreturn str(int(xc + pResx)), str(int(yc + pResy))def totxt(xml_path, out_path):# 想要生成的txt文件保存的路徑,這里可以自己修改files = os.listdir(xml_path)i = 0for file in files:tree = ET.parse(xml_path + os.sep + file)root = tree.getroot()name = file.split('.')[0]output = out_path + '\\' + name + '.txt'file = open(output, 'w')i = i + 1objs = tree.findall('object')for obj in objs:cls = obj.find('name').textbox = obj.find('bndbox')x0 = int(float(box.find('x0').text))y0 = int(float(box.find('y0').text))x1 = int(float(box.find('x1').text))y1 = int(float(box.find('y1').text))x2 = int(float(box.find('x2').text))y2 = int(float(box.find('y2').text))x3 = int(float(box.find('x3').text))y3 = int(float(box.find('y3').text))if x0 < 0:x0 = 0if x1 < 0:x1 = 0if x2 < 0:x2 = 0if x3 < 0:x3 = 0if y0 < 0:y0 = 0if y1 < 0:y1 = 0if y2 < 0:y2 = 0if y3 < 0:y3 = 0for cls_index, cls_name in enumerate(cls_list):if cls == cls_name:file.write("{} {} {} {} {} {} {} {} {} {}\n".format(x0, y0, x1, y1, x2, y2, x3, y3, cls, cls_index))file.close()# print(output)print(i)if __name__ == '__main__':# -----**** 第一步:把xml文件統一轉換成旋轉框的xml文件 ****-----roxml_path = r'D:\data\yolov8_obb\origin_xml' # labelimg2標注生成的原始xml文件路徑dotaxml_path = r'D:\data\yolov8_obb\dota_xml' # 轉換后dota能識別的xml文件路徑,路徑需存在,不然報錯out_path = r'D:\data\yolov8_obb\dota_txt' # 轉換后dota格式的txt文件路徑,路徑需存在,不然報錯filelist = os.listdir(roxml_path)for file in filelist:edit_xml(os.path.join(roxml_path, file), os.path.join(dotaxml_path, file))# -----**** 第二步:把旋轉框xml文件轉換成txt格式 ****-----totxt(dotaxml_path, out_path)
????????轉換后的TXT格式的標簽文件(此時的標簽還不是OBB數據集的格式,還需要再轉換):
?(3)劃分數據集
????????接下來劃分數據集:使用下面的代碼劃分數據集
import os
import random
import shutilrandom.seed(42)"""
該腳本用于將給定的數據集分割成訓練集和測試集。
數據集應包含圖像和對應的標注文件。
腳本會按照90%訓練集和10%測試集的比例進行分割,并將圖像和標注文件分別復制到相應的文件夾中。
"""# 設置數據集文件夾路徑和輸出文件夾路徑
data_folder = 'data_mouse_ro'
img_folder = 'data_mouse_ro/dataset/images'
label_folder = 'data_mouse_ro/dataset/labels'# 計算每個子集的大小
# 總文件數乘以0.9得到訓練集大小,其余為測試集大小
total_files = len(os.listdir(os.path.join(data_folder, 'img')))
train_size = int(total_files * 0.9)
test_size = int(total_files - train_size)# 獲取所有圖像文件的文件名列表,并進行隨機打亂
image_files = os.listdir(os.path.join(data_folder, 'img'))
random.shuffle(image_files)# 復制圖像和標注文件到相應的子集文件夾中
# 枚舉每個圖像文件,根據索引決定復制到訓練集還是測試集文件夾
for i, image_file in enumerate(image_files):base_file_name = os.path.splitext(image_file)[0] # 獲取文件名(不包括擴展名)image_path = os.path.join(data_folder, 'img', image_file)label_path = os.path.join(data_folder, 'dotatxt', base_file_name + '.txt')# 根據索引判斷文件應復制到訓練集還是測試集if i < train_size:shutil.copy(image_path, os.path.join(img_folder, 'train')) # 復制圖像到訓練集shutil.copy(label_path, os.path.join(label_folder, 'train_original')) # 復制標注到訓練集else:shutil.copy(image_path, os.path.join(img_folder, 'val')) # 復制圖像到測試集shutil.copy(label_path, os.path.join(label_folder, 'val_original')) # 復制標注到測試集
? ? ? ? 運行代碼前文件夾結構如下(所有圖像放在img文件夾下,所有txt放在dotatxt文件夾下)
????????運行代碼后dataset中的train和val文件夾就已經有了劃分好的圖像,labels中的train_original和val_original有對應的train和val標簽
(4)DOTA格式標簽文件轉換為YOLO V8訓練所需的YOLO格式
? ? ? ? 【1】在項目代碼根目錄下面創建下面的文件夾結構,然后將劃分好的圖像和標簽文件放到相應的文件夾中?
? ? ? ? 【2】由于官方源碼轉換代碼用的是VOC數據集,所以這里我們需要修改ultralytics/data/converter.py中的類別名,改成自己的數據集類別名。修改ultralytics/data/converter.py中的代碼
? ? ? ? 【3】運行下面的代碼,將DOTA格式的標簽文件轉換為OBB數據集格式,其中的參數根據自己的情況設置
import syssys.path.append('F:\object_detection\yolov8_obb_version2\yolov8')from ultralytics.data.converter import convert_dota_to_yolo_obbconvert_dota_to_yolo_obb('F:\object_detection\yolov8_obb_version2\yolov8\data')
????????運行提示:
? ? ? ? 轉換后的OBB數據集格式的標簽會保存在labels\train和labels\val中(訓練需要使用的就是這兩個文件夾,train_original和val_original用不到)
? ? ? ? ?轉換后的OBB數據集格式的標簽文件中的內容
三、模型配置
(1)新建模型配置文件my-data8-obb.yaml
????????在yolov8\ultralytics\cfg\datasets路徑下,新建my-data8-obb.yaml文件(復制粘貼其中某一個yaml文件改個名字),寫入如下代碼,其中參數根據自己的情況設置
path: F:\object_detection\yolov8_obb_version2\yolov8\data # dataset root dir
train: images/train # train images (relative to 'path') 4 images
val: images/val # val images (relative to 'path') 4 images# Classes for DOTA 1.0
names:0: dog
(2)修改模型配置文件yolov8-obb.yaml
????????在yolov8\ultralytics\cfg\models\v8路徑下,修改yolov8-obb.yaml文件,將nc參數修改為自己的數據集類別數
四、訓練
(1)根據自己的實際情況修改yolov8\ultralytics\cfg\default.yaml文件中的訓練參數
????????如果自己的數據集類別只有一種,就將single-cls參數設置為True
(2)運行下面的代碼即可開始訓練
????????如果你使用的權重是“yolov8n-obb.pt”,只需要把下面代碼中的配置文件yolov8x-obb.yaml改成yolov8n-obb.yaml,依此類推
from ultralytics import YOLOdef main():model = YOLO('ultralytics/cfg/models/v8/yolov8x-obb.yaml').load('pt/yolov8x-obb.pt') # build from YAML and transfer weightsmodel.train(data='ultralytics/cfg/datasets/my-data8-obb.yaml', epochs=5, imgsz=640, batch=16, workers=4)if __name__ == '__main__':main()
????????訓練過程及結果(5個Epoch)
五、驗證
? ? ? ? 在根目錄下創建一個名為eval.py的腳本,寫入下面的代碼,其中的參數根據自己的情況設置
from ultralytics import YOLOdef main():model = YOLO(r'runs/obb/train/weights/best.pt')model.val(data='ultralytics/cfg/datasets/my-data8-obb.yaml', imgsz=640, batch=4, workers=4)if __name__ == '__main__':main()
? ? ? ? 運行代碼的結果:
? ? ? ? ?下面是驗證保存的圖像(標簽為dog是因為在使用lableimg2制作標簽的時候懶得改了,采用了軟件默認的dog)
六、推理
????????在根目錄下創建一個名為predict.py的腳本,寫入下面的代碼,其中的參數根據自己的情況設置
from ultralytics import YOLOmodel = YOLO('runs/obb/train/weights/best.pt')
results = model('predict_images/2024_0018.jpg', save=True)
print(results[0].obb.xywhr[:, -1] * 180 / 3.14159265358979323846)
? ? ? ? 運行代碼的結果
????????推理保存的圖像(標簽為dog是因為在使用lableimg2制作標簽的時候懶得改了,采用了軟件默認的dog)
? ? ? ? 目前就做了這些工作,在數據集數量和質量方面還存在不足,在接下來會解決這部分的問題
????????這只是一個篇分享經驗的文章,難免有錯誤或者遺漏的地方,歡迎交流指正