Delete raven/manifest.py
Browse files- raven/manifest.py +0 -176
raven/manifest.py
DELETED
|
@@ -1,176 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
from collections import Counter
|
| 3 |
-
from pathlib import Path
|
| 4 |
-
import numpy as np
|
| 5 |
-
from PIL import Image, ImageOps
|
| 6 |
-
try:
|
| 7 |
-
import imagehash
|
| 8 |
-
except Exception:
|
| 9 |
-
imagehash = None
|
| 10 |
-
from tqdm import tqdm
|
| 11 |
-
from .utils import iter_images, stable_unit, write_jsonl
|
| 12 |
-
from .parquet_io import discover_parquet_files, materialize_parquet
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def _phash(path: Path, hash_size: int = 12) -> str | None:
|
| 16 |
-
try:
|
| 17 |
-
with Image.open(path) as im:
|
| 18 |
-
im = ImageOps.exif_transpose(im).convert("RGB")
|
| 19 |
-
if imagehash is not None:
|
| 20 |
-
return str(imagehash.phash(im, hash_size=hash_size))
|
| 21 |
-
g = im.convert("L").resize((32, 32), Image.Resampling.LANCZOS)
|
| 22 |
-
a = np.asarray(g, dtype=np.float32) / 255.0
|
| 23 |
-
low = np.abs(np.fft.fft2(a))[:hash_size, :hash_size]
|
| 24 |
-
flat = low.ravel(); med = float(np.median(flat[1:])) if flat.size > 1 else float(flat[0])
|
| 25 |
-
bits = (flat > med).astype(np.uint8)
|
| 26 |
-
value = 0
|
| 27 |
-
for bit in bits: value = (value << 1) | int(bit)
|
| 28 |
-
width = (len(bits) + 3) // 4
|
| 29 |
-
return f"{value:0{width}x}"
|
| 30 |
-
except Exception:
|
| 31 |
-
return None
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def _hamming_hex(a: str, b: str) -> int:
|
| 35 |
-
try: return (int(a, 16) ^ int(b, 16)).bit_count()
|
| 36 |
-
except Exception: return 9999
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
class _HammingIndex:
|
| 40 |
-
|
| 41 |
-
def __init__(self, max_distance:int):
|
| 42 |
-
self.d=max(0,int(max_distance)); self.values=[]; self.buckets={}
|
| 43 |
-
|
| 44 |
-
def _segments(self, value:int, bits:int):
|
| 45 |
-
parts=max(1,self.d+1); base=bits//parts; rem=bits%parts; shift=0
|
| 46 |
-
for i in range(parts):
|
| 47 |
-
width=base+(1 if i<rem else 0); mask=(1<<width)-1
|
| 48 |
-
yield i,(value>>shift)&mask
|
| 49 |
-
shift+=width
|
| 50 |
-
|
| 51 |
-
def duplicate_or_add(self, ph:str|None)->bool:
|
| 52 |
-
if not ph or self.d<0: return False
|
| 53 |
-
try: value=int(ph,16)
|
| 54 |
-
except Exception: return False
|
| 55 |
-
bits=max(4,len(ph)*4); cand=set(); segs=list(self._segments(value,bits))
|
| 56 |
-
for key in segs:
|
| 57 |
-
cand.update(self.buckets.get(key,()))
|
| 58 |
-
for idx in cand:
|
| 59 |
-
if (value^self.values[idx]).bit_count()<=self.d:
|
| 60 |
-
return True
|
| 61 |
-
idx=len(self.values); self.values.append(value)
|
| 62 |
-
for key in segs:
|
| 63 |
-
self.buckets.setdefault(key,[]).append(idx)
|
| 64 |
-
return False
|
| 65 |
-
|
| 66 |
-
def _stats(path: Path) -> dict:
|
| 67 |
-
try:
|
| 68 |
-
with Image.open(path) as im:
|
| 69 |
-
fmt = (im.format or path.suffix.lstrip(".")).upper(); exif = im.getexif(); has_exif = bool(exif)
|
| 70 |
-
im = ImageOps.exif_transpose(im).convert("RGB"); w, h = im.size
|
| 71 |
-
tiny = im.copy(); tiny.thumbnail((192, 192)); a = np.asarray(tiny, dtype=np.float32) / 255.0
|
| 72 |
-
y = a[..., 0] * 0.2126 + a[..., 1] * 0.7152 + a[..., 2] * 0.0722
|
| 73 |
-
mean = float(y.mean()); std = float(y.std()); dark_fraction = float((y < 0.12).mean())
|
| 74 |
-
if mean < 0.08: bucket = "extreme-dark"
|
| 75 |
-
elif mean < 0.20: bucket = "dark"
|
| 76 |
-
elif mean < 0.38: bucket = "dim"
|
| 77 |
-
else: bucket = "normal"
|
| 78 |
-
return {"width":int(w),"height":int(h),"megapixels":float(w*h/1e6),"aspect_ratio":float(w/max(1,h)),"format":fmt,"has_exif":has_exif,"mean_luma":mean,"std_luma":std,"dark_fraction":dark_fraction,"light_bucket":bucket}
|
| 79 |
-
except Exception:
|
| 80 |
-
return {"width":None,"height":None,"megapixels":None,"aspect_ratio":None,"format":path.suffix.lstrip(".").upper(),"has_exif":False,"mean_luma":None,"std_luma":None,"dark_fraction":None,"light_bucket":"unknown"}
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def _normal_descriptors(root_spec:str|Path,label:int) -> list[dict]:
|
| 84 |
-
root=Path(root_spec).resolve(); out=[]
|
| 85 |
-
if not root.exists(): return out
|
| 86 |
-
|
| 87 |
-
if root.is_file(): return out
|
| 88 |
-
for p in iter_images(root):
|
| 89 |
-
try: rel=p.relative_to(root)
|
| 90 |
-
except Exception: continue
|
| 91 |
-
source_leaf=rel.parts[0] if len(rel.parts)>1 else ("unknown-camera" if label==0 else "unknown-generator")
|
| 92 |
-
out.append({"path":str(p.resolve()),"label":int(label),"source_leaf":source_leaf,"group":str(rel.with_suffix("")),"origin_kind":"image","origin_parquet":None,"origin_row":None})
|
| 93 |
-
return out
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def _parquet_descriptors(root_spec:str|Path,label:int,cache_root:Path,image_column:str|None,source_column:str|None) -> list[dict]:
|
| 97 |
-
out=[]
|
| 98 |
-
for pq in discover_parquet_files(root_spec):
|
| 99 |
-
default_source=pq.stem
|
| 100 |
-
rows=materialize_parquet(pq,cache_root,label,default_source,image_column,source_column)
|
| 101 |
-
out.extend(rows)
|
| 102 |
-
return out
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def build_manifest(
|
| 106 |
-
real_dir: str,
|
| 107 |
-
ai_dir: str,
|
| 108 |
-
out: str,
|
| 109 |
-
holdout_sources: list[str] | None = None,
|
| 110 |
-
val_fraction: float = 0.15,
|
| 111 |
-
real_test_fraction: float = 0.15,
|
| 112 |
-
dedupe_hamming_distance: int = 3,
|
| 113 |
-
parquet_cache_dir: str | None = None,
|
| 114 |
-
real_parquet_image_column: str | None = None,
|
| 115 |
-
real_parquet_source_column: str | None = None,
|
| 116 |
-
ai_parquet_image_column: str | None = None,
|
| 117 |
-
ai_parquet_source_column: str | None = None,
|
| 118 |
-
force_real_source: str | None = None,
|
| 119 |
-
force_ai_source: str | None = None,
|
| 120 |
-
) -> list[dict]:
|
| 121 |
-
holdout = {s.strip().lower() for s in (holdout_sources or []) if s.strip()}
|
| 122 |
-
rows=[]; dedupe_index=_HammingIndex(dedupe_hamming_distance)
|
| 123 |
-
manifest_parent=Path(out).resolve().parent
|
| 124 |
-
pq_cache=Path(parquet_cache_dir).resolve() if parquet_cache_dir else (manifest_parent/"cache"/"parquet_materialized")
|
| 125 |
-
pq_cache.mkdir(parents=True,exist_ok=True)
|
| 126 |
-
|
| 127 |
-
def duplicate(ph):
|
| 128 |
-
if dedupe_hamming_distance < 0:
|
| 129 |
-
return False
|
| 130 |
-
return dedupe_index.duplicate_or_add(ph)
|
| 131 |
-
|
| 132 |
-
specs=[
|
| 133 |
-
(0,real_dir,"real",real_parquet_image_column,real_parquet_source_column),
|
| 134 |
-
(1,ai_dir,"ai",ai_parquet_image_column,ai_parquet_source_column),
|
| 135 |
-
]
|
| 136 |
-
for label,root_spec,kind,img_col,src_col in specs:
|
| 137 |
-
descriptors=_normal_descriptors(root_spec,label)
|
| 138 |
-
pq_desc=_parquet_descriptors(root_spec,label,pq_cache/kind,img_col,src_col)
|
| 139 |
-
descriptors.extend(pq_desc)
|
| 140 |
-
print(f"[{kind}] normal_images={len(descriptors)-len(pq_desc)} parquet_images={len(pq_desc)}")
|
| 141 |
-
for d in tqdm(descriptors,desc=f"manifest-{kind}"):
|
| 142 |
-
p=Path(d["path"]); raw_source_leaf=str(d.get("source_leaf") or ("unknown-camera" if label==0 else "unknown-generator"))
|
| 143 |
-
forced = force_real_source if label==0 else force_ai_source
|
| 144 |
-
source_leaf = str(forced).strip() if forced and str(forced).strip() else raw_source_leaf
|
| 145 |
-
ph=_phash(p)
|
| 146 |
-
if duplicate(ph): continue
|
| 147 |
-
group=str(d.get("group") or p.stem)
|
| 148 |
-
if label==1 and source_leaf.lower() in holdout: split="test"
|
| 149 |
-
else:
|
| 150 |
-
r=stable_unit(f"{kind}:{group}")
|
| 151 |
-
if label==0:
|
| 152 |
-
if r<real_test_fraction: split="test"
|
| 153 |
-
elif r<real_test_fraction+val_fraction: split="val"
|
| 154 |
-
else: split="train"
|
| 155 |
-
else: split="val" if r<val_fraction else "train"
|
| 156 |
-
rows.append({
|
| 157 |
-
"path":str(p.resolve()),"label":int(label),
|
| 158 |
-
"source":(f"real:{source_leaf}" if label==0 else source_leaf),
|
| 159 |
-
"raw_source":raw_source_leaf,
|
| 160 |
-
"generator":source_leaf if label==1 else None,"camera_bucket":source_leaf if label==0 else None,
|
| 161 |
-
"group":group,"phash":ph,"split":split,
|
| 162 |
-
"origin_kind":d.get("origin_kind","image"),"origin_parquet":d.get("origin_parquet"),"origin_row":d.get("origin_row"),
|
| 163 |
-
**_stats(p),
|
| 164 |
-
})
|
| 165 |
-
write_jsonl(out,rows); return rows
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
def manifest_summary(rows:list[dict])->str:
|
| 169 |
-
c=Counter((r.get("split"),int(r.get("label",-1))) for r in rows); src=Counter((r.get("split"),str(r.get("source","unknown"))) for r in rows); origin=Counter((r.get("split"),str(r.get("origin_kind","image"))) for r in rows)
|
| 170 |
-
lines=["Counts by split / label (0=REAL, 1=AI):"]
|
| 171 |
-
for k,v in sorted(c.items(),key=lambda kv:str(kv[0])): lines.append(f" {k}: {v}")
|
| 172 |
-
lines.append("\nCounts by split / source:")
|
| 173 |
-
for k,v in sorted(src.items(),key=lambda kv:str(kv[0])): lines.append(f" {k}: {v}")
|
| 174 |
-
lines.append("\nCounts by split / storage origin:")
|
| 175 |
-
for k,v in sorted(origin.items(),key=lambda kv:str(kv[0])): lines.append(f" {k}: {v}")
|
| 176 |
-
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|