DEV Community

Cover image for สร้าง API ตรวจจับภาพ AI ด้วย C2PA และตัวจำแนกประเภท
Thanawat Wongchai
Thanawat Wongchai

Posted on • Originally published at apidog.com

สร้าง API ตรวจจับภาพ AI ด้วย C2PA และตัวจำแนกประเภท

มีคนอัปโหลดรูปภาพลงในผลิตภัณฑ์ของคุณแล้วอ้างว่ารูปนั้นถ่ายจากกล้อง แบ็กเอนด์ของคุณพิสูจน์หรือหักล้างได้ไหม? ทุกวันนี้ตัวสร้างภาพทำให้รูปดูสมจริงพอจะหลอกสายตามนุษย์ได้ ดังนั้นวิธีที่ใช้งานได้จริงคือรวมสัญญาณอิสระ 2 แบบเข้าด้วยกัน: แหล่งที่มาที่ตรวจสอบได้ด้วยวิทยาการเข้ารหัส และตัวจำแนก Machine Learning แล้วสรุปเป็นคำตัดสินเดียวที่น่าเชื่อถือกว่าใช้สัญญาณใดสัญญาณหนึ่งลำพัง

ลองใช้ Apidog วันนี้

บทความนี้จะแนะนำการสร้างบริการแบ็กเอนด์ด้วย Python และ FastAPI ที่มีเอนด์พอยต์ POST /verify สำหรับรับรูปภาพ แล้วส่ง JSON กลับมาพร้อมคำตัดสิน คะแนนความเชื่อมั่น และรายละเอียดแหล่งที่มาที่ตรวจพบ เราจะใช้เครื่องมือ C2PA แบบโอเพนซอร์สสำหรับสัญญาณ provenance และ API ตรวจจับแบบโฮสต์สำหรับสัญญาณ classifier เพราะนี่เป็นโปรเจกต์ API เราจะเริ่มจากการออกแบบสัญญาเอนด์พอยต์ก่อน แล้วใช้ Apidog เพื่อ mock และทดสอบ API เพื่อให้ทีมฟรอนต์เอนด์เริ่มเชื่อมต่อได้ก่อนแบ็กเอนด์เสร็จ

สรุป (TL;DR)

คุณจะสร้างบริการ FastAPI ที่เปิดเผย POST /verify โดยทำงานตามลำดับนี้:

  1. รับไฟล์รูปภาพผ่าน multipart/form-data
  2. ตรวจสอบ C2PA Content Credentials ด้วย c2pa-python
  3. เรียก API ตรวจจับภาพ AI แบบโฮสต์เป็นสัญญาณที่สอง
  4. รวมผลลัพธ์เป็นคำตัดสินเดียว:
    • likely_authentic
    • likely_ai
    • uncertain
  5. ส่งคืน JSON ที่มี confidence, signals, explanation, และ checked_at
  6. ออกแบบ OpenAPI schema แล้วใช้ Apidog สำหรับ mock server และ endpoint testing

ทำไมต้องใช้สองสัญญาณแทนสัญญาณเดียว

ก่อนเขียนโค้ด ต้องแยกให้ออกว่าคุณกำลัง “ตรวจจับ” อะไร ไม่มีคุณสมบัติเดียวของไฟล์ที่บอกได้แน่นอนว่า “มนุษย์สร้าง” หรือ “AI สร้าง” มีแต่เบาะแส ซึ่งแต่ละแบบมีจุดแข็งและจุดอ่อนต่างกัน

สัญญาณที่ 1: Provenance จาก C2PA

C2PA หรือ Coalition for Content Provenance and Authenticity เป็นมาตรฐานเปิดสำหรับแนบ metadata ที่ป้องกันการปลอมแปลงและลงนามด้วยวิทยาการเข้ารหัสกับไฟล์สื่อ metadata นี้เรียกว่า manifest และชื่อที่ผู้ใช้คุ้นเคยคือ Content Credentials

เมื่อกล้อง โปรแกรมแก้ไขภาพ หรือเครื่องมือสร้างภาพที่รองรับ C2PA สร้างหรือแก้ไขภาพ เครื่องมือนั้นสามารถเขียน manifest เพื่อบันทึกประวัติและลงนามด้วยใบรับรองได้ ถ้าคุณอ่านและตรวจสอบ manifest ได้ คุณจะได้หลักฐานที่ตรวจสอบได้เกี่ยวกับประวัติของรูป

ข้อจำกัดคือ C2PA เป็นแบบ opt-in และ manifest หายได้ง่าย เช่น:

  • การจับภาพหน้าจอ
  • การบีบอัดหรือ re-encode ผ่านแอปแชต
  • การอัปโหลดไปยังแพลตฟอร์มที่ลบ metadata

ดังนั้น “ไม่พบ manifest” ไม่ได้แปลว่ารูปปลอม และไม่ได้แปลว่ารูปจริง

สัญญาณที่ 2: Statistical classifier

ตัวจำแนกภาพ AI เป็นโมเดลที่เรียนรู้จากรูปจริงและรูปที่สร้างโดย AI จำนวนมาก แล้วให้คะแนนความน่าจะเป็นว่าภาพนั้นถูกสร้างโดย AI หรือไม่

ข้อดีคือทำงานกับรูปภาพใดก็ได้ แม้ไม่มี metadata แต่ข้อเสียคือเป็นความน่าจะเป็น ไม่ใช่ข้อเท็จจริง อาจผิดพลาดได้ โดยเฉพาะกับรูปที่ถูกบีบอัดหนัก หรือรูปที่อยู่นอก distribution ของข้อมูลฝึก

สรุปคือ:

  • Provenance แม่นยำกว่าเมื่อมี manifest ที่ตรวจสอบได้ แต่พบไม่บ่อย
  • Classifier ใช้ได้กับเกือบทุกรูป แต่ไม่แน่นอน

การรวมสองสัญญาณช่วยให้ API ตอบได้ว่า “cryptography พิสูจน์อะไร”, “โมเดลประเมินอะไร”, และ “เรามั่นใจแค่ไหน” ถ้าต้องการอ่านเพิ่มเติมว่าทำไมวิธีแบบสัญญาณเดียวจึงล้มเหลว ดูบทความ ทำไมการตรวจจับภาพ AI จึงล้มเหลว

ภาพรวมสถาปัตยกรรม

บริการนี้ตั้งใจให้เล็กและตรงไปตรงมา: endpoint เดียว, downstream call สองชุด, response เดียว

                ┌─────────────────────────────┐
   image  ──▶   │   FastAPI POST /verify       │
                │                              │
                │   1. ตรวจสอบการอัปโหลด       │
                │   2. ┌──────────────────┐    │
                │      │ C2PA manifest     │    │  สัญญาณแหล่งที่มา
                │      │ c2pa-python       │    │
                │      └──────────────────┘    │
                │   3. ┌──────────────────┐    │
                │      │ classifier API    │    │  สัญญาณทางสถิติ
                │      │ hosted detector   │    │
                │      └──────────────────┘    │
                │   4. รวมเป็นคำตัดสิน        │
                └─────────────────────────────┘
                              │
                              ▼
                   JSON verdict + confidence
Enter fullscreen mode Exit fullscreen mode

ขั้นตอนหลัก:

  1. ตรวจสอบว่าไฟล์อัปโหลดเป็นรูปภาพที่รองรับและไม่เกินขนาดที่กำหนด
  2. อ่าน C2PA manifest ภายในเครื่องด้วย c2pa-python
  3. ส่งไบต์รูปภาพไปยัง hosted classifier ผ่าน HTTPS
  4. รวมผลลัพธ์ด้วย rule-based policy แล้วส่งคืน JSON

ติดตั้ง dependency:

pip install fastapi "uvicorn[standard]" python-multipart httpx c2pa-python
Enter fullscreen mode Exit fullscreen mode

ต้องใช้ Python 3.10 ขึ้นไป เพราะไลบรารี C2PA ต้องการเวอร์ชันนี้

สร้างสัญญาณ C2PA

Content Authenticity Initiative ภายใต้องค์กร GitHub contentauth เผยแพร่เครื่องมือ C2PA แบบโอเพนซอร์ส โดยมีส่วนที่เกี่ยวข้องหลัก ๆ คือ:

  • c2patool: CLI สำหรับดูและเพิ่ม manifest เหมาะกับการตรวจสอบจาก terminal โปรดทราบว่า standalone repository ถูกเก็บถาวรแล้ว และ CLI อยู่ในโปรเจกต์ Rust c2pa-rs
  • c2pa-python: Python binding สำหรับไลบรารี Rust c2pa-rs ใช้งานผ่าน PyPI ด้วย pip install c2pa-python

สร้างไฟล์ provenance.py:

# provenance.py
import json
import c2pa


def read_provenance(image_path: str) -> dict:
    """
    อ่านและตรวจสอบ C2PA manifest จากรูปภาพ
    คืนค่า dict ที่ normalize แล้ว เพื่อให้ API ใช้งานต่อได้ง่าย
    """
    try:
        with c2pa.Reader(image_path) as reader:
            manifest_store = json.loads(reader.json())

    except c2pa.C2paError as err:
        # รูปส่วนใหญ่จะไม่มี manifest นี่เป็นกรณีปกติ ไม่ใช่ error ของระบบ
        if str(err).startswith("ManifestNotFound"):
            return {
                "has_manifest": False,
                "validation": "none",
                "detail": "ไม่พบ C2PA manifest ในรูปภาพนี้",
            }

        # มีข้อมูล C2PA แต่ parse หรือตรวจสอบไม่ได้
        return {
            "has_manifest": True,
            "validation": "error",
            "detail": f"ไม่สามารถแยกวิเคราะห์ manifest: {err}",
        }

    active_label = manifest_store.get("active_manifest")
    manifests = manifest_store.get("manifests", {})
    active = manifests.get(active_label, {})

    # validation_status จะมีค่าเมื่อมีปัญหาในการตรวจสอบ
    validation_status = manifest_store.get("validation_status", [])
    validation = "valid" if not validation_status else "invalid"

    claim_generator = active.get("claim_generator", "unknown")
    signature_issuer = active.get("signature_info", {}).get("issuer", "unknown")

    return {
        "has_manifest": True,
        "validation": validation,
        "claim_generator": claim_generator,
        "signature_issuer": signature_issuer,
        "validation_status": validation_status,
        "detail": "อ่าน manifest สำเร็จแล้ว",
    }
Enter fullscreen mode Exit fullscreen mode

จุดสำคัญของโค้ดนี้:

  • ใช้ c2pa.Reader เป็น context manager เพื่อคืน resource ให้ถูกต้อง
  • reader.json() คืน manifest store เป็น JSON string
  • ถ้าต้องการรายงานละเอียดกว่าเดิม สามารถใช้ reader.detailed_json()
  • ManifestNotFound เป็น expected case สำหรับรูปส่วนใหญ่
  • validation_status ว่าง แปลว่า manifest ตรวจสอบผ่าน
  • validation_status มีค่า แปลว่า manifest มีปัญหา เช่น signature หรือ hash ไม่ผ่าน

เมื่อมี manifest ฟิลด์ที่มักใช้ตัดสินคือ:

  • claim_generator: เครื่องมือที่สร้างหรือแก้ไข manifest เช่น กล้อง โปรแกรมแก้ไขภาพ หรือเครื่องมือ AI
  • signature_info.issuer: ผู้ออกใบรับรอง
  • validation_status: สถานะการตรวจสอบ

สิ่งที่สัญญาณนี้ทำไม่ได้คือให้คำตอบเมื่อไม่มี manifest ดังนั้นเราต้องมี classifier เป็นสัญญาณสำรอง

สร้างสัญญาณ classifier

ตัวจำแนกคือ hosted API ที่ให้คะแนนความน่าจะเป็นว่าภาพถูกสร้างโดย AI มีผู้ให้บริการหลายราย บทความนี้ใช้ Sightengine เพราะมี HTTP API และ response format ที่ชัดเจน แต่คุณสามารถเปลี่ยนเป็นผู้ให้บริการอื่นได้โดยปรับ URL, parameter และฟิลด์ response ที่อ่าน

ถ้ากำลังเปรียบเทียบ provider อ่านเพิ่มเติมได้ที่ API การตรวจจับภาพ AI ที่ดีที่สุด

Sightengine endpoint:

https://api.sightengine.com/1.0/check.json
Enter fullscreen mode Exit fullscreen mode

ส่งรูปเป็น field media, กำหนด models=genai, และส่ง api_user กับ api_secret ผลลัพธ์จะมี type.ai_generated เป็นคะแนน 0 ถึง 1

สร้างไฟล์ classifier.py:

# classifier.py
import httpx

SIGHTENGINE_URL = "https://api.sightengine.com/1.0/check.json"


async def classify_image(
    image_bytes: bytes,
    filename: str,
    api_user: str,
    api_secret: str,
    timeout_seconds: float = 8.0,
) -> dict:
    """
    ส่งรูปภาพไปยัง hosted detector
    คืนค่า dict ที่ normalize แล้วพร้อม ai_score
    """
    data = {
        "models": "genai",
        "api_user": api_user,
        "api_secret": api_secret,
    }
    files = {"media": (filename, image_bytes)}

    try:
        async with httpx.AsyncClient(timeout=timeout_seconds) as client:
            response = await client.post(SIGHTENGINE_URL, data=data, files=files)
            response.raise_for_status()
            payload = response.json()

    except httpx.TimeoutException:
        return {"available": False, "reason": "classifier_timeout"}

    except httpx.HTTPStatusError as err:
        return {
            "available": False,
            "reason": f"classifier_http_{err.response.status_code}",
        }

    except httpx.HTTPError as err:
        return {"available": False, "reason": f"classifier_error: {err}"}

    if payload.get("status") != "success":
        return {
            "available": False,
            "reason": payload.get("error", {}).get("message", "unknown_error"),
        }

    ai_score = payload.get("type", {}).get("ai_generated")
    if ai_score is None:
        return {"available": False, "reason": "missing_score_in_response"}

    return {"available": True, "ai_score": float(ai_score)}
Enter fullscreen mode Exit fullscreen mode

แนวทางสำคัญ:

  • ใช้ async เพื่อไม่บล็อก event loop
  • ตั้ง timeout ชัดเจน เช่น 8 วินาที
  • ทุก failure path คืน available: False แทนการโยน exception
  • classifier ล่มหรือ timeout ไม่ควรทำให้ /verify ล่ม
  • ค่า ai_score=0.92 แปลว่า “โมเดลค่อนข้างมั่นใจ” ไม่ใช่ “พิสูจน์แล้วว่าเป็น AI”

อ่านภาพรวมเพิ่มเติมได้ที่ วิธีตรวจสอบว่าภาพถูกสร้างโดย AI หรือไม่

ออกแบบสัญญา /verify

นี่คือจุดที่ Apidog มีประโยชน์มาก ก่อนเขียน route handler ให้ออกแบบ request/response เป็น OpenAPI schema ก่อน เพื่อให้ได้:

  1. contract เดียวที่ backend และ frontend เห็นตรงกัน
  2. mock server ที่ frontend เรียกได้ทันที
  3. test case ที่รันซ้ำได้เมื่อ backend พร้อม

Request

POST /verify รับ multipart/form-data โดยมี field เดียว:

Field Type Required Description
image file yes รูปภาพที่ต้องการตรวจสอบ

รองรับ MIME type เช่น:

  • image/jpeg
  • image/png
  • image/webp

Response

ตัวอย่าง response:

{
  "verdict": "likely_ai",
  "confidence": 0.86,
  "signals": {
    "provenance": {
      "has_manifest": true,
      "validation": "valid",
      "claim_generator": "SomeImageTool/2.1",
      "signature_issuer": "Some Issuing CA"
    },
    "classifier": {
      "available": true,
      "ai_score": 0.91
    }
  },
  "explanation": "A valid C2PA manifest names an AI image tool, and the classifier scored the image as likely AI-generated.",
  "checked_at": "2026-05-21T09:30:00Z"
}
Enter fullscreen mode Exit fullscreen mode

ค่า verdict มี 3 แบบ:

  • likely_authentic
  • likely_ai
  • uncertain

ต้องมี uncertain เพราะในระบบตรวจจับแบบนี้ สัญญาณอาจอ่อนหรือขัดแย้งกันได้ การตอบว่าไม่แน่ใจเป็นพฤติกรรมที่ถูกต้องกว่าเดา

OpenAPI component:

components:
  schemas:
    VerifyResponse:
      type: object
      required: [verdict, confidence, signals, checked_at]
      properties:
        verdict:
          type: string
          enum: [likely_authentic, likely_ai, uncertain]
        confidence:
          type: number
          format: float
          minimum: 0
          maximum: 1
        signals:
          type: object
          properties:
            provenance:
              type: object
              properties:
                has_manifest:
                  type: boolean
                validation:
                  type: string
                  enum: [valid, invalid, error, none]
                claim_generator:
                  type: string
                signature_issuer:
                  type: string
            classifier:
              type: object
              properties:
                available:
                  type: boolean
                ai_score:
                  type: number
                  format: float
        explanation:
          type: string
        checked_at:
          type: string
          format: date-time
Enter fullscreen mode Exit fullscreen mode

คุณสามารถเขียน schema นี้ใน visual designer ของ Apidog หรือนำเข้าไฟล์ OpenAPI ที่มีอยู่ ถ้าต้องการ workflow แบบ spec-first ดู คู่มือการใช้งานโหมด spec-first ของ Apidog

เขียนตรรกะรวมสัญญาณ

สร้างไฟล์ verdict.py เพื่อรวม provenance และ classifier เป็นคำตัดสินเดียว

# verdict.py


def combine_signals(provenance: dict, classifier: dict) -> dict:
    """รวมสัญญาณแหล่งที่มาและตัวจำแนกเป็นคำตัดสินเดียว"""
    has_manifest = provenance.get("has_manifest", False)
    validation = provenance.get("validation", "none")
    generator = (provenance.get("claim_generator") or "").lower()

    classifier_ok = classifier.get("available", False)
    ai_score = classifier.get("ai_score")

    # heuristic: เครื่องมือ AI มักระบุตัวเองใน claim_generator
    ai_keywords = (
        "firefly",
        "dall-e",
        "dalle",
        "midjourney",
        "stable",
        "gpt",
        "gemini",
        "imagen",
        "generat",
    )
    generator_looks_ai = any(keyword in generator for keyword in ai_keywords)

    # 1. Manifest ถูกต้องและดูเหมือนมาจากเครื่องมือ AI
    if has_manifest and validation == "valid" and generator_looks_ai:
        return _verdict(
            "likely_ai",
            0.95,
            "C2PA manifest ที่ถูกต้องระบุว่าเป็นเครื่องมือสร้างภาพ AI",
        )

    # 2. Manifest ถูกต้องและไม่ใช่เครื่องมือ AI
    if has_manifest and validation == "valid" and not generator_looks_ai:
        if classifier_ok and ai_score is not None and ai_score > 0.85:
            return _verdict(
                "uncertain",
                0.55,
                "Manifest ดูเป็นของแท้แต่ classifier ให้คะแนน AI สูง; สัญญาณขัดแย้งกัน",
            )

        return _verdict(
            "likely_authentic",
            0.9,
            "พบ C2PA manifest ที่ถูกต้องจากเครื่องมือที่ไม่ใช่ AI",
        )

    # 3. Manifest มีอยู่แต่ตรวจสอบไม่ผ่าน
    if has_manifest and validation in ("invalid", "error"):
        return _verdict(
            "uncertain",
            0.6,
            "ภาพมี C2PA manifest ที่ตรวจสอบไม่ผ่าน; ประวัติที่อ้างสิทธิ์ไม่ได้รับการยืนยัน",
        )

    # 4. ไม่มี manifest ใช้ classifier เป็นหลัก
    if classifier_ok and ai_score is not None:
        if ai_score >= 0.7:
            return _verdict(
                "likely_ai",
                round(ai_score, 2),
                "ไม่มีข้อมูลแหล่งที่มา; classifier ให้คะแนนภาพว่าน่าจะสร้างโดย AI",
            )

        if ai_score <= 0.3:
            return _verdict(
                "likely_authentic",
                round(1 - ai_score, 2),
                "ไม่มีข้อมูลแหล่งที่มา; classifier ให้คะแนนภาพว่าน่าจะเป็นของแท้",
            )

        return _verdict(
            "uncertain",
            0.5,
            "ไม่มีข้อมูลแหล่งที่มาและคะแนน classifier ยังสรุปไม่ได้",
        )

    # 5. ไม่มีทั้ง manifest และ classifier
    return _verdict(
        "uncertain",
        0.0,
        "ไม่มีข้อมูลแหล่งที่มาและ classifier ไม่พร้อมใช้งาน",
    )


def _verdict(verdict: str, confidence: float, explanation: str) -> dict:
    return {
        "verdict": verdict,
        "confidence": confidence,
        "explanation": explanation,
    }
Enter fullscreen mode Exit fullscreen mode

policy นี้ตั้งใจให้ conservative:

  • manifest ที่ถูกต้องมีน้ำหนักสูงกว่า classifier
  • manifest ที่ตรวจสอบไม่ผ่านเป็น warning ไม่ใช่หลักฐานว่าปลอม
  • ถ้า manifest และ classifier ขัดแย้งกัน ให้ตอบ uncertain
  • ถ้าไม่มีสัญญาณ ให้ตอบ uncertain พร้อม confidence เป็น 0.0
  • threshold เช่น 0.7, 0.3, 0.85 ควรปรับตาม risk tolerance ของระบบจริง

สร้าง FastAPI app

สร้างไฟล์ main.py:

# main.py
import os
import tempfile
from datetime import datetime, timezone

from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import JSONResponse

from provenance import read_provenance
from classifier import classify_image
from verdict import combine_signals

app = FastAPI(title="AI Image Detector API", version="1.0.0")

ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp"}
MAX_BYTES = 12 * 1024 * 1024  # 12 MB

SIGHTENGINE_USER = os.environ.get("SIGHTENGINE_API_USER", "")
SIGHTENGINE_SECRET = os.environ.get("SIGHTENGINE_API_SECRET", "")


@app.post("/verify")
async def verify(image: UploadFile = File(...)):
    # 1. ตรวจสอบ MIME type
    if image.content_type not in ALLOWED_TYPES:
        raise HTTPException(
            status_code=415,
            detail=(
                f"ประเภท {image.content_type} ไม่รองรับ "
                "โปรดส่ง JPEG, PNG, หรือ WebP"
            ),
        )

    image_bytes = await image.read()

    if len(image_bytes) == 0:
        raise HTTPException(status_code=400, detail="ไฟล์ว่างเปล่า")

    if len(image_bytes) > MAX_BYTES:
        raise HTTPException(status_code=413, detail="ไฟล์เกินขีดจำกัด 12 MB")

    # 2. อ่าน C2PA manifest
    # c2pa.Reader ต้องการ path จึงเขียนเป็นไฟล์ชั่วคราวก่อน
    suffix = os.path.splitext(image.filename or "")[1] or ".img"

    with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
        tmp.write(image_bytes)
        tmp_path = tmp.name

    try:
        provenance = read_provenance(tmp_path)
    finally:
        os.unlink(tmp_path)

    # 3. เรียก classifier ถ้าตั้งค่า credential แล้ว
    if SIGHTENGINE_USER and SIGHTENGINE_SECRET:
        classifier = await classify_image(
            image_bytes=image_bytes,
            filename=image.filename or "upload",
            api_user=SIGHTENGINE_USER,
            api_secret=SIGHTENGINE_SECRET,
        )
    else:
        classifier = {
            "available": False,
            "reason": "classifier_not_configured",
        }

    # 4. รวมสัญญาณและส่ง response
    result = combine_signals(provenance, classifier)

    return JSONResponse(
        {
            "verdict": result["verdict"],
            "confidence": result["confidence"],
            "signals": {
                "provenance": {
                    key: provenance.get(key)
                    for key in (
                        "has_manifest",
                        "validation",
                        "claim_generator",
                        "signature_issuer",
                    )
                },
                "classifier": {
                    "available": classifier.get("available", False),
                    "ai_score": classifier.get("ai_score"),
                },
            },
            "explanation": result["explanation"],
            "checked_at": datetime.now(timezone.utc).isoformat(),
        }
    )
Enter fullscreen mode Exit fullscreen mode

รันเซิร์ฟเวอร์:

uvicorn main:app --reload
Enter fullscreen mode Exit fullscreen mode

endpoint จะอยู่ที่:

http://127.0.0.1:8000/verify
Enter fullscreen mode Exit fullscreen mode

ทดสอบด้วย curl:

curl -X POST http://127.0.0.1:8000/verify \
  -F "image=@sample.jpg"
Enter fullscreen mode Exit fullscreen mode

ตั้งค่า credential ของ Sightengine:

export SIGHTENGINE_API_USER="your_api_user"
export SIGHTENGINE_API_SECRET="your_api_secret"
uvicorn main:app --reload
Enter fullscreen mode Exit fullscreen mode

จุดที่ควรสังเกต:

  • manifest หายไม่ทำให้ API ล้ม
  • classifier timeout ไม่ทำให้ API ล้ม
  • ทุกผลลัพธ์ถูก normalize เป็น JSON เดียวกัน
  • temporary file ถูกลบใน finally
  • response มี raw signals ให้ caller ใช้สร้าง UI หรือ policy เพิ่มเติมได้

แนวทางนี้เหมาะกับบริการที่เปิดเผย capability หลักผ่าน API ถ้าสนใจแนวคิดนี้ ดูบทความ ซอฟต์แวร์ที่เปลี่ยนเป็นแบบ Headless

Mock และทดสอบด้วย Apidog

ปัญหาที่พบบ่อยคือ frontend ต้องเริ่มทำ upload UI และ result panel ก่อน backend พร้อม คุณแก้ได้ด้วย mock server จาก schema

สร้าง mock server จาก OpenAPI schema

ใน Apidog:

  1. สร้าง project ใหม่
  2. เพิ่ม endpoint POST /verify
  3. กำหนด request body เป็น multipart/form-data
  4. เพิ่ม field image เป็น File
  5. เพิ่ม response schema ตาม VerifyResponse
  6. สร้างตัวอย่าง response สำหรับแต่ละ verdict
  7. เปิด mock server

เมื่อ schema กำหนด type และ enum แล้ว mock response จะมีโครงสร้างเหมือน API จริง เช่น:

  • verdict เป็นหนึ่งใน likely_authentic, likely_ai, uncertain
  • confidence เป็น number ระหว่าง 0 ถึง 1
  • signals.provenance.has_manifest เป็น boolean
  • signals.classifier.ai_score เป็น number

Frontend สามารถชี้ fetch ไปที่ mock URL ได้ทันที และเมื่อ backend จริงพร้อมก็เปลี่ยน base URL เท่านั้น

ตัวอย่างสถานะที่ควร mock:

  • likely_authentic พร้อม manifest กล้องที่ตรวจสอบผ่าน
  • likely_ai พร้อม manifest ที่ระบุเครื่องมือ AI
  • uncertain เมื่อ classifier ไม่พร้อมใช้งาน
  • uncertain เมื่อ manifest กับ classifier ขัดแย้งกัน
  • 415 Unsupported Media Type
  • 413 Payload Too Large

รัน endpoint test ใน Apidog

เมื่อ backend ทำงานแล้ว ให้สร้าง request ใน Apidog:

  1. Method: POST
  2. URL: http://127.0.0.1:8000/verify
  3. Body: form-data
  4. Field: image
  5. Type: File
  6. เลือกรูปทดสอบจากเครื่อง
  7. Send request

เพิ่ม assertion เพื่อให้รันซ้ำได้:

  • response status ต้องเป็น 200
  • verdict ต้องมีค่าและอยู่ใน enum ที่อนุญาต
  • confidence ต้องเป็น number ระหว่าง 0 ถึง 1
  • signals.provenance.has_manifest ต้องเป็น boolean
  • checked_at ต้องเป็น date-time string

สร้าง test scenario หลายเคส:

  1. รูปที่มี Content Credentials
  2. JPEG ปกติที่ไม่มี manifest
  3. ไฟล์ใหญ่เกิน 12 MB
  4. ไฟล์ text ที่เปลี่ยนนามสกุลเป็น .jpg
  5. รูปที่ทำให้ classifier timeout หรือปิด credential เพื่อดู classifier_not_configured

การบันทึก scenario ใน Apidog ช่วยให้รัน regression test หลังแก้ policy หรือ threshold ได้ง่ายกว่าการใช้ curl ทีละคำสั่ง

เสริมความแข็งแกร่งและจัดการกรณีขอบ

บริการตรวจสอบภาพต้องถือว่า input มีความเสี่ยง เพราะผู้ใช้อาจพยายามทำให้ภาพดูเหมือนเป็นอย่างอื่น

ไฟล์เสียหรือไฟล์ที่ถูกตัด

MIME type อาจบอกว่าเป็นรูป แต่เนื้อหาจริงอาจไม่ใช่รูป

แนวทางที่ควรเพิ่ม:

  • ใช้ Pillow decode รูปก่อนประมวลผล
  • ถ้า decode ไม่ผ่าน ให้ตอบ 400
  • อย่าพึ่งพา file extension อย่างเดียว

ตัวอย่าง:

pip install pillow
Enter fullscreen mode Exit fullscreen mode
from io import BytesIO
from PIL import Image, UnidentifiedImageError


def validate_image_bytes(image_bytes: bytes) -> None:
    try:
        with Image.open(BytesIO(image_bytes)) as img:
            img.verify()
    except UnidentifiedImageError:
        raise HTTPException(status_code=400, detail="ไฟล์ไม่ใช่รูปภาพที่อ่านได้")
Enter fullscreen mode Exit fullscreen mode

Manifest หาย

นี่คือกรณีที่พบบ่อยที่สุด ต้องจำไว้ว่า:

  • ไม่มี manifest ไม่ใช่ error
  • ไม่มี manifest ไม่ใช่หลักฐานว่าปลอม
  • ไม่มี manifest ไม่ใช่หลักฐานว่าจริง

ให้จับ ManifestNotFound แล้วไปใช้ classifier ต่อ

Classifier timeout หรือ API ล่ม

Hosted classifier เป็นระบบภายนอก จึงต้องออกแบบให้ล้มได้:

  • ตั้ง timeout ชัดเจน
  • คืน available: False
  • อย่าให้ request หลักล้มเป็น 500
  • ถ้าไม่มี provenance ด้วย ให้ตอบ uncertain

Manifest ปลอมแปลง

manifest อาจมีอยู่แต่ตรวจสอบไม่ผ่าน เช่น:

  • signature ไม่ถูกต้อง
  • hash ไม่ตรงกับ pixel
  • certificate มีปัญหา

ต้องตรวจ validation_status เสมอ:

  • array ว่าง = ตรวจสอบผ่าน
  • array มีค่า = ตรวจสอบไม่ผ่าน

อย่าเชื่อ manifest ที่ตรวจสอบไม่ผ่าน ให้ตอบ uncertain

ไฟล์ใหญ่และ abuse

ควรมีการป้องกัน:

  • จำกัดขนาด upload เช่น 12 MB
  • ตั้ง reverse proxy limit เช่น Nginx client_max_body_size
  • เพิ่ม rate limit หน้า endpoint
  • จำกัดจำนวน request ต่อ user/API key
  • log เฉพาะ metadata อย่า log image bytes

ความเป็นส่วนตัว

คุณกำลังรับรูปจากผู้ใช้ จึงควร:

  • เก็บไฟล์ชั่วคราวให้น้อยที่สุด
  • ลบ temporary file ทันที
  • อย่า log รูปภาพ
  • ถ้าส่งรูปไป third-party classifier ต้องระบุใน privacy policy
  • ตรวจสอบว่า provider อนุญาต use case ของคุณ

แต่ละสัญญาณจับอะไรได้และพลาดอะไร

สถานการณ์ สัญญาณแหล่งที่มา C2PA สัญญาณ classifier
ภาพ AI จากเครื่องมือที่เขียน Content Credentials จับได้: manifest ระบุชื่อเครื่องมือสร้าง มักจับได้จากลักษณะภาพ
ภาพ AI ที่ metadata ถูกลบ เช่น screenshot หรือ re-upload พลาด: ไม่มี manifest ให้อ่าน จับได้จาก pixel โดยไม่ต้องใช้ metadata
ภาพจริงจากกล้องที่ลงนาม Content Credentials ยืนยันได้: manifest ถูกต้องและไม่ใช่เครื่องมือ AI อาจผิดพลาดถ้ารูปถูกบีบอัดหรือแก้ไขหนัก
ภาพจริงที่ไม่มี metadata ไม่มีสัญญาณ ให้ได้แค่ความน่าจะเป็น
ภาพที่มี manifest ถูกดัดแปลง จับได้จาก validation_status อาจจับได้หรือไม่ได้ ขึ้นกับ pixel
เครื่องมือสร้างใหม่ที่ classifier ไม่เคยเห็น จับได้เฉพาะถ้าเครื่องมือเขียน manifest อาจพลาดเพราะอยู่นอก training distribution
ภาพจริงที่ถูกแก้ไขด้วย AI บางส่วน manifest อาจบันทึกประวัติการแก้ไข คะแนนอาจอยู่กลาง ๆ

โมเดลความคิดคือ: เมื่อสัญญาณหนึ่งมองไม่เห็น อีกสัญญาณหนึ่งอาจยังให้ข้อมูลได้ Provenance แม่นยำแต่พบไม่บ่อย ส่วน classifier ใช้ได้กว้างแต่ไม่แน่นอน ดังนั้นคำตัดสินแบบรวมจึงเหมาะกว่า และ uncertain เป็นผลลัพธ์ที่จำเป็น

กรณีใช้งานจริง

รูปแบบ API นี้นำไปใช้ได้หลายบริบท:

  • แพลตฟอร์มเนื้อหาที่ผู้ใช้สร้างขึ้น: ตรวจรูปตอน upload แล้ว map verdict เป็น allow, flag, หรือ human review
  • ห้องข่าวและ fact-checking: ให้บรรณาธิการเห็นทั้ง provenance และ model score ใน response เดียว
  • ประกันภัยและเคลม: flag รูปที่ดูเหมือนถูกสร้างหรือมี manifest ผิดปกติก่อนส่งให้เจ้าหน้าที่
  • asset pipeline ภายในองค์กร: ใช้ /verify เป็น gate ก่อนนำภาพเข้าคลัง
  • CMS ที่สนใจ provenance: แสดง badge เมื่อ manifest ตรวจสอบผ่าน และใช้ classifier เมื่อไม่มี manifest

จุดร่วมคือ API นี้ช่วยคัดกรองเบื้องต้นโดยยอมรับความไม่แน่นอนอย่างตรงไปตรงมา และส่งงานให้มนุษย์เฉพาะกรณีที่จำเป็น

สรุป

การตรวจจับภาพที่สร้างโดย AI ไม่ควรพึ่งการทดสอบเดียว แต่ควรรวมสัญญาณอิสระและตอบพร้อมระดับความเชื่อมั่น

สิ่งที่สร้างในบทความนี้:

  • FastAPI endpoint POST /verify
  • C2PA provenance reader ด้วย c2pa-python
  • Hosted AI image classifier ด้วย httpx
  • Rule-based verdict function
  • Response JSON ที่มี verdict, confidence, signals, explanation, และ checked_at
  • OpenAPI schema สำหรับ mock และ test ใน Apidog

แนวทางนี้ไม่ทำให้การตรวจจับสมบูรณ์แบบ แต่ทำให้ระบบซื่อสัตย์ขึ้น ตรวจสอบได้ขึ้น และ integrate ได้ง่ายขึ้น เพราะ caller เห็นทั้งคำตัดสินและสัญญาณดิบที่ใช้ตัดสิน

ถ้าจะนำไปใช้จริง ให้เริ่มจากออกแบบ schema /verify, สร้าง mock server, ให้ frontend เชื่อมต่อกับ mock, แล้วรัน endpoint tests กับ backend จริงเมื่อพร้อม จากนั้นค่อยปรับ threshold และ policy ตาม risk tolerance ของผลิตภัณฑ์คุณ ใช้ Apidog เพื่อออกแบบ mock และทดสอบ API ได้ใน workflow เดียว แล้วเปลี่ยนจาก mock ไป backend จริงด้วยการเปลี่ยน base URL เพียงครั้งเดียว.

Top comments (0)