leonett commited on
Commit
2c06cd2
·
verified ·
1 Parent(s): ad91331

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +391 -219
app.py CHANGED
@@ -1,279 +1,451 @@
1
- import gradio as gr
2
- from PIL import Image, ExifTags
3
  import cv2
4
  import numpy as np
5
  import os
6
  import zipfile
7
  import hashlib
8
  import logging
 
 
9
  import plotly.graph_objects as go
10
- import tempfile
11
- import time
12
 
13
- # Configuración
14
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
15
  logger = logging.getLogger(__name__)
16
 
17
- EVIDENCE_DIR = "/tmp/evidence_ela"
18
- os.makedirs(EVIDENCE_DIR, exist_ok=True)
 
 
19
 
20
- def get_exif_data(image):
21
- """Extrae metadatos EXIF completos"""
22
  try:
23
- exif_data = image.getexif()
24
  if not exif_data:
25
  return {}
26
-
27
  metadata = {}
28
  for tag_id, value in exif_data.items():
29
- tag_name = ExifTags.TAGS.get(tag_id, tag_id)
30
-
31
- # Limpieza de datos para visualización
32
- if isinstance(value, bytes):
33
- try:
34
- value = value.decode('utf-8', errors='ignore')
35
- except:
36
- value = str(value)
37
- elif isinstance(value, tuple):
38
- value = str(value)
39
-
40
- metadata[tag_name] = value
41
  return metadata
42
  except Exception as e:
43
- logger.error(f"Error EXIF: {e}")
44
  return {}
45
 
46
- def get_gps_coords(exif_data):
47
- """Convierte coordenadas GPS a decimales"""
48
- if "GPSInfo" not in exif_data:
49
  return None
50
-
51
- gps = exif_data["GPSInfo"]
52
-
53
- def convert_to_degrees(value):
54
- d, m, s = value
55
- return float(d) + (float(m) / 60.0) + (float(s) / 3600.0)
56
-
57
  try:
58
- lat = gps.get(2)
59
- lon = gps.get(4)
60
- lat_ref = gps.get(1)
61
- lon_ref = gps.get(3)
62
-
 
 
 
 
 
63
  if lat and lon and lat_ref and lon_ref:
64
- lat_deg = convert_to_degrees(lat)
65
- lon_deg = convert_to_degrees(lon)
66
-
67
- if lat_ref == "S": lat_deg *= -1
68
- if lon_ref == "W": lon_deg *= -1
69
-
70
  return lat_deg, lon_deg
71
- except:
72
- pass
73
  return None
74
 
75
- def calculate_hash(image):
76
- """Hash SHA3-256 del contenido de la imagen"""
77
- return hashlib.sha3_256(np.array(image).tobytes()).hexdigest()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- def perform_ela_analysis(image):
80
- """Realiza Error Level Analysis"""
81
  try:
82
- if image.mode != 'RGB':
83
- image = image.convert('RGB')
84
-
85
- img_np = np.array(image)
86
  img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
87
-
88
- # Compresión temporal para ELA
89
- with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp:
90
- temp_path = tmp.name
91
-
92
- cv2.imwrite(temp_path, img_cv, [cv2.IMWRITE_JPEG_QUALITY, 75])
93
- compressed = cv2.imread(temp_path)
94
-
95
- if compressed is None:
96
- raise ValueError("Fallo en compresión JPEG")
97
-
98
- # Diferencia
99
- diff = cv2.absdiff(img_cv.astype(np.float32), compressed.astype(np.float32))
100
- scaled = np.clip(diff * 15, 0, 255).astype(np.uint8)
101
-
102
- # Máscara y Color
103
- gray = cv2.cvtColor(scaled, cv2.COLOR_BGR2GRAY)
104
- _, mask = cv2.threshold(gray, 5, 255, cv2.THRESH_BINARY)
105
- color_map = cv2.applyColorMap(gray, cv2.COLORMAP_TURBO)
106
-
107
- # Composición final
108
- bg_gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
109
- bg_gray = cv2.cvtColor(bg_gray, cv2.COLOR_GRAY2BGR)
110
- result = np.where(mask[..., None] > 0, color_map, bg_gray)
111
-
112
  os.remove(temp_path)
113
  return result, mask
114
-
115
  except Exception as e:
116
- logger.error(f"Error ELA: {e}")
117
- err_img = np.zeros((300, 600, 3), dtype=np.uint8) + 50
118
- cv2.putText(err_img, "ERROR EN PROCESO ELA", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255,255,255), 2)
119
- return err_img, None
120
-
121
- def generate_plots(percentage):
122
- """Genera gráficos estadísticos con Plotly"""
123
- # Gráfico de Barra
124
- color = '#FF4B4B' if percentage > 5 else '#4CAF50'
125
- fig_bar = go.Figure(data=[go.Bar(x=['Nivel ELA'], y=[percentage], marker_color=color)])
126
- fig_bar.update_layout(title_text="Porcentaje de Anomalías", height=250, margin=dict(t=30, b=10))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
- # Gráfico de Gauge (Riesgo)
129
- level = "Bajo" if percentage < 2 else "Medio" if percentage < 10 else "Alto"
130
- fig_gauge = go.Figure(go.Indicator(
131
- mode = "gauge+number",
132
- value = percentage,
133
- domain = {'x': [0, 1], 'y': [0, 1]},
134
- title = {'text': f"Riesgo: {level}"},
135
- gauge = {'axis': {'range': [None, 20]}, 'steps': [
136
- {'range': [0, 2], 'color': "lightgreen"},
137
- {'range': [2, 10], 'color': "yellow"},
138
- {'range': [10, 20], 'color': "red"}]}
139
  ))
140
- fig_gauge.update_layout(height=250, margin=dict(t=30, b=10))
 
 
 
 
 
141
 
142
- return fig_bar, fig_gauge
 
 
 
 
143
 
144
- def process_forensic_image(file_path):
145
- """Función principal de procesamiento"""
146
- if file_path is None:
147
- return None, "Sube una imagen para comenzar.", None, "", None, None
148
 
149
- start_time = time.time()
150
  try:
151
- img = Image.open(file_path)
152
- filename = os.path.basename(file_path)
153
- name_no_ext = os.path.splitext(filename)[0]
154
-
155
- # 1. Metadatos
156
- exif = get_exif_data(img)
157
- gps = get_gps_coords(exif)
158
- google_maps_link = ""
159
-
160
- report = f"📂 ARCHIVO: {filename}\n"
161
- report += f"📏 DIMENSIONES: {img.size[0]} x {img.size[1]} px\n"
162
- report += f"🎨 MODO: {img.mode}\n"
163
- report += f"#️⃣ HASH SHA3-256: {calculate_hash(img)}\n\n"
164
-
165
- report += "📋 METADATOS EXIF:\n"
166
- for k, v in exif.items():
167
- if k not in ["GPSInfo"]:
168
- report += f" • {k}: {v}\n"
169
-
170
- if gps:
171
- lat, lon = gps
172
- report += f"\n🌍 GPS DETECTADO: {lat:.6f}, {lon:.6f}\n"
173
- google_maps_link = f"https://www.google.com/maps?q={lat},{lon}"
 
 
 
 
 
 
 
174
  else:
175
- report += "\n🌍 GPS: No disponible\n"
176
-
177
- # 2. Análisis ELA
178
- ela_img, mask = perform_ela_analysis(img)
179
- percentage = 0.0
180
- if mask is not None:
181
- total_pixels = mask.shape[0] * mask.shape[1]
182
- white_pixels = np.count_nonzero(mask)
183
- percentage = round((white_pixels / total_pixels) * 100, 2)
184
-
185
- report += f"\n🔍 ANÁLISIS ELA:\n"
186
- report += f" • Porcentaje de diferencia: {percentage}%\n"
187
-
188
- if percentage < 1:
189
- report += " • Veredicto: Imagen probablemente ORIGINAL.\n"
190
- elif percentage < 5:
191
- report += " • Veredicto: Posible compresión o edición LEVE.\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  else:
193
- report += " • Veredicto: ALTA PROBABILIDAD de MANIPULACIÓN.\n"
194
-
195
- # 3. Generar ZIP
196
- zip_path = os.path.join(EVIDENCE_DIR, f"{name_no_ext}_forensic.zip")
197
- with zipfile.ZipFile(zip_path, 'w') as zf:
198
- # Guardar original temporalmente para el zip
199
- orig_temp = os.path.join(EVIDENCE_DIR, f"{name_no_ext}_orig.jpg")
200
- img.save(orig_temp, "JPEG")
201
- zf.write(orig_temp, os.path.basename(orig_temp))
202
-
203
- # Guardar ELA temporalmente
204
- ela_temp = os.path.join(EVIDENCE_DIR, f"{name_no_ext}_ela.jpg")
205
- cv2.imwrite(ela_temp, ela_img)
206
- zf.write(ela_temp, os.path.basename(ela_temp))
207
-
208
- # Guardar reporte
209
- txt_temp = os.path.join(EVIDENCE_DIR, f"{name_no_ext}_report.txt")
210
- with open(txt_temp, 'w', encoding='utf-8') as f:
211
- f.write(report)
212
- zf.write(txt_temp, os.path.basename(txt_temp))
213
-
214
- # 4. Gráficos
215
- fig1, fig2 = generate_plots(percentage)
216
-
217
- # Convertir ELA a RGB para Gradio
218
- ela_rgb = cv2.cvtColor(ela_img, cv2.COLOR_BGR2RGB)
219
-
220
- logger.info(f"Proceso completado en {time.time() - start_time:.2f}s")
221
- return zip_path, report, ela_rgb, google_maps_link, fig1, fig2
222
 
223
  except Exception as e:
224
- logger.error(f"Error crítico: {e}", exc_info=True)
225
- return None, f"❌ ERROR: {str(e)}", None, "", None, None
 
 
 
226
 
227
- # --- INTERFAZ GRADIO ---
228
- theme = gr.themes.Soft(primary_hue="blue")
229
 
230
- with gr.Blocks(title="Forense Digital ELA", theme=theme) as demo:
231
- gr.Markdown("# 🕵️‍♂️ Análisis Forense de Imágenes (ELA)")
232
- gr.Markdown("Herramienta profesional para detección de manipulaciones mediante Error Level Analysis.")
233
-
234
- with gr.Row():
235
- with gr.Column(scale=1):
236
- input_image = gr.Image(label="Cargar Imagen (JPG/PNG)", type="filepath", height=400)
237
- btn_analyze = gr.Button("🚀 INICIAR ANÁLISIS FORENSE", variant="primary", size="lg")
238
-
239
- with gr.Accordion("📥 Descargas y Enlaces", open=False):
240
- download_zip = gr.DownloadButton(label="Descargar Informe Completo (ZIP)", visible=False)
241
- btn_maps = gr.Button("🗺️ Abrir Google Maps", visible=False, link=None)
242
-
243
- with gr.Column(scale=1):
244
- output_ela = gr.Image(label="Visualización ELA (Zonas Turquesas = Sospechosas)", type="numpy", height=400)
245
- report_box = gr.Textbox(label="Informe Técnico", lines=10, show_copy_button=True)
 
 
 
 
 
 
246
 
247
  with gr.Row():
248
- plot_stats = gr.Plot(label="Estadísticas de Anomalías")
249
- plot_risk = gr.Plot(label="Nivel de Riesgo")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
- # Variables de estado ocultas para manejar enlaces
252
- url_state = gr.State("")
 
 
 
 
 
 
 
253
 
254
- def update_links(url):
255
- if url:
256
- return gr.update(visible=True), gr.update(visible=True, link=url)
257
- return gr.update(visible=False), gr.update(visible=False)
258
 
259
- btn_analyze.click(
260
- fn=process_forensic_image,
 
 
 
 
 
 
 
 
 
 
 
261
  inputs=input_image,
262
- outputs=[download_zip, report_box, output_ela, url_state, plot_stats, plot_risk]
 
 
 
 
 
263
  ).then(
264
  fn=lambda: gr.update(visible=True),
265
  inputs=None,
266
  outputs=download_zip
267
  ).then(
268
- fn=update_links,
269
- inputs=url_state,
270
- outputs=[btn_maps, btn_maps] # Actualiza visibilidad y link
271
  )
272
 
273
- input_image.clear(
274
- fn=lambda: (None, "", None, "", None, None, gr.update(visible=False), gr.update(visible=False)),
275
- outputs=[output_ela, report_box, download_zip, url_state, plot_stats, plot_risk, btn_maps, download_zip]
 
 
 
 
 
 
 
 
276
  )
277
 
 
278
  if __name__ == "__main__":
279
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
1
+ tengo este programa emmi canal de huggiefaces gratuito de analisis de ela y estab funcionando bien y ahora dice error de api, podrias revisar que sucede y generarme el codigo completo, verifica haqsta 3 veces que funciones correctamente "import gradio as gr
2
+ from PIL import Image
3
  import cv2
4
  import numpy as np
5
  import os
6
  import zipfile
7
  import hashlib
8
  import logging
9
+ from pathlib import Path
10
+ from PIL import ExifTags
11
  import plotly.graph_objects as go
 
 
12
 
13
+ # Configuración de logs
14
+ logging.basicConfig(level=logging.INFO)
15
  logger = logging.getLogger(__name__)
16
 
17
+ # Directorio de evidencia
18
+ evidence_dir = "/home/user/app"
19
+ os.makedirs(evidence_dir, exist_ok=True)
20
+ logger.info(f"Directorio de evidencia creado en: {evidence_dir}")
21
 
22
+ def obtener_metadatos(imagen):
 
23
  try:
24
+ exif_data = imagen.getexif()
25
  if not exif_data:
26
  return {}
 
27
  metadata = {}
28
  for tag_id, value in exif_data.items():
29
+ try:
30
+ tag = ExifTags.TAGS.get(tag_id, tag_id)
31
+ metadata[tag] = value
32
+ except Exception as e:
33
+ logger.debug(f"Error al procesar etiqueta EXIF: {str(e)}")
 
 
 
 
 
 
 
34
  return metadata
35
  except Exception as e:
36
+ logger.error(f"Error al obtener metadatos: {str(e)}")
37
  return {}
38
 
39
+ def obtener_coordenadas(exif_data):
40
+ if not exif_data or "GPSInfo" not in exif_data:
 
41
  return None
 
 
 
 
 
 
 
42
  try:
43
+ gps_info = exif_data["GPSInfo"]
44
+ if not gps_info:
45
+ return None
46
+ def gps_to_degrees(coord):
47
+ d, m, s = coord
48
+ return d + (m / 60.0) + (s / 3600.0)
49
+ lat = gps_info.get(2)
50
+ lon = gps_info.get(4)
51
+ lat_ref = gps_info.get(1)
52
+ lon_ref = gps_info.get(3)
53
  if lat and lon and lat_ref and lon_ref:
54
+ lat_deg = gps_to_degrees(lat)
55
+ lon_deg = gps_to_degrees(lon)
56
+ if lat_ref == "S":
57
+ lat_deg = -lat_deg
58
+ if lon_ref == "W":
59
+ lon_deg = -lon_deg
60
  return lat_deg, lon_deg
61
+ except Exception as e:
62
+ logger.error(f"Error al procesar coordenadas GPS: {str(e)}")
63
  return None
64
 
65
+ def calcular_hash(imagen):
66
+ return hashlib.sha3_256(imagen.tobytes()).hexdigest()
67
+
68
+ def analizar_manipulacion(imagen, metadatos):
69
+ manipulada = False
70
+ razones = []
71
+ if imagen.mode == "P":
72
+ razones.append("La imagen tiene marcas de agua o es una imagen indexada.")
73
+ manipulada = True
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(mask):
80
+ if mask is None or mask.size == 0:
81
+ return 0.0
82
+ total_pixeles = mask.size
83
+ pixeles_detectados = np.count_nonzero(mask)
84
+ porcentaje = (pixeles_detectados / total_pixeles) * 100
85
+ return porcentaje
86
+
87
+ def estimar_probabilidad_manipulacion(porcentaje_ela):
88
+ if porcentaje_ela < 0.5:
89
+ return "Muy baja (< 0.5%) - Imagen probablemente auténtica."
90
+ elif porcentaje_ela < 2.0:
91
+ return "Baja (0.5% - 2%) - Posible compresión o ruido, pero no manipulación evidente."
92
+ elif porcentaje_ela < 5.0:
93
+ return "Moderada (2% - 5%) - Posible edición o retoque localizado."
94
+ elif porcentaje_ela < 15.0:
95
+ return "Alta (5% - 15%) - Alta probabilidad de manipulación o zonas borradas."
96
+ else:
97
+ return "Muy alta (> 15%) - Manipulación extensa o generación por IA detectada."
98
+
99
 
100
+ def realizar_ela(imagen):
 
101
  try:
102
+ img_np = np.array(imagen.convert("RGB"))
 
 
 
103
  img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
104
+
105
+ # Fuerza la compresión para detectar artefactos
106
+ quality = 75
107
+ temp_path = "/tmp/temp_image.jpg"
108
+ cv2.imwrite(temp_path, img_cv, [cv2.IMWRITE_JPEG_QUALITY, quality])
109
+ img_comprimida = cv2.imread(temp_path)
110
+ if img_comprimida is None:
111
+ raise ValueError("No se pudo leer la imagen comprimida.")
112
+
113
+ # Calcula diferencias
114
+ diferencia = cv2.absdiff(img_cv.astype(np.float32), img_comprimida.astype(np.float32))
115
+ scaled_diff = np.clip(diferencia * 15, 0, 255).astype(np.uint8) # Escalado fuerte
116
+
117
+ # Convertir a escala de grises y aplicar umbral más bajo
118
+ gray_diff = cv2.cvtColor(scaled_diff, cv2.COLOR_BGR2GRAY)
119
+ _, mask = cv2.threshold(gray_diff, 5, 255, cv2.THRESH_BINARY)
120
+
121
+ # Resalta con color donde hay diferencias
122
+ ela_color = cv2.applyColorMap(gray_diff, cv2.COLORMAP_TURBO)
123
+
124
+ # Combinar con la original en escala de grises
125
+ img_gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
126
+ img_gray = cv2.cvtColor(img_gray, cv2.COLOR_GRAY2BGR)
127
+ result = np.where(mask[..., None] > 0, ela_color, img_gray)
128
+
129
  os.remove(temp_path)
130
  return result, mask
131
+
132
  except Exception as e:
133
+ logger.error(f"Error en ELA: {str(e)}")
134
+ error_img = np.zeros((300, 600, 3), dtype=np.uint8) + 30
135
+ cv2.putText(error_img, "ERROR AL PROCESAR ELA", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
136
+ return error_img, None
137
+
138
+
139
+
140
+
141
+ def crear_graficos_estadisticas(porcentaje_ela, dimensiones, metadatos):
142
+ """Crear gráficos estadísticos para el análisis ELA"""
143
+
144
+ # Gráfico 1: Porcentaje de manipulación (Barra)
145
+ fig_bar = go.Figure()
146
+ fig_bar.add_trace(go.Bar(
147
+ x=['Áreas Detectadas'],
148
+ y=[porcentaje_ela],
149
+ marker_color=['#4CAF50' if porcentaje_ela > 2 else '#36A2EB'],
150
+ text=[f'{porcentaje_ela:.3f}%'],
151
+ textposition='auto',
152
+ ))
153
+ fig_bar.update_layout(
154
+ title=dict(text="<b>Porcentaje de Áreas Manipuladas</b>", x=0.5),
155
+ yaxis_title="Porcentaje (%)",
156
+ height=250,
157
+ showlegend=False,
158
+ margin=dict(l=20, r=20, t=40, b=20)
159
+ )
160
+
161
+ # Gráfico 2: Probabilidad de manipulación (Pie)
162
+ niveles = ['Muy baja', 'Baja', 'Moderada', 'Alta', 'Muy alta']
163
+ color_map = ['#00CC96', '#36A2EB', '#FFCE56', '#FF9F40', '#FF6384']
164
+
165
+ prob_index = 0
166
+ if porcentaje_ela >= 15:
167
+ prob_index = 4
168
+ elif porcentaje_ela >= 5:
169
+ prob_index = 3
170
+ elif porcentaje_ela >= 2:
171
+ prob_index = 2
172
+ elif porcentaje_ela >= 0.5:
173
+ prob_index = 1
174
+
175
+ fig_pie = go.Figure()
176
+ fig_pie.add_trace(go.Pie(
177
+ labels=[niveles[prob_index]],
178
+ values=[100],
179
+ hole=.6,
180
+ marker_colors=[color_map[prob_index]],
181
+ textinfo='label'
182
+ ))
183
+ fig_pie.update_layout(
184
+ title=dict(text="<b>Nivel de Probabilidad</b>", x=0.5),
185
+ height=250,
186
+ showlegend=False,
187
+ margin=dict(l=20, r=20, t=40, b=20)
188
+ )
189
+
190
+ # Gráfico 3: Distribución de píxeles (Box plot)
191
+ fig_box = go.Figure()
192
+ fig_box.add_trace(go.Box(
193
+ y=[porcentaje_ela],
194
+ name="Distribución ELA",
195
+ marker_color='#FF6B35',
196
+ boxpoints='all'
197
+ ))
198
+ fig_box.update_layout(
199
+ title=dict(text="<b>Distribución de Anomalías</b>", x=0.5),
200
+ yaxis_title="Porcentaje (%)",
201
+ height=250,
202
+ showlegend=False,
203
+ margin=dict(l=20, r=20, t=40, b=20)
204
+ )
205
 
206
+ # Gráfico 4: Tendencias temporales (Scatter)
207
+ fig_scatter = go.Figure()
208
+ fig_scatter.add_trace(go.Scatter(
209
+ x=['Compresión', 'Ruido', 'Manipulación'],
210
+ y=[max(0.1, porcentaje_ela-1), max(0.1, porcentaje_ela-0.5), max(0.1, porcentaje_ela)],
211
+ mode='lines+markers',
212
+ line=dict(color='#FF6B35', width=3),
213
+ marker=dict(size=10)
 
 
 
214
  ))
215
+ fig_scatter.update_layout(
216
+ title=dict(text="<b>Análisis de Componentes</b>", x=0.5),
217
+ yaxis_title="Intensidad",
218
+ height=250,
219
+ margin=dict(l=20, r=20, t=40, b=20)
220
+ )
221
 
222
+ return fig_bar, fig_pie, fig_box, fig_scatter
223
+
224
+ def procesar_imagen(archivo_imagen):
225
+ if not archivo_imagen:
226
+ return [None, "❌ **ERROR: Por favor, cargue una imagen antes de analizar.**", None, ""] + [None] * 4
227
 
228
+ if not os.path.exists(archivo_imagen):
229
+ return [None, "❌ **ERROR: El archivo de imagen no existe o es inválido.**", None, ""] + [None] * 4
 
 
230
 
 
231
  try:
232
+ img = Image.open(archivo_imagen)
233
+ logger.info(f"Imagen cargada: {archivo_imagen}")
234
+
235
+ nombre_original = os.path.basename(archivo_imagen)
236
+ nombre = os.path.splitext(nombre_original)[0]
237
+
238
+ original_path = os.path.join(evidence_dir, f"{nombre}_original.jpg")
239
+ ela_path = os.path.join(evidence_dir, f"{nombre}_ela.jpg")
240
+ text_path = os.path.join(evidence_dir, f"{nombre}_analisis.txt")
241
+ zip_path = os.path.join(evidence_dir, f"{nombre}_errorELA.zip")
242
+
243
+ img.save(original_path, "JPEG")
244
+ ela_result, mask = realizar_ela(img)
245
+
246
+ if mask is None:
247
+ raise Exception("No se pudo generar el análisis ELA.")
248
+
249
+ cv2.imwrite(ela_path, ela_result)
250
+
251
+ porcentaje_ela = calcular_porcentaje_ela(mask)
252
+ probabilidad = estimar_probabilidad_manipulacion(porcentaje_ela)
253
+
254
+ # Crear gráficos estadísticos
255
+ fig_bar, fig_pie, fig_box, fig_scatter = crear_graficos_estadisticas(
256
+ porcentaje_ela, img.size, obtener_metadatos(img)
257
+ )
258
+
259
+ file_size_bytes = os.path.getsize(archivo_imagen)
260
+ if file_size_bytes < 1024 * 1024:
261
+ file_size = f"{file_size_bytes / 1024:.2f} KB"
262
  else:
263
+ file_size = f"{file_size_bytes / (1024 * 1024):.2f} MB"
264
+
265
+ info_basica = f"Nombre del archivo: {nombre_original}\r\n"
266
+ info_basica += f"Tamaño del archivo: {file_size}\r\n"
267
+ info_basica += f"Dimensiones: {img.size[0]} x {img.size[1]} píxeles\r\n"
268
+ info_basica += f"Formato: {img.format}\r\n"
269
+ info_basica += f"Modo: {img.mode}\r\n\r\n"
270
+
271
+ metadatos = obtener_metadatos(img)
272
+ info_metadatos = "ANÁLISIS FORENSE DE LOS METADATOS:\r\n\r\n"
273
+
274
+ google_maps_url = None
275
+
276
+ if metadatos:
277
+ for tag, value in metadatos.items():
278
+ if tag == "DateTime":
279
+ info_metadatos += f"- Fecha y hora de captura: {value}\r\n"
280
+ elif tag == "Make":
281
+ info_metadatos += f"- Fabricante de cámara: {value}\r\n"
282
+ elif tag == "Model":
283
+ info_metadatos += f"- Modelo de cámara: {value}\r\n"
284
+ elif tag == "Software":
285
+ info_metadatos += f"- Software de edición: {value}\r\n"
286
+ elif tag == "GPSInfo":
287
+ coords = obtener_coordenadas(metadatos)
288
+ if coords:
289
+ lat, lon = coords
290
+ info_metadatos += f"- Coordenadas GPS: {lat:.6f}, {lon:.6f}\r\n"
291
+ google_maps_url = f"https://www.google.com/maps?q={lat},{lon}"
292
+ info_metadatos += f"- Enlace a Google Maps: {google_maps_url}\r\n"
293
+ else:
294
+ info_metadatos += "- **Coordenadas GPS:** No se encontraron coordenadas válidas\r\n"
295
+ else:
296
+ info_metadatos += f"- **{tag}:** {value}\r\n"
297
  else:
298
+ info_metadatos += "- **Metadatos EXIF:** No se encontraron metadatos EXIF\r\n"
299
+
300
+ sha3_hash = calcular_hash(img)
301
+ info_metadatos += f"\r\n SHA3-256: {sha3_hash}\r\n\r\n"
302
+
303
+ manipulada, razones = analizar_manipulacion(img, metadatos)
304
+ info_manipulacion = "ANÁLISIS DE MANIPULACIÓN:\r\n\r\n"
305
+ info_manipulacion += f"- Porcentaje de áreas detectadas: {porcentaje_ela:.3f}%\r\n"
306
+ info_manipulacion += f"- Estimación forense: {probabilidad}\r\n\r\n"
307
+
308
+ analysis_text = info_basica + info_metadatos + info_manipulacion
309
+
310
+ with open(text_path, "w", encoding="utf-8", newline="\r\n") as f:
311
+ f.write(analysis_text)
312
+
313
+ with zipfile.ZipFile(zip_path, "w") as zipf:
314
+ zipf.write(original_path, os.path.basename(original_path))
315
+ zipf.write(ela_path, os.path.basename(ela_path))
316
+ zipf.write(text_path, os.path.basename(text_path))
317
+
318
+ logger.info(f"Análisis completado. Archivo ZIP: {zip_path}")
319
+
320
+ ela_rgb = cv2.cvtColor(ela_result, cv2.COLOR_BGR2RGB)
321
+
322
+ return [zip_path, analysis_text, ela_rgb, google_maps_url or "", fig_bar, fig_pie, fig_box, fig_scatter]
 
 
 
 
323
 
324
  except Exception as e:
325
+ logger.error(f"Error en procesamiento: {str(e)}")
326
+ mensaje_error = f"❌ **ERROR CRÍTICO:** {str(e)}"
327
+ error_img = np.zeros((300, 600, 3), dtype=np.uint8)
328
+ cv2.putText(error_img, "ERROR INTERNO", (80, 160), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
329
+ return [None, mensaje_error, error_img, ""] + [None] * 4
330
 
331
+ # 🎨 Tema moderno
332
+ theme = gr.themes.Soft(primary_hue="blue", secondary_hue="slate", neutral_hue="stone")
333
 
334
+ # 🖥️ Interfaz Gradio
335
+ with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme, css="""
336
+ .orange-download-btn {
337
+ background: linear-gradient(45deg, #FF6B35, #FF8E53) !important;
338
+ color: white !important;
339
+ border: none !important;
340
+ font-weight: bold !important;
341
+ }
342
+ .orange-download-btn:hover {
343
+ background: linear-gradient(45deg, #E55A2B, #FF7B42) !important;
344
+ transform: scale(1.02) !important;
345
+ box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3) !important;
346
+ }
347
+ .equal-height {
348
+ height: 400px !important;
349
+ }
350
+ """) as demo:
351
+ gr.Markdown("""
352
+ # 📸 Análisis Forense de Imágenes (Error Level Analysis - ELA)
353
+ **Programa de computación forense para analizar imágenes en busca de evidencia de manipulación o edición.**
354
+ """)
355
+ gr.Markdown("**Desarrollado por José R. Leonett para el Grupo de Peritos Forenses Digitales de Guatemala - [www.forensedigital.gt](https://www.forensedigital.gt)**")
356
 
357
  with gr.Row():
358
+ with gr.Column():
359
+ input_image = gr.Image(
360
+ label="Subir imagen (JPG/PNG)",
361
+ type="filepath",
362
+ height=400,
363
+ sources=["upload"],
364
+ elem_classes=["equal-height"]
365
+ )
366
+ process_btn = gr.Button("Analizar imagen", variant="primary")
367
+
368
+ download_zip = gr.DownloadButton(
369
+ label="⬇️ Descargar resultados (ZIP)",
370
+ variant="secondary",
371
+ visible=False,
372
+ interactive=True,
373
+ elem_classes=["orange-download-btn"]
374
+ )
375
+
376
+ # Estadísticas en 2x2 grid
377
+ with gr.Row(visible=False) as stats_row:
378
+ with gr.Column():
379
+ stats_bar = gr.Plot(label="Porcentaje de Manipulación", show_label=True)
380
+ stats_pie = gr.Plot(label="Nivel de Probabilidad", show_label=True)
381
+ with gr.Column():
382
+ stats_box = gr.Plot(label="Distribución de Anomalías", show_label=True)
383
+ stats_scatter = gr.Plot(label="Análisis de Componentes", show_label=True)
384
 
385
+ with gr.Column():
386
+ with gr.Accordion(open=True):
387
+ ela_image = gr.Image(
388
+ label="ÁREAS TURQUESA = manipulaciones o borrados",
389
+ type="numpy",
390
+ height=400,
391
+ show_label=True,
392
+ elem_classes=["equal-height"]
393
+ )
394
 
395
+ with gr.Accordion("Informe Detallado", open=True):
396
+ analysis_text = gr.Textbox(label="📝 Resultados del análisis forense", lines=15, max_lines=25)
397
+ google_maps_btn = gr.Button("📍 Ver ubicación en Google Maps", visible=False)
398
+ google_maps_url_state = gr.State("")
399
 
400
+ def reset_on_upload():
401
+ return (
402
+ gr.update(value=None),
403
+ gr.update(value=None),
404
+ gr.update(visible=False),
405
+ gr.update(visible=False),
406
+ "",
407
+ gr.update(visible=False),
408
+ None, None, None, None
409
+ )
410
+
411
+ process_btn.click(
412
+ fn=procesar_imagen,
413
  inputs=input_image,
414
+ outputs=[download_zip, analysis_text, ela_image, google_maps_url_state, stats_bar, stats_pie, stats_box, stats_scatter],
415
+ api_name="analyze_image"
416
+ ).then(
417
+ fn=lambda url: gr.update(visible=bool(url)),
418
+ inputs=google_maps_url_state,
419
+ outputs=google_maps_btn
420
  ).then(
421
  fn=lambda: gr.update(visible=True),
422
  inputs=None,
423
  outputs=download_zip
424
  ).then(
425
+ fn=lambda: gr.update(visible=True),
426
+ inputs=None,
427
+ outputs=stats_row
428
  )
429
 
430
+ google_maps_btn.click(
431
+ fn=lambda url: url,
432
+ inputs=google_maps_url_state,
433
+ outputs=None,
434
+ js="(url) => { window.open(url, '_blank'); }"
435
+ )
436
+
437
+ input_image.upload(
438
+ fn=reset_on_upload,
439
+ inputs=None,
440
+ outputs=[analysis_text, ela_image, download_zip, google_maps_btn, google_maps_url_state, stats_row, stats_bar, stats_pie, stats_box, stats_scatter]
441
  )
442
 
443
+ # ▶️ Ejecución
444
  if __name__ == "__main__":
445
+ demo.launch(
446
+ server_name="0.0.0.0",
447
+ server_port=7860,
448
+ share=True,
449
+ inbrowser=True,
450
+ favicon_path="https://www.forensedigital.gt/wp-content/uploads/2019/07/cropped-40fb84a6-c75a-4c38-bfa0-c0a9777430cd-1.jpg"
451
+ )