Update app.py
Browse files
app.py
CHANGED
|
@@ -21,7 +21,7 @@ logger.info(f"Directorio de evidencia creado en: {evidence_dir}")
|
|
| 21 |
def obtener_metadatos(imagen):
|
| 22 |
try:
|
| 23 |
exif_data = imagen.getexif()
|
| 24 |
-
if not
|
| 25 |
return {}
|
| 26 |
metadata = {}
|
| 27 |
for tag_id, value in exif_data.items():
|
|
@@ -36,7 +36,7 @@ def obtener_metadatos(imagen):
|
|
| 36 |
return {}
|
| 37 |
|
| 38 |
def obtener_coordenadas(exif_data):
|
| 39 |
-
if not exif_data or "GPSInfo" not in
|
| 40 |
return None
|
| 41 |
try:
|
| 42 |
gps_info = exif_data["GPSInfo"]
|
|
@@ -70,25 +70,20 @@ def analizar_manipulacion(imagen, metadatos):
|
|
| 70 |
if imagen.mode == "P":
|
| 71 |
razones.append("La imagen tiene marcas de agua o es una imagen indexada.")
|
| 72 |
manipulada = True
|
| 73 |
-
# ❌ Eliminado: ya no se añade por defecto "no tiene metadatos" ni "hash no coincide"
|
| 74 |
if "Software" in metadatos:
|
| 75 |
razones.append(f"La imagen fue editada con: {metadatos['Software']}")
|
| 76 |
manipulada = True
|
| 77 |
return manipulada, razones
|
| 78 |
|
| 79 |
def calcular_porcentaje_ela(ela_imagen, mask):
|
| 80 |
-
"""Calcula porcentaje de píxeles blancos/amarillos (altos valores) en la imagen ELA."""
|
| 81 |
if mask is None or mask.size == 0:
|
| 82 |
return 0.0
|
| 83 |
-
|
| 84 |
total_pixeles = mask.size
|
| 85 |
pixeles_detectados = np.count_nonzero(mask)
|
| 86 |
porcentaje = (pixeles_detectados / total_pixeles) * 100
|
| 87 |
-
|
| 88 |
return porcentaje
|
| 89 |
|
| 90 |
def estimar_probabilidad_manipulacion(porcentaje_ela):
|
| 91 |
-
"""Estima probabilidad de manipulación basado en porcentaje de áreas detectadas."""
|
| 92 |
if porcentaje_ela < 0.5:
|
| 93 |
return "Muy baja (< 0.5%) - Imagen probablemente auténtica."
|
| 94 |
elif porcentaje_ela < 2.0:
|
|
@@ -101,15 +96,13 @@ def estimar_probabilidad_manipulacion(porcentaje_ela):
|
|
| 101 |
return "Muy alta (> 15%) - Manipulación extensa o generación por IA detectada."
|
| 102 |
|
| 103 |
def realizar_ela(imagen):
|
| 104 |
-
"""Realiza ELA optimizado para detectar manipulaciones incluso en imágenes generadas por IA."""
|
| 105 |
try:
|
| 106 |
img_np = np.array(imagen.convert("RGB"))
|
| 107 |
img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
error_scale = 15 # Más sensible a errores
|
| 113 |
brightness_pct = 100
|
| 114 |
equalize_histogram = False
|
| 115 |
|
|
@@ -117,15 +110,14 @@ def realizar_ela(imagen):
|
|
| 117 |
cv2.imwrite(temp_path, img_cv, [cv2.IMWRITE_JPEG_QUALITY, quality])
|
| 118 |
img_comprimida = cv2.imread(temp_path)
|
| 119 |
if img_comprimida is None:
|
| 120 |
-
raise ValueError("
|
| 121 |
|
| 122 |
diferencia = cv2.absdiff(img_cv.astype(np.float32), img_comprimida.astype(np.float32))
|
| 123 |
scaled_diff = diferencia * (noise_level / 10.0) * (error_scale / 100.0) * 20.0
|
| 124 |
scaled_diff = np.clip(scaled_diff, 0, 255).astype(np.uint8)
|
| 125 |
|
| 126 |
-
# ✅ Reducimos umbral para captar sutilezas de IA
|
| 127 |
gray_diff = cv2.cvtColor(scaled_diff, cv2.COLOR_BGR2GRAY)
|
| 128 |
-
_, mask = cv2.threshold(gray_diff, 20, 255, cv2.THRESH_BINARY)
|
| 129 |
mask = mask.astype(np.uint8)
|
| 130 |
|
| 131 |
if equalize_histogram:
|
|
@@ -133,7 +125,7 @@ def realizar_ela(imagen):
|
|
| 133 |
gray_eq = cv2.equalizeHist(gray)
|
| 134 |
scaled_diff = cv2.cvtColor(gray_eq, cv2.COLOR_GRAY2BGR)
|
| 135 |
|
| 136 |
-
brightness_factor = min(1.
|
| 137 |
scaled_diff = cv2.convertScaleAbs(scaled_diff, alpha=brightness_factor, beta=0)
|
| 138 |
|
| 139 |
ela_color = scaled_diff.copy()
|
|
@@ -144,7 +136,7 @@ def realizar_ela(imagen):
|
|
| 144 |
result = np.where(mask[..., None] > 0, ela_color, img_gray)
|
| 145 |
|
| 146 |
os.remove(temp_path)
|
| 147 |
-
return result, mask
|
| 148 |
|
| 149 |
except Exception as e:
|
| 150 |
logger.error(f"Error en ELA: {str(e)}")
|
|
@@ -153,10 +145,12 @@ def realizar_ela(imagen):
|
|
| 153 |
return error_img, None
|
| 154 |
|
| 155 |
def procesar_imagen(archivo_imagen):
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
| 160 |
|
| 161 |
try:
|
| 162 |
img = Image.open(archivo_imagen)
|
|
@@ -171,25 +165,23 @@ def procesar_imagen(archivo_imagen):
|
|
| 171 |
zip_path = os.path.join(evidence_dir, f"{nombre}_errorELA.zip")
|
| 172 |
|
| 173 |
img.save(original_path)
|
| 174 |
-
ela_result, mask = realizar_ela(img)
|
| 175 |
|
| 176 |
if mask is None:
|
| 177 |
-
raise Exception("No se pudo generar
|
| 178 |
|
| 179 |
cv2.imwrite(ela_path, ela_result)
|
| 180 |
|
| 181 |
-
# Calcular porcentaje de áreas detectadas
|
| 182 |
porcentaje_ela = calcular_porcentaje_ela(ela_result, mask)
|
| 183 |
probabilidad = estimar_probabilidad_manipulacion(porcentaje_ela)
|
| 184 |
|
| 185 |
-
# ✅ Obtener tamaño del archivo
|
| 186 |
file_size_bytes = os.path.getsize(archivo_imagen)
|
| 187 |
if file_size_bytes < 1024 * 1024:
|
| 188 |
file_size = f"{file_size_bytes / 1024:.2f} KB"
|
| 189 |
else:
|
| 190 |
file_size = f"{file_size_bytes / (1024 * 1024):.2f} MB"
|
| 191 |
|
| 192 |
-
#
|
| 193 |
info_basica = f"**Nombre del archivo:** {nombre_original}\r\n"
|
| 194 |
info_basica += f"**Tamaño del archivo:** {file_size}\r\n"
|
| 195 |
info_basica += f"**Dimensiones:** {img.size[0]} x {img.size[1]} píxeles\r\n"
|
|
@@ -226,12 +218,10 @@ def procesar_imagen(archivo_imagen):
|
|
| 226 |
info_metadatos += "- No se encontraron metadatos EXIF\r\n"
|
| 227 |
|
| 228 |
sha3_hash = calcular_hash(img)
|
| 229 |
-
info_metadatos += f"\r\n
|
| 230 |
|
| 231 |
manipulada, razones = analizar_manipulacion(img, metadatos)
|
| 232 |
info_manipulacion = "**ANÁLISIS DE MANIPULACIÓN:**\r\n\r\n"
|
| 233 |
-
|
| 234 |
-
# ✅ Añadir porcentaje y estimación
|
| 235 |
info_manipulacion += f"- **Porcentaje de áreas detectadas (blanco/amarillo en ELA):** {porcentaje_ela:.3f}%\r\n"
|
| 236 |
info_manipulacion += f"- **Estimación forense:** {probabilidad}\r\n\r\n"
|
| 237 |
|
|
@@ -260,9 +250,10 @@ def procesar_imagen(archivo_imagen):
|
|
| 260 |
|
| 261 |
except Exception as e:
|
| 262 |
logger.error(f"Error en procesamiento: {str(e)}")
|
|
|
|
| 263 |
error_img = np.zeros((300, 600, 3), dtype=np.uint8)
|
| 264 |
-
cv2.putText(error_img,
|
| 265 |
-
return
|
| 266 |
|
| 267 |
# 🎨 Tema moderno
|
| 268 |
theme = gr.themes.Soft(primary_hue="blue", secondary_hue="slate", neutral_hue="stone")
|
|
@@ -284,7 +275,14 @@ with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as d
|
|
| 284 |
sources=["upload"]
|
| 285 |
)
|
| 286 |
process_btn = gr.Button("Analizar imagen", variant="primary")
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
with gr.Column():
|
| 290 |
with gr.Accordion("Resultado del Análisis ELA", open=True):
|
|
@@ -309,6 +307,7 @@ with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as d
|
|
| 309 |
""
|
| 310 |
)
|
| 311 |
|
|
|
|
| 312 |
process_btn.click(
|
| 313 |
fn=procesar_imagen,
|
| 314 |
inputs=input_image,
|
|
@@ -344,5 +343,5 @@ if __name__ == "__main__":
|
|
| 344 |
server_port=7860,
|
| 345 |
share=True,
|
| 346 |
inbrowser=True,
|
| 347 |
-
favicon_path="https://
|
| 348 |
)
|
|
|
|
| 21 |
def obtener_metadatos(imagen):
|
| 22 |
try:
|
| 23 |
exif_data = imagen.getexif()
|
| 24 |
+
if not exif_
|
| 25 |
return {}
|
| 26 |
metadata = {}
|
| 27 |
for tag_id, value in exif_data.items():
|
|
|
|
| 36 |
return {}
|
| 37 |
|
| 38 |
def obtener_coordenadas(exif_data):
|
| 39 |
+
if not exif_data or "GPSInfo" not in exif_
|
| 40 |
return None
|
| 41 |
try:
|
| 42 |
gps_info = exif_data["GPSInfo"]
|
|
|
|
| 70 |
if imagen.mode == "P":
|
| 71 |
razones.append("La imagen tiene marcas de agua o es una imagen indexada.")
|
| 72 |
manipulada = True
|
|
|
|
| 73 |
if "Software" in metadatos:
|
| 74 |
razones.append(f"La imagen fue editada con: {metadatos['Software']}")
|
| 75 |
manipulada = True
|
| 76 |
return manipulada, razones
|
| 77 |
|
| 78 |
def calcular_porcentaje_ela(ela_imagen, mask):
|
|
|
|
| 79 |
if mask is None or mask.size == 0:
|
| 80 |
return 0.0
|
|
|
|
| 81 |
total_pixeles = mask.size
|
| 82 |
pixeles_detectados = np.count_nonzero(mask)
|
| 83 |
porcentaje = (pixeles_detectados / total_pixeles) * 100
|
|
|
|
| 84 |
return porcentaje
|
| 85 |
|
| 86 |
def estimar_probabilidad_manipulacion(porcentaje_ela):
|
|
|
|
| 87 |
if porcentaje_ela < 0.5:
|
| 88 |
return "Muy baja (< 0.5%) - Imagen probablemente auténtica."
|
| 89 |
elif porcentaje_ela < 2.0:
|
|
|
|
| 96 |
return "Muy alta (> 15%) - Manipulación extensa o generación por IA detectada."
|
| 97 |
|
| 98 |
def realizar_ela(imagen):
|
|
|
|
| 99 |
try:
|
| 100 |
img_np = np.array(imagen.convert("RGB"))
|
| 101 |
img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
| 102 |
|
| 103 |
+
quality = 90
|
| 104 |
+
noise_level = 8
|
| 105 |
+
error_scale = 25
|
|
|
|
| 106 |
brightness_pct = 100
|
| 107 |
equalize_histogram = False
|
| 108 |
|
|
|
|
| 110 |
cv2.imwrite(temp_path, img_cv, [cv2.IMWRITE_JPEG_QUALITY, quality])
|
| 111 |
img_comprimida = cv2.imread(temp_path)
|
| 112 |
if img_comprimida is None:
|
| 113 |
+
raise ValueError("No se pudo leer la imagen comprimida.")
|
| 114 |
|
| 115 |
diferencia = cv2.absdiff(img_cv.astype(np.float32), img_comprimida.astype(np.float32))
|
| 116 |
scaled_diff = diferencia * (noise_level / 10.0) * (error_scale / 100.0) * 20.0
|
| 117 |
scaled_diff = np.clip(scaled_diff, 0, 255).astype(np.uint8)
|
| 118 |
|
|
|
|
| 119 |
gray_diff = cv2.cvtColor(scaled_diff, cv2.COLOR_BGR2GRAY)
|
| 120 |
+
_, mask = cv2.threshold(gray_diff, 20, 255, cv2.THRESH_BINARY)
|
| 121 |
mask = mask.astype(np.uint8)
|
| 122 |
|
| 123 |
if equalize_histogram:
|
|
|
|
| 125 |
gray_eq = cv2.equalizeHist(gray)
|
| 126 |
scaled_diff = cv2.cvtColor(gray_eq, cv2.COLOR_GRAY2BGR)
|
| 127 |
|
| 128 |
+
brightness_factor = min(1.1, brightness_pct / 100.0)
|
| 129 |
scaled_diff = cv2.convertScaleAbs(scaled_diff, alpha=brightness_factor, beta=0)
|
| 130 |
|
| 131 |
ela_color = scaled_diff.copy()
|
|
|
|
| 136 |
result = np.where(mask[..., None] > 0, ela_color, img_gray)
|
| 137 |
|
| 138 |
os.remove(temp_path)
|
| 139 |
+
return result, mask
|
| 140 |
|
| 141 |
except Exception as e:
|
| 142 |
logger.error(f"Error en ELA: {str(e)}")
|
|
|
|
| 145 |
return error_img, None
|
| 146 |
|
| 147 |
def procesar_imagen(archivo_imagen):
|
| 148 |
+
# ✅ VALIDACIÓN URGENTE: Si no hay imagen, detener y avisar
|
| 149 |
+
if not archivo_imagen:
|
| 150 |
+
return None, "❌ **ERROR: Por favor, cargue una imagen antes de analizar.**", None, ""
|
| 151 |
+
|
| 152 |
+
if not os.path.exists(archivo_imagen):
|
| 153 |
+
return None, "❌ **ERROR: El archivo de imagen no existe o es inválido.**", None, ""
|
| 154 |
|
| 155 |
try:
|
| 156 |
img = Image.open(archivo_imagen)
|
|
|
|
| 165 |
zip_path = os.path.join(evidence_dir, f"{nombre}_errorELA.zip")
|
| 166 |
|
| 167 |
img.save(original_path)
|
| 168 |
+
ela_result, mask = realizar_ela(img)
|
| 169 |
|
| 170 |
if mask is None:
|
| 171 |
+
raise Exception("No se pudo generar el análisis ELA.")
|
| 172 |
|
| 173 |
cv2.imwrite(ela_path, ela_result)
|
| 174 |
|
|
|
|
| 175 |
porcentaje_ela = calcular_porcentaje_ela(ela_result, mask)
|
| 176 |
probabilidad = estimar_probabilidad_manipulacion(porcentaje_ela)
|
| 177 |
|
|
|
|
| 178 |
file_size_bytes = os.path.getsize(archivo_imagen)
|
| 179 |
if file_size_bytes < 1024 * 1024:
|
| 180 |
file_size = f"{file_size_bytes / 1024:.2f} KB"
|
| 181 |
else:
|
| 182 |
file_size = f"{file_size_bytes / (1024 * 1024):.2f} MB"
|
| 183 |
|
| 184 |
+
# ✅ TODOS LOS CAMPOS EN NEGRITA como solicitaste
|
| 185 |
info_basica = f"**Nombre del archivo:** {nombre_original}\r\n"
|
| 186 |
info_basica += f"**Tamaño del archivo:** {file_size}\r\n"
|
| 187 |
info_basica += f"**Dimensiones:** {img.size[0]} x {img.size[1]} píxeles\r\n"
|
|
|
|
| 218 |
info_metadatos += "- No se encontraron metadatos EXIF\r\n"
|
| 219 |
|
| 220 |
sha3_hash = calcular_hash(img)
|
| 221 |
+
info_metadatos += f"\r\n**SHA3-256:** {sha3_hash}\r\n\r\n"
|
| 222 |
|
| 223 |
manipulada, razones = analizar_manipulacion(img, metadatos)
|
| 224 |
info_manipulacion = "**ANÁLISIS DE MANIPULACIÓN:**\r\n\r\n"
|
|
|
|
|
|
|
| 225 |
info_manipulacion += f"- **Porcentaje de áreas detectadas (blanco/amarillo en ELA):** {porcentaje_ela:.3f}%\r\n"
|
| 226 |
info_manipulacion += f"- **Estimación forense:** {probabilidad}\r\n\r\n"
|
| 227 |
|
|
|
|
| 250 |
|
| 251 |
except Exception as e:
|
| 252 |
logger.error(f"Error en procesamiento: {str(e)}")
|
| 253 |
+
mensaje_error = f"❌ **ERROR CRÍTICO:** {str(e)}"
|
| 254 |
error_img = np.zeros((300, 600, 3), dtype=np.uint8)
|
| 255 |
+
cv2.putText(error_img, "ERROR INTERNO", (80, 160), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
|
| 256 |
+
return None, mensaje_error, error_img, ""
|
| 257 |
|
| 258 |
# 🎨 Tema moderno
|
| 259 |
theme = gr.themes.Soft(primary_hue="blue", secondary_hue="slate", neutral_hue="stone")
|
|
|
|
| 275 |
sources=["upload"]
|
| 276 |
)
|
| 277 |
process_btn = gr.Button("Analizar imagen", variant="primary")
|
| 278 |
+
|
| 279 |
+
# ✅ BOTÓN DE DESCARGA NARANJA MODERNO
|
| 280 |
+
download_zip = gr.DownloadButton(
|
| 281 |
+
label="⬇️ Descargar resultados (ZIP)",
|
| 282 |
+
variant="primary",
|
| 283 |
+
visible=False,
|
| 284 |
+
interactive=True
|
| 285 |
+
)
|
| 286 |
|
| 287 |
with gr.Column():
|
| 288 |
with gr.Accordion("Resultado del Análisis ELA", open=True):
|
|
|
|
| 307 |
""
|
| 308 |
)
|
| 309 |
|
| 310 |
+
# ✅ Conexión del evento de análisis — ahora con manejo de errores visible
|
| 311 |
process_btn.click(
|
| 312 |
fn=procesar_imagen,
|
| 313 |
inputs=input_image,
|
|
|
|
| 343 |
server_port=7860,
|
| 344 |
share=True,
|
| 345 |
inbrowser=True,
|
| 346 |
+
favicon_path="https://www.forensedigital.gt/wp-content/uploads/2019/07/cropped-40fb84a6-c75a-4c38-bfa0-c0a9777430cd-1.jpg"
|
| 347 |
)
|