Upload agents/metadata_agent.py with huggingface_hub
Browse files- agents/metadata_agent.py +47 -22
agents/metadata_agent.py
CHANGED
|
@@ -44,6 +44,10 @@ def d02_software_check(img):
|
|
| 44 |
return {"test":"Software Detection","score":s,"note":n}
|
| 45 |
|
| 46 |
def d03_ela(img, quality=90):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
buf=io.BytesIO(); img_rgb=img.convert("RGB"); img_rgb.save(buf,"JPEG",quality=quality); buf.seek(0)
|
| 48 |
resaved=Image.open(buf).convert("RGB"); ela=ImageChops.difference(img_rgb,resaved)
|
| 49 |
ext=ela.getextrema(); mx=max(e[1] for e in ext) or 1
|
|
@@ -53,9 +57,12 @@ def d03_ela(img, quality=90):
|
|
| 53 |
for i in range(0,h-bs,bs):
|
| 54 |
for j in range(0,w-bs,bs): bm.append(float(np.mean(ea[i:i+bs,j:j+bs])))
|
| 55 |
bm=np.array(bm); bstd=float(np.std(bm)); br=float(np.max(bm)-np.min(bm))
|
| 56 |
-
if
|
|
|
|
|
|
|
| 57 |
elif bstd>4: s,n=0.3,f"Moderate ELA (σ={bstd:.1f})"
|
| 58 |
-
elif float(np.std(ea))<1: s,n=0.2,"Uniform ELA — AI"
|
|
|
|
| 59 |
else: s,n=-0.2,f"Consistent ELA (σ={bstd:.1f})"
|
| 60 |
return {"test":"Error Level Analysis","block_std":round(bstd,3),"score":s,"note":n,"ela_image":ela_vis}
|
| 61 |
|
|
@@ -129,12 +136,36 @@ def d10_gps_plausibility(img):
|
|
| 129 |
gps=exif.get(34853)
|
| 130 |
if not gps: return {"test":"GPS Plausibility","score":0.0,"note":"No GPS data"}
|
| 131 |
try:
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
return {"test":"GPS Plausibility","score":s,"note":n}
|
| 139 |
|
| 140 |
def d11_maker_note(img):
|
|
@@ -163,17 +194,11 @@ ALL_TESTS=[d01_exif_completeness,d02_software_check,d03_ela,d04_ai_metadata,d05_
|
|
| 163 |
d10_gps_plausibility,d11_maker_note,d12_file_structure]
|
| 164 |
|
| 165 |
def run_metadata_agent(img):
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
viol=[f["test"] for f in findings if f.get("score",0)>0.2]
|
| 175 |
-
comp=[f["test"] for f in findings if f.get("score",0)<-0.1]
|
| 176 |
-
rat=f"Metadata violations: {', '.join(viol)}." if viol else f"Metadata consistent: {', '.join(comp)}." if comp else "Metadata inconclusive."
|
| 177 |
-
for f in findings:
|
| 178 |
-
if f.get("note"): rat+=f" [{f['test']}]: {f['note']}."
|
| 179 |
-
return AgentEvidence("Metadata Agent",np.clip(avg,-1,1),conf,max(0,1-len(scores)/len(ALL_TESTS)),rat,findings,ela_img)
|
|
|
|
| 44 |
return {"test":"Software Detection","score":s,"note":n}
|
| 45 |
|
| 46 |
def d03_ela(img, quality=90):
|
| 47 |
+
# P4: Detect source format — ELA is only meaningful for JPEG inputs
|
| 48 |
+
source_format = getattr(img, 'format', None)
|
| 49 |
+
is_jpeg = source_format and source_format.upper() in ("JPEG", "JPG")
|
| 50 |
+
|
| 51 |
buf=io.BytesIO(); img_rgb=img.convert("RGB"); img_rgb.save(buf,"JPEG",quality=quality); buf.seek(0)
|
| 52 |
resaved=Image.open(buf).convert("RGB"); ela=ImageChops.difference(img_rgb,resaved)
|
| 53 |
ext=ela.getextrema(); mx=max(e[1] for e in ext) or 1
|
|
|
|
| 57 |
for i in range(0,h-bs,bs):
|
| 58 |
for j in range(0,w-bs,bs): bm.append(float(np.mean(ea[i:i+bs,j:j+bs])))
|
| 59 |
bm=np.array(bm); bstd=float(np.std(bm)); br=float(np.max(bm)-np.min(bm))
|
| 60 |
+
if not is_jpeg and float(np.std(ea))<1:
|
| 61 |
+
s,n=0.0,"PNG/lossless source — ELA comparison is lossless→JPEG, not meaningful"
|
| 62 |
+
elif bstd>8 and br>30: s,n=0.6,f"High ELA variance (σ={bstd:.1f}) — manipulation"
|
| 63 |
elif bstd>4: s,n=0.3,f"Moderate ELA (σ={bstd:.1f})"
|
| 64 |
+
elif float(np.std(ea))<1 and is_jpeg: s,n=0.2,"Uniform ELA on JPEG — possible AI"
|
| 65 |
+
elif float(np.std(ea))<1: s,n=0.0,"Uniform ELA (non-JPEG source)"
|
| 66 |
else: s,n=-0.2,f"Consistent ELA (σ={bstd:.1f})"
|
| 67 |
return {"test":"Error Level Analysis","block_std":round(bstd,3),"score":s,"note":n,"ela_image":ela_vis}
|
| 68 |
|
|
|
|
| 136 |
gps=exif.get(34853)
|
| 137 |
if not gps: return {"test":"GPS Plausibility","score":0.0,"note":"No GPS data"}
|
| 138 |
try:
|
| 139 |
+
def _dms_to_dd(dms):
|
| 140 |
+
"""Convert (degrees, minutes, seconds) tuple to decimal degrees."""
|
| 141 |
+
if isinstance(dms, (list, tuple)) and len(dms) == 3:
|
| 142 |
+
d = float(dms[0]) if not hasattr(dms[0], 'numerator') else float(dms[0])
|
| 143 |
+
m = float(dms[1]) if not hasattr(dms[1], 'numerator') else float(dms[1])
|
| 144 |
+
s_val = float(dms[2]) if not hasattr(dms[2], 'numerator') else float(dms[2])
|
| 145 |
+
return d + m/60.0 + s_val/3600.0
|
| 146 |
+
return float(dms)
|
| 147 |
+
|
| 148 |
+
lat_ref = gps.get(1, "N")
|
| 149 |
+
lon_ref = gps.get(3, "E")
|
| 150 |
+
lat_raw = gps.get(2, (0,0,0))
|
| 151 |
+
lon_raw = gps.get(4, (0,0,0))
|
| 152 |
+
|
| 153 |
+
lat = _dms_to_dd(lat_raw)
|
| 154 |
+
lon = _dms_to_dd(lon_raw)
|
| 155 |
+
if lat_ref == "S": lat = -lat
|
| 156 |
+
if lon_ref == "W": lon = -lon
|
| 157 |
+
|
| 158 |
+
# Bounds check
|
| 159 |
+
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
|
| 160 |
+
s,n = 0.4, f"Impossible GPS coordinates (lat={lat:.4f}, lon={lon:.4f})"
|
| 161 |
+
elif abs(lat) < 0.001 and abs(lon) < 0.001:
|
| 162 |
+
s,n = 0.3, f"GPS at Null Island (0,0) — likely fabricated"
|
| 163 |
+
elif abs(lat) < 0.1 and abs(lon) < 0.1:
|
| 164 |
+
s,n = 0.2, f"GPS near (0,0) — suspicious (lat={lat:.4f}, lon={lon:.4f})"
|
| 165 |
+
else:
|
| 166 |
+
s,n = -0.2, f"Plausible GPS ({lat:.4f}°{'N' if lat>=0 else 'S'}, {lon:.4f}°{'E' if lon>=0 else 'W'})"
|
| 167 |
+
except Exception as e:
|
| 168 |
+
s,n = 0.0, f"GPS parse error: {str(e)[:50]}"
|
| 169 |
return {"test":"GPS Plausibility","score":s,"note":n}
|
| 170 |
|
| 171 |
def d11_maker_note(img):
|
|
|
|
| 194 |
d10_gps_plausibility,d11_maker_note,d12_file_structure]
|
| 195 |
|
| 196 |
def run_metadata_agent(img):
|
| 197 |
+
from agents.utils import run_agent_tests
|
| 198 |
+
findings_raw, avg, conf, fail, rat = run_agent_tests(ALL_TESTS, img, "Metadata Agent")
|
| 199 |
+
# Extract ELA image for visual evidence
|
| 200 |
+
ela_img = None
|
| 201 |
+
for f in findings_raw:
|
| 202 |
+
if "ela_image" in f:
|
| 203 |
+
ela_img = f.pop("ela_image")
|
| 204 |
+
return AgentEvidence("Metadata Agent", np.clip(avg,-1,1), conf, fail, rat, findings_raw, ela_img)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|