leonett commited on
Commit
dce0111
·
verified ·
1 Parent(s): 0a29f26

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -75
app.py CHANGED
@@ -13,18 +13,16 @@ from PIL import ExifTags
13
  logging.basicConfig(level=logging.INFO)
14
  logger = logging.getLogger(__name__)
15
 
16
- # Directorio de evidencia (seguro en Hugging Face Spaces)
17
  evidence_dir = "/home/user/app"
18
  os.makedirs(evidence_dir, exist_ok=True)
19
  logger.info(f"Directorio de evidencia creado en: {evidence_dir}")
20
 
21
  def obtener_metadatos(imagen):
22
- """Obtiene los metadatos EXIF de la imagen."""
23
  try:
24
  exif_data = imagen.getexif()
25
  if not exif_data:
26
  return {}
27
-
28
  metadata = {}
29
  for tag_id, value in exif_data.items():
30
  try:
@@ -38,52 +36,40 @@ def obtener_metadatos(imagen):
38
  return {}
39
 
40
  def obtener_coordenadas(exif_data):
41
- """Extrae coordenadas GPS de los metadatos EXIF."""
42
  if not exif_data or "GPSInfo" not in exif_data:
43
  return None
44
-
45
  try:
46
  gps_info = exif_data["GPSInfo"]
47
  if not gps_info:
48
  return None
49
-
50
  def gps_to_degrees(coord):
51
  d, m, s = coord
52
  return d + (m / 60.0) + (s / 3600.0)
53
-
54
  lat = gps_info.get(2)
55
  lon = gps_info.get(4)
56
  lat_ref = gps_info.get(1)
57
  lon_ref = gps_info.get(3)
58
-
59
  if lat and lon and lat_ref and lon_ref:
60
  lat_deg = gps_to_degrees(lat)
61
  lon_deg = gps_to_degrees(lon)
62
-
63
  if lat_ref == "S":
64
  lat_deg = -lat_deg
65
  if lon_ref == "W":
66
  lon_deg = -lon_deg
67
-
68
  return lat_deg, lon_deg
69
  except Exception as e:
70
  logger.error(f"Error al procesar coordenadas GPS: {str(e)}")
71
-
72
  return None
73
 
74
  def calcular_hash(imagen):
75
- """Calcula el hash SHA3-256 de la imagen."""
76
  return hashlib.sha3_256(imagen.tobytes()).hexdigest()
77
 
78
  def analizar_manipulacion(imagen, metadatos):
79
- """Analiza si la imagen ha sido manipulada."""
80
  manipulada = False
81
  razones = []
82
-
83
  if imagen.mode == "P":
84
  razones.append("La imagen tiene marcas de agua o es una imagen indexada.")
85
  manipulada = True
86
-
87
  if not metadatos:
88
  razones.append("La imagen no tiene metadatos EXIF.")
89
  manipulada = True
@@ -91,62 +77,70 @@ def analizar_manipulacion(imagen, metadatos):
91
  if "Software" in metadatos:
92
  razones.append(f"La imagen fue editada con: {metadatos['Software']}")
93
  manipulada = True
94
-
95
  hash_conocido = "d8e3d0e0d7a5e2b2c9d5f9d1c8e7a6f5b0d4e7c3f9d1a2b3c4d5e6f7a8b9c0d1"
96
  hash_actual = calcular_hash(imagen)
97
  if hash_actual != hash_conocido:
98
  razones.append("El hash de la imagen no coincide con el hash conocido.")
99
  manipulada = True
100
-
101
  return manipulada, razones
102
 
103
- def realizar_ela(imagen, quality=95, scale=100):
104
- """Realiza el Error Level Analysis (ELA) en la imagen."""
105
  try:
106
- img_np = np.array(imagen)
107
  img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
108
-
109
  temp_path = "/tmp/temp_image.jpg"
110
  cv2.imwrite(temp_path, img_cv, [cv2.IMWRITE_JPEG_QUALITY, quality])
111
-
112
  img_comprimida = cv2.imread(temp_path)
113
  if img_comprimida is None:
114
  raise ValueError("Error al leer la imagen comprimida")
115
-
116
  diferencia = cv2.absdiff(img_cv, img_comprimida)
117
  ela_imagen = scale * diferencia
118
- ela_imagen_gray = cv2.cvtColor(ela_imagen, cv2.COLOR_BGR2GRAY)
119
-
 
 
 
 
 
 
 
 
120
  os.remove(temp_path)
121
-
122
- return ela_imagen_gray
123
  except Exception as e:
124
  logger.error(f"Error en ELA: {str(e)}")
125
- return np.zeros((100, 100), dtype=np.uint8)
 
 
 
126
 
127
  def procesar_imagen(archivo_imagen):
128
- """Procesa la imagen completa y genera resultados."""
129
  try:
130
  img = Image.open(archivo_imagen)
131
  logger.info(f"Imagen cargada: {archivo_imagen}")
132
-
133
  nombre = os.path.splitext(os.path.basename(archivo_imagen))[0]
134
-
135
  original_path = os.path.join(evidence_dir, f"{nombre}_original.jpg")
136
  ela_path = os.path.join(evidence_dir, f"{nombre}_ela.jpg")
137
  text_path = os.path.join(evidence_dir, f"{nombre}_analisis.txt")
138
  zip_path = os.path.join(evidence_dir, f"{nombre}_errorELA.zip")
139
-
140
  img.save(original_path)
141
- ela_imagen = realizar_ela(img)
142
- ela_color = cv2.applyColorMap(ela_imagen, cv2.COLORMAP_JET)
143
- cv2.imwrite(ela_path, ela_color)
144
-
145
  info_basica = f"Formato: {img.format}\nTamaño: {img.size} píxeles\nModo: {img.mode}\n"
146
-
147
  metadatos = obtener_metadatos(img)
148
  info_metadatos = "ANÁLISIS FORENSE DE LOS METADATOS:\n"
149
-
150
  if metadatos:
151
  for tag, value in metadatos.items():
152
  if tag == "DateTime":
@@ -170,10 +164,10 @@ def procesar_imagen(archivo_imagen):
170
  info_metadatos += f"{tag}: {value}\n"
171
  else:
172
  info_metadatos += "No se encontraron metadatos EXIF\n"
173
-
174
  sha3_hash = calcular_hash(img)
175
  info_metadatos += f"\nSHA3-256: {sha3_hash}\n"
176
-
177
  manipulada, razones = analizar_manipulacion(img, metadatos)
178
  info_manipulacion = "\nANÁLISIS DE MANIPULACIÓN:\n"
179
  if manipulada:
@@ -182,33 +176,32 @@ def procesar_imagen(archivo_imagen):
182
  info_manipulacion += f"- {r}\n"
183
  else:
184
  info_manipulacion += "LA IMAGEN NO HA SIDO MANIPULADA.\n"
185
-
186
  analysis_text = f"{info_basica}\n{info_metadatos}\n{info_manipulacion}"
187
-
188
  with open(text_path, "w") as f:
189
  f.write(analysis_text)
190
-
191
  with zipfile.ZipFile(zip_path, "w") as zipf:
192
  zipf.write(original_path, os.path.basename(original_path))
193
  zipf.write(ela_path, os.path.basename(ela_path))
194
  zipf.write(text_path, os.path.basename(text_path))
195
-
196
  logger.info(f"Análisis completado. Archivo ZIP: {zip_path}")
197
-
198
- # Devolver solo 2 valores (coincide con los 2 outputs definidos)
199
- return zip_path, analysis_text
 
 
200
 
201
  except Exception as e:
202
  logger.error(f"Error en procesamiento: {str(e)}")
203
- return f"Error: {str(e)}", f"Error al procesar la imagen: {str(e)}"
 
 
204
 
205
- # 🎨 Tema moderno y compatible (¡sin errores!)
206
- # Soft ya incluye modo oscuro automático y es visualmente agradable
207
- theme = gr.themes.Soft(
208
- primary_hue="blue",
209
- secondary_hue="slate",
210
- neutral_hue="stone",
211
- )
212
 
213
  # 🖥️ Interfaz Gradio
214
  with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as demo:
@@ -216,36 +209,48 @@ with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as d
216
  # 📸 Análisis Forense de Imágenes con Error Level Analysis (ELA)
217
  Programa de computación forense para analizar imágenes en busca de evidencia de manipulación o edición.
218
  """)
219
-
220
  with gr.Row():
221
  with gr.Column():
222
- input_image = gr.Image(label="Subir imagen (JPG/PNG)", type="filepath", height=400)
 
 
 
 
 
 
223
  process_btn = gr.Button("Analizar imagen", variant="primary")
224
-
 
 
 
225
  with gr.Column():
226
- with gr.Accordion("Descargar resultados ZIP", open=True):
227
- download_zip = gr.File(label="Descargar archivo ZIP", interactive=False)
228
- metadata_text = gr.Textbox(label="Metadatos y SHA3-256", lines=10, max_lines=20)
229
-
230
- with gr.Accordion("Análisis detallado", open=True):
231
- ela_image = gr.Image(label="Imagen con Error Level Analysis (ELA)", type="numpy", elem_id="ela_image")
232
- analysis_text = gr.Textbox(label="Análisis detallado", lines=15, max_lines=25)
233
-
234
- # ⚙️ Conexión de eventos
 
 
 
 
235
  process_btn.click(
236
  fn=procesar_imagen,
237
  inputs=input_image,
238
- outputs=[download_zip, analysis_text], # 👈 Solo 2 outputs (ZIP y texto)
239
  api_name="analyze_image"
240
- )
241
-
242
- input_image.change(
243
- fn=lambda x: f"Análisis en progreso para: {os.path.basename(x)}" if x else "",
244
- inputs=input_image,
245
- outputs=analysis_text
246
  )
247
 
248
- # 👇👇👇 AÑADIDO: Línea de crédito solicitada
249
  gr.Markdown("Desarrollado por José R. Leonett para el Grupo de Peritos Forenses Digitales de Guatemala - [www.forensedigital.gt](https://www.forensedigital.gt)")
250
 
251
  # ▶️ Ejecución
 
13
  logging.basicConfig(level=logging.INFO)
14
  logger = logging.getLogger(__name__)
15
 
16
+ # Directorio de evidencia
17
  evidence_dir = "/home/user/app"
18
  os.makedirs(evidence_dir, exist_ok=True)
19
  logger.info(f"Directorio de evidencia creado en: {evidence_dir}")
20
 
21
  def obtener_metadatos(imagen):
 
22
  try:
23
  exif_data = imagen.getexif()
24
  if not exif_data:
25
  return {}
 
26
  metadata = {}
27
  for tag_id, value in exif_data.items():
28
  try:
 
36
  return {}
37
 
38
  def obtener_coordenadas(exif_data):
 
39
  if not exif_data or "GPSInfo" not in exif_data:
40
  return None
 
41
  try:
42
  gps_info = exif_data["GPSInfo"]
43
  if not gps_info:
44
  return None
 
45
  def gps_to_degrees(coord):
46
  d, m, s = coord
47
  return d + (m / 60.0) + (s / 3600.0)
 
48
  lat = gps_info.get(2)
49
  lon = gps_info.get(4)
50
  lat_ref = gps_info.get(1)
51
  lon_ref = gps_info.get(3)
 
52
  if lat and lon and lat_ref and lon_ref:
53
  lat_deg = gps_to_degrees(lat)
54
  lon_deg = gps_to_degrees(lon)
 
55
  if lat_ref == "S":
56
  lat_deg = -lat_deg
57
  if lon_ref == "W":
58
  lon_deg = -lon_deg
 
59
  return lat_deg, lon_deg
60
  except Exception as e:
61
  logger.error(f"Error al procesar coordenadas GPS: {str(e)}")
 
62
  return None
63
 
64
  def calcular_hash(imagen):
 
65
  return hashlib.sha3_256(imagen.tobytes()).hexdigest()
66
 
67
  def analizar_manipulacion(imagen, metadatos):
 
68
  manipulada = False
69
  razones = []
 
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
 
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
+ 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)
92
+
93
  temp_path = "/tmp/temp_image.jpg"
94
  cv2.imwrite(temp_path, img_cv, [cv2.IMWRITE_JPEG_QUALITY, quality])
95
+
96
  img_comprimida = cv2.imread(temp_path)
97
  if img_comprimida is None:
98
  raise ValueError("Error al leer la imagen comprimida")
99
+
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")
132
  text_path = os.path.join(evidence_dir, f"{nombre}_analisis.txt")
133
  zip_path = os.path.join(evidence_dir, f"{nombre}_errorELA.zip")
134
+
135
  img.save(original_path)
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":
 
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:
 
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)
184
+
185
  with zipfile.ZipFile(zip_path, "w") as zipf:
186
  zipf.write(original_path, os.path.basename(original_path))
187
  zipf.write(ela_path, os.path.basename(ela_path))
188
  zipf.write(text_path, os.path.basename(text_path))
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
207
  with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme) as demo:
 
209
  # 📸 Análisis Forense de Imágenes con Error Level Analysis (ELA)
210
  Programa de computación forense para analizar imágenes en busca de evidencia de manipulación o edición.
211
  """)
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í 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
235
+ )
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