main.py

Code

from fastapi import FastAPI, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
import shutil, uuid, os, cv2, numpy as np
from ultralytics import YOLO
import tensorflow as tf
from tensorflow.keras.applications.mobilenet_v2 import preprocess_input
import json

# สร้างเซิร์ฟเวอร์หลักชื่อ app ที่จะคอยรับส่งข้อมูลกับภายนอก
app = FastAPI()

# การตั้งค่า CORS (Cross-Origin Resource Sharing) สำหรับ FastAPI เพื่อให้ API สามารถรับคำขอจากทุกโดเมน
app.add_middleware( #   
    CORSMiddleware,
    allow_origins=["*"], # อนุญาตทุกโดเมน (ทุกเว็บไซต์สามารถเรียก API นี้ได้)
    allow_credentials=True, # อนุญาตให้ส่งข้อมูลพวก cookie หรือ header ที่เกี่ยวกับการยืนยันตัวตน
    allow_methods=["*"],    # อนุญาตทุก HTTP method เช่น GET, POST, PUT, DELETE
    allow_headers=["*"],    # อนุญาตทุก heade
)

# โฟลเดอร์ชั่วคราว
UPLOAD_FOLDER = "temp_images"
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

# โหลดโมเดล YOLOv11 สำหรับการจำแนกสายพันธุ์ สุนัขและแมว
yolo_model = YOLO("best.pt")
# โหลดโมเดล MobileNetV2 สำหรับการจำแนกอายุ
age_model = "mobilenetv2.tflite"

# โหลด TFLite interpreter
age_interpreter = tf.lite.Interpreter(model_path=age_model)
age_interpreter.allocate_tensors()

# เตรียมรายละเอียด input/output ของ TFLite
age_input_details = age_interpreter.get_input_details()
age_output_details = age_interpreter.get_output_details()

# ช่วงวัย (ตามที่โมเดลเทรนไว้)
age_labels = ["cat_adult","cat_kitten","cat_senior","cat_young",
              "dog_adult","dog_puppy","dog_senior","dog_young"]

# สร้าง mapping สำหรับแปลง label ระหว่างแมวกับหมา เพื่อให้ YOLO + Age model ตรงกัน
age_mapping = {
    # cat → dog
    "cat_kitten": "dog_puppy",
    "cat_young": "dog_young",
    "cat_adult": "dog_adult",
    "cat_senior": "dog_senior",
    # dog → cat
    "dog_puppy": "cat_kitten",
    "dog_young": "cat_young",
    "dog_adult": "cat_adult",
    "dog_senior": "cat_senior"
}

# =========================
# Helper Functions
# =========================
# สร้างฟังก์ชันสำหรับเตรียมภาพก่อนส่งเข้าโมเดลทำนายอายุ
def preprocess_for_age(crop_img):
    """
    รับ crop_img (BGR uint8) -> คืนค่าเป็น array shape (1,224,224,3) float32 ที่ preprocess_input ทำแล้ว
    NOTE: ถ้า TFLite รองรับ uint8 จะถูกแปลงใน predict function
    """
    # ตรวจสอบว่าภาพที่รับเข้ามาว่างหรือไม่ ถ้าว่างให้คืนค่า None
    if crop_img is None or crop_img.size == 0:
        return None
    # แปลงภาพจากสี BGR (ที่ OpenCV ใช้) เป็น RGB (ที่โมเดลต้องการ)
    img = cv2.cvtColor(crop_img, cv2.COLOR_BGR2RGB)
    # ปรับขนาดภาพให้เป็น 224x224 พิกเซล ตามที่โมเดล MobileNetV2 ต้องการ
    img = cv2.resize(img, (224,224))
    # เพิ่มมิติให้กลายเป็น (1,224,224,3) เพื่อให้เหมาะกับ input ของโมเดล และแปลงเป็น float32
    img = np.expand_dims(img, axis=0).astype(np.float32)  # (1,224,224,3)
    # ใช้ฟังก์ชัน preprocess_input ของ MobileNetV2 เพื่อปรับค่าสีให้อยู่ในช่วง [-1,1] ตามที่โมเดลเทรนไว้
    img = preprocess_input(img)
    return img # คืนค่าภาพที่เตรียมเสร็จแล้วสำหรับนำไปทำนายอายุ

# ทำนายอายุ
def tflite_predict_age(interpreter, input_details, output_details, preprocessed_img):
    """
    ทำ inference ด้วย tflite interpreter
    - preprocessed_img: numpy array float32 shape (1,224,224,3) in [-1,1]
    - จัดการกรณี input dtype เป็น uint8 (quantized) หรือ float32
    คืนค่า: (age_idx, probs_array) หรือ (None, None) ถ้ามีปัญหา
    """
    # ตรวจสอบว่า input ที่เตรียมมาไม่ว่าง ถ้าว่างคืน None
    if preprocessed_img is None:
        return None, None

    # ดึงชนิดข้อมูล (dtype), index ของ input และ output tensor จาก interpreter
    input_dtype = input_details[0]['dtype']
    input_index = input_details[0]['index']
    output_index = output_details[0]['index']

    # เตรียมข้อมูลให้ตรงกับชนิดที่โมเดลต้องการ (uint8 หรือ float32)
    if input_dtype == np.uint8:
        # ถ้าโมเดล quantized รับ uint8, แปลงจาก [-1,1] -> [0,255] (ประมาณ)
        # วิธีแปลงนี้ขึ้นกับวิธีแปลงที่ใช้ตอน convert tflite; ปรับได้ถ้าต้องการ
        input_data = ((preprocessed_img + 1.0) * 127.5).astype(np.uint8)
    else:
        # default float32
        input_data = preprocessed_img.astype(np.float32)

    try:
        # ส่งข้อมูลเข้าโมเดล, รันโมเดล, ดึงผลลัพธ์ออกมา
        interpreter.set_tensor(input_index, input_data)
        interpreter.invoke()
        output_data = interpreter.get_tensor(output_index)  # shape (1, num_classes)
        
        # ลดมิติ output ให้เหลือแค่ 1D (เช่น [0.1, 0.7, 0.2])
        probs = np.squeeze(output_data)
        
        # ถ้าผลรวมของ probs ไม่ใกล้ 1 (ยังไม่ softmax) ให้ทำ softmax เพื่อ normalize
        if probs.ndim == 1 and not (0.99 <= probs.sum() <= 1.01):
            e = np.exp(probs - np.max(probs))
            probs = e / e.sum()
        
        # หา index ของ class ที่มีค่าความมั่นใจสูงสุด (ช่วงอายุที่โมเดลทำนาย)
        age_idx = int(np.argmax(probs))

        # คืนค่า index ของช่วงอายุ และ array ของความมั่นใจแต่ละ class
        return age_idx, probs
    except Exception as e:
        # ถ้าการ inference ผิดพลาด ให้คืน None
        print("TFLite inference error:", e)
        return None, None

# =========================
# Endpoint
# =========================
# รับไฟล์รูปหลายรูป ผ่าน API, ตรวจจับสัตว์ด้วย YOLO, ตัดภาพ, ทำนายอายุ, และคืนผลลัพธ์เป็น JSON
@app.post("/analyze")
async def analyze_images(files: list[UploadFile] = File(...)):
    """
    รับไฟล์รูปหลายรูป, ทำการตรวจจับด้วย YOLO แล้วประเมินอายุด้วย TFLite model
    คืน JSON ที่มี path ของรูปผลลัพธ์ในเซิร์ฟเวอร์ และรายละเอียด detections
    """
    all_results = [] # สร้างลิสต์สำหรับเก็บผลลัพธ์ของแต่ละไฟล์

    # เริ่มการวิเคราะห์: โปรแกรมจะเริ่มทำงานกับรูปภาพทีละรูปที่ส่งมา
    for file in files:

        # สร้างชื่อไฟล์ใหม่แบบสุ่มและบันทึกไฟล์ลงโฟลเดอร์ชั่วคราว
        file_id = str(uuid.uuid4())
        filename = file.filename
        file_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{filename}")

        # เขียนข้อมูลไฟล์ที่อัพโหลดลงดิสก์
        with open(file_path, "wb") as buffer:
            shutil.copyfileobj(file.file, buffer)

        # YOLO ตรวจจับ
        try:
            # ส่งรูปที่บันทึกไว้ให้ YOLO เพื่อตรวจจับและระบุตำแหน่ง/ชนิดของสัตว์
            yolo_out = yolo_model(file_path)[0]  # ผลของ ultralytics
        except Exception as e:
            # ถ้า YOLO ตรวจจับผิดพลาด ให้บันทึก error แล้วข้ามไฟล์นี้
            all_results.append({
                "original_file": filename,
                "error": f"YOLO inference error: {str(e)}"
            })
            continue

        # เตรียมลิสต์สำหรับเก็บข้อมูลการตรวจจับและภาพที่ถูก crop
        detections = []
        cropped_animals = []

        # อ่านภาพต้นฉบับ: โหลดรูปภาพกลับมาเพื่อเตรียมพร้อมสำหรับการตัดภาพตามผลตรวจจับ
        img_cv_full = cv2.imread(file_path)
        if img_cv_full is None:
            all_results.append({
                "original_file": filename,
                "error": "ไม่สามารถอ่านไฟล์ภาพได้"
            })
            continue
        h_full, w_full = img_cv_full.shape[:2]

        # ตัดรูปเฉพาะตัวสัตว์: วนลูปดูว่า YOLO ตรวจพบสัตว์กี่ตัว แล้วใช้พิกัดที่ตรวจพบ ตัดภาพ เฉพาะส่วนที่เป็นตัวสัตว์ออกมา
        for det in yolo_out.boxes:
            try:
                # ดึงค่าพิกัด, ความมั่นใจ, คลาส และ label ของสัตว์แต่ละตัว
                x1, y1, x2, y2 = det.xyxy[0].tolist()
                conf = float(det.conf[0])
                cls = int(det.cls[0])
                label = yolo_model.names[cls] if hasattr(yolo_model, "names") else str(cls)
                
                # ปรับพิกัดให้อยู่ในขอบเขตภาพ
                x1i, y1i = max(0, int(x1)), max(0, int(y1))
                x2i, y2i = min(w_full-1, int(x2)), min(h_full-1, int(y2))

                # เก็บข้อมูลการตรวจจับแต่ละตัว
                detections.append({
                    "label": label,
                    "confidence": conf,
                    "bbox": [x1i, y1i, x2i, y2i]
                })

                # ตัดภาพเฉพาะส่วนที่เป็นสัตว์แต่ละตัว
                if x2i > x1i and y2i > y1i:
                    crop_img = img_cv_full[y1i:y2i, x1i:x2i]
                else:
                    crop_img = None
                cropped_animals.append(crop_img)
            except Exception as e:
                print("Error parsing detection:", e)
                continue

        # ถ้าไม่พบสัตว์เลย ให้บันทึกข้อความและลบไฟล์
        if not detections:
            all_results.append({
                "original_file": filename,
                "message": "ไม่พบสัตว์ในภาพ (หลัง parse)"
            })
            if os.path.exists(file_path):
                os.remove(file_path)
            continue

        # ประเมินอายุสัตว์แต่ละตัวด้วยโมเดล TFLite และเก็บผลลัพธ์
        age_results = []
        for crop in cropped_animals:
            if crop is None or crop.size == 0:
                age_results.append(None)
                continue
            pre = preprocess_for_age(crop)  # float32 [-1,1]
            # นำภาพที่ตัดแล้วมาประมวลผลและส่งให้โมเดลอายุเพื่อทำนายช่วงอายุ
            age_idx, probs = tflite_predict_age(
                age_interpreter,
                age_input_details,
                age_output_details,
                pre
            )
            if age_idx is None or probs is None:
                age_results.append(None)
            else:
                age_results.append({
                    "age_range": age_labels[age_idx] if age_idx < len(age_labels) else f"idx_{age_idx}",
                    "confidence": float(probs[age_idx]) if len(probs) > age_idx else float(np.max(probs)),
                })

        # จัด JSON ผลลัพธ์
        result = {
            "original_file": file.filename,
            "detections": []
        }

        # สร้าง dictionary สำหรับแต่ละ detection (สัตว์แต่ละตัว) 
        # โดยใส่ label, confidence, bbox
        for i, det in enumerate(detections):
            entry = {
                "label": det["label"],
                "confidence": det["confidence"],
                "bbox": det["bbox"]
            }

            # ถ้ามีผลลัพธ์การทำนายอายุสำหรับตัวนี้ ให้ดึงช่วงอายุและความมั่นใจ
            if i < len(age_results) and age_results[i] is not None:
                predicted_age = age_results[i]["age_range"]
                predicted_conf = age_results[i]["confidence"]

                # ปรับอายุให้ตรงประเภทสัตว์จาก YOLO โดยใช้ mapping
                # ตรวจสอบว่า label เป็นหมาหรือแมว แล้วปรับช่วงอายุให้ตรงกับชนิดสัตว์ (ถ้า label กับ age ไม่ตรงกัน)
                if "dog" in det["label"].lower():
                    entry["animalType"] = "dog"
                    if predicted_age in age_mapping and predicted_age.startswith("cat_"):
                        predicted_age = age_mapping[predicted_age]
                elif "cat" in det["label"].lower():
                    entry["animalType"] = "cat"
                    if predicted_age in age_mapping and predicted_age.startswith("dog_"):
                        predicted_age = age_mapping[predicted_age]

                # เพิ่มข้อมูลช่วงอายุและความมั่นใจเข้าไปใน entry
                entry.update({
                    "age_range": predicted_age,
                    "age_confidence": predicted_conf,
                })
            else:
                # ถ้าไม่มีผลลัพธ์อายุ ให้ใส่ None
                entry.update({
                    "age_range": None,
                    "age_confidence": None,
                })

                # ถ้า label ลงท้ายด้วย _cat หรือ _dog ให้ระบุชนิดสัตว์
                if det["label"].endswith("_cat"):
                    entry["animalType"] = "cat"
                elif det["label"].endswith("_dog"):
                    entry["animalType"] = "dog"

            # เพิ่ม entry นี้เข้าไปในลิสต์ detections ของผลลัพธ์ไฟล์นี้
            result["detections"].append(entry)

        # รวบรวมผลลัพธ์: จัดเก็บข้อมูลทั้งหมด 
        # (ชื่อไฟล์, ชนิดสัตว์, พิกัด, ช่วงอายุ, ความมั่นใจ) เข้าเป็นชุดข้อมูลสำหรับรูปนั้นๆ
        all_results.append(result)

        # ลบไฟล์ input ชั่วคราว
        if os.path.exists(file_path):
            os.remove(file_path)

    # ส่งผลลัพธ์กลับ: ส่งชุดข้อมูลสรุปผลการวิเคราะห์ทั้งหมดกลับไปยังผู้ใช้ในรูปแบบ JSON
    return JSONResponse(content={"results": all_results})

ผลลัพธ์


อธิบาย

โครงสร้างและการนำเข้า (Imports and Setup)

โค้ด (Code)
คำอธิบายหน้าที่ (Function/Purpose)
เหตุผลที่ต้องเขียน (Necessity)
ผลลัพธ์/ผลกระทบ (Effect)

from fastapi import FastAPI, UploadFile, File, Form

นำเข้าคลาสและฟังก์ชันหลักจากไลบรารี FastAPI

เป็นส่วนสำคัญในการสร้าง API: FastAPI คือตัวสร้างเซิร์ฟเวอร์, UploadFile สำหรับการรับไฟล์, และ File/Form เป็นตัวกำหนดประเภทของพารามิเตอร์ที่รับเข้า.

สามารถสร้างแอปพลิเคชัน API และกำหนด Endpoint ที่รับไฟล์อัปโหลดได้.

from fastapi.middleware.cors import CORSMiddleware

นำเข้าคลาส CORSMiddleware สำหรับจัดการ CORS

จำเป็นต้องจัดการ CORS (Cross-Origin Resource Sharing) เพื่อให้ API สามารถถูกเรียกใช้ได้จากโดเมนอื่น (เช่น หน้าเว็บ frontend ที่ไม่ใช่โดเมนเดียวกัน).

ทำให้ API สามารถให้บริการแก่ Client ที่มาจากโดเมนอื่นได้อย่างปลอดภัย.

from fastapi.responses import JSONResponse

นำเข้าคลาส JSONResponse

ใช้สำหรับส่งข้อมูลตอบกลับในรูปแบบ JSON โดยเฉพาะในกรณีที่ต้องการกำหนด HTTP Status Code หรือ Content ที่ซับซ้อนกว่าการคืนค่า Python Dictionary ปกติ.

ทำให้การตอบกลับของ API เป็นรูปแบบ JSON ที่ถูกต้อง.

import shutil, uuid, os, cv2, numpy as np

นำเข้าไลบรารีมาตรฐานและไลบรารีสำหรับการประมวลผลภาพ

นำเข้าเครื่องมือที่จำเป็น: shutil (จัดการไฟล์), uuid (สร้าง ID สุ่ม), os (จัดการระบบไฟล์), cv2 (OpenCV) สำหรับการประมวลผลภาพ, และ numpy สำหรับการจัดการ Array ทางคณิตศาสตร์.

สามารถจัดการไฟล์, สร้างชื่อไฟล์เฉพาะ, อ่าน/เขียน/ตัด/ปรับภาพ, และประมวลผลข้อมูลในรูปแบบ Array ได้.

from ultralytics import YOLO

นำเข้าคลาส YOLO จากไลบรารี Ultralytics

ใช้สำหรับโหลดและรันโมเดล YOLO (You Only Look Once) ที่เป็นโมเดลสำหรับ Object Detection (ตรวจจับสายพันธุ์และตำแหน่งสัตว์).

สามารถโหลดและใช้โมเดล YOLO เพื่อตรวจจับสัตว์ในภาพได้.

import tensorflow as tf

นำเข้าไลบรารี TensorFlow

ใช้สำหรับการโหลดและรันโมเดลในรูปแบบ TFLite (TensorFlow Lite) ซึ่งใช้สำหรับทำนายอายุสัตว์.

สามารถใช้งาน TFLite Interpreter เพื่อประมวลผลโมเดล MobileNetV2 ได้.

from tensorflow.keras.applications.mobilenet_v2 import preprocess_input

นำเข้าฟังก์ชัน preprocess_input จาก MobileNetV2

ฟังก์ชันนี้ใช้สำหรับปรับค่าพิกเซลของภาพ (Normalization) ให้อยู่ในช่วง [1,1][−1,1] ตามที่โมเดล MobileNetV2 ถูกฝึกมา.

ทำให้ภาพที่นำเข้าโมเดลอายุมีค่าสีที่ถูกต้องตามที่โมเดลคาดหวัง.

import json

นำเข้าไลบรารี json

ใช้ในกรณีที่ต้องการจัดการข้อมูล JSON โดยตรง แม้ว่า FastAPI จะจัดการการแปลง Dict เป็น JSON โดยอัตโนมัติ. (ในโค้ดนี้ไม่ได้ใช้ json โดยตรง แต่มีการนำเข้าไว้).

-

app = FastAPI()

สร้างเซิร์ฟเวอร์หลักชื่อ app

เป็นขั้นตอนแรกในการเริ่มต้นแอปพลิเคชัน FastAPI. ตัวแปร app จะถูกใช้ในการกำหนด Middleware และ Endpoint ต่างๆ.

สร้างอินสแตนซ์ของเซิร์ฟเวอร์ FastAPI พร้อมใช้งาน.


การตั้งค่า (Configuration)

โค้ด (Code)
คำอธิบายหน้าที่ (Function/Purpose)
เหตุผลที่ต้องเขียน (Necessity)
ผลลัพธ์/ผลกระทบ (Effect)

app.add_middleware(

เริ่มต้นการกำหนดค่า Middleware

Middleware จะถูกรันก่อน/หลังการประมวลผลคำขอ (Request) หลัก. ในที่นี้ใช้สำหรับกำหนด CORS.

เริ่มการตั้งค่า CORS.

CORSMiddleware,

ระบุว่าใช้ CORSMiddleware

-

-

allow_origins=["*"],

อนุญาตให้คำขอมาจาก ทุกโดเมน (*)

เพื่อให้ API สามารถถูกเรียกใช้ได้จากทุกเว็บไซต์ (เหมาะสำหรับการทดสอบหรือ API สาธารณะ).

เซิร์ฟเวอร์จะยอมรับคำขอจากโดเมนใดๆ ก็ตาม.

allow_credentials=True,

อนุญาตให้ส่ง Credentials (เช่น Cookie, Header)

-

-

allow_methods=["*"],

อนุญาตให้ใช้ ทุก HTTP Method (GET, POST, ฯลฯ)

-

-

allow_headers=["*"],

อนุญาตให้ใช้ ทุก Header ในคำขอ

-

-

)

สิ้นสุดการกำหนด Middleware

-

-

UPLOAD_FOLDER = "temp_images"

กำหนดชื่อโฟลเดอร์ชั่วคราว

ใช้เป็นที่เก็บรูปภาพที่อัปโหลดเข้ามาก่อนการประมวลผล.

ตัวแปร UPLOAD_FOLDER ถูกกำหนดเป็น "temp_images".

os.makedirs(UPLOAD_FOLDER, exist_ok=True)

สร้างโฟลเดอร์ชั่วคราว

จำเป็นต้องมีโฟลเดอร์สำหรับเก็บไฟล์ที่อัปโหลด. exist_ok=True จะป้องกัน Error หากโฟลเดอร์มีอยู่แล้ว.

โฟลเดอร์ชื่อ temp_images จะถูกสร้างขึ้นใน Directory เดียวกันกับโค้ด.

yolo_model = YOLO("best.pt")

โหลดโมเดล YOLOv11

ใช้ไฟล์น้ำหนักโมเดล (best.pt) เพื่อโหลดโมเดล Object Detection สำหรับการตรวจจับสายพันธุ์และตำแหน่งสัตว์.

ตัวแปร yolo_model จะเป็นอินสแตนซ์ของโมเดล YOLO ที่พร้อมใช้งาน.

age_model = "mobilenetv2.tflite"

กำหนดชื่อไฟล์โมเดลอายุ

กำหนด path ของโมเดล TFLite ที่ใช้ในการจำแนกช่วงอายุ.

ตัวแปร age_model ถูกกำหนดเป็นชื่อไฟล์.

age_interpreter = tf.lite.Interpreter(model_path=age_model)

โหลด TFLite Interpreter

TFLite Interpreter เป็นเครื่องมือสำหรับรันโมเดล TFLite อย่างมีประสิทธิภาพ.

ตัวแปร age_interpreter จะถูกสร้างขึ้น พร้อมโหลดโครงสร้างโมเดลจากไฟล์.

age_interpreter.allocate_tensors()

จัดสรรหน่วยความจำสำหรับ Tensor

เป็นขั้นตอนที่จำเป็นสำหรับการรัน TFLite Interpreter เพื่อสำรองหน่วยความจำให้กับ Input/Output Tensors ของโมเดล.

โมเดลอายุพร้อมสำหรับการรัน Inference.

age_input_details = age_interpreter.get_input_details()

ดึงรายละเอียด Input Tensor

เก็บรายละเอียดของ Input Layer ที่โมเดล TFLite ต้องการ (เช่น Shape, Type, Index).

ตัวแปร age_input_details จะเก็บข้อมูลที่จำเป็นสำหรับการส่งภาพเข้าโมเดล.

age_output_details = age_interpreter.get_output_details()

ดึงรายละเอียด Output Tensor

เก็บรายละเอียดของ Output Layer ที่โมเดล TFLite จะส่งออกมา (เช่น Shape, Index).

ตัวแปร age_output_details จะเก็บข้อมูลที่จำเป็นสำหรับการดึงผลลัพธ์จากโมเดล.

age_labels = ["cat_adult",...,"dog_young"]

กำหนดป้ายชื่อ (Labels) ของช่วงอายุ

ลิสต์ของป้ายชื่อที่โมเดลอายุถูกฝึกมา ซึ่งเป็น Output ที่ทำนายได้.

ตัวแปร age_labels จะถูกใช้ในการแปลง Index (ตัวเลข) จากโมเดลให้เป็นช่วงอายุ (ข้อความ).

age_mapping = {...}

สร้าง Dictionary สำหรับการแปลง Label

ใช้สำหรับแก้ไขผลการทำนายอายุที่ได้จากโมเดลอายุ ให้สอดคล้องกับประเภทสัตว์ (Cat/Dog) ที่ตรวจจับได้จากโมเดล YOLO.

หาก YOLO บอกว่าเป็นสุนัข แต่โมเดลอายุทำนายว่าเป็น cat_kitten, Mapping นี้จะเปลี่ยนผลลัพธ์เป็น dog_puppy.


ฟังก์ชันตัวช่วย (Helper Functions)

โค้ด (Code)
คำอธิบายหน้าที่ (Function/Purpose)
เหตุผลที่ต้องเขียน (Necessity)
ผลลัพธ์/ผลกระทบ (Effect)

def preprocess_for_age(crop_img):

ฟังก์ชันสำหรับเตรียมภาพตัด (Crop Image) ก่อนนำเข้าโมเดลอายุ

โมเดล MobileNetV2 ต้องการ Input เป็นภาพขนาด 224×224224×224 พิกเซล, สี RGB, และค่าพิกเซลถูก Normalize อยู่ในช่วง [1,1][−1,1].

เมื่อเรียกใช้ จะได้ Array ที่พร้อมเป็น Input สำหรับโมเดลอายุ.

if crop_img is None or crop_img.size == 0:

ตรวจสอบภาพว่าง

ป้องกัน Error หากภาพที่ตัดออกมาไม่มีข้อมูล.

คืนค่า None หากภาพไม่ถูกต้อง.

img = cv2.cvtColor(crop_img, cv2.COLOR_BGR2RGB)

แปลงสีจาก BGR เป็น RGB

OpenCV อ่านภาพเป็น BGR แต่โมเดล ML ส่วนใหญ่ใช้ RGB.

ได้ภาพในรูปแบบ RGB.

img = cv2.resize(img, (224,224))

ปรับขนาดภาพ

ปรับขนาดภาพให้ตรงกับ Input Shape (224,224)(224,224) ที่โมเดล MobileNetV2 ถูกฝึกมา.

ได้ภาพขนาด 224×224224×224.

img = np.expand_dims(img, axis=0).astype(np.float32)

เพิ่มมิติและแปลงชนิดข้อมูล

เพิ่มมิติ Batch Size (จาก (224,224,3)(224,224,3) เป็น (1,224,224,3)(1,224,224,3)) และแปลงเป็น Float 32.

ได้ Input Array ที่มี Shape ถูกต้อง (1,224,224,3)(1,224,224,3).

img = preprocess_input(img)

Normalize ค่าสี

ปรับค่าพิกเซลให้อยู่ในช่วง [1,1][−1,1].

ได้ Array ที่มีค่าสีพร้อมสำหรับการทำนายของ MobileNetV2.

return img

คืนค่าภาพที่เตรียมเสร็จแล้ว

-

คืนค่า Array (1, 224, 224, 3) Float 32.

def tflite_predict_age(...)

ฟังก์ชันสำหรับทำนายอายุด้วย TFLite Interpreter

ใช้สำหรับรันโมเดล TFLite โดยรองรับทั้ง Input ชนิด float32 และ uint8 (Quantized model).

เมื่อเรียกใช้ จะคืนค่า Index ของช่วงอายุ และ Array ของความมั่นใจ (probs).

input_dtype = input_details[0]['dtype']

ดึงชนิดข้อมูลที่โมเดลต้องการ

ตรวจสอบว่าโมเดลอายุต้องการ Input เป็น uint8 (Quantized) หรือ float32.

ได้ชนิดข้อมูลของ Input.

if input_dtype == np.uint8:

จัดการกรณี Input เป็น uint8

แปลงค่าจาก [1,1][−1,1] (หลัง preprocess_input) กลับเป็น [0,255][0,255] โดยประมาณ เพื่อให้เข้ากันกับโมเดล Quantized.

ตัวแปร input_data เป็น Array uint8.

else: input_data = preprocessed_img.astype(np.float32)

จัดการกรณี Input เป็น float32

ใช้ค่าที่เตรียมมาแล้ว (เป็น Float 32 อยู่แล้ว).

ตัวแปร input_data เป็น Array float32.

interpreter.set_tensor(input_index, input_data)

ส่งข้อมูลเข้าโมเดล

กำหนดข้อมูล Input Array เข้าไปยัง Tensor ที่ถูกต้องใน Interpreter.

โมเดลได้รับข้อมูลภาพ.

interpreter.invoke()

รันโมเดล (Inference)

สั่งให้ TFLite Interpreter ประมวลผลภาพเพื่อทำนายผลลัพธ์.

ผลลัพธ์ถูกเก็บไว้ใน Output Tensor.

output_data = interpreter.get_tensor(output_index)

ดึงผลลัพธ์

ดึง Array ผลลัพธ์ (Logits หรือ Probabilities) ออกมาจาก Output Tensor.

ตัวแปร output_data เก็บ Array ผลลัพธ์ (เช่น (1,8)(1,8)).

probs = np.squeeze(output_data)

ลดมิติของผลลัพธ์

ลดมิติ Batch Size (จาก (1,8)(1,8) เป็น (8,)(8,)) เพื่อให้ง่ายต่อการประมวลผล.

ได้ Array ความมั่นใจ 1D1D (เช่น [0.1,0.7,0.2,...][0.1,0.7,0.2,...]).

if probs.ndim == 1 and not (0.99 <= probs.sum() <= 1.01):

ตรวจสอบและทำ Softmax

ถ้าผลลัพธ์ยังไม่ใช่ Probabilities (ผลรวมไม่ได้ 1≈1) ให้ทำการคำนวณ Softmax เพื่อหาค่าความน่าจะเป็นที่ถูกต้อง.

ตัวแปร probs เป็น Array ที่มีค่าความน่าจะเป็นแต่ละ Class ที่ถูกต้อง.

age_idx = int(np.argmax(probs))

หา Index ของ Class ที่มั่นใจสูงสุด

หา Index (ตัวเลข 0-7) ที่สอดคล้องกับช่วงอายุที่โมเดลทำนายว่ามีความมั่นใจสูงสุด.

ได้ Index ของช่วงอายุที่ทำนายได้.

return age_idx, probs

คืนผลลัพธ์

-

คืนค่า Index และ Array ความน่าจะเป็นทั้งหมด.


Endpoint API (/analyze)

โค้ด (Code)
คำอธิบายหน้าที่ (Function/Purpose)
เหตุผลที่ต้องเขียน (Necessity)
ผลลัพธ์/ผลกระทบ (Effect)

@app.post("/analyze")

กำหนด Endpoint POST ที่ Path /analyze

เป็น API Endpoint หลักที่ Client จะใช้ส่งไฟล์รูปภาพ. POST ใช้สำหรับการส่งข้อมูล (Upload File).

เมื่อ Client ส่งคำขอ POST มาที่ /analyze, ฟังก์ชัน analyze_images จะถูกเรียกใช้.

async def analyze_images(files: list[UploadFile] = File(...)):

กำหนดฟังก์ชัน API ที่รับไฟล์หลายไฟล์

รับ Input เป็นลิสต์ของไฟล์อัปโหลดหลายไฟล์ (รองรับการอัปโหลดหลายรูปในครั้งเดียว).

ฟังก์ชันนี้จะประมวลผลรูปภาพแต่ละรูปที่อัปโหลด.

all_results = []

สร้างลิสต์สำหรับเก็บผลลัพธ์รวม

ใช้สำหรับเก็บผลลัพธ์ JSON ของการวิเคราะห์แต่ละไฟล์รูป.

-

for file in files:

เริ่มต้นวนลูปประมวลผลทีละไฟล์

ประมวลผลไฟล์รูปภาพทั้งหมดที่ถูกอัปโหลดเข้ามา.

-

file_id = str(uuid.uuid4())

สร้าง ID สุ่ม (UUID)

ใช้สำหรับสร้างชื่อไฟล์ชั่วคราวที่ไม่ซ้ำกันเพื่อป้องกันชื่อไฟล์ซ้ำซ้อน.

ได้ String ID ที่ไม่ซ้ำ.

file_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{filename}")

กำหนด Path สำหรับบันทึกไฟล์

สร้าง Path เต็มสำหรับบันทึกไฟล์ชั่วคราวในโฟลเดอร์ temp_images.

-

with open(file_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer)

บันทึกไฟล์ที่อัปโหลดลงดิสก์

จำเป็นต้องบันทึกไฟล์ลงในระบบไฟล์ชั่วคราวเพื่อนำไปใช้กับโมเดล YOLO (ที่ต้องการ Path ในการอ่านไฟล์).

ไฟล์รูปภาพถูกบันทึกใน temp_images/ (เช่น temp_images/d0dc...3ad7_35.jpg).

yolo_out = yolo_model(file_path)[0]

รันการตรวจจับด้วย YOLO

ส่ง Path ของไฟล์รูปภาพเข้าโมเดล YOLO เพื่อทำการตรวจจับ (Object Detection).

ตัวแปร yolo_out เก็บผลลัพธ์การตรวจจับทั้งหมดในภาพนั้น (ตำแหน่ง, Class, Confidence).

img_cv_full = cv2.imread(file_path)

อ่านภาพต้นฉบับด้วย OpenCV

โหลดภาพต้นฉบับกลับมาในรูปแบบ NumPy Array (BGR) เพื่อใช้ในการตัดภาพตามพิกัดที่ YOLO ตรวจจับได้.

ตัวแปร img_cv_full เก็บภาพในรูปแบบ Array.

for det in yolo_out.boxes:

วนลูปดูผลลัพธ์การตรวจจับแต่ละตัว

ประมวลผลสัตว์แต่ละตัวที่ YOLO ตรวจจับได้.

-

x1, y1, x2, y2 = det.xyxy[0].tolist()

ดึงพิกัด Bounding Box

ดึงพิกัดของกรอบสี่เหลี่ยมที่ล้อมรอบสัตว์ออกมา.

ได้พิกัดขอบบนซ้าย (x1,y1)(x1,y1) และขอบล่างขวา (x2,y2)(x2,y2).

label = yolo_model.names[cls]

ดึง Label (สายพันธุ์)

แปลง Class ID (ตัวเลข) ที่ได้จาก YOLO ให้เป็นชื่อสายพันธุ์ (String).

ได้ชื่อสายพันธุ์ของสัตว์ที่ตรวจจับได้ (เช่น "british_shorthair_cat").

crop_img = img_cv_full[y1i:y2i, x1i:x2i]

ตัดภาพ (Crop) เฉพาะส่วนของสัตว์

ใช้พิกัดที่ปรับแล้วตัดภาพส่วนที่เป็นสัตว์ออกจากภาพต้นฉบับ.

ตัวแปร crop_img เก็บภาพของสัตว์ตัวเดียว.

cropped_animals.append(crop_img)

เก็บภาพที่ตัดไว้

เตรียมภาพที่ตัดแล้วทั้งหมดสำหรับนำไปทำนายอายุในขั้นตอนต่อไป.

-

for crop in cropped_animals:

วนลูปเพื่อทำนายอายุสัตว์แต่ละตัว

-

-

pre = preprocess_for_age(crop)

เตรียมภาพสำหรับโมเดลอายุ

เรียกใช้ฟังก์ชันตัวช่วยเพื่อเตรียมภาพตัด (Resize, Normalize) ให้พร้อมสำหรับโมเดล TFLite.

ตัวแปร pre เป็น Array พร้อม Input สำหรับโมเดลอายุ.

age_idx, probs = tflite_predict_age(...)

รันการทำนายอายุด้วย TFLite

ส่งภาพที่เตรียมแล้วเข้าโมเดล TFLite.

ได้ Index ของช่วงอายุและ Array ความน่าจะเป็น.

predicted_age = age_results[i]["age_range"]

ดึงช่วงอายุที่ทำนายได้

ดึงชื่อช่วงอายุ (Label) ออกจากผลลัพธ์การทำนาย.

-

if "dog" in det["label"].lower(): ...

ตรวจสอบประเภทสัตว์และ ปรับช่วงอายุ

ถ้า YOLO บอกว่าเป็นสุนัข แต่โมเดลอายุทำนายเป็น Cat (เช่น cat_kitten) ให้ใช้ age_mapping แปลงให้เป็น Dog (เช่น dog_puppy). (ทำกลับกันสำหรับแมว).

predicted_age จะถูกเปลี่ยนให้ตรงกับประเภทสัตว์ที่ YOLO ตรวจจับได้.

entry.update({...})

เพิ่มข้อมูลอายุและความมั่นใจเข้าไปในผลลัพธ์

-

ผลลัพธ์ JSON สำหรับสัตว์ตัวนี้จะมี age_range และ age_confidence เพิ่มเข้ามา.

all_results.append(result)

เก็บผลลัพธ์ของไฟล์นี้

รวบรวมข้อมูลการวิเคราะห์ทั้งหมดของไฟล์รูปปัจจุบัน.

-

if os.path.exists(file_path): os.remove(file_path)

ลบไฟล์ Input ชั่วคราว

เพื่อทำความสะอาดพื้นที่ดิสก์หลังการประมวลผลเสร็จสิ้น.

ไฟล์รูปภาพที่อัปโหลดถูกลบออกจาก temp_images/.

return JSONResponse(content={"results": all_results})

ส่งผลลัพธ์กลับในรูปแบบ JSON

คืนค่าผลการวิเคราะห์ทั้งหมดในรูปแบบ JSON.

Client ได้รับผลลัพธ์การวิเคราะห์ทั้งหมด.

Last updated