File size: 11,945 Bytes
6255687 9233144 e155d31 b38aa7a ca3d549 b38aa7a 4896db0 b38aa7a 7826bec b38aa7a 9233144 ca3d549 9233144 b06e788 ca3d549 b38aa7a 2babd47 b38aa7a 7826bec b38aa7a 59c9a48 ca3d549 b6cf9b2 ca3d549 b6cf9b2 2ec496c b6cf9b2 b38aa7a b6cf9b2 7826bec 2babd47 2ec496c b38aa7a 2ec496c ca3d549 b38aa7a ca3d549 b38aa7a ca3d549 b38aa7a 7826bec 2ec496c 7826bec b38aa7a 7826bec 2ec496c b38aa7a 2ec496c ca3d549 2ec496c 7826bec 2ec496c 7826bec 2ec496c 7826bec 2ec496c 7826bec 2ec496c 9233144 2ec496c b38aa7a 2ec496c b38aa7a ca3d549 2ec496c ca3d549 2ec496c ca3d549 | 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 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 | import os
os.environ["CUDA_VISIBLE_DEVICES"] = ""
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
import gradio as gr
import numpy as np
import cv2
import tensorflow as tf
from PIL import Image
from huggingface_hub import hf_hub_download
from skimage.filters import gabor
from skimage.color import rgb2gray as sk_rgb2gray
from scipy.spatial import cKDTree
# ββ Config βββββββββββββββββββββββββββββββββββββββββββββββββββ
MODEL_REPO = "Unearthed/artifact-classifier"
MODEL_FILE = "lithic_classifier_hf_fixed.keras"
IMG_SIZE = (224, 224)
CONF_THRESHOLD = 0.55
ENTROPY_THRESHOLD = 1.35
TEXTURE_THRESHOLD = 15.0
REGULARITY_THRESHOLD = 0.30
SYMMETRY_THRESHOLD = 0.38
ARTIFACT_OVERRIDE_THR = 0.45
class_names = ["arrowheads", "axehammerstones", "notartifact", "scrapers"]
DISPLAY_CATEGORY = {
"arrowheads": "Projectile Points",
"axehammerstones": "Other Lithics",
"scrapers": "Other Lithics",
"notartifact": "Non-Artifacts",
}
def display_category(cls_name):
for key, label in DISPLAY_CATEGORY.items():
if key in cls_name.lower():
return label
return cls_name
# ββ Load model at startup βββββββββββββββββββββββββββββββββββββ
print("Loading model...")
model_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)
model = tf.keras.models.load_model(model_path, compile=False)
print("Model ready!")
# ββ Feature extraction βββββββββββββββββββββββββββββββββββββββ
def build_5channel_input(img_np):
rgb = img_np.astype(np.float32) / 255.0
gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY).astype(np.float32)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
lap = np.abs(cv2.Laplacian(blur, cv2.CV_32F))
lap = (lap - lap.min()) / (lap.max() - lap.min() + 1e-8)
gray_sk = sk_rgb2gray(img_np).astype(np.float64)
resps = [np.abs(gabor(gray_sk, frequency=0.15, theta=t)[0])
for t in [0, np.pi/4, np.pi/2]]
gab = np.mean(resps, axis=0).astype(np.float32)
gab = (gab - gab.min()) / (gab.max() - gab.min() + 1e-8)
return np.concatenate([rgb, lap[:,:,np.newaxis], gab[:,:,np.newaxis]], axis=-1)
def texture_complexity_score(img_np):
gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
return float(cv2.Laplacian(gray, cv2.CV_64F).var())
def edge_regularity_score(img_np):
gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
edges = cv2.Canny(gray, 50, 150)
ys, xs = np.where(edges > 0)
if len(xs) < 20:
return 0.0
pts = np.stack([xs, ys], axis=1).astype(np.float32)
tree = cKDTree(pts)
dists, _ = tree.query(pts, k=2)
return float(np.clip(1.0 / (1.0 + np.std(dists[:, 1])), 0, 1))
def symmetry_score(img_np):
gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY).astype(np.float32)
h, w = gray.shape
left = gray[:, :w//2]
right = gray[:, w//2:][:, ::-1]
min_w = min(left.shape[1], right.shape[1])
left, right = left[:, :min_w], right[:, :min_w]
corr = np.corrcoef(left.flatten(), right.flatten())[0, 1]
return float(np.clip((corr + 1) / 2, 0, 1))
def stem_notch_score(img_np):
gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
_, bw = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
h, w = bw.shape
def row_width(region):
widths = []
for row in region:
cols = np.where(row > 0)[0]
if len(cols) > 2:
widths.append(cols[-1] - cols[0])
return np.mean(widths) if widths else 0
full_width = row_width(bw)
base_width = row_width(bw[int(h * 0.75):, :])
if full_width < 1:
return 0.0
return float(np.clip(1.0 - (base_width / full_width), 0, 1))
def is_likely_broken_artifact(tex, reg, sym):
return tex > 80 and reg > 0.55 and sym < 0.70
# ββ Main prediction ββββββββββββββββββββββββββββββββββββββββββ
def predict(image):
print("=== PREDICT CALLED ===")
if image is None:
print("=== IMAGE IS NONE ===")
return "Please upload an image."
print("=== CONVERTING IMAGE ===")
img_np = np.array(image.convert("RGB").resize(IMG_SIZE))
print(f"=== IMAGE SHAPE: {img_np.shape} ===")
tex_score = texture_complexity_score(img_np)
print(f"=== TEX SCORE: {tex_score} ===")
reg_score = edge_regularity_score(img_np)
sym_score = symmetry_score(img_np)
stem_score = stem_notch_score(img_np)
lines = []
lines.append("=" * 55)
lines.append(" FEATURE ANALYSIS (archaeological gates)")
lines.append("=" * 55)
lines.append(f" Texture complexity : {tex_score:.1f} (>15 = real object, >80 = worked stone)")
lines.append(f" Edge regularity : {reg_score:.3f} (>0.30 = systematic flaking)")
lines.append(f" Bilateral symmetry : {sym_score:.3f} (>0.38 = hafted point geometry)")
lines.append(f" Stem/notch evidence : {stem_score:.3f} (>0.25 = base constriction for hafting)")
lines.append("")
lines.append(f"[Gate 1] Texture complexity: {tex_score:.1f}")
if tex_score < TEXTURE_THRESHOLD:
lines.append(" β REJECTED β surface too uniform (not a real stone photo)")
return "\n".join(lines)
lines.append(f"[Gate 2] Edge regularity: {reg_score:.3f}")
x5 = build_5channel_input(img_np)
preds = model.predict(np.expand_dims(x5, 0), verbose=0)[0]
top_idx = int(np.argmax(preds))
top_cls = class_names[top_idx]
top_conf = float(preds[top_idx])
ent = float(-np.sum(preds * np.log(preds + 1e-8)))
notartifact_idx = next((i for i, c in enumerate(class_names) if "notartifact" in c.lower()), None)
artifact_score_sum = sum(float(preds[i]) for i, c in enumerate(class_names) if "notartifact" not in c.lower())
broken = is_likely_broken_artifact(tex_score, reg_score, sym_score)
lines.append("")
lines.append(" Model raw scores:")
merged_scores = {}
for i, cls in enumerate(class_names):
lbl = display_category(cls)
merged_scores[lbl] = merged_scores.get(lbl, 0.0) + float(preds[i])
top_display = display_category(top_cls)
for lbl, score in merged_scores.items():
bar = "β" * int(score * 30)
tag = " β predicted" if lbl == top_display else ""
lines.append(f" {lbl:22s} {score:.3f} {bar}{tag}")
lines.append("")
lines.append(f" Top class : {display_category(top_cls)}")
lines.append(f" Confidence : {top_conf:.3f}")
lines.append(f" Entropy : {ent:.3f}")
lines.append(f" Artifact sum: {artifact_score_sum:.3f} (combined non-notartifact score)")
if broken:
lines.append("")
lines.append(f" π FRAGMENT INDICATOR: High texture ({tex_score:.0f}) + "
f"High regularity ({reg_score:.2f}) + Reduced symmetry ({sym_score:.2f})")
lines.append(" β Characteristic signature of a BROKEN/FRAGMENTARY worked stone tool")
override_triggered = False
if top_cls == "notartifact":
if tex_score > 80 and reg_score > 0.55 and artifact_score_sum > ARTIFACT_OVERRIDE_THR:
artifact_scores = {c: float(preds[i]) for i, c in enumerate(class_names) if i != notartifact_idx}
top_cls = max(artifact_scores, key=artifact_scores.get)
top_conf = artifact_scores[top_cls]
override_triggered = True
lines.append("")
lines.append(" β‘ ARCHAEOLOGICAL OVERRIDE TRIGGERED")
lines.append(f" Overriding notartifact β {display_category(top_cls)}")
else:
lines.append("")
lines.append(" β
PREDICTION: NON-ARTIFACTS")
lines.append(f" Feature gates did not trigger override:")
lines.append(f" Texture {tex_score:.0f}, Regularity {reg_score:.2f}, "
f"Artifact sum {artifact_score_sum:.2f}")
return "\n".join(lines)
lines.append(f"[Gate 3] Bilateral symmetry: {sym_score:.3f}")
if "arrowhead" in top_cls.lower():
if sym_score < SYMMETRY_THRESHOLD:
if broken or override_triggered:
lines.append(f" β οΈ Low symmetry ({sym_score:.2f}) but broken fragment detected β gate bypassed")
else:
lines.append(f" β οΈ Low symmetry for arrowhead prediction β may be debitage")
lines.append(f"[Gate 4] Confidence: {top_conf:.3f} | Entropy: {ent:.3f}")
if not override_triggered:
if top_conf < CONF_THRESHOLD or ent > ENTROPY_THRESHOLD:
lines.append("")
lines.append(" β οΈ NOT CONFIDENT β ambiguous result")
lines.append(" (Broken or unusual specimen β consider manual review)")
return "\n".join(lines)
lines.append("")
disp = display_category(top_cls)
if override_triggered:
lines.append(f" β
FINAL PREDICTION: {disp.upper()} (via archaeological override)")
lines.append(f" (Internal model class: {top_cls})")
else:
lines.append(f" β
PREDICTION: {disp.upper()}")
lines.append(f" (Internal model class: {top_cls})")
if broken:
lines.append(" π NOTE: Fragmentary/broken specimen. Morphology may be incomplete.")
if stem_score > 0.25 and ("arrowhead" in top_cls.lower() or override_triggered):
lines.append(f" π Stem/notch evidence detected (score {stem_score:.2f}) β consistent with hafted projectile point")
interp = {
"arrowhead": (
"Projectile point. Key features to verify:\n"
" β’ Bifacial flaking on both faces\n"
" β’ Thinned cross-section (blade thinner than base)\n"
" β’ Stem or notching for hafting\n"
" β’ Regular, intentional edge serrations\n"
" β’ Conchoidal fracture surface texture (not granular)"
),
"scraper": (
"Scraper. Key features to verify:\n"
" β’ Unifacial working (one face only)\n"
" β’ Steep retouch angle on working edge (~70Β°)\n"
" β’ Cortex usually present on dorsal face\n"
" β’ No stem or hafting preparation"
),
"axehammerstone": (
"Axe/Hammerstone. Key features to verify:\n"
" β’ Pecking marks from repeated impact use\n"
" β’ Ground or polished facets\n"
" β’ Heavier, more rounded form\n"
" β’ May show battered/crushed areas"
),
}
for key, msg in interp.items():
if key in top_cls.lower():
lines.append("")
lines.append(" π Archaeological checklist:")
lines.append(f" {msg}")
break
lines.append("=" * 55)
return "\n".join(lines)
# ββ Gradio UI ββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Blocks(title="Unearthed β Artifact Classifier") as demo:
gr.Markdown("""
# πͺ¨ Unearthed β Artifact Classifier
Upload a photo of a stone object to classify it.
**Categories: Projectile Points Β· Other Lithics Β· Non-Artifacts**
""")
with gr.Row():
with gr.Column():
image_input = gr.Image(type="pil", label="Upload image")
submit_btn = gr.Button("Classify", variant="primary")
with gr.Column():
output_text = gr.Textbox(
label="Analysis Result",
lines=30,
max_lines=40,
)
submit_btn.click(fn=predict, inputs=image_input, outputs=output_text)
demo.launch() |