File size: 6,839 Bytes
ee2a1ac
 
 
 
41ddd18
ee2a1ac
32450de
ee2a1ac
57ef3c4
ee2a1ac
57ef3c4
 
 
 
 
 
 
32450de
 
 
 
 
ee2a1ac
 
32450de
41ddd18
ee2a1ac
4c40bba
 
 
ee2a1ac
4c40bba
 
 
 
 
 
 
 
ee2a1ac
32450de
4c40bba
41ddd18
32450de
41ddd18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d785524
41ddd18
 
 
 
af3b295
41ddd18
 
 
 
af3b295
41ddd18
 
4c40bba
41ddd18
 
 
 
 
af3b295
32450de
41ddd18
 
 
 
32450de
41ddd18
32450de
ee2a1ac
d785524
41ddd18
d785524
 
 
41ddd18
3eaeb3f
 
 
d785524
41ddd18
 
3eaeb3f
41ddd18
 
 
 
 
 
d785524
41ddd18
d785524
32450de
 
ee2a1ac
41ddd18
ee2a1ac
525c40c
 
 
 
 
ee2a1ac
41ddd18
 
4f69190
ee2a1ac
 
 
 
3eaeb3f
41ddd18
525c40c
41ddd18
 
3eaeb3f
41ddd18
ee2a1ac
e979870
41ddd18
 
525c40c
 
 
 
41ddd18
 
525c40c
 
41ddd18
525c40c
 
 
 
 
 
 
 
41ddd18
 
525c40c
 
 
 
 
 
ee2a1ac
525c40c
 
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
import gradio as gr
import cv2
import os
import zipfile
from PIL import Image, ImageOps
from datetime import datetime
import hashlib

def procesar_video(video):
    try:
        if isinstance(video, dict):
            original_name = video.get("name", "video")
            video_path = video.get("file", video.get("data"))
        else:
            original_name = os.path.basename(video)
            video_path = video

        allowed_extensions = ('.mp4', '.avi', '.mov', '.mkv')
        if not original_name.lower().endswith(allowed_extensions):
            raise gr.Error("Solo se permiten archivos de video (mp4, avi, mov, mkv)")

        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        temp_dir = f"temp_{datetime.now().strftime('%Y%m%d%H%M%S')}"
        os.makedirs(temp_dir, exist_ok=True)

        # Extracción de todos los fotogramas
        cap = cv2.VideoCapture(video_path)
        frame_count = 0
        frame_paths = []
        while True:
            ret, frame = cap.read()
            if not ret:
                break
            frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            img = Image.fromarray(frame_rgb)
            img_path = os.path.join(temp_dir, f"frame_{frame_count:04d}.jpg")
            img.save(img_path)
            frame_paths.append(img_path)
            frame_count += 1
        cap.release()

        if frame_count == 0:
            raise gr.Error("No se pudieron extraer fotogramas del video")

        # Selección estratégica de 4 fotogramas equidistantes
        n_seleccion = 4
        step = max(1, frame_count // (n_seleccion + 1))
        selected_indices = [step * (i+1) for i in range(n_seleccion)]
        selected_frames = [frame_paths[min(i, len(frame_paths)-1)] for i in selected_indices]

        # Creación de collage profesional
        images = []
        for img_path in selected_frames:
            img = Image.open(img_path)
            bordered_img = ImageOps.expand(img, border=2, fill='white')  # Borde blanco
            images.append(bordered_img)

        # Configuración del diseño
        img_w, img_h = images[0].size
        margin = 30
        border_size = 20
        shadow_offset = 5
        
        collage_width = (img_w * 2) + margin + (border_size * 2)
        collage_height = (img_h * 2) + margin + (border_size * 2)
        
        collage = Image.new('RGB', 
            (collage_width, collage_height), 
            (230, 230, 230))  # Fondo gris claro
        
        # Posiciones con efecto de profundidad
        positions = [
            (border_size, border_size),
            (border_size + img_w + margin, border_size),
            (border_size, border_size + img_h + margin),
            (border_size + img_w + margin, border_size + img_h + margin)
        ]
        
        # Pegar imágenes con sombra
        for i, img in enumerate(images):
            # Sombra
            shadow = Image.new('RGBA', (img_w + shadow_offset, img_h + shadow_offset), (0,0,0,50))
            collage.paste(shadow, (positions[i][0]+shadow_offset, positions[i][1]+shadow_offset), shadow)
            
            # Imagen principal
            collage.paste(img, positions[i])

        collage_path = os.path.join(temp_dir, "collage_forense.jpg")
        collage.save(collage_path, quality=95, dpi=(300, 300))

        # Generación del ZIP con cadena de custodia
        base_name = os.path.splitext(original_name)[0]
        zip_filename = f"{base_name}_Fotogramas.zip"
        final_zip_path = os.path.join(temp_dir, zip_filename)
        
        with zipfile.ZipFile(final_zip_path, mode="w") as zipf:
            # Añadir todos los frames
            for img_path in frame_paths:
                zipf.write(img_path, os.path.basename(img_path))
            
            # Archivo TXT con formato profesional
            with open(video_path, "rb") as f:
                video_hash = hashlib.md5(f.read()).hexdigest()
            
            chain_content = (
                "=== CADENA DE CUSTODIA DIGITAL ===\r\n\r\n"
                f"• Archivo original: {original_name}\r\n"
                f"• Fecha de procesamiento: {timestamp}\r\n"
                f"• Fotogramas totales: {frame_count}\r\n"
                f"• Hash MD5 video: {video_hash}\r\n"
                f"• Fotogramas muestra: {', '.join([f'#{i+1}' for i in selected_indices])}\r\n\r\n"
                "Este documento certifica la integridad del proceso de extracción.\n"
                "Sistema Certificado por Peritos Forenses Digitales de Guatemala. \n"
                "www.forensedigital.gt"
            )
            zipf.writestr("00_CADENA_CUSTODIA.txt", chain_content)

        return collage_path, final_zip_path, temp_dir

    except Exception as e:
        raise gr.Error(f"Error en procesamiento: {str(e)}")

def limpiar_cache(temp_dir):
    if temp_dir and os.path.exists(temp_dir):
        for file in os.listdir(temp_dir):
            os.remove(os.path.join(temp_dir, file))
        os.rmdir(temp_dir)

with gr.Blocks(title="Extractior Forense de Fotogramas") as demo:
    gr.Markdown("# 📷 Extractor Forense de Fotogramas de Videos")
    gr.Markdown("**Herramienta certificada para extracción forense de fotogramas de videos** (No se guarda ninguna informació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():
            video_input = gr.Video(
                label="CARGAR VIDEO",
                sources=["upload"],
                format="mp4",
                interactive=True
            )
            procesar_btn = gr.Button("🔍 INICIAR ANÁLISIS", interactive=False)
        with gr.Column():
            # gr.Markdown("## Resultados:")
            gallery_output = gr.Image(label="COLLAGE DE REFERENCIA", height=400)
            download_file = gr.File(label="DESCARGAR EVIDENCIAS", visible=True)
    
    temp_dir_state = gr.State()
    zip_path_state = gr.State()
    
    def habilitar_procesado(video):
        return gr.update(interactive=True) if video else gr.update(interactive=False)
    
    video_input.change(
        fn=habilitar_procesado,
        inputs=video_input,
        outputs=procesar_btn,
        queue=False
    )
    
    def procesar_y_mostrar(video):
        if temp_dir_state.value:
            limpiar_cache(temp_dir_state.value)
        collage, zip_path, temp_dir = procesar_video(video)
        return collage, zip_path, temp_dir, zip_path
    
    procesar_btn.click(
        fn=procesar_y_mostrar,
        inputs=video_input,
        outputs=[gallery_output, download_file, temp_dir_state, zip_path_state]
    )

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860)