File size: 5,239 Bytes
f211247 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | #!/usr/bin/env python3
"""
Build tablet-level structured manifest from mark.txt annotations.
Each tablet → list of lines → list of signs with:
- bbox (x, y, w, h) normalized
- sign label (ABZ/comment)
- language (Hittite/Sumerian/Akkadian/...)
- damage (inferred: 'intact' | 'partial' | 'broken')
- reading order (row_no, col_no, x-sort within line)
Output: datasets/sources/hitit_local/tablet_structure.jsonl
{tablet_id, image_path, width, height, lines: [{row, col, signs:[...]}]}
"""
import argparse
import json
from collections import defaultdict
from pathlib import Path
from PIL import Image
SOURCES = Path("/arf/scratch/stakan/hitit-proje/datasets/sources/hitit_local")
LANG_MAP = {
"Hititçe": "hit", "Hittite": "hit", "Hit": "hit",
"Sümerce": "sum", "Sumerian": "sum", "Sum": "sum",
"Akadca": "akk", "Akkadian": "akk", "Akk": "akk",
"Luvice": "luw", "Luwian": "luw",
"Hurrice": "hur", "Hurrian": "hur",
"Hattice": "hat",
"Palaca": "pal", "Palaic": "pal",
"": "unk",
}
# Damage inference from comment
DAMAGE_MARKS = {
'x': 'broken', # 'x' standard Assyriology marker for lost sign
'X': 'broken',
'?': 'uncertain',
'▒': 'broken',
'…': 'broken',
'[': 'partial', # half-bracket usually
']': 'partial',
'⸢': 'partial',
'⸣': 'partial',
}
def classify_damage(comment, quality=None):
"""Return 'intact' | 'partial' | 'broken' | 'uncertain'."""
if comment is None:
return 'broken'
c = comment.strip().rstrip(",.")
if not c or c in {"x", "X", "_SLASH__SLASH_", "//", "▒", "…"}:
return 'broken'
if any(m in c for m in ('[', ']', '⸢', '⸣', '〈', '〉')):
return 'partial'
if '?' in c:
return 'uncertain'
return 'intact'
def normalize_bbox(mark, W, H):
x = mark.get("x", 0); y = mark.get("y", 0)
w = mark.get("width", 0); h = mark.get("height", 0)
if w < 0: x += w; w = abs(w)
if h < 0: y += h; h = abs(h)
return x / W, y / H, w / W, h / H
def process_tablet(tid):
tdir = SOURCES / tid
mark = tdir / "mark.txt"
if not mark.exists() or mark.stat().st_size == 0:
return None
# Find image
img_path = None
for ext in (".JPG", ".jpg", ".jpeg", ".png", ".PNG"):
for f in tdir.glob(f"*{ext}"):
img_path = f
break
if img_path: break
if not img_path: return None
try:
with Image.open(img_path) as im:
W, H = im.size
except Exception:
return None
try:
data = json.loads(mark.read_text())
except Exception:
return None
anns = data.get("annotations", [])
if not anns:
return None
# Build sign list with reading-order metadata; dedup by (pk_id, bbox)
signs = []
seen_pk = set()
for idx, a in enumerate(anns):
pk = a.get("pk_id")
if pk is not None and pk in seen_pk:
continue
m = a.get("mark", {})
bx, by, bw, bh = normalize_bbox(m, W, H)
comment = (a.get("comment") or "").strip().rstrip(",.")
lang_raw = a.get("lang") or ""
signs.append({
"idx": idx,
"sign": comment if comment else "x",
"lang": LANG_MAP.get(lang_raw, "unk"),
"lang_raw": lang_raw,
"damage": classify_damage(comment),
"bbox": [round(bx, 4), round(by, 4), round(bw, 4), round(bh, 4)],
"row": a.get("row_no", 0),
"col": a.get("col_no", 0),
"pk": pk,
})
if pk is not None:
seen_pk.add(pk)
# Group by (row, col), sort within row by x (reading order)
by_line = defaultdict(list)
for s in signs:
by_line[(s["row"], s["col"])].append(s)
lines = []
for (row, col), items in sorted(by_line.items()):
items.sort(key=lambda s: s["bbox"][0]) # left → right
lines.append({"row": row, "col": col, "signs": items})
return {
"tablet_id": tid,
"image_path": str(img_path),
"width": W,
"height": H,
"n_signs": len(signs),
"n_lines": len(lines),
"lang_dist": dict((k, sum(1 for s in signs if s["lang"] == k))
for k in set(s["lang"] for s in signs)),
"damage_dist": dict((k, sum(1 for s in signs if s["damage"] == k))
for k in set(s["damage"] for s in signs)),
"lines": lines,
}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--output", default=str(SOURCES / "tablet_structure.jsonl"))
ap.add_argument("--limit", type=int, default=0)
args = ap.parse_args()
tids = sorted(d.name for d in SOURCES.iterdir()
if d.is_dir() and (d / "mark.txt").exists())
print(f"Scanning {len(tids)} tablets")
n = 0
with open(args.output, "w") as out:
for tid in tids:
t = process_tablet(tid)
if t is None:
continue
out.write(json.dumps(t, ensure_ascii=False) + "\n")
n += 1
if args.limit and n >= args.limit:
break
print(f"DONE: {n} tablets → {args.output}")
if __name__ == "__main__":
main()
|