แบ่งdataset.py

Code

import os
import shutil
import random
from collections import defaultdict

# พาธต้นทาง กำหนดที่อยู่รูปภาพต้นทางและไฟล์ label
image_dir = 'C:/Users/Acer/Desktop/Project/PetBreed_Identifier/model/yolov11/datasets/train/images'
label_dir = 'C:/Users/Acer/Desktop/Project/PetBreed_Identifier/model/yolov11/datasets/train/labels'

# พาธปลายทาง
output_base = 'C:/Users/Acer/Desktop/Project/PetBreed_Identifier/model/yolov11/datasets_breeds'
# กำหนดสัดส่วนการแบ่งข้อมูล
splits = ['train', 'valid', 'test']
split_ratio = {'train': 0.8, 'valid': 0.15, 'test': 0.05}

# สร้างโฟลเดอร์สำหรับชุดข้อมูลที่แบ่งแล้ว
for split in splits:
    os.makedirs(os.path.join(output_base, split, 'images'), exist_ok=True)
    os.makedirs(os.path.join(output_base, split, 'labels'), exist_ok=True)

# Step 1: จัดกลุ่มภาพตามคลาส
# สร้างกล่องเก็บข้อมูลที่เตรียมไว้สำหรับจัดกลุ่มรูปภาพ โดยกุญแจคือ หมายเลขคลาส และค่าที่เก็บคือ รายการไฟล์รูปภาพ
class_to_files = defaultdict(list)

# อ่านไฟล์ label เพื่อจัดกลุ่มรูปภาพตามคลาส
for label_file in os.listdir(label_dir):
    if not label_file.endswith('.txt'):
        continue
    path = os.path.join(label_dir, label_file)
    with open(path, 'r') as f:
        lines = f.readlines()
        
        # ดึงหมายเลขคลาสจากไฟล์ label
        classes_in_file = set(int(line.split()[0]) for line in lines if len(line.split()) >= 5)
        
        # เพิ่มไฟล์รูปภาพลงในกลุ่มตามคลาส เช่น ถ้ารูปมีหมาคลาส 0 และแมวคลาส 1, รูปภาพนี้ก็จะถูกเก็บไว้ทั้งในกลุ่มของคลาส 0 และคลาส 1
        for cls in classes_in_file:
            image_filename = os.path.splitext(label_file)[0] + '.jpg'
            class_to_files[cls].append(image_filename)

# Step 2: แบ่งข้อมูลโดยให้คลาสมีสัดส่วนใกล้เคียงกัน
# สร้างกล่องเก็บไฟล์ที่ถูกแบ่งแล้ว
final_split_files = {'train': set(), 'valid': set(), 'test': set()}

# สำหรับแต่ละคลาส ให้ทำการสับไฟล์และแบ่งตามสัดส่วนที่กำหนด
for cls, files in class_to_files.items():
    unique_files = list(set(files))  # ป้องกันซ้ำ
    random.shuffle(unique_files) # สับไฟล์เพื่อให้การแบ่งข้อมูลเป็นแบบสุ่ม
    total = len(unique_files)

    # คำนวณจำนวนไฟล์ในแต่ละชุด
    train_count = int(total * split_ratio['train'])
    val_count = int(total * split_ratio['valid'])
    test_count = total - train_count - val_count  # ส่วนที่เหลือ

    # เพิ่มไฟล์ไปยังชุดข้อมูลที่แบ่งแล้ว
    final_split_files['train'].update(unique_files[:train_count])
    final_split_files['valid'].update(unique_files[train_count:train_count + val_count])
    final_split_files['test'].update(unique_files[train_count + val_count:])

# Step 3: คัดลอกไฟล์รูปและ label ไปยังโฟลเดอร์ปลายทาง
# ฟังก์ชันสำหรับคัดลอกไฟล์
def copy_files(split, filenames):
    for img_file in filenames:
        label_file = os.path.splitext(img_file)[0] + '.txt'

        src_img = os.path.join(image_dir, img_file)
        src_lbl = os.path.join(label_dir, label_file)

        dst_img = os.path.join(output_base, split, 'images', img_file)
        dst_lbl = os.path.join(output_base, split, 'labels', label_file)

        if os.path.exists(src_img) and os.path.exists(src_lbl):
            shutil.copy2(src_img, dst_img) # คัดลอกรูปภาพ จากต้นทางไปยังปลายทาง
            shutil.copy2(src_lbl, dst_lbl) # คัดลอกไฟล์ label จากต้นทางไปยังปลายทาง

# คัดลอกแต่ละชุด
for split in splits:
    copy_files(split, final_split_files[split])

    # แสดงผลลัพธ์
    print(f"{split}: {len(final_split_files[split])} files copied.")

print("✅ Dataset split completed.")

ผลลัพธ์


อธิบาย

การนำเข้าโมดูลและการตั้งค่าพาธ

  • ทำหน้าที่: นำเข้าโมดูล os

  • ทำไมต้องเขียน: โมดูล os (Operating System) ให้ฟังก์ชันสำหรับการโต้ตอบกับระบบปฏิบัติการ เช่น การจัดการไฟล์และไดเร็กทอรี (การสร้างโฟลเดอร์, การรวมพาธ, การตรวจสอบว่าไฟล์มีอยู่หรือไม่)

  • ผลลัพธ์: ทำให้สามารถใช้คำสั่งเกี่ยวกับระบบไฟล์ได้

  • ทำหน้าที่: นำเข้าโมดูล shutil

  • ทำไมต้องเขียน: โมดูล shutil (Shell Utilities) ให้ฟังก์ชันการทำงานระดับสูงของไฟล์ เช่น การคัดลอกไฟล์และโฟลเดอร์

  • ผลลัพธ์: ทำให้สามารถใช้ฟังก์ชัน shutil.copy2 สำหรับคัดลอกไฟล์ได้

  • ทำหน้าที่: นำเข้าโมดูล random

  • ทำไมต้องเขียน: โมดูล random ใช้สำหรับสร้างตัวเลขสุ่ม ซึ่งในที่นี้จะใช้ฟังก์ชัน random.shuffle เพื่อสุ่มลำดับไฟล์ก่อนแบ่ง

  • ผลลัพธ์: ทำให้สามารถสุ่มการจัดเรียงไฟล์ได้

  • ทำหน้าที่: นำเข้าคลาส defaultdict จากโมดูล collections

  • ทำไมต้องเขียน: defaultdict เป็นซับคลาสของ dict ที่ช่วยให้เมื่อพยายามเข้าถึงคีย์ที่ยังไม่มีในดิกชันนารี มันจะสร้างค่าดีฟอลต์สำหรับคีย์นั้นโดยอัตโนมัติ ซึ่งสะดวกกว่า dict ปกติมากเมื่อต้องมีการเพิ่มค่าเข้าไปในลิสต์ภายใต้คีย์นั้นๆ

  • ผลลัพธ์: ทำให้สามารถสร้าง class_to_files เพื่อเก็บรายการไฟล์รูปภาพ (เป็นลิสต์) สำหรับแต่ละหมายเลขคลาสได้อย่างง่ายดาย


  • ทำหน้าที่: กำหนดตัวแปรสำหรับ พาธของโฟลเดอร์รูปภาพต้นทาง (image_dir) และ พาธของโฟลเดอร์ไฟล์ป้ายกำกับต้นทาง (label_dir)

  • ทำไมต้องเขียน: เพื่อให้สคริปต์รู้ว่าไฟล์ข้อมูลต้นฉบับอยู่ที่ไหน

  • ผลลัพธ์: ตัวแปรทั้งสองจะเก็บสตริงที่เป็นที่อยู่ของโฟลเดอร์ข้อมูลอินพุต

  • ทำหน้าที่: กำหนดตัวแปรสำหรับ พาธโฟลเดอร์ปลายทางหลัก ที่จะสร้างชุดข้อมูลที่ถูกแบ่งแล้ว

  • ทำไมต้องเขียน: เพื่อกำหนดที่จัดเก็บเอาต์พุต

  • ผลลัพธ์: ตัวแปร output_base เก็บที่อยู่โฟลเดอร์เอาต์พุต

  • ทำหน้าที่: กำหนดรายการชื่อชุดข้อมูลที่จะแบ่ง

  • ทำไมต้องเขียน: เพื่อให้ง่ายต่อการวนซ้ำ (loop) และการจัดการชื่อชุดข้อมูล

  • ทำหน้าที่: กำหนด ดิกชันนารีของสัดส่วนการแบ่งข้อมูล สำหรับแต่ละชุด

  • ทำไมต้องเขียน: กำหนดสัดส่วนตามที่ต้องการสำหรับการฝึกฝน, ตรวจสอบ, และทดสอบ

  • ข้อสังเกต: ผลรวมของสัดส่วนนี้ () เกิน 1.0 ซึ่งดูเหมือนจะเป็น ข้อผิดพลาด หรือ ตั้งใจทำเพื่อวัตถุประสงค์เฉพาะ ในโค้ด แต่การคำนวณจำนวนไฟล์ใน Step 2 จะใช้สัดส่วนเหล่านี้ในการคำนวณจำนวนไฟล์สำหรับแต่ละชุด (เช่น สำหรับ train, สำหรับ valid, และที่เหลือทั้งหมดสำหรับ test)


  • ทำหน้าที่: วนซ้ำผ่านชื่อชุดข้อมูล (train, valid, test) และสร้างโครงสร้างโฟลเดอร์ปลายทาง

  • ทำไมต้องเขียน: จำเป็นต้องสร้างโครงสร้างไดเร็กทอรีที่ถูกต้องก่อนที่จะคัดลอกไฟล์ไปใส่

  • os.path.join(...): ใช้รวมส่วนประกอบของพาธเข้าด้วยกันอย่างถูกต้องตามระบบปฏิบัติการ (เช่น เป็น / ใน Linux หรือ \ ใน Windows)

  • os.makedirs(..., exist_ok=True): สร้างโฟลเดอร์ที่ระบุ หากโฟลเดอร์มีอยู่แล้ว (exist_ok=True) จะไม่เกิดข้อผิดพลาด

  • ผลลัพธ์: โครงสร้างโฟลเดอร์ดังนี้จะถูกสร้างภายใต้ datasets_breeds:


จัดกลุ่มภาพตามคลาส

  • ทำหน้าที่: สร้างดิกชันนารีพิเศษที่ค่าดีฟอลต์จะเป็นลิสต์ว่าง (list)

  • ทำไมต้องเขียน: เพื่อเก็บแมปปิ้งระหว่าง หมายเลขคลาส (กุญแจ) กับ รายการไฟล์รูปภาพ ที่มีวัตถุของคลาสนั้นอยู่ (ค่า)

  • ทำหน้าที่: เริ่มวนซ้ำผ่านชื่อไฟล์ทั้งหมดในโฟลเดอร์ไฟล์ป้ายกำกับ (label_dir)

  • ทำหน้าที่: ตรวจสอบให้แน่ใจว่าไฟล์ที่กำลังประมวลผลอยู่เป็นไฟล์ป้ายกำกับ YOLO ที่มีนามสกุล .txt เท่านั้น ถ้าไม่ใช่ ให้ข้ามไปไฟล์ถัดไป

  • ทำหน้าที่: สร้างพาธแบบเต็มของไฟล์ป้ายกำกับและเปิดไฟล์เพื่ออ่านเนื้อหาทั้งหมดเป็นรายการของบรรทัด (lines)

  • ทำหน้าที่: ดึงหมายเลขคลาสทั้งหมดที่มีอยู่ในไฟล์ป้ายกำกับปัจจุบัน

    • ไฟล์ป้ายกำกับ YOLO หนึ่งไฟล์อาจมีหลายบรรทัด ซึ่งแต่ละบรรทัดแทนวัตถุหนึ่งชิ้น (เช่น 0 0.5 0.5 0.1 0.1)

    • line.split(): แยกบรรทัดด้วยช่องว่าง ได้เป็นรายการของสตริง

    • len(line.split()) >= 5: กรองเฉพาะบรรทัดที่มีข้อมูลครบถ้วน (อย่างน้อยต้องมี 5 ค่า: หมายเลขคลาส , , , , )

    • int(line.split()[0]): แปลงหมายเลขคลาส (ตัวแรกของบรรทัด) เป็นจำนวนเต็ม

    • set(...): ใช้ set เพื่อให้ได้หมายเลขคลาสที่ ไม่ซ้ำกัน ในไฟล์นั้น (รูปภาพหนึ่งอาจมีวัตถุคลาส 0 หลายตัว ก็จะนับคลาส 0 แค่ครั้งเดียว)

    • ผลลัพธ์: เช่น ถ้าไฟล์ image1.txt มีบรรทัดที่ขึ้นต้นด้วย 0... และ 1... จะได้ classes_in_file = {0, 1}

  • ทำหน้าที่: สำหรับแต่ละคลาส (cls) ที่พบในไฟล์ป้ายกำกับ

    • image_filename = os.path.splitext(label_file)[0] + '.jpg': แปลงชื่อไฟล์ป้ายกำกับ (เช่น image1.txt) ให้เป็นชื่อไฟล์รูปภาพที่สัมพันธ์กัน (เช่น image1.jpg)

    • class_to_files[cls].append(image_filename): เพิ่มชื่อไฟล์รูปภาพนี้เข้าในรายการของคลาสที่พบ

    • ผลลัพธ์: class_to_files จะเก็บข้อมูล:

      Python


แบ่งข้อมูล

  • ทำหน้าที่: สร้างดิกชันนารีเพื่อเก็บชื่อไฟล์รูปภาพที่ถูกจัดสรรเข้าสู่ชุดข้อมูลแต่ละชุด

  • ทำไมต้องเขียน: การใช้ set() (เซต) เป็นค่าที่เก็บ เพื่อให้แน่ใจว่า รูปภาพแต่ละรูปจะถูกจัดอยู่ในชุดข้อมูลเพียงชุดเดียวเท่านั้น ถึงแม้ว่ารูปภาพนั้นจะมีวัตถุหลายคลาสก็ตาม

  • ทำหน้าที่: เริ่มวนซ้ำเพื่อประมวลผลการแบ่งไฟล์สำหรับ ทีละคลาส

    • unique_files = list(set(files)): สร้างลิสต์ของไฟล์ที่ไม่ซ้ำกันสำหรับคลาสปัจจุบัน (แม้ว่า Step 1 จะไม่น่าสร้างไฟล์ซ้ำ แต่เป็นการป้องกันที่ดี)

    • random.shuffle(unique_files): สุ่มลำดับของไฟล์เพื่อป้องกันไม่ให้เกิดความลำเอียงในการแบ่งข้อมูล (เช่น ถ้าไฟล์ถูกเรียงตามชื่อเดิม)

    • total = len(unique_files): จำนวนไฟล์ทั้งหมดที่มีวัตถุของคลาสปัจจุบัน

  • ทำหน้าที่: คำนวณจำนวนไฟล์ที่ควรจะถูกจัดสรรให้กับชุด train, valid, และ test สำหรับคลาสปัจจุบัน

    • ตัวอย่าง: หาก total = 1000 และ split_ratio = {'train': 0.8, 'valid': 0.15}

      • train_count = int(1000 * 0.8) = 800

      • val_count = int(1000 * 0.15) = 150

      • test_count = 1000 - 800 - 150 = 50

    • ผลลัพธ์: สำหรับคลาสนี้ ไฟล์ 800 รูปแรกจะไป train, 150 รูปถัดไปจะไป valid, และ 50 รูปสุดท้ายจะไป test

  • ทำหน้าที่: จัดสรรไฟล์ ตามจำนวนที่คำนวณไว้ไปยังเซตของชุดข้อมูลปลายทาง

    • unique_files[:train_count]: ไฟล์ตั้งแต่ต้นจนถึงลำดับที่ train_count จะถูกจัดสรรให้ชุด train

    • unique_files[train_count:train_count + val_count]: ไฟล์ถัดไปจะถูกจัดสรรให้ชุด valid

    • unique_files[train_count + val_count:]: ไฟล์ที่เหลือทั้งหมดจะถูกจัดสรรให้ชุด test

    • update(...): ใช้เพื่อเพิ่มรายการของไฟล์เข้าในเซตของชุดข้อมูล (ถ้าไฟล์นั้นถูกเพิ่มไปแล้วจากคลาสก่อนหน้า จะไม่ถูกเพิ่มซ้ำ)

    • ผลลัพธ์: รูปภาพแต่ละรูปจะถูกเพิ่มเข้าสู่ชุดข้อมูลชุดใดชุดหนึ่งอย่างน้อยหนึ่งครั้งตามหลักการแบ่งของคลาสที่มันสังกัดอยู่


คัดลอกไฟล์ไปยังโฟลเดอร์ปลายทาง

  • ทำหน้าที่: กำหนดฟังก์ชันชื่อ copy_files ที่รับชื่อชุดข้อมูล (split) และรายการชื่อไฟล์รูปภาพ (filenames) มาดำเนินการคัดลอก

  • ทำไมต้องเขียน: เพื่อความเป็นระเบียบและใช้ซ้ำได้ง่าย

  • ภายในฟังก์ชัน: สำหรับไฟล์รูปภาพแต่ละไฟล์, แปลงชื่อไฟล์รูปภาพ (เช่น img1.jpg) กลับเป็นชื่อไฟล์ป้ายกำกับที่สัมพันธ์กัน (เช่น img1.txt)

  • ทำหน้าที่: สร้างพาธต้นทางแบบเต็มสำหรับไฟล์รูปภาพ (src_img) และไฟล์ป้ายกำกับ (src_lbl)

  • ทำหน้าที่: สร้างพาธปลายทางแบบเต็มสำหรับไฟล์รูปภาพ (dst_img) และไฟล์ป้ายกำกับ (dst_lbl) (เช่น datasets_breeds/train/images/img1.jpg)

  • ทำหน้าที่: ตรวจสอบว่าไฟล์ต้นทางทั้งรูปภาพและป้ายกำกับมีอยู่จริงหรือไม่

  • shutil.copy2(...): ถ้ามีอยู่จริง ให้คัดลอกไฟล์ทั้งรูปภาพและป้ายกำกับไปยังโฟลเดอร์ปลายทางที่กำหนด (ฟังก์ชัน copy2 คัดลอกข้อมูลและเมตาดาต้าของไฟล์ด้วย)


  • ทำหน้าที่: วนซ้ำผ่านชุดข้อมูล (train, valid, test) และเรียกใช้ฟังก์ชัน copy_files เพื่อคัดลอกไฟล์ทั้งหมดที่ถูกจัดสรรไว้ใน final_split_files

  • ผลลัพธ์: แสดงจำนวนไฟล์ที่ถูกคัดลอกสำหรับแต่ละชุดข้อมูล

  • ทำหน้าที่: แสดงข้อความยืนยันเมื่อกระบวนการทั้งหมดเสร็จสิ้น

  • ผลลัพธ์: ✅ Dataset split completed. (พร้อมกับตัวเลขของไฟล์ที่คัดลอกในบรรทัดก่อนหน้า)

Last updated