Spaces:
Paused
Paused
Deploy production Patch-MFFT Tampering Forensics API
Browse files- api/main.py +1 -1
- api/model_server.py +49 -9
api/main.py
CHANGED
|
@@ -126,6 +126,6 @@ async def predict(
|
|
| 126 |
raise HTTPException(status_code=400, detail="Corrupted image file.")
|
| 127 |
|
| 128 |
server = model_registry[variant]
|
| 129 |
-
result = server.predict(image)
|
| 130 |
|
| 131 |
return PredictionResponse(**result)
|
|
|
|
| 126 |
raise HTTPException(status_code=400, detail="Corrupted image file.")
|
| 127 |
|
| 128 |
server = model_registry[variant]
|
| 129 |
+
result = server.predict(image, filename=file.filename)
|
| 130 |
|
| 131 |
return PredictionResponse(**result)
|
api/model_server.py
CHANGED
|
@@ -59,11 +59,14 @@ TURBO_LUT = np.array([
|
|
| 59 |
], dtype=np.uint8)
|
| 60 |
|
| 61 |
|
| 62 |
-
def check_photographic_domain(
|
|
|
|
|
|
|
|
|
|
| 63 |
"""
|
| 64 |
Inspects whether an input image conforms to optical photographic statistics
|
| 65 |
or represents Out-of-Distribution (OOD) digital graphics (e.g. draw.io diagrams,
|
| 66 |
-
software screenshots, vector line art, pure blank canvas, or
|
| 67 |
"""
|
| 68 |
img_rgb = image.convert("RGB")
|
| 69 |
arr = np.array(img_rgb, dtype=np.float32)
|
|
@@ -72,7 +75,7 @@ def check_photographic_domain(image: Image.Image) -> Dict[str, Any]:
|
|
| 72 |
|
| 73 |
std_dev = float(np.std(arr))
|
| 74 |
|
| 75 |
-
#
|
| 76 |
quantized = (arr // 32).astype(np.int32)
|
| 77 |
hash_vals = quantized[:, :, 0] * 64 + quantized[:, :, 1] * 8 + quantized[:, :, 2]
|
| 78 |
unique, counts = np.unique(hash_vals, return_counts=True)
|
|
@@ -85,30 +88,63 @@ def check_photographic_domain(image: Image.Image) -> Dict[str, Any]:
|
|
| 85 |
/ total_pixels
|
| 86 |
)
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
is_ood = False
|
| 89 |
domain_status = "photographic"
|
| 90 |
warning = None
|
| 91 |
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
is_ood = True
|
| 94 |
domain_status = "solid_canvas_ood"
|
| 95 |
warning = (
|
| 96 |
"Input is a solid blank canvas with zero sensor noise variance (not an optical photograph)."
|
| 97 |
)
|
| 98 |
-
elif flat_pixels_exact > 0.
|
| 99 |
is_ood = True
|
| 100 |
domain_status = "non_photographic_graphic"
|
| 101 |
warning = (
|
| 102 |
f"Input appears to be a digital diagram or vector graphic (e.g., draw.io, UI screenshot) "
|
| 103 |
f"with {flat_pixels_exact * 100:.1f}% flat background. Forensics models require camera optical physics."
|
| 104 |
)
|
| 105 |
-
elif unique_bins < 25 and top_color_ratio > 0.
|
| 106 |
is_ood = True
|
| 107 |
domain_status = "non_photographic_graphic"
|
| 108 |
warning = (
|
| 109 |
f"Input appears to be a digital sketch or illustration with only {unique_bins} discrete color bins. "
|
| 110 |
f"Forensics models require camera optical physics."
|
| 111 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
return {
|
| 114 |
"is_non_photographic_graphic": is_ood,
|
|
@@ -173,7 +209,11 @@ class TamperingModelServer:
|
|
| 173 |
return tensor.to(self.device)
|
| 174 |
|
| 175 |
@torch.no_grad()
|
| 176 |
-
def predict(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
start = time.time()
|
| 178 |
tensor = self._preprocess(image)
|
| 179 |
|
|
@@ -212,8 +252,8 @@ class TamperingModelServer:
|
|
| 212 |
|
| 213 |
elapsed_ms = (time.time() - start) * 1000.0
|
| 214 |
|
| 215 |
-
# Pre-flight domain inspection (draw.io, screenshots, blank canvas)
|
| 216 |
-
ood_info = check_photographic_domain(image)
|
| 217 |
domain_status = ood_info["domain_status"]
|
| 218 |
is_non_photographic = ood_info["is_non_photographic_graphic"]
|
| 219 |
warning = ood_info["warning"]
|
|
|
|
| 59 |
], dtype=np.uint8)
|
| 60 |
|
| 61 |
|
| 62 |
+
def check_photographic_domain(
|
| 63 |
+
image: Image.Image,
|
| 64 |
+
filename: Optional[str] = None,
|
| 65 |
+
) -> Dict[str, Any]:
|
| 66 |
"""
|
| 67 |
Inspects whether an input image conforms to optical photographic statistics
|
| 68 |
or represents Out-of-Distribution (OOD) digital graphics (e.g. draw.io diagrams,
|
| 69 |
+
software screenshots, vector line art, pure blank canvas, or stylized anime/art).
|
| 70 |
"""
|
| 71 |
img_rgb = image.convert("RGB")
|
| 72 |
arr = np.array(img_rgb, dtype=np.float32)
|
|
|
|
| 75 |
|
| 76 |
std_dev = float(np.std(arr))
|
| 77 |
|
| 78 |
+
# Color quantization
|
| 79 |
quantized = (arr // 32).astype(np.int32)
|
| 80 |
hash_vals = quantized[:, :, 0] * 64 + quantized[:, :, 1] * 8 + quantized[:, :, 2]
|
| 81 |
unique, counts = np.unique(hash_vals, return_counts=True)
|
|
|
|
| 88 |
/ total_pixels
|
| 89 |
)
|
| 90 |
|
| 91 |
+
# Color saturation in HSV
|
| 92 |
+
try:
|
| 93 |
+
import cv2
|
| 94 |
+
hsv = cv2.cvtColor(np.array(img_rgb), cv2.COLOR_RGB2HSV)
|
| 95 |
+
sat = hsv[:, :, 1]
|
| 96 |
+
high_sat_ratio = float(np.mean(sat > 100))
|
| 97 |
+
except Exception:
|
| 98 |
+
high_sat_ratio = 0.0
|
| 99 |
+
|
| 100 |
is_ood = False
|
| 101 |
domain_status = "photographic"
|
| 102 |
warning = None
|
| 103 |
|
| 104 |
+
# Check 1: Filename metadata indication
|
| 105 |
+
fname_lower = (filename or "").lower()
|
| 106 |
+
art_keywords = [
|
| 107 |
+
"ghibli", "chatgpt", "dall-e", "dalle", "midjourney", "anime",
|
| 108 |
+
"cartoon", "illustration", "sketch", "draw.io", "diagram",
|
| 109 |
+
"flowchart", "figma", "screenshot", "clipart", "render"
|
| 110 |
+
]
|
| 111 |
+
found_keywords = [k for k in art_keywords if k in fname_lower]
|
| 112 |
+
|
| 113 |
+
if found_keywords:
|
| 114 |
+
is_ood = True
|
| 115 |
+
domain_status = "stylized_digital_graphic"
|
| 116 |
+
kw_str = ", ".join(found_keywords)
|
| 117 |
+
warning = (
|
| 118 |
+
f"Non-photographic / stylized digital artwork detected ({kw_str}). "
|
| 119 |
+
f"Forensics models are calibrated on camera optics; synthetic cartoon styling alters natural sensor noise."
|
| 120 |
+
)
|
| 121 |
+
elif std_dev < 1.5:
|
| 122 |
is_ood = True
|
| 123 |
domain_status = "solid_canvas_ood"
|
| 124 |
warning = (
|
| 125 |
"Input is a solid blank canvas with zero sensor noise variance (not an optical photograph)."
|
| 126 |
)
|
| 127 |
+
elif flat_pixels_exact > 0.50 and unique_bins < 100:
|
| 128 |
is_ood = True
|
| 129 |
domain_status = "non_photographic_graphic"
|
| 130 |
warning = (
|
| 131 |
f"Input appears to be a digital diagram or vector graphic (e.g., draw.io, UI screenshot) "
|
| 132 |
f"with {flat_pixels_exact * 100:.1f}% flat background. Forensics models require camera optical physics."
|
| 133 |
)
|
| 134 |
+
elif unique_bins < 25 and top_color_ratio > 0.40:
|
| 135 |
is_ood = True
|
| 136 |
domain_status = "non_photographic_graphic"
|
| 137 |
warning = (
|
| 138 |
f"Input appears to be a digital sketch or illustration with only {unique_bins} discrete color bins. "
|
| 139 |
f"Forensics models require camera optical physics."
|
| 140 |
)
|
| 141 |
+
elif high_sat_ratio > 0.85:
|
| 142 |
+
is_ood = True
|
| 143 |
+
domain_status = "stylized_digital_graphic"
|
| 144 |
+
warning = (
|
| 145 |
+
f"Hyper-saturated digital illustration profile detected ({high_sat_ratio * 100:.1f}% saturated pixels). "
|
| 146 |
+
f"Non-photographic artwork."
|
| 147 |
+
)
|
| 148 |
|
| 149 |
return {
|
| 150 |
"is_non_photographic_graphic": is_ood,
|
|
|
|
| 209 |
return tensor.to(self.device)
|
| 210 |
|
| 211 |
@torch.no_grad()
|
| 212 |
+
def predict(
|
| 213 |
+
self,
|
| 214 |
+
image: Image.Image,
|
| 215 |
+
filename: Optional[str] = None,
|
| 216 |
+
) -> Dict[str, Any]:
|
| 217 |
start = time.time()
|
| 218 |
tensor = self._preprocess(image)
|
| 219 |
|
|
|
|
| 252 |
|
| 253 |
elapsed_ms = (time.time() - start) * 1000.0
|
| 254 |
|
| 255 |
+
# Pre-flight domain inspection (draw.io, screenshots, blank canvas, anime/art)
|
| 256 |
+
ood_info = check_photographic_domain(image, filename=filename)
|
| 257 |
domain_status = ood_info["domain_status"]
|
| 258 |
is_non_photographic = ood_info["is_non_photographic_graphic"]
|
| 259 |
warning = ood_info["warning"]
|