มีคนอัปโหลดรูปภาพลงในผลิตภัณฑ์ของคุณแล้วอ้างว่ารูปนั้นถ่ายจากกล้อง แบ็กเอนด์ของคุณพิสูจน์หรือหักล้างได้ไหม? ทุกวันนี้ตัวสร้างภาพทำให้รูปดูสมจริงพอจะหลอกสายตามนุษย์ได้ ดังนั้นวิธีที่ใช้งานได้จริงคือรวมสัญญาณอิสระ 2 แบบเข้าด้วยกัน: แหล่งที่มาที่ตรวจสอบได้ด้วยวิทยาการเข้ารหัส และตัวจำแนก Machine Learning แล้วสรุปเป็นคำตัดสินเดียวที่น่าเชื่อถือกว่าใช้สัญญาณใดสัญญาณหนึ่งลำพัง
บทความนี้จะแนะนำการสร้างบริการแบ็กเอนด์ด้วย Python และ FastAPI ที่มีเอนด์พอยต์ POST /verify สำหรับรับรูปภาพ แล้วส่ง JSON กลับมาพร้อมคำตัดสิน คะแนนความเชื่อมั่น และรายละเอียดแหล่งที่มาที่ตรวจพบ เราจะใช้เครื่องมือ C2PA แบบโอเพนซอร์สสำหรับสัญญาณ provenance และ API ตรวจจับแบบโฮสต์สำหรับสัญญาณ classifier เพราะนี่เป็นโปรเจกต์ API เราจะเริ่มจากการออกแบบสัญญาเอนด์พอยต์ก่อน แล้วใช้ Apidog เพื่อ mock และทดสอบ API เพื่อให้ทีมฟรอนต์เอนด์เริ่มเชื่อมต่อได้ก่อนแบ็กเอนด์เสร็จ
สรุป (TL;DR)
คุณจะสร้างบริการ FastAPI ที่เปิดเผย POST /verify โดยทำงานตามลำดับนี้:
- รับไฟล์รูปภาพผ่าน
multipart/form-data - ตรวจสอบ C2PA Content Credentials ด้วย
c2pa-python - เรียก API ตรวจจับภาพ AI แบบโฮสต์เป็นสัญญาณที่สอง
- รวมผลลัพธ์เป็นคำตัดสินเดียว:
likely_authenticlikely_aiuncertain
- ส่งคืน JSON ที่มี
confidence,signals,explanation, และchecked_at - ออกแบบ 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
ขั้นตอนหลัก:
- ตรวจสอบว่าไฟล์อัปโหลดเป็นรูปภาพที่รองรับและไม่เกินขนาดที่กำหนด
- อ่าน C2PA manifest ภายในเครื่องด้วย
c2pa-python - ส่งไบต์รูปภาพไปยัง hosted classifier ผ่าน HTTPS
- รวมผลลัพธ์ด้วย rule-based policy แล้วส่งคืน JSON
ติดตั้ง dependency:
pip install fastapi "uvicorn[standard]" python-multipart httpx c2pa-python
ต้องใช้ Python 3.10 ขึ้นไป เพราะไลบรารี C2PA ต้องการเวอร์ชันนี้
สร้างสัญญาณ C2PA
Content Authenticity Initiative ภายใต้องค์กร GitHub contentauth เผยแพร่เครื่องมือ C2PA แบบโอเพนซอร์ส โดยมีส่วนที่เกี่ยวข้องหลัก ๆ คือ:
-
c2patool: CLI สำหรับดูและเพิ่ม manifest เหมาะกับการตรวจสอบจาก terminal โปรดทราบว่า standalone repository ถูกเก็บถาวรแล้ว และ CLI อยู่ในโปรเจกต์ Rustc2pa-rs -
c2pa-python: Python binding สำหรับไลบรารี Rustc2pa-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 สำเร็จแล้ว",
}
จุดสำคัญของโค้ดนี้:
- ใช้
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
ส่งรูปเป็น 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)}
แนวทางสำคัญ:
- ใช้ 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 ก่อน เพื่อให้ได้:
- contract เดียวที่ backend และ frontend เห็นตรงกัน
- mock server ที่ frontend เรียกได้ทันที
- test case ที่รันซ้ำได้เมื่อ backend พร้อม
Request
POST /verify รับ multipart/form-data โดยมี field เดียว:
| Field | Type | Required | Description |
|---|---|---|---|
image |
file | yes | รูปภาพที่ต้องการตรวจสอบ |
รองรับ MIME type เช่น:
image/jpegimage/pngimage/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"
}
ค่า verdict มี 3 แบบ:
likely_authenticlikely_aiuncertain
ต้องมี 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
คุณสามารถเขียน 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,
}
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(),
}
)
รันเซิร์ฟเวอร์:
uvicorn main:app --reload
endpoint จะอยู่ที่:
http://127.0.0.1:8000/verify
ทดสอบด้วย curl:
curl -X POST http://127.0.0.1:8000/verify \
-F "image=@sample.jpg"
ตั้งค่า credential ของ Sightengine:
export SIGHTENGINE_API_USER="your_api_user"
export SIGHTENGINE_API_SECRET="your_api_secret"
uvicorn main:app --reload
จุดที่ควรสังเกต:
- 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:
- สร้าง project ใหม่
- เพิ่ม endpoint
POST /verify - กำหนด request body เป็น
multipart/form-data - เพิ่ม field
imageเป็น File - เพิ่ม response schema ตาม
VerifyResponse - สร้างตัวอย่าง response สำหรับแต่ละ verdict
- เปิด 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 Type413 Payload Too Large
รัน endpoint test ใน Apidog
เมื่อ backend ทำงานแล้ว ให้สร้าง request ใน Apidog:
- Method:
POST - URL:
http://127.0.0.1:8000/verify - Body:
form-data - Field:
image - Type:
File - เลือกรูปทดสอบจากเครื่อง
- Send request
เพิ่ม assertion เพื่อให้รันซ้ำได้:
- response status ต้องเป็น
200 -
verdictต้องมีค่าและอยู่ใน enum ที่อนุญาต -
confidenceต้องเป็น number ระหว่าง 0 ถึง 1 -
signals.provenance.has_manifestต้องเป็น boolean -
checked_atต้องเป็น date-time string
สร้าง test scenario หลายเคส:
- รูปที่มี Content Credentials
- JPEG ปกติที่ไม่มี manifest
- ไฟล์ใหญ่เกิน 12 MB
- ไฟล์ text ที่เปลี่ยนนามสกุลเป็น
.jpg - รูปที่ทำให้ 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
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="ไฟล์ไม่ใช่รูปภาพที่อ่านได้")
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)