File size: 16,923 Bytes
0e5b3b7
 
7898207
 
 
0e5b3b7
 
 
9af8d8a
79fe9a9
e7831a7
0e5b3b7
 
 
 
7898207
dce0111
a04af06
bd5e34a
 
 
7898207
 
0e5b3b7
372cb24
0e5b3b7
 
 
9af8d8a
79fe9a9
9af8d8a
 
 
0e5b3b7
7898207
0e5b3b7
 
7898207
 
cbd6f1c
7898207
 
0e5b3b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7898207
0e5b3b7
7898207
 
 
bd5e34a
7898207
 
 
 
 
 
 
2605ed5
 
7898207
 
 
a6ad3b3
2605ed5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6644d97
1617d49
3f143c8
f471bf5
dce0111
0e5b3b7
dce0111
45f6148
 
bd5e34a
0e5b3b7
 
 
314ee6f
dce0111
45f6148
6644d97
45f6148
afd8502
45f6148
6644d97
45f6148
1617d49
45f6148
 
1617d49
45f6148
1617d49
 
 
 
 
314ee6f
dce0111
f471bf5
0e5b3b7
f30be7c
b508a69
2605ed5
6644d97
1617d49
 
 
e7831a7
 
 
97b8bd6
e7831a7
 
 
 
7946ce0
e7831a7
 
 
 
97b8bd6
e7831a7
 
a6ad3b3
 
e7831a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97b8bd6
e7831a7
a6ad3b3
 
e7831a7
 
 
 
 
 
 
97b8bd6
e7831a7
 
 
97b8bd6
e7831a7
 
a6ad3b3
 
e7831a7
 
 
 
 
 
a6ad3b3
e7831a7
97b8bd6
e7831a7
 
 
97b8bd6
e7831a7
a6ad3b3
 
e7831a7
 
 
a9fca4f
7898207
314ee6f
97b8bd6
314ee6f
 
97b8bd6
2605ed5
7898207
0e5b3b7
 
dce0111
4d7e9f3
 
dce0111
a04af06
 
 
 
dce0111
a6ad3b3
314ee6f
2605ed5
 
314ee6f
2605ed5
dce0111
 
a6ad3b3
2605ed5
 
e7831a7
 
 
 
 
3f143c8
 
 
 
 
 
7946ce0
 
 
 
 
dce0111
0e5b3b7
7946ce0
4d7e9f3
 
dce0111
0e5b3b7
 
 
7946ce0
0e5b3b7
7946ce0
0e5b3b7
7946ce0
0e5b3b7
7946ce0
0e5b3b7
 
 
 
7946ce0
4d7e9f3
7946ce0
0e5b3b7
97b8bd6
7898207
97b8bd6
0e5b3b7
97b8bd6
dce0111
79fe9a9
2966c35
dce0111
0e5b3b7
7946ce0
 
 
2605ed5
4d7e9f3
dce0111
97b8bd6
0e5b3b7
dce0111
0e5b3b7
 
 
 
dce0111
0e5b3b7
dce0111
 
 
97b8bd6
6695c83
0e5b3b7
 
97b8bd6
dce0111
314ee6f
97b8bd6
0e5b3b7
4d7e9f3
dce0111
6695c83
f30be7c
e7831a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f2a016
9ed5cb1
97b8bd6
9f2a016
97b8bd6
d98b2a0
0e5b3b7
 
dce0111
2966c35
dce0111
 
e7831a7
 
dce0111
2966c35
314ee6f
 
2966c35
e7831a7
314ee6f
e7831a7
 
314ee6f
dce0111
e7831a7
 
 
2966c35
 
e7831a7
2966c35
 
e7831a7
0e5b3b7
93f78d0
dce0111
93f78d0
dce0111
 
e7831a7
 
dce0111
 
2966c35
 
 
97b8bd6
4d7e9f3
 
 
97b8bd6
3f143c8
 
 
e7831a7
 
 
4d7e9f3
 
0e5b3b7
9f2a016
0e5b3b7
97b8bd6
0e5b3b7
dce0111
4d7e9f3
 
 
 
dce0111
 
 
e7831a7
 
 
 
9f2a016
7898207
4d7e9f3
 
 
 
 
 
 
 
 
 
97b8bd6
4d7e9f3
 
6695c83
9f2a016
f471bf5
 
 
0e5b3b7
9af8d8a
314ee6f
3f143c8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
import gradio as gr
from PIL import Image
import cv2
import numpy as np
import os
import zipfile
import hashlib
import logging
from pathlib import Path
from PIL import ExifTags
import plotly.graph_objects as go

# Configuración de logs
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Directorio de evidencia
evidence_dir = "/home/user/app"
os.makedirs(evidence_dir, exist_ok=True)
logger.info(f"Directorio de evidencia creado en: {evidence_dir}")

def obtener_metadatos(imagen):
    try:
        exif_data = imagen.getexif()
        if not exif_data:
            return {}
        metadata = {}
        for tag_id, value in exif_data.items():
            try:
                tag = ExifTags.TAGS.get(tag_id, tag_id)
                metadata[tag] = value
            except Exception as e:
                logger.debug(f"Error al procesar etiqueta EXIF: {str(e)}")
        return metadata
    except Exception as e:
        logger.error(f"Error al obtener metadatos: {str(e)}")
        return {}

def obtener_coordenadas(exif_data):
    if not exif_data or "GPSInfo" not in exif_data:
        return None
    try:
        gps_info = exif_data["GPSInfo"]
        if not gps_info:
            return None
        def gps_to_degrees(coord):
            d, m, s = coord
            return d + (m / 60.0) + (s / 3600.0)
        lat = gps_info.get(2)
        lon = gps_info.get(4)
        lat_ref = gps_info.get(1)
        lon_ref = gps_info.get(3)
        if lat and lon and lat_ref and lon_ref:
            lat_deg = gps_to_degrees(lat)
            lon_deg = gps_to_degrees(lon)
            if lat_ref == "S":
                lat_deg = -lat_deg
            if lon_ref == "W":
                lon_deg = -lon_deg
            return lat_deg, lon_deg
    except Exception as e:
        logger.error(f"Error al procesar coordenadas GPS: {str(e)}")
    return None

def calcular_hash(imagen):
    return hashlib.sha3_256(imagen.tobytes()).hexdigest()

def analizar_manipulacion(imagen, metadatos):
    manipulada = False
    razones = []
    if imagen.mode == "P":
        razones.append("La imagen tiene marcas de agua o es una imagen indexada.")
        manipulada = True
    if "Software" in metadatos:
        razones.append(f"La imagen fue editada con: {metadatos['Software']}")
        manipulada = True
    return manipulada, razones

def calcular_porcentaje_ela(mask):
    if mask is None or mask.size == 0:
        return 0.0
    total_pixeles = mask.size
    pixeles_detectados = np.count_nonzero(mask)
    porcentaje = (pixeles_detectados / total_pixeles) * 100
    return porcentaje

def estimar_probabilidad_manipulacion(porcentaje_ela):
    if porcentaje_ela < 0.5:
        return "Muy baja (< 0.5%) - Imagen probablemente auténtica."
    elif porcentaje_ela < 2.0:
        return "Baja (0.5% - 2%) - Posible compresión o ruido, pero no manipulación evidente."
    elif porcentaje_ela < 5.0:
        return "Moderada (2% - 5%) - Posible edición o retoque localizado."
    elif porcentaje_ela < 15.0:
        return "Alta (5% - 15%) - Alta probabilidad de manipulación o zonas borradas."
    else:
        return "Muy alta (> 15%) - Manipulación extensa o generación por IA detectada."


def realizar_ela(imagen):
    try:
        img_np = np.array(imagen.convert("RGB"))
        img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)

        # Fuerza la compresión para detectar artefactos
        quality = 75  
        temp_path = "/tmp/temp_image.jpg"
        cv2.imwrite(temp_path, img_cv, [cv2.IMWRITE_JPEG_QUALITY, quality])
        img_comprimida = cv2.imread(temp_path)
        if img_comprimida is None:
            raise ValueError("No se pudo leer la imagen comprimida.")

        # Calcula diferencias
        diferencia = cv2.absdiff(img_cv.astype(np.float32), img_comprimida.astype(np.float32))
        scaled_diff = np.clip(diferencia * 15, 0, 255).astype(np.uint8)  # Escalado fuerte

        # Convertir a escala de grises y aplicar umbral más bajo
        gray_diff = cv2.cvtColor(scaled_diff, cv2.COLOR_BGR2GRAY)
        _, mask = cv2.threshold(gray_diff, 5, 255, cv2.THRESH_BINARY)

        # Resalta con color donde hay diferencias
        ela_color = cv2.applyColorMap(gray_diff, cv2.COLORMAP_TURBO)

        # Combinar con la original en escala de grises
        img_gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
        img_gray = cv2.cvtColor(img_gray, cv2.COLOR_GRAY2BGR)
        result = np.where(mask[..., None] > 0, ela_color, img_gray)

        os.remove(temp_path)
        return result, mask

    except Exception as e:
        logger.error(f"Error en ELA: {str(e)}")
        error_img = np.zeros((300, 600, 3), dtype=np.uint8) + 30
        cv2.putText(error_img, "ERROR AL PROCESAR ELA", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
        return error_img, None




def crear_graficos_estadisticas(porcentaje_ela, dimensiones, metadatos):
    """Crear gráficos estadísticos para el análisis ELA"""
    
    # Gráfico 1: Porcentaje de manipulación (Barra)
    fig_bar = go.Figure()
    fig_bar.add_trace(go.Bar(
        x=['Áreas Detectadas'],
        y=[porcentaje_ela],
        marker_color=['#4CAF50' if porcentaje_ela > 2 else '#36A2EB'],
        text=[f'{porcentaje_ela:.3f}%'],
        textposition='auto',
    ))
    fig_bar.update_layout(
        title=dict(text="<b>Porcentaje de Áreas Manipuladas</b>", x=0.5),
        yaxis_title="Porcentaje (%)",
        height=250,
        showlegend=False,
        margin=dict(l=20, r=20, t=40, b=20)
    )
    
    # Gráfico 2: Probabilidad de manipulación (Pie)
    niveles = ['Muy baja', 'Baja', 'Moderada', 'Alta', 'Muy alta']
    color_map = ['#00CC96', '#36A2EB', '#FFCE56', '#FF9F40', '#FF6384']
    
    prob_index = 0
    if porcentaje_ela >= 15:
        prob_index = 4
    elif porcentaje_ela >= 5:
        prob_index = 3
    elif porcentaje_ela >= 2:
        prob_index = 2
    elif porcentaje_ela >= 0.5:
        prob_index = 1
        
    fig_pie = go.Figure()
    fig_pie.add_trace(go.Pie(
        labels=[niveles[prob_index]],
        values=[100],
        hole=.6,
        marker_colors=[color_map[prob_index]],
        textinfo='label'
    ))
    fig_pie.update_layout(
        title=dict(text="<b>Nivel de Probabilidad</b>", x=0.5),
        height=250,
        showlegend=False,
        margin=dict(l=20, r=20, t=40, b=20)
    )
    
    # Gráfico 3: Distribución de píxeles (Box plot)
    fig_box = go.Figure()
    fig_box.add_trace(go.Box(
        y=[porcentaje_ela],
        name="Distribución ELA",
        marker_color='#FF6B35',
        boxpoints='all'
    ))
    fig_box.update_layout(
        title=dict(text="<b>Distribución de Anomalías</b>", x=0.5),
        yaxis_title="Porcentaje (%)",
        height=250,
        showlegend=False,
        margin=dict(l=20, r=20, t=40, b=20)
    )
    
    # Gráfico 4: Tendencias temporales (Scatter)
    fig_scatter = go.Figure()
    fig_scatter.add_trace(go.Scatter(
        x=['Compresión', 'Ruido', 'Manipulación'],
        y=[max(0.1, porcentaje_ela-1), max(0.1, porcentaje_ela-0.5), max(0.1, porcentaje_ela)],
        mode='lines+markers',
        line=dict(color='#FF6B35', width=3),
        marker=dict(size=10)
    ))
    fig_scatter.update_layout(
        title=dict(text="<b>Análisis de Componentes</b>", x=0.5),
        yaxis_title="Intensidad",
        height=250,
        margin=dict(l=20, r=20, t=40, b=20)
    )
    
    return fig_bar, fig_pie, fig_box, fig_scatter

def procesar_imagen(archivo_imagen):
    if not archivo_imagen:
        return [None, "❌ **ERROR: Por favor, cargue una imagen antes de analizar.**", None, ""] + [None] * 4

    if not os.path.exists(archivo_imagen):
        return [None, "❌ **ERROR: El archivo de imagen no existe o es inválido.**", None, ""] + [None] * 4

    try:
        img = Image.open(archivo_imagen)
        logger.info(f"Imagen cargada: {archivo_imagen}")

        nombre_original = os.path.basename(archivo_imagen)
        nombre = os.path.splitext(nombre_original)[0]

        original_path = os.path.join(evidence_dir, f"{nombre}_original.jpg")
        ela_path = os.path.join(evidence_dir, f"{nombre}_ela.jpg")
        text_path = os.path.join(evidence_dir, f"{nombre}_analisis.txt")
        zip_path = os.path.join(evidence_dir, f"{nombre}_errorELA.zip")

        img.save(original_path, "JPEG")
        ela_result, mask = realizar_ela(img)

        if mask is None:
            raise Exception("No se pudo generar el análisis ELA.")

        cv2.imwrite(ela_path, ela_result)

        porcentaje_ela = calcular_porcentaje_ela(mask)
        probabilidad = estimar_probabilidad_manipulacion(porcentaje_ela)

        # Crear gráficos estadísticos
        fig_bar, fig_pie, fig_box, fig_scatter = crear_graficos_estadisticas(
            porcentaje_ela, img.size, obtener_metadatos(img)
        )

        file_size_bytes = os.path.getsize(archivo_imagen)
        if file_size_bytes < 1024 * 1024:
            file_size = f"{file_size_bytes / 1024:.2f} KB"
        else:
            file_size = f"{file_size_bytes / (1024 * 1024):.2f} MB"

        info_basica = f"Nombre del archivo: {nombre_original}\r\n"
        info_basica += f"Tamaño del archivo: {file_size}\r\n"
        info_basica += f"Dimensiones: {img.size[0]} x {img.size[1]} píxeles\r\n"
        info_basica += f"Formato: {img.format}\r\n"
        info_basica += f"Modo: {img.mode}\r\n\r\n"

        metadatos = obtener_metadatos(img)
        info_metadatos = "ANÁLISIS FORENSE DE LOS METADATOS:\r\n\r\n"

        google_maps_url = None

        if metadatos:
            for tag, value in metadatos.items():
                if tag == "DateTime":
                    info_metadatos += f"- Fecha y hora de captura: {value}\r\n"
                elif tag == "Make":
                    info_metadatos += f"- Fabricante de cámara: {value}\r\n"
                elif tag == "Model":
                    info_metadatos += f"- Modelo de cámara: {value}\r\n"
                elif tag == "Software":
                    info_metadatos += f"- Software de edición: {value}\r\n"
                elif tag == "GPSInfo":
                    coords = obtener_coordenadas(metadatos)
                    if coords:
                        lat, lon = coords
                        info_metadatos += f"- Coordenadas GPS: {lat:.6f}, {lon:.6f}\r\n"
                        google_maps_url = f"https://www.google.com/maps?q={lat},{lon}"
                        info_metadatos += f"- Enlace a Google Maps: {google_maps_url}\r\n"
                    else:
                        info_metadatos += "- **Coordenadas GPS:** No se encontraron coordenadas válidas\r\n"
                else:
                    info_metadatos += f"- **{tag}:** {value}\r\n"
        else:
            info_metadatos += "- **Metadatos EXIF:** No se encontraron metadatos EXIF\r\n"

        sha3_hash = calcular_hash(img)
        info_metadatos += f"\r\n SHA3-256: {sha3_hash}\r\n\r\n"

        manipulada, razones = analizar_manipulacion(img, metadatos)
        info_manipulacion = "ANÁLISIS DE MANIPULACIÓN:\r\n\r\n"
        info_manipulacion += f"- Porcentaje de áreas detectadas: {porcentaje_ela:.3f}%\r\n"
        info_manipulacion += f"- Estimación forense: {probabilidad}\r\n\r\n"

        analysis_text = info_basica + info_metadatos + info_manipulacion

        with open(text_path, "w", encoding="utf-8", newline="\r\n") as f:
            f.write(analysis_text)

        with zipfile.ZipFile(zip_path, "w") as zipf:
            zipf.write(original_path, os.path.basename(original_path))
            zipf.write(ela_path, os.path.basename(ela_path))
            zipf.write(text_path, os.path.basename(text_path))

        logger.info(f"Análisis completado. Archivo ZIP: {zip_path}")

        ela_rgb = cv2.cvtColor(ela_result, cv2.COLOR_BGR2RGB)

        return [zip_path, analysis_text, ela_rgb, google_maps_url or "", fig_bar, fig_pie, fig_box, fig_scatter]

    except Exception as e:
        logger.error(f"Error en procesamiento: {str(e)}")
        mensaje_error = f"❌ **ERROR CRÍTICO:** {str(e)}"
        error_img = np.zeros((300, 600, 3), dtype=np.uint8)
        cv2.putText(error_img, "ERROR INTERNO", (80, 160), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
        return [None, mensaje_error, error_img, ""] + [None] * 4

# 🎨 Tema moderno
theme = gr.themes.Soft(primary_hue="blue", secondary_hue="slate", neutral_hue="stone")

# 🖥️ Interfaz Gradio
with gr.Blocks(title="Análisis Forense de Imágenes con ELA", theme=theme, css="""
    .orange-download-btn {
        background: linear-gradient(45deg, #FF6B35, #FF8E53) !important;
        color: white !important;
        border: none !important;
        font-weight: bold !important;
    }
    .orange-download-btn:hover {
        background: linear-gradient(45deg, #E55A2B, #FF7B42) !important;
        transform: scale(1.02) !important;
        box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3) !important;
    }
    .equal-height {
        height: 400px !important;
    }
""") as demo:
    gr.Markdown("""
    # 📸 Análisis Forense de Imágenes (Error Level Analysis - ELA)
    **Programa de computación forense para analizar imágenes en busca de evidencia de manipulación o edición.**
    """)
    gr.Markdown("**Desarrollado por José R. Leonett para el Grupo de Peritos Forenses Digitales de Guatemala - [www.forensedigital.gt](https://www.forensedigital.gt)**")

    with gr.Row():
        with gr.Column():
            input_image = gr.Image(
                label="Subir imagen (JPG/PNG)",
                type="filepath",
                height=400,
                sources=["upload"],
                elem_classes=["equal-height"]
            )
            process_btn = gr.Button("Analizar imagen", variant="primary")

            download_zip = gr.DownloadButton(
                label="⬇️ Descargar resultados (ZIP)",
                variant="secondary",
                visible=False,
                interactive=True,
                elem_classes=["orange-download-btn"]
            )

            # Estadísticas en 2x2 grid
            with gr.Row(visible=False) as stats_row:
                with gr.Column():
                    stats_bar = gr.Plot(label="Porcentaje de Manipulación", show_label=True)
                    stats_pie = gr.Plot(label="Nivel de Probabilidad", show_label=True)
                with gr.Column():
                    stats_box = gr.Plot(label="Distribución de Anomalías", show_label=True)
                    stats_scatter = gr.Plot(label="Análisis de Componentes", show_label=True)

        with gr.Column():
            with gr.Accordion(open=True):
                ela_image = gr.Image(
                    label="ÁREAS TURQUESA = manipulaciones o borrados",
                    type="numpy",
                    height=400,
                    show_label=True,
                    elem_classes=["equal-height"]
                )

            with gr.Accordion("Informe Detallado", open=True):
                analysis_text = gr.Textbox(label="📝 Resultados del análisis forense", lines=15, max_lines=25)
                google_maps_btn = gr.Button("📍 Ver ubicación en Google Maps", visible=False)
                google_maps_url_state = gr.State("")

    def reset_on_upload():
        return (
            gr.update(value=None),
            gr.update(value=None),
            gr.update(visible=False),
            gr.update(visible=False),
            "",
            gr.update(visible=False),
            None, None, None, None
        )

    process_btn.click(
        fn=procesar_imagen,
        inputs=input_image,
        outputs=[download_zip, analysis_text, ela_image, google_maps_url_state, stats_bar, stats_pie, stats_box, stats_scatter],
        api_name="analyze_image"
    ).then(
        fn=lambda url: gr.update(visible=bool(url)),
        inputs=google_maps_url_state,
        outputs=google_maps_btn
    ).then(
        fn=lambda: gr.update(visible=True),
        inputs=None,
        outputs=download_zip
    ).then(
        fn=lambda: gr.update(visible=True),
        inputs=None,
        outputs=stats_row
    )

    google_maps_btn.click(
        fn=lambda url: url,
        inputs=google_maps_url_state,
        outputs=None,
        js="(url) => { window.open(url, '_blank'); }"
    )

    input_image.upload(
        fn=reset_on_upload,
        inputs=None,
        outputs=[analysis_text, ela_image, download_zip, google_maps_btn, google_maps_url_state, stats_row, stats_bar, stats_pie, stats_box, stats_scatter]
    )

# ▶️ Ejecución
if __name__ == "__main__":
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=True,
        inbrowser=True,
        favicon_path="https://www.forensedigital.gt/wp-content/uploads/2019/07/cropped-40fb84a6-c75a-4c38-bfa0-c0a9777430cd-1.jpg"
    )