leonett commited on
Commit
2605ed5
·
verified ·
1 Parent(s): 7b8c0c7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +58 -55
app.py CHANGED
@@ -70,98 +70,94 @@ 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
- if not metadatos:
74
- razones.append("La imagen no tiene metadatos EXIF.")
75
- manipulada = True
76
- else:
77
- if "Software" in metadatos:
78
- razones.append(f"La imagen fue editada con: {metadatos['Software']}")
79
- manipulada = True
80
- hash_conocido = "d8e3d0e0d7a5e2b2c9d5f9d1c8e7a6f5b0d4e7c3f9d1a2b3c4d5e6f7a8b9c0d1"
81
- hash_actual = calcular_hash(imagen)
82
- if hash_actual != hash_conocido:
83
- razones.append("El hash de la imagen no coincide con el hash conocido.")
84
  manipulada = True
85
  return manipulada, razones
86
 
87
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  def realizar_ela(imagen):
90
- """Realiza ELA con los parámetros y algoritmo exactos del código JavaScript, valores por defecto."""
91
  try:
92
- # Convertir imagen a array numpy y a BGR para OpenCV
93
  img_np = np.array(imagen.convert("RGB"))
94
  img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
95
 
96
- # ✅ Parámetros por defecto del código JS
97
- quality = 95 # Calidad JPG (0-100)
98
- noise_level = 5 # Amplitud del ruido (1-30)
99
- error_scale = 15 # Escala de error (0-100 → 0.0-1.0)
100
- brightness_pct = 100 # Brillo (0-150%)
101
- equalize_histogram = False # Por defecto desactivado
102
 
103
- # Guardar como JPG con calidad especificada
104
  temp_path = "/tmp/temp_image.jpg"
105
  cv2.imwrite(temp_path, img_cv, [cv2.IMWRITE_JPEG_QUALITY, quality])
106
  img_comprimida = cv2.imread(temp_path)
107
  if img_comprimida is None:
108
  raise ValueError("Error al leer la imagen comprimida")
109
 
110
- # Calcular diferencia absoluta
111
  diferencia = cv2.absdiff(img_cv.astype(np.float32), img_comprimida.astype(np.float32))
112
-
113
- # Aplicar "noiseLevel" y "errorScale" como en el código JS
114
- # En JS: diff = Math.max(diffR, diffG, diffB) * errorScale * 20;
115
- # Y cada canal se multiplica por (noiseLevel / 10)
116
- scaled_diff = diferencia * (noise_level / 10.0) * error_scale * 20.0
117
-
118
- # Limitar valores a 0-255 y convertir a uint8
119
  scaled_diff = np.clip(scaled_diff, 0, 255).astype(np.uint8)
120
 
121
- # Crear máscara: áreas con error significativo (simulando elaData.data[i + 3] > 0)
122
  gray_diff = cv2.cvtColor(scaled_diff, cv2.COLOR_BGR2GRAY)
123
- _, mask = cv2.threshold(gray_diff, 30, 255, cv2.THRESH_BINARY) # Umbral arbitrario para "diff > 0.3"
124
  mask = mask.astype(np.uint8)
125
 
126
- # ✅ Aplicar ecualización de histograma si estuviera activada (aunque por defecto es False)
127
  if equalize_histogram:
128
- # Convertir a escala de grises, ecualizar, y volver a BGR
129
  gray = cv2.cvtColor(scaled_diff, cv2.COLOR_BGR2GRAY)
130
  gray_eq = cv2.equalizeHist(gray)
131
  scaled_diff = cv2.cvtColor(gray_eq, cv2.COLOR_GRAY2BGR)
132
 
133
- # ✅ Aplicar brillo (brightness)
134
  brightness_factor = min(1.1, brightness_pct / 100.0)
135
  scaled_diff = cv2.convertScaleAbs(scaled_diff, alpha=brightness_factor, beta=0)
136
 
137
- # ✅ Crear imagen final: fondo negro, áreas detectadas en blanco/amarillo
138
- # Convertimos la diferencia escalada a una imagen en color
139
  ela_color = scaled_diff.copy()
140
-
141
- # Fondo: imagen original en escala de grises oscurecida
142
  img_gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
143
  img_gray = cv2.cvtColor(img_gray, cv2.COLOR_GRAY2BGR)
144
- img_gray = cv2.convertScaleAbs(img_gray, alpha=0.5, beta=0) # Fondo oscuro
145
 
146
- # Combinar: donde hay máscara, mostrar color; donde no, fondo gris
147
  result = np.where(mask[..., None] > 0, ela_color, img_gray)
148
 
149
  os.remove(temp_path)
150
- return result
151
 
152
  except Exception as e:
153
  logger.error(f"Error en ELA: {str(e)}")
154
  error_img = np.zeros((300, 600, 3), dtype=np.uint8) + 30
155
  cv2.putText(error_img, "ERROR AL PROCESAR ELA", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
156
- return error_img
157
-
158
-
159
-
160
-
161
-
162
 
163
  def procesar_imagen(archivo_imagen):
164
  """Procesa la imagen y devuelve ZIP + texto de análisis + imagen ELA + URL de Google Maps (si aplica)."""
 
 
 
 
165
  try:
166
  img = Image.open(archivo_imagen)
167
  logger.info(f"Imagen cargada: {archivo_imagen}")
@@ -175,9 +171,17 @@ def procesar_imagen(archivo_imagen):
175
  zip_path = os.path.join(evidence_dir, f"{nombre}_errorELA.zip")
176
 
177
  img.save(original_path)
178
- ela_result = realizar_ela(img)
 
 
 
 
179
  cv2.imwrite(ela_path, ela_result)
180
 
 
 
 
 
181
  # ✅ Obtener tamaño del archivo
182
  file_size_bytes = os.path.getsize(archivo_imagen)
183
  if file_size_bytes < 1024 * 1024:
@@ -226,6 +230,11 @@ def procesar_imagen(archivo_imagen):
226
 
227
  manipulada, razones = analizar_manipulacion(img, metadatos)
228
  info_manipulacion = "**ANÁLISIS DE MANIPULACIÓN:**\r\n\r\n"
 
 
 
 
 
229
  if manipulada:
230
  info_manipulacion += "⚠️ **LA IMAGEN HA SIDO MANIPULADA.**\r\nRazones:\r\n"
231
  for r in razones:
@@ -245,7 +254,6 @@ def procesar_imagen(archivo_imagen):
245
 
246
  logger.info(f"Análisis completado. Archivo ZIP: {zip_path}")
247
 
248
- # Convertir imagen ELA a RGB para Gradio
249
  ela_rgb = cv2.cvtColor(ela_result, cv2.COLOR_BGR2RGB)
250
 
251
  return zip_path, analysis_text, ela_rgb, google_maps_url or ""
@@ -265,7 +273,6 @@ with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as d
265
  # 📸 Análisis Forense de Imágenes con Error Level Analysis (ELA)
266
  Programa de computación forense para analizar imágenes en busca de evidencia de manipulación o edición.
267
  """)
268
- # ✅ Línea de crédito correctamente indentada y con URL limpia
269
  gr.Markdown("Desarrollado por José R. Leonett para el Grupo de Peritos Forenses Digitales de Guatemala - [www.forensedigital.gt](https://www.forensedigital.gt)")
270
 
271
  with gr.Row():
@@ -282,7 +289,7 @@ with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as d
282
  with gr.Column():
283
  with gr.Accordion("Resultado del Análisis ELA", open=True):
284
  ela_image = gr.Image(
285
- label="🔍 Blanco = áreas borradas o manipuladas",
286
  type="numpy",
287
  height=400,
288
  show_label=True
@@ -293,7 +300,6 @@ with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as d
293
  google_maps_btn = gr.Button("📍 Ver ubicación en Google Maps", visible=False)
294
  google_maps_url_state = gr.State("")
295
 
296
- # Función para resetear todo al cargar nueva imagen
297
  def reset_on_upload():
298
  return (
299
  gr.update(value=None),
@@ -303,7 +309,6 @@ with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as d
303
  ""
304
  )
305
 
306
- # Evento de análisis
307
  process_btn.click(
308
  fn=procesar_imagen,
309
  inputs=input_image,
@@ -319,7 +324,6 @@ with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as d
319
  outputs=download_zip
320
  )
321
 
322
- # Evento para abrir Google Maps
323
  google_maps_btn.click(
324
  fn=lambda url: url,
325
  inputs=google_maps_url_state,
@@ -327,7 +331,6 @@ with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as d
327
  js="(url) => { window.open(url, '_blank'); }"
328
  )
329
 
330
- # Resetear al subir nueva imagen
331
  input_image.upload(
332
  fn=reset_on_upload,
333
  inputs=None,
 
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:
95
+ return "Baja (0.5% - 2%) - Posible compresión o ruido, pero no manipulación evidente."
96
+ elif porcentaje_ela < 5.0:
97
+ return "Moderada (2% - 5%) - Posible edición o retoque localizado."
98
+ elif porcentaje_ela < 15.0:
99
+ return "Alta (5% - 15%) - Alta probabilidad de manipulación o zonas borradas."
100
+ else:
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
+ # ✅ Parámetros ajustados para mayor sensibilidad (especialmente en IA)
110
+ quality = 90 # Bajamos calidad para resaltar más diferencias
111
+ noise_level = 8 # Aumentamos sensibilidad al ruido
112
+ error_scale = 25 # Más sensible a errores
113
+ brightness_pct = 100
114
+ equalize_histogram = False
115
 
 
116
  temp_path = "/tmp/temp_image.jpg"
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("Error al leer la imagen comprimida")
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) # ¡MÁS SENSIBLE!
129
  mask = mask.astype(np.uint8)
130
 
 
131
  if equalize_histogram:
 
132
  gray = cv2.cvtColor(scaled_diff, cv2.COLOR_BGR2GRAY)
133
  gray_eq = cv2.equalizeHist(gray)
134
  scaled_diff = cv2.cvtColor(gray_eq, cv2.COLOR_GRAY2BGR)
135
 
 
136
  brightness_factor = min(1.1, brightness_pct / 100.0)
137
  scaled_diff = cv2.convertScaleAbs(scaled_diff, alpha=brightness_factor, beta=0)
138
 
 
 
139
  ela_color = scaled_diff.copy()
 
 
140
  img_gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
141
  img_gray = cv2.cvtColor(img_gray, cv2.COLOR_GRAY2BGR)
142
+ img_gray = cv2.convertScaleAbs(img_gray, alpha=0.5, beta=0)
143
 
 
144
  result = np.where(mask[..., None] > 0, ela_color, img_gray)
145
 
146
  os.remove(temp_path)
147
+ return result, mask # ← ¡Ahora devuelve también la máscara para cálculo!
148
 
149
  except Exception as e:
150
  logger.error(f"Error en ELA: {str(e)}")
151
  error_img = np.zeros((300, 600, 3), dtype=np.uint8) + 30
152
  cv2.putText(error_img, "ERROR AL PROCESAR ELA", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
153
+ return error_img, None
 
 
 
 
 
154
 
155
  def procesar_imagen(archivo_imagen):
156
  """Procesa la imagen y devuelve ZIP + texto de análisis + imagen ELA + URL de Google Maps (si aplica)."""
157
+ # ✅ Validación: si no hay imagen, devolver mensaje
158
+ if not archivo_imagen or not os.path.exists(archivo_imagen):
159
+ return None, "❌ Por favor, cargue una imagen válida antes de analizar.", None, ""
160
+
161
  try:
162
  img = Image.open(archivo_imagen)
163
  logger.info(f"Imagen cargada: {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) # ← Recibimos máscara
175
+
176
+ if mask is None:
177
+ raise Exception("No se pudo generar la máscara ELA.")
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:
 
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
+
238
  if manipulada:
239
  info_manipulacion += "⚠️ **LA IMAGEN HA SIDO MANIPULADA.**\r\nRazones:\r\n"
240
  for r in razones:
 
254
 
255
  logger.info(f"Análisis completado. Archivo ZIP: {zip_path}")
256
 
 
257
  ela_rgb = cv2.cvtColor(ela_result, cv2.COLOR_BGR2RGB)
258
 
259
  return zip_path, analysis_text, ela_rgb, google_maps_url or ""
 
273
  # 📸 Análisis Forense de Imágenes con Error Level Analysis (ELA)
274
  Programa de computación forense para analizar imágenes en busca de evidencia de manipulación o edición.
275
  """)
 
276
  gr.Markdown("Desarrollado por José R. Leonett para el Grupo de Peritos Forenses Digitales de Guatemala - [www.forensedigital.gt](https://www.forensedigital.gt)")
277
 
278
  with gr.Row():
 
289
  with gr.Column():
290
  with gr.Accordion("Resultado del Análisis ELA", open=True):
291
  ela_image = gr.Image(
292
+ label="🔍 Blanco/Amarillo = áreas manipuladas o generadas por IA",
293
  type="numpy",
294
  height=400,
295
  show_label=True
 
300
  google_maps_btn = gr.Button("📍 Ver ubicación en Google Maps", visible=False)
301
  google_maps_url_state = gr.State("")
302
 
 
303
  def reset_on_upload():
304
  return (
305
  gr.update(value=None),
 
309
  ""
310
  )
311
 
 
312
  process_btn.click(
313
  fn=procesar_imagen,
314
  inputs=input_image,
 
324
  outputs=download_zip
325
  )
326
 
 
327
  google_maps_btn.click(
328
  fn=lambda url: url,
329
  inputs=google_maps_url_state,
 
331
  js="(url) => { window.open(url, '_blank'); }"
332
  )
333
 
 
334
  input_image.upload(
335
  fn=reset_on_upload,
336
  inputs=None,