1. ภาพรวมและขอบเขต
Box & Center Annotation Studio (ชื่อในสเปก: Unified Box & Center Annotator)
เป็น single-page app ที่รันในเบราว์เซอร์ล้วน ๆ สำหรับ annotate ภาพด้วย
bounding box และ center point / aiming guide
ผลลัพธ์คือไฟล์ canonical เพียงไฟล์เดียวคือ annotations.json
ที่เป็น single source of truth สำหรับ pipeline detection และ pose ปลายทาง
ไม่มี backend, ไม่มี bundler, ไม่มี runtime network dependency — ทุกอย่างทำงานในเบราว์เซอร์
ตัวแอปเองมี Content-Security-Policy ที่จำกัด origin ไว้ที่ตัวเองเท่านั้น
เครื่องมือนี้เป็น research workflow tool ไม่ใช่ diagnostic device label ฝั่งภาพเป็นข้อมูลเชิงตำแหน่ง (positional) เว้นแต่จะได้รับการตรวจสอบยืนยันอิสระ และ dataset ต้องผ่านการ de-identify ก่อน annotate — แอปนี้ไม่ใช่เครื่องมือ de-identification และไม่ทำการตีความเชิงวินิจฉัยใด ๆ
หลักการออกแบบที่ควรรู้ก่อนใช้
| หลักการ | ความหมายในทางปฏิบัติ |
|---|---|
| Canonical single source of truth | มีแค่ annotations.json ไฟล์เดียวที่ถือเป็นจริง legacy boxes.json/center.json ใช้เป็น input migrate เท่านั้น ไม่ใช่ source คู่ขนาน |
| Explicit over inferred | สถานะ negative / not_visible / skipped เกิดจากการกระทำของผู้ใช้เท่านั้น ระบบไม่เดาให้ |
| No silent coordinate rewrite | EXIF orientation ≠ 1 หรือมิติภาพไม่ตรง จะ เตือน เสมอ ไม่หมุน/ไม่เขียนพิกัดทับเงียบ ๆ |
| Separate task tracks | งาน box และงาน center มีสถานะ แยกกันคนละชุด ต่อภาพ |
2. การเข้าใช้งาน (GitHub Pages / Local)
เนื่องจากแอปนี้เป็น Single-page application แบบ 100% Client-side จึงสามารถใช้งานได้ 2 รูปแบบหลัก:
2.1 ใช้งานผ่าน GitHub Pages (Static Hosting)
หากนำโค้ดขึ้น GitHub และเปิดใช้งาน GitHub Pages คุณสามารถเข้า URL ของโปรเจกต์
(เช่น https://<username>.github.io/<repo>/) เพื่อใช้งานได้ทันที
โดย File System Access API สำหรับเซฟไฟล์ตรงเข้าเครื่องจะทำงานได้สมบูรณ์เนื่องจากทำงานบน HTTPS
2.2 ใช้งานแบบ Local (Docker)
สำหรับการรันในเครื่อง โปรเจกต์นี้ใช้ Docker ในการ serve ไฟล์ โดยไม่ต้องติดตั้ง Python หรือ Node.js
(ใช้ python:3.9-slim สำหรับ serve ไฟล์ และ annotator-dev:latest สำหรับรันเทสต์)
รันแอป (serve)
รันจาก project root แล้วเปิด http://localhost:8080 — คำสั่ง copy/paste ได้เลย (PowerShell):
docker run --rm `
-p 8080:8080 `
-v "${PWD}:/workspace:ro" `
-w /workspace `
python:3.9-slim `
python -m http.server 8080 --bind 0.0.0.0
ES modules และ File System Access API ต้องการ secure/local origin ที่สม่ำเสมอ
การดับเบิลคลิกเปิด index.html แบบ file:// จะทำให้แอปไม่ทำงาน
ความหมายของแต่ละ argument
| Argument | ค่าในตัวอย่าง | ความหมาย |
|---|---|---|
--rm | — | ลบ container ทิ้งอัตโนมัติเมื่อหยุด |
-p | 8080:8080 | map พอร์ต host:container (เปลี่ยนตัวซ้ายได้ถ้าพอร์ตชน) |
-v | ${PWD}:/workspace:ro | mount โฟลเดอร์ปัจจุบันเข้า container แบบ read-only (:ro) |
-w | /workspace | working directory ภายใน container |
| image | python:3.9-slim | image ที่ใช้ serve (ไม่ใช่ dependency ของแอป) |
| command | --bind 0.0.0.0 | ให้ http.server รับ connection จากนอก container ได้ |
Build test image (ทำครั้งเดียว)
docker build -f Dockerfile.dev -t annotator-dev:latest .
รันเทสต์
ใช้ Node built-in runner (node --test) กับ ES modules — ไม่มี bundler และไม่มี test dependency ภายนอก:
docker run --rm -v "${PWD}:/workspace" -w /workspace annotator-dev:latest node --test
รัน suite เดียว:
docker run --rm -v "${PWD}:/workspace" -w /workspace annotator-dev:latest node --test tests/downstream.test.js
2.3 รองรับเบราว์เซอร์ใด
ออกแบบสำหรับเบราว์เซอร์ตระกูล Chromium (Chrome / Edge) ที่รองรับ File System Access API เพื่อ save แบบ in-place เบราว์เซอร์อื่นใช้ได้ผ่าน path สำรอง folder picker + download เน้น desktop-first (≥ 1024 px) — บนจอแคบ grid จะยุบเหลือคอลัมน์เดียว รองรับ touch ผ่าน Pointer Events แต่การ annotate แม่นยำบน touch ไม่ใช่เป้าหมายของรุ่นนี้
3. เริ่มต้นใช้งานใน 5 นาที
วงจรการทำงานหลักมี 4 ขั้น: Open → Annotate → Validate → Save
เปิดโฟลเดอร์ภาพ
กด Open Folder เลือกโฟลเดอร์ที่มีภาพ .png/.jpg/.jpeg
หากมี annotations.json อยู่แล้วจะถูก validate และโหลดเข้ามา ถ้าเจอ legacy
boxes.json/center.json จะแสดง migration preview ก่อน merge
เลือก tool แล้ววาด
กด B เพื่อวาด box, C เพื่อวาง center, V เพื่อเลือก/แก้ ทำงานเร็ว ๆ บน grid หรือเปิด Focus เพื่อ zoom/pan แก้ละเอียด ทั้งสองมุมมองใช้ store เดียวกัน
กำหนดสถานะงาน
ตั้งสถานะ box task และ center task แยกกัน — completed เมื่อทำเสร็จ, negative/not_visible เมื่อไม่มีเป้าหมาย, skipped เมื่อขอข้าม
Validate แล้ว Save
กด Validate ตรวจทั้ง dataset → error จะ block การ save, warning save ผ่านได้หลังยืนยัน
จากนั้นกด Save เพื่อเขียน annotations.json กลับเข้าโฟลเดอร์
4. หน้าจอและเครื่องมือ
4.1 Header toolbar (แถวบนสุด)
| ปุ่ม | หน้าที่ |
|---|---|
| Open Folder | เปิดโฟลเดอร์ภาพ (directory handle) หรือ fallback เป็น folder picker |
| Import | นำเข้าไฟล์ annotations เพื่อ merge (เปิด preview ก่อน) |
| Save primary | เขียน canonical กลับโฟลเดอร์ หรือ download fallback |
| Validate | รัน full-dataset validation แล้วเปิดรายงาน |
| Autosave (checkbox) | opt-in checkpoint ลง IndexedDB ในเบราว์เซอร์นี้ (default ปิด) |
| Toggle Theme | สลับ light/dark (จำค่าใน localStorage) |
4.2 Secondary toolbar (แถวรอง)
ประกอบด้วยกลุ่มควบคุม: Tool (Select/Box/Center/Pan), Class + ปุ่ม Manage Classes, Guide (โผล่เฉพาะตอนใช้ Center tool), Filter, pagination + Rows/Columns, และกลุ่ม Batch
ตัวเลือก Filter
| Filter | แสดงภาพที่... |
|---|---|
| All | ทั้งหมด |
| Box pending | box task ยัง pending |
| Center pending | center task ยัง pending |
| Any pending | มี task ใด task หนึ่งยัง pending |
| Completed | เสร็จทั้งสอง task |
| Negative/Not visible | มีสถานะ negative หรือ not_visible |
| Orphan | มี record แต่หาไฟล์ภาพไม่เจอ |
| Invalid | มี validation issue |
Rows และ Columns ปรับได้ 1–8 (default 2×2) ค่าถูกจำใน localStorage
(app-grid-layout) เพดานจำนวน tile ต่อหน้าคือ MAX_GRID_TILES = 64
กลุ่ม Batch
Batch Box… / Batch Center… ตั้งสถานะ task ให้ภาพทุกใบ ที่ตรง filter ปัจจุบัน พร้อมกัน (เลือกได้เฉพาะ pending / negative หรือ not_visible / skipped — completed ตั้งแบบ batch ไม่ได้เพราะต้องมี item) ส่วน Clean Orphans ลบ record ที่หาไฟล์ภาพไม่เจอออกจาก dataset
4.3 Status bar
แถบสถานะแสดง: Folder (path), Images (จำนวนรวม), Box Progress และ Center Progress (เสร็จ/ทั้งหมด), Orphans, Issues (badge) และ badge Unsaved Changes เมื่อมีการแก้ที่ยังไม่ save
5. เปิดโฟลเดอร์ & Image identity
เมื่อเปิดโฟลเดอร์ แอปจะอ่านไฟล์นามสกุล .png, .jpg, .jpeg
(case-insensitive) จากทั้งโฟลเดอร์และ subfolder ทุกชั้น
Image identity = normalized path สัมพัทธ์กับ root ของโฟลเดอร์ ดังนั้น basename เดียวกันที่อยู่คนละ subfolder จะ ไม่ชนกัน — เช่นa/img.jpgกับb/img.jpgเป็นคนละimage_id
การใช้ path สัมพัทธ์ที่ normalize แล้วทำให้ identity เป็น deterministic และพกพาข้ามเครื่องได้ โดยไม่ผูกกับ absolute path ของ host และกันการชนกันของ basename ซ้ำในต่าง subfolder ซึ่งเป็นปัญหาคลาสสิกของ dataset ที่จัดเป็นโฟลเดอร์ย่อย
ลำดับการค้นหาไฟล์ annotation
- ถ้าพบ
annotations.json→ parse + validate แล้วโหลด - ถ้าไม่พบ แต่เจอ
boxes.jsonและ/หรือcenter.json→ สร้าง migration preview (modal Import) ให้ยืนยันก่อน merge - ภาพที่มี record แต่ไม่พบไฟล์จริงในรอบนี้ จะถูกทำเครื่องหมายเป็น orphan
6. การ Annotate
6.1 Tools
| Tool | คีย์ | หน้าที่ |
|---|---|---|
| Select | V | เลือก / ย้าย / resize / แก้ object |
| Box | B | วาด bounding box |
| Center | C | วาง center point พร้อม guide |
| Pan | H หรือกดค้าง Space | เลื่อนมุมมอง |
6.2 พิกัด (coordinate contract)
พิกัดเป็น integer pixel บน raster ที่ผู้ใช้เห็นจริง (oriented) origin ที่มุมซ้ายบน
- Box =
x, y, width, height(มุมซ้ายบน + ขนาดบวก) ขั้นต่ำMIN_BOX_SIZE = 3px และ clamp ให้อยู่ในขอบภาพ - Center =
x, y
ภาพที่มี EXIF orientation ≠ 1 จะ เตือนเท่านั้น (WARN_EXIF_ORIENTATION)
แอปไม่หมุนอัตโนมัติและไม่เขียนพิกัดทับเงียบ ๆ ส่วนกรณีมิติที่เก็บไว้กับมิติที่ decode ได้ไม่ตรงกัน
จะถูกยกขึ้นมาให้ตัดสินอย่างชัดเจน (ดูหัวข้อ Dimension mismatch)
6.3 สถานะของแต่ละ task
งาน box และ center มีชุดสถานะแยกกัน และสถานะที่บ่งบอก "ไม่มี/ข้าม" ต้องมี item เป็นศูนย์เสมอ:
| Task | สถานะที่ใช้ได้ |
|---|---|
| Box task | pending completed negative skipped |
| Center task | pending completed not_visible skipped |
completed ต้องมี item อย่างน้อย 1 ชิ้น ขณะที่
pending / negative /
not_visible / skipped
ต้องมี item เป็น 0 มิฉะนั้นจะได้ error ERR_STATUS_MISMATCH
6.4 Undo / Redo
Ctrl/Cmd+Z undo และ Ctrl/Cmd+Y redo ครอบคลุม add/move/resize/delete, เปลี่ยนสถานะ, guide, class, batch, copy/paste และ import merge (เป็น 1 command อะตอมมิก ไม่ว่าจะกระทบกี่ภาพก็ตาม) รองรับอย่างน้อย 100 ขั้น
6.5 Manage Classes & Expected Counts
ปุ่ม Manage Classes เปิด modal สำหรับ เพิ่ม/ลบ/เปลี่ยนชื่อ/จัดลำดับ class ของ box และ center
และตั้ง Expected Counts Policy (min/max ของจำนวน box และ center) ค่า default คือ
box {min:1, max:2} และ center {min:1, max:1} —
ใช้ตรวจตอน task เป็น completed ถ้าจำนวนหลุดช่วงจะได้ ERR_EXPECTED_COUNT
downstream อ้าง class ผ่าน label → name → id โดย id มาจาก classes.boxes[].id
ไม่ใช่ตำแหน่งใน array ดังนั้นการจัดลำดับ class ใหม่ใน UI จึงไม่ทำให้ class index ของ output เพี้ยน
7. Aiming guides
center ทุกจุดเก็บ guide ไว้เสมอ crosshair/midpoint จะอยู่ตรง center.x/y เป๊ะ
ขนาด guide เป็น native pixel และ ไม่เปลี่ยนตาม zoom
| Guide | พารามิเตอร์ที่เก็บ | ขนาด default | ขั้นต่ำ (hit-safe) |
|---|---|---|---|
point_crosshair | — | — | — |
circle_crosshair | radius_px | max(12, round(0.10 × min(w, h))) | MIN_CIRCLE_RADIUS = 12 |
horizontal_span | span_width_px | max(24, round(0.25 × w)) | MIN_SPAN_WIDTH = 24 |
endpoint ของ horizontal_span คำนวณจาก center.x ± span_width_px / 2 และ
ไม่ถูก serialize guide ไม่เปลี่ยนความหมายของ center label และ
ไม่กระทบ output ปลายทาง (pose ใช้แค่ center.x/y)
ถ้าขนาด guide ต่ำกว่าขั้นต่ำจะได้ warning WARN_GUIDE_SIZE ไม่ใช่ error
8. Focus editor
เปิด Focus จาก tile เพื่อแก้แบบ zoom/pan ละเอียด มี loupe (แว่นขยาย) ช่วยเล็งพิกัด sidebar ขวาแบ่งเป็นสองส่วน: Workflow Actions และ Selection Details
Workflow Actions
ปุ่ม Previous Image, Next Incomplete (primary), Next Image, Copy Annotations, Paste Annotations และ dropdown ตั้งสถานะ Box Task / Center Task โดยตรง
Selection Details
เมื่อเลือก object หนึ่ง จะแก้ค่าได้ละเอียดผ่านช่อง input:
- X, Y และ (box) W, H — พิกัด/ขนาดเป็น integer pixel
- Class — เปลี่ยน class ของ object
- Guide (center) — point/circle/horizontal_span พร้อมช่อง Radius (min 12) หรือ Span (min 24)
- Link Box (center) — ผูก center เข้ากับ box ในภาพเดียวกัน (ถ้าผูกแล้ว center ต้องอยู่ในกรอบ box นั้น)
9. Import & merge
ปุ่ม Import เลือกไฟล์ JSON มา merge ระบบจะแสดง preview จำนวนภาพ/box/center, class ใหม่ที่จะถูกสร้าง และรายการภาพที่ขัดแย้ง
นโยบายแก้ conflict
| นโยบาย | ผล |
|---|---|
keep_current | เก็บของเดิม (ข้าม conflict ที่ import เข้ามา) |
use_imported | ใช้ของที่ import (เขียนทับ annotation เดิม) |
เลือกได้ รายภาพ หรือใช้ Apply-to-all mirror ค่าลงทุกภาพพร้อมกัน การกด Cancel ต้องไม่มีผลต่อ state ที่ใช้งานอยู่
ไม่ว่า merge จะกระทบกี่ภาพ จะนับเป็น undoable command เดียว — กด undo ครั้งเดียวย้อนทั้งชุดได้ และ merge รักษา orphan record / จัดการ duplicate ID โดยไม่ mutate object input
10. Validate & error codes
Validate ตรวจทั้ง dataset (root, class, path, asset, expected-count, geometry, status ฯลฯ) ผลแบ่งเป็น 2 ระดับ: error และ warning คลิกแต่ละ issue ในรายงานเพื่อกระโดดไปยังภาพที่เกี่ยวข้องได้
error = block การ save แบบ canonical ทั้งหมด ต้องแก้ก่อน · warning = save ผ่านได้ แต่ต้องกดยืนยันรับทราบก่อน
10.1 Error codes (block save)
| Code | ความหมาย |
|---|---|
ERR_STRUCTURAL | โครงสร้าง/ชนิดข้อมูล/timestamp/metadata contract ไม่ถูกต้อง |
ERR_DUPLICATE_ID | annotation id ซ้ำ หรือ key ของ image map ไม่ตรง image_id |
ERR_INVALID_GEOMETRY | พิกัด/ขนาดไม่ใช่ finite integer หรือ width/height ไม่เป็นบวก |
ERR_GEOMETRY_BOUNDS | box หรือ center หลุดขอบภาพ |
ERR_STATUS_MISMATCH | completed แต่ไม่มี item / สถานะ "ไม่มี" แต่กลับมี item |
ERR_INVALID_ASSOCIATION | box_id ผิดชนิด, ชี้ box ที่ไม่มี, หรือ center อยู่นอก box ที่ผูก |
ERR_INVALID_GUIDE | guide ไม่มี type, point_crosshair มีพารามิเตอร์ขนาด, หรือ size ไม่ใช่ integer บวก |
ERR_EXPECTED_COUNT | จำนวน box/center หลุดช่วง expected counts ตอนสถานะ completed |
ERR_MISSING_COORDINATE | พิกัดที่จำเป็นหายไป |
ERR_CLASS_MISMATCH | label ไม่ตรงกับ class ที่นิยามไว้ |
ERR_CLASS_DEFINITION | นิยาม class ผิด (id/name ซ้ำ, ชนิดผิด, หรือ counter ≤ id สูงสุด) |
ERR_INVALID_PATH | relative_path / image map key ไม่ใช่ normalized relative path หรือ file_name ไม่ตรง basename |
ERR_DIMENSION_MISMATCH | มิติที่เก็บไว้กับมิติที่ decode ได้ต่างกัน |
ERR_DECODE_FAILURE | decode ภาพไม่สำเร็จ |
10.2 Warning codes (save ผ่านได้หลังยืนยัน)
| Code | ความหมาย |
|---|---|
WARN_UNKNOWN_GUIDE | guide type ไม่รู้จัก (นอกเหนือ 3 ชนิดมาตรฐาน) |
WARN_GUIDE_SIZE | ขนาด guide ต่ำกว่าขั้นต่ำ hit-safe ของ UI |
WARN_GUIDE_BOUNDS | guide ยื่นเลยขอบภาพ |
WARN_ORPHAN_RECORD | มี record แต่หาไฟล์ภาพไม่เจอ |
WARN_MISSING_RECORD | มีไฟล์ภาพแต่ไม่มี canonical record |
WARN_EXIF_ORIENTATION | EXIF orientation ≠ 1 (พิกัดไม่ถูกหมุน) |
11. Save / Download / Autosave
11.1 ลำดับการ Save
- ถ้ามี error > 0 → block และเปิดรายงาน validation
- ถ้ามีแต่ warning → ถามยืนยัน "Save anyway?" ก่อน
- ถ้ามี writable directory handle → เขียน
annotations.jsonกลับโฟลเดอร์ - ถ้าไม่มี handle → fallback เป็น Blob download
สถานะ Unsaved Changes จะหายก็ต่อเมื่อเขียน canonical ลงดิสก์สำเร็จจริง ๆ การ download เป็นแค่ checkpoint — ไม่ยืนยันว่าไฟล์ไปถึงโฟลเดอร์ dataset แล้ว ระบบจึงคง dirty state ไว้โดยตั้งใจ (และถ้าเขียน in-place ล้มเหลว จะ fallback เป็น download โดยไม่ทิ้งการแก้)
11.2 Autosave & Recovery
Autosave เป็น opt-in (default ปิด) checkpoint metadata ของ annotation ลง
IndexedDB ผูกกับ identity ของ dataset (hash ของ dataset.name + dataset.created_at)
เมื่อเปิด dataset เดิม อีกครั้งและพบ checkpoint ที่ใหม่กว่าไฟล์ที่เพิ่งโหลด
จะมี dialog ให้เลือก:
| ตัวเลือก | ผล |
|---|---|
| Recover | เอา checkpoint มาแทนที่ annotation ที่โหลด (ไม่เขียนไฟล์ ต้อง Save เองภายหลัง) |
| Keep loaded file | คงไฟล์ที่โหลดไว้ ไม่แตะ checkpoint |
| Discard checkpoint | ลบ checkpoint ทิ้ง |
Autosave เก็บใน IndexedDB ของเบราว์เซอร์นี้เครื่องนี้เท่านั้น และ ไม่เก็บ image bytes อย่าใช้แทนการ Save จริงลงไฟล์ canonical
12. Dimension mismatch
ถ้าภาพที่โหลดมีมิติจริง (decoded) ไม่ตรงกับมิติที่เก็บไว้ใน annotation (และ task ไม่ใช่ pending) แอปจะเปิด dialog ให้เลือกวิธีแก้อย่างชัดเจน 3 ทาง:
| ปุ่ม | ผล |
|---|---|
| Rescale Bounding Boxes primary | ปรับ scale พิกัด box ให้เข้ากับมิติใหม่ |
| Replace Image Asset | ใช้ไฟล์ภาพใหม่แทน (คงพิกัดเดิม) |
| Remove Annotation danger | ลบ record ของภาพนี้ออก |
มิติไม่ตรงอาจมาจากภาพถูกแทนที่, ถูก resize, หรือ EXIF orientation — แต่ละกรณีมีวิธีแก้ที่ถูกต้องต่างกัน แอปจึงไม่เดา rescale เงียบ ๆ แต่ยกขึ้นมาเป็นการตัดสิน rescale / replace / remove อย่างเปิดเผย สอดคล้องกับหลัก "no silent coordinate rewrite"
13. Keyboard shortcuts
shortcut จะถูกเพิกเฉยเมื่อ focus อยู่ในช่อง INPUT / SELECT / TEXTAREA (เพื่อไม่ขัดการพิมพ์)
| คีย์ | การทำงาน |
|---|---|
| V / B / C / H | เลือก tool Select / Box / Center / Pan |
| กดค้าง Space | pan ชั่วคราว |
| Ctrl/Cmd+Z | undo |
| Ctrl/Cmd+Y หรือ Ctrl/Cmd+Shift+Z | redo |
| Ctrl/Cmd+C | copy annotation ของภาพที่ active |
| Ctrl/Cmd+V | paste annotation ไปยังภาพที่ active |
| [ / ] | ภาพก่อนหน้า / ถัดไป (ตาม filter) |
| Alt+N | ไปภาพ incomplete ถัดไป |
| Arrow | nudge object ที่เลือก 1 px |
| Shift+Arrow | nudge object ที่เลือก 10 px |
| Delete / Backspace | ลบ object ที่เลือก |
| Esc | ยกเลิกการเลือก object หรือปิด Focus editor |
14. Downstream output (YOLO)
โมดูล js/downstream.js อ่าน canonical file โดยตรงเพื่อแปลงเป็น label YOLO
(ไม่ใช่ปุ่มใน UI แต่เป็นส่วนของ contract สำหรับสาย data/ML) ค่าทุกตัว normalize ด้วยมิติภาพ
ที่ precision 6 ตำแหน่ง
14.1 Detection
convertCanonicalToDetection — emit 1 บรรทัดต่อ box รูปแบบ
"<class_id> <xc> <yc> <w> <h>"
เฉพาะภาพที่ box task เป็น completed หรือ
negative โดยภาพ negative ได้ label set ว่าง (ไม่มี box)
class index มาจาก classes.boxes[].id ผ่าน label → name → id
14.2 Pose
convertCanonicalToPose — emit center เป็น keypoint เดียวบน full-image box รูปแบบ
"<class_id> 0.5 0.5 1 1 <kx> <ky> 2"
เฉพาะ center task ที่ completed
ภาพที่ center task เป็น completed แต่มีจำนวน center ≠ 1 จะถูก reject พร้อม error ชัดเจน
output ใช้แค่ center.x/y — guide type/พารามิเตอร์ไม่มีผลต่อผลลัพธ์
15. โครงสร้าง annotations.json
ไฟล์ถูก serialize แบบ deterministic (natural sort, indent 2 ช่อง, timestamp เป็น UTC ...Z)
ทำให้ parse แล้ว re-serialize ได้ผลเหมือนเดิมแบบ byte-identical ตัวอย่างย่อ (จาก fixture จริง):
{
"schema_version": "1.0.0",
"app": { "name": "box-center-annotator", "version": "0.1.0" },
"dataset": {
"name": "test",
"center_guide_default": "point_crosshair",
"expected_counts": { "boxes": { "min": 1, "max": 2 }, "centers": { "min": 1, "max": 3 } },
"created_at": "2026-06-27T10:00:00Z",
"updated_at": "2026-06-27T10:00:00Z",
"coordinate_frame": "oriented_image_pixels",
"orientation_policy": "browser_from_image"
},
"classes": {
"boxes": [ { "id": 0, "name": "Target" } ],
"centers": [ { "id": 0, "name": "Center" } ]
},
"images": {
"completed.jpg": {
"image_id": "completed.jpg",
"file_name": "completed.jpg",
"relative_path": "completed.jpg",
"image_width": 100,
"image_height": 100,
"box_task": {
"status": "completed",
"items": [ { "id": "box_1", "label": "Target", "x": 10, "y": 10, "width": 20, "height": 20 } ],
"last_updated": "2026-06-27T10:00:00Z"
},
"center_task": {
"status": "completed",
"items": [ {
"id": "center_1", "label": "Center", "x": 20, "y": 20,
"box_id": "box_1", "guide": { "type": "point_crosshair" }
} ],
"last_updated": "2026-06-27T10:00:00Z"
}
}
}
}
coordinate_frame ต้องเป็น "oriented_image_pixels" และ
orientation_policy ต้องเป็น "browser_from_image" เสมอ
ส่วน class_id_counters ต้องมากกว่า id ของทุก class ที่ active —
มิฉะนั้นจะได้ ERR_STRUCTURAL / ERR_CLASS_DEFINITION
16. Privacy & data boundary
- ไม่มี analytics, telemetry, remote font, CDN หรือ external API — CSP meta tag จำกัด origin ไว้ที่ตัวแอป
- ภาพอยู่ใน memory ของเบราว์เซอร์และ ไม่เคยถูกฝังลงใน
annotations.json localStorageเก็บเฉพาะ UI preference เท่านั้น:app-theme,app-grid-layout,app-autosave-enabled— ไม่มีชื่อไฟล์/พิกัด/ข้อมูลภาพ- Autosave (opt-in) เก็บ metadata (รวมชื่อไฟล์และพิกัด) ใน IndexedDB เท่านั้น ผูกกับ dataset identity ไม่มี image bytes / object URL / file handle และมีคำสั่งล้างได้
แอปนี้ ไม่ทำ de-identification — dataset ต้องถูก de-identify ก่อนนำเข้ามา annotate เสมอ
17. ค่า default ของระบบ
สกัดจาก js/config.js และ js/geometry.js:
| ค่า | Default | ความหมาย |
|---|---|---|
SUPPORTED_EXTENSIONS | jpg, jpeg, png | นามสกุลภาพที่อ่าน (case-insensitive) |
MIN_BOX_SIZE | 3 | ขนาด box ขั้นต่ำ (px) |
HIT_SLOP_DISPLAY_PX | 6 | ระยะ tolerance ของการคลิกโดน object |
DEFAULT_GRID_ROWS × COLS | 2 × 2 | เลย์เอาต์ grid เริ่มต้น |
MAX_GRID_TILES | 64 | เพดาน tile ต่อหน้า |
DEFAULT_EXPECTED_COUNTS | box {1,2}, center {1,1} | ช่วงจำนวนที่คาดหวังตอน completed |
MIN_GUIDE_SIZE | 12 | ขนาด guide ขั้นต่ำทั่วไป |
MIN_CIRCLE_RADIUS | 12 | รัศมีขั้นต่ำของ circle_crosshair |
MIN_SPAN_WIDTH | 24 | ความกว้างขั้นต่ำของ horizontal_span |
18. ข้อจำกัดที่ทราบ
- การ save in-place ต้องใช้เบราว์เซอร์ Chromium ที่รองรับ File System Access API — ที่อื่นต้องใช้ download fallback แล้วย้ายไฟล์เข้าโฟลเดอร์ dataset เอง
- การ download สำเร็จไม่ได้ยืนยันว่าไฟล์ถูกวางถูกที่ — dirty state คงอยู่จนกว่าจะเขียน canonical จริงสำเร็จ
- การ annotate แม่นยำบน touch อยู่นอกขอบเขตของรุ่นนี้
- แอปไม่ทำ de-identification และไม่ตีความเชิงคลินิก/วินิจฉัยใด ๆ
19. Glossary
| คำ | ความหมาย |
|---|---|
| Canonical file | annotations.json ไฟล์เดียวที่เป็น single source of truth |
| Image identity / image_id | normalized path สัมพัทธ์กับ root ของโฟลเดอร์ ใช้เป็นกุญแจระบุภาพ |
| Orphan record | record ที่มีอยู่ใน dataset แต่หาไฟล์ภาพจริงไม่เจอในรอบโหลดนี้ |
| Aiming guide | metadata รูปทรงช่วยเล็ง (point/circle/horizontal_span) ที่ผูกกับ center จุดหนึ่ง |
| Task | หน่วยงาน annotation ต่อภาพ มี 2 track แยกกัน: box_task และ center_task |
| Directory handle | object จาก File System Access API ที่ให้สิทธิ์เขียนไฟล์กลับโฟลเดอร์โดยตรง |
| Dirty state | สถานะที่มีการแก้ค้างยังไม่ถูกเขียนลง canonical (แสดงเป็น Unsaved Changes) |
| Checkpoint | สำเนา metadata จาก autosave (IndexedDB) หรือไฟล์ download ที่ยังไม่ยืนยันตำแหน่ง |
| Migration | การแปลง legacy boxes.json/center.json เป็น canonical schema |
| Detection / Pose label | output ทรง YOLO ที่ downstream.js สร้างจาก canonical file |