leonett commited on
Commit
4d7e9f3
·
verified ·
1 Parent(s): 6932d9c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +79 -46
app.py CHANGED
@@ -85,7 +85,7 @@ def analizar_manipulacion(imagen, metadatos):
85
  return manipulada, razones
86
 
87
  def realizar_ela(imagen, quality=95, scale=15):
88
- """Realiza ELA y devuelve imagen en color sobre fondo oscuro para resaltar errores."""
89
  try:
90
  img_np = np.array(imagen.convert("RGB"))
91
  img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
@@ -100,32 +100,33 @@ def realizar_ela(imagen, quality=95, scale=15):
100
  diferencia = cv2.absdiff(img_cv, img_comprimida)
101
  ela_imagen = scale * diferencia
102
 
103
- # Aplicar mapa de color JET (rojo/amarillo = alto error)
104
- ela_color = cv2.applyColorMap(ela_imagen, cv2.COLORMAP_JET)
105
 
106
- # Crear fondo oscuro (gris-negro) para mejorar contraste
107
- background = np.zeros_like(ela_color) + 20 # Gris muy oscuro
108
- mask = ela_imagen > 5 # Solo resaltar donde hay error significativo
109
- mask = np.any(mask, axis=2, keepdims=True)
110
- result = np.where(mask, ela_color, background)
 
111
 
112
  os.remove(temp_path)
113
- return result
114
 
115
  except Exception as e:
116
  logger.error(f"Error en ELA: {str(e)}")
117
- # Devolver imagen de error con fondo oscuro y texto blanco
118
- error_img = np.zeros((300, 600, 3), dtype=np.uint8) + 30
119
- cv2.putText(error_img, "Error al procesar ELA", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,255,255), 2)
120
  return error_img
121
 
122
  def procesar_imagen(archivo_imagen):
123
- """Procesa la imagen y devuelve ZIP + texto de análisis + imagen ELA."""
124
  try:
125
  img = Image.open(archivo_imagen)
126
  logger.info(f"Imagen cargada: {archivo_imagen}")
127
 
128
- nombre = os.path.splitext(os.path.basename(archivo_imagen))[0]
 
129
 
130
  original_path = os.path.join(evidence_dir, f"{nombre}_original.jpg")
131
  ela_path = os.path.join(evidence_dir, f"{nombre}_ela.jpg")
@@ -136,48 +137,54 @@ def procesar_imagen(archivo_imagen):
136
  ela_result = realizar_ela(img)
137
  cv2.imwrite(ela_path, ela_result)
138
 
139
- info_basica = f"Formato: {img.format}\nTamaño: {img.size} píxeles\nModo: {img.mode}\n"
 
 
 
 
140
 
141
  metadatos = obtener_metadatos(img)
142
- info_metadatos = "ANÁLISIS FORENSE DE LOS METADATOS:\n"
 
 
143
 
144
  if metadatos:
145
  for tag, value in metadatos.items():
146
  if tag == "DateTime":
147
- info_metadatos += f"Fecha y hora de captura: {value}\n"
148
  elif tag == "Make":
149
- info_metadatos += f"Fabricante de cámara: {value}\n"
150
  elif tag == "Model":
151
- info_metadatos += f"Modelo de cámara: {value}\n"
152
  elif tag == "Software":
153
- info_metadatos += f"Software de edición: {value}\n"
154
  elif tag == "GPSInfo":
155
- info_metadatos += "Ubicación GPS:\n"
156
  coords = obtener_coordenadas(metadatos)
157
  if coords:
158
  lat, lon = coords
159
- info_metadatos += f"- Coordenadas: {lat:.5f}, {lon:.5f}\n"
160
- info_metadatos += f"- Enlace a Google Maps: https://www.google.com/maps?q={lat},{lon}\n"
 
161
  else:
162
- info_metadatos += "- No se encontraron coordenadas GPS\n"
163
  else:
164
- info_metadatos += f"{tag}: {value}\n"
165
  else:
166
- info_metadatos += "No se encontraron metadatos EXIF\n"
167
 
168
  sha3_hash = calcular_hash(img)
169
- info_metadatos += f"\nSHA3-256: {sha3_hash}\n"
170
 
171
  manipulada, razones = analizar_manipulacion(img, metadatos)
172
- info_manipulacion = "\nANÁLISIS DE MANIPULACIÓN:\n"
173
  if manipulada:
174
- info_manipulacion += "LA IMAGEN HA SIDO MANIPULADA.\nRazones:\n"
175
  for r in razones:
176
  info_manipulacion += f"- {r}\n"
177
  else:
178
- info_manipulacion += "LA IMAGEN NO HA SIDO MANIPULADA.\n"
179
 
180
- analysis_text = f"{info_basica}\n{info_metadatos}\n{info_manipulacion}"
181
 
182
  with open(text_path, "w") as f:
183
  f.write(analysis_text)
@@ -189,18 +196,18 @@ def procesar_imagen(archivo_imagen):
189
 
190
  logger.info(f"Análisis completado. Archivo ZIP: {zip_path}")
191
 
192
- # Convertir imagen ELA de BGR a RGB para mostrar en Gradio
193
  ela_rgb = cv2.cvtColor(ela_result, cv2.COLOR_BGR2RGB)
194
 
195
- return zip_path, analysis_text, ela_rgb
196
 
197
  except Exception as e:
198
  logger.error(f"Error en procesamiento: {str(e)}")
199
  error_img = np.zeros((300, 600, 3), dtype=np.uint8)
200
  cv2.putText(error_img, f"ERROR: {str(e)[:50]}", (10, 150), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,255,255), 2)
201
- return f"Error: {str(e)}", f"Error al procesar la imagen: {str(e)}", error_img
202
 
203
- # 🎨 Tema moderno y compatible
204
  theme = gr.themes.Soft(primary_hue="blue", secondary_hue="slate", neutral_hue="stone")
205
 
206
  # 🖥️ Interfaz Gradio
@@ -212,23 +219,19 @@ with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as d
212
 
213
  with gr.Row():
214
  with gr.Column():
215
- # 👇 SIN WEBCAM — Solo carga de archivo
216
  input_image = gr.Image(
217
  label="Subir imagen (JPG/PNG)",
218
  type="filepath",
219
  height=400,
220
- sources=["upload"] # ← ¡Esto elimina la webcam!
221
  )
222
  process_btn = gr.Button("Analizar imagen", variant="primary")
223
-
224
- # 👇 ZIP aparece SOLO después del análisis
225
  download_zip = gr.File(label="⬇️ Descargar resultados (ZIP)", interactive=False, visible=False)
226
 
227
  with gr.Column():
228
  with gr.Accordion("Resultado del Análisis ELA", open=True):
229
- # 👇 Aquí SÍ se muestra la imagen ELA procesada
230
  ela_image = gr.Image(
231
- label="🔍 Áreas resaltadas = posibles manipulaciones",
232
  type="numpy",
233
  height=400,
234
  show_label=True
@@ -236,21 +239,51 @@ with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as d
236
 
237
  with gr.Accordion("Informe Detallado", open=True):
238
  analysis_text = gr.Textbox(label="📝 Resultados del análisis forense", lines=15, max_lines=25)
239
-
240
- # ⚙️ Evento de análisis — ahora devuelve 3 valores
 
 
 
 
 
 
 
 
 
 
 
 
241
  process_btn.click(
242
  fn=procesar_imagen,
243
  inputs=input_image,
244
- outputs=[download_zip, analysis_text, ela_image],
245
  api_name="analyze_image"
246
  ).then(
247
- # 👇 Hacer visible el ZIP después del análisis
 
 
 
248
  fn=lambda: gr.update(visible=True),
249
  inputs=None,
250
  outputs=download_zip
251
  )
252
 
253
- # 👇 Línea de crédito solicitada
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  gr.Markdown("Desarrollado por José R. Leonett para el Grupo de Peritos Forenses Digitales de Guatemala - [www.forensedigital.gt](https://www.forensedigital.gt)")
255
 
256
  # ▶️ Ejecución
 
85
  return manipulada, razones
86
 
87
  def realizar_ela(imagen, quality=95, scale=15):
88
+ """Realiza ELA y devuelve imagen en blanco sobre negro para resaltar errores."""
89
  try:
90
  img_np = np.array(imagen.convert("RGB"))
91
  img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
 
100
  diferencia = cv2.absdiff(img_cv, img_comprimida)
101
  ela_imagen = scale * diferencia
102
 
103
+ # Convertir a escala de grises
104
+ ela_gray = cv2.cvtColor(ela_imagen, cv2.COLOR_BGR2GRAY)
105
 
106
+ # Invertir y normalizar para que las áreas modificadas sean BLANCAS sobre NEGRO
107
+ ela_gray = cv2.normalize(ela_gray, None, 0, 255, cv2.NORM_MINMAX)
108
+ _, ela_thresh = cv2.threshold(ela_gray, 30, 255, cv2.THRESH_BINARY)
109
+
110
+ # Convertir a 3 canales para Gradio
111
+ ela_result = cv2.merge([ela_thresh, ela_thresh, ela_thresh])
112
 
113
  os.remove(temp_path)
114
+ return ela_result
115
 
116
  except Exception as e:
117
  logger.error(f"Error en ELA: {str(e)}")
118
+ error_img = np.zeros((300, 600, 3), dtype=np.uint8)
119
+ cv2.putText(error_img, "ERROR AL PROCESAR ELA", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,255,255), 2)
 
120
  return error_img
121
 
122
  def procesar_imagen(archivo_imagen):
123
+ """Procesa la imagen y devuelve ZIP + texto de análisis + imagen ELA + URL de Google Maps (si aplica)."""
124
  try:
125
  img = Image.open(archivo_imagen)
126
  logger.info(f"Imagen cargada: {archivo_imagen}")
127
 
128
+ nombre_original = os.path.basename(archivo_imagen)
129
+ nombre = os.path.splitext(nombre_original)[0]
130
 
131
  original_path = os.path.join(evidence_dir, f"{nombre}_original.jpg")
132
  ela_path = os.path.join(evidence_dir, f"{nombre}_ela.jpg")
 
137
  ela_result = realizar_ela(img)
138
  cv2.imwrite(ela_path, ela_result)
139
 
140
+ # Información básica
141
+ info_basica = f"**Nombre del archivo:** {nombre_original}\n"
142
+ info_basica += f"**Formato:** {img.format}\n"
143
+ info_basica += f"**Tamaño:** {img.size[0]} x {img.size[1]} píxeles\n"
144
+ info_basica += f"**Modo:** {img.mode}\n\n"
145
 
146
  metadatos = obtener_metadatos(img)
147
+ info_metadatos = "**ANÁLISIS FORENSE DE LOS METADATOS:**\n"
148
+
149
+ google_maps_url = None
150
 
151
  if metadatos:
152
  for tag, value in metadatos.items():
153
  if tag == "DateTime":
154
+ info_metadatos += f"- Fecha y hora de captura: {value}\n"
155
  elif tag == "Make":
156
+ info_metadatos += f"- Fabricante de cámara: {value}\n"
157
  elif tag == "Model":
158
+ info_metadatos += f"- Modelo de cámara: {value}\n"
159
  elif tag == "Software":
160
+ info_metadatos += f"- Software de edición: {value}\n"
161
  elif tag == "GPSInfo":
 
162
  coords = obtener_coordenadas(metadatos)
163
  if coords:
164
  lat, lon = coords
165
+ info_metadatos += f"- Coordenadas GPS: {lat:.6f}, {lon:.6f}\n"
166
+ google_maps_url = f"https://www.google.com/maps?q={lat},{lon}"
167
+ info_metadatos += f"- Enlace a Google Maps: {google_maps_url}\n"
168
  else:
169
+ info_metadatos += "- No se encontraron coordenadas GPS válidas\n"
170
  else:
171
+ info_metadatos += f"- {tag}: {value}\n"
172
  else:
173
+ info_metadatos += "- No se encontraron metadatos EXIF\n"
174
 
175
  sha3_hash = calcular_hash(img)
176
+ info_metadatos += f"\n- SHA3-256: {sha3_hash}\n\n"
177
 
178
  manipulada, razones = analizar_manipulacion(img, metadatos)
179
+ info_manipulacion = "**ANÁLISIS DE MANIPULACIÓN:**\n"
180
  if manipulada:
181
+ info_manipulacion += "⚠️ **LA IMAGEN HA SIDO MANIPULADA.**\nRazones:\n"
182
  for r in razones:
183
  info_manipulacion += f"- {r}\n"
184
  else:
185
+ info_manipulacion += "✅ **LA IMAGEN NO HA SIDO MANIPULADA.**\n"
186
 
187
+ analysis_text = info_basica + info_metadatos + info_manipulacion
188
 
189
  with open(text_path, "w") as f:
190
  f.write(analysis_text)
 
196
 
197
  logger.info(f"Análisis completado. Archivo ZIP: {zip_path}")
198
 
199
+ # Convertir imagen ELA a RGB para Gradio
200
  ela_rgb = cv2.cvtColor(ela_result, cv2.COLOR_BGR2RGB)
201
 
202
+ return zip_path, analysis_text, ela_rgb, google_maps_url or ""
203
 
204
  except Exception as e:
205
  logger.error(f"Error en procesamiento: {str(e)}")
206
  error_img = np.zeros((300, 600, 3), dtype=np.uint8)
207
  cv2.putText(error_img, f"ERROR: {str(e)[:50]}", (10, 150), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,255,255), 2)
208
+ return f"Error: {str(e)}", f"Error al procesar la imagen: {str(e)}", error_img, ""
209
 
210
+ # 🎨 Tema moderno
211
  theme = gr.themes.Soft(primary_hue="blue", secondary_hue="slate", neutral_hue="stone")
212
 
213
  # 🖥️ Interfaz Gradio
 
219
 
220
  with gr.Row():
221
  with gr.Column():
 
222
  input_image = gr.Image(
223
  label="Subir imagen (JPG/PNG)",
224
  type="filepath",
225
  height=400,
226
+ sources=["upload"]
227
  )
228
  process_btn = gr.Button("Analizar imagen", variant="primary")
 
 
229
  download_zip = gr.File(label="⬇️ Descargar resultados (ZIP)", interactive=False, visible=False)
230
 
231
  with gr.Column():
232
  with gr.Accordion("Resultado del Análisis ELA", open=True):
 
233
  ela_image = gr.Image(
234
+ label="🔍 Blanco = áreas con posible manipulación",
235
  type="numpy",
236
  height=400,
237
  show_label=True
 
239
 
240
  with gr.Accordion("Informe Detallado", open=True):
241
  analysis_text = gr.Textbox(label="📝 Resultados del análisis forense", lines=15, max_lines=25)
242
+ google_maps_btn = gr.Button("📍 Ver ubicación en Google Maps", visible=False)
243
+ google_maps_url_state = gr.State("")
244
+
245
+ # Función para resetear todo al cargar nueva imagen
246
+ def reset_on_upload():
247
+ return (
248
+ gr.update(value=None), # Limpiar texto
249
+ gr.update(value=None), # Limpiar imagen ELA
250
+ gr.update(visible=False), # Ocultar ZIP
251
+ gr.update(visible=False), # Ocultar botón Google Maps
252
+ "" # Limpiar URL
253
+ )
254
+
255
+ # Evento de análisis
256
  process_btn.click(
257
  fn=procesar_imagen,
258
  inputs=input_image,
259
+ outputs=[download_zip, analysis_text, ela_image, google_maps_url_state],
260
  api_name="analyze_image"
261
  ).then(
262
+ fn=lambda url: gr.update(visible=bool(url)),
263
+ inputs=google_maps_url_state,
264
+ outputs=google_maps_btn
265
+ ).then(
266
  fn=lambda: gr.update(visible=True),
267
  inputs=None,
268
  outputs=download_zip
269
  )
270
 
271
+ # Evento para abrir Google Maps
272
+ google_maps_btn.click(
273
+ fn=lambda url: url,
274
+ inputs=google_maps_url_state,
275
+ outputs=None,
276
+ js="(url) => { window.open(url, '_blank'); }"
277
+ )
278
+
279
+ # Resetear al subir nueva imagen
280
+ input_image.upload(
281
+ fn=reset_on_upload,
282
+ inputs=None,
283
+ outputs=[analysis_text, ela_image, download_zip, google_maps_btn, google_maps_url_state]
284
+ )
285
+
286
+ # 👇 Línea de crédito
287
  gr.Markdown("Desarrollado por José R. Leonett para el Grupo de Peritos Forenses Digitales de Guatemala - [www.forensedigital.gt](https://www.forensedigital.gt)")
288
 
289
  # ▶️ Ejecución