dfernandezl12 commited on
Commit
846a453
·
verified ·
1 Parent(s): e26f213

Upload 5 files

Browse files
Files changed (5) hide show
  1. main.py +109 -0
  2. modelo.keras +3 -0
  3. static/script.js +59 -0
  4. static/style.css +110 -0
  5. templates/index.html +30 -0
main.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import tensorflow as tf
4
+ from tensorflow.keras import layers, models
5
+ from flask import Flask, request, jsonify, render_template
6
+ from PIL import Image
7
+ import io
8
+ from datasets import load_dataset
9
+
10
+ # ------------------------------
11
+ # Configuración
12
+ # ------------------------------
13
+ MODEL_PATH = "modelo.keras" # Extensión .keras (Keras 3)
14
+ IMG_SIZE = (64, 64)
15
+
16
+ def build_model():
17
+ model = models.Sequential([
18
+ layers.Conv2D(32, (3, 3), activation='relu', input_shape=(64, 64, 3)),
19
+ layers.MaxPooling2D((2, 2)),
20
+ layers.Conv2D(64, (3, 3), activation='relu'),
21
+ layers.Flatten(),
22
+ layers.Dense(64, activation='relu'),
23
+ layers.Dense(1, activation='sigmoid') # 1 = blanco, 0 = negro
24
+ ])
25
+ model.compile(optimizer='adam',
26
+ loss='binary_crossentropy',
27
+ metrics=['accuracy'])
28
+ return model
29
+
30
+ def train_and_save_model():
31
+ print("Cargando dataset FairFace...")
32
+ ds = load_dataset("HuggingFaceM4/FairFace", "0.25")
33
+
34
+ ds_filtered = ds['train']
35
+
36
+ # Preparar imágenes y etiquetas
37
+ images = []
38
+ labels = []
39
+ for item in ds_filtered:
40
+ img = item['image'].convert('RGB').resize(IMG_SIZE)
41
+ images.append(np.array(img) / 255.0)
42
+ labels.append(1.0 if item['gender'] == 0 else 0.0)
43
+
44
+ X = np.array(images)
45
+ y = np.array(labels)
46
+
47
+ print(f"Datos cargados: {len(X)} imágenes (blancas: {sum(y)}, negras: {len(y)-sum(y)})")
48
+
49
+ # Entrenar modelo
50
+ model = build_model()
51
+ print("Entrenando clasificador de género...")
52
+ model.fit(X, y, epochs=5, batch_size=32, validation_split=0.2, verbose=1)
53
+
54
+ # Guardar en formato Keras 3 (.keras)
55
+ model.save(MODEL_PATH)
56
+ print(f"Modelo guardado como {MODEL_PATH}")
57
+ return model
58
+
59
+ # Cargar modelo con reintento si falla
60
+ def load_or_train_model():
61
+ if os.path.exists(MODEL_PATH):
62
+ try:
63
+ model = tf.keras.models.load_model(MODEL_PATH)
64
+ print("✅ Modelo cargado desde disco correctamente")
65
+ return model
66
+ except Exception as e:
67
+ print(f"⚠️ Error al cargar el modelo: {e}")
68
+ print("Entrenando uno nuevo...")
69
+ return train_and_save_model()
70
+ else:
71
+ print("Modelo no encontrado. Entrenando desde cero...")
72
+ return train_and_save_model()
73
+ # El resto de tu código Flask sigue igual debajo de esto...
74
+ model = load_or_train_model()
75
+
76
+ # ------------------------------
77
+ # Aplicación Flask
78
+ # ------------------------------
79
+ app = Flask(__name__)
80
+
81
+ def predecir_sexo(imagen_bytes):
82
+ img = Image.open(io.BytesIO(imagen_bytes)).convert('RGB')
83
+ img = img.resize(IMG_SIZE)
84
+ img_array = np.array(img) / 255.0
85
+ img_array = np.expand_dims(img_array, axis=0)
86
+ prediccion = model.predict(img_array, verbose=0)[0][0]
87
+ return "mujer" if prediccion >= 0.5 else "hombre"
88
+
89
+ @app.route('/')
90
+ def index():
91
+ return render_template('index.html')
92
+
93
+ @app.route('/predict', methods=['POST'])
94
+ def predict():
95
+ if 'image' not in request.files:
96
+ return jsonify({'error': 'No se encontró ninguna imagen'}), 400
97
+ file = request.files['image']
98
+ if file.filename == '':
99
+ return jsonify({'error': 'Nombre de archivo vacío'}), 400
100
+ try:
101
+ imagen_bytes = file.read()
102
+ sexo = predecir_sexo(imagen_bytes)
103
+ return jsonify({'sexo': sexo})
104
+ except Exception as e:
105
+ return jsonify({'error': str(e)}), 500
106
+
107
+ if __name__ == '__main__':
108
+ port = int(os.environ.get("PORT", 7860))
109
+ app.run(host='0.0.0.0', port=port)
modelo.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:312abcb4bec5ed5a5d69ae0f622bd220cf33cdcccfe89ba0874277c6315113d9
3
+ size 41604508
static/script.js ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const imageInput = document.getElementById('imageInput');
2
+ const previewImg = document.getElementById('previewImg');
3
+ const classifyBtn = document.getElementById('classifyBtn');
4
+ const resultDiv = document.getElementById('result');
5
+
6
+ imageInput.addEventListener('change', (event) => {
7
+ const file = event.target.files[0];
8
+ if (file) {
9
+ const reader = new FileReader();
10
+ reader.onload = function(e) {
11
+ previewImg.src = e.target.result;
12
+ previewImg.style.display = 'block';
13
+ classifyBtn.disabled = false;
14
+ resultDiv.innerHTML = '';
15
+ resultDiv.className = 'result';
16
+ };
17
+ reader.readAsDataURL(file);
18
+ } else {
19
+ previewImg.style.display = 'none';
20
+ classifyBtn.disabled = true;
21
+ }
22
+ });
23
+
24
+ classifyBtn.addEventListener('click', async () => {
25
+ const file = imageInput.files[0];
26
+ if (!file) return;
27
+
28
+ classifyBtn.disabled = true;
29
+ classifyBtn.innerText = '🔍 Analizando...';
30
+ resultDiv.innerHTML = '<em>Procesando imagen...</em>';
31
+
32
+ const formData = new FormData();
33
+ formData.append('image', file);
34
+
35
+ try {
36
+ const response = await fetch('/predict', {
37
+ method: 'POST',
38
+ body: formData
39
+ });
40
+
41
+ if (!response.ok) {
42
+ const errorData = await response.json();
43
+ throw new Error(errorData.error || 'Error del servidor');
44
+ }
45
+
46
+ const data = await response.json();
47
+ const sexo = data.sexo;
48
+
49
+ resultDiv.innerHTML = `🧬 La persona es <strong>${sexo}</strong>.`;
50
+ resultDiv.classList.add(sexo === 'mujer' ? 'mujer' : 'hombre');
51
+ } catch (error) {
52
+ console.error(error);
53
+ resultDiv.innerHTML = `❌ Error: ${error.message}`;
54
+ resultDiv.classList.add('error');
55
+ } finally {
56
+ classifyBtn.disabled = false;
57
+ classifyBtn.innerText = '🔍 Clasificar';
58
+ }
59
+ });
static/style.css ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
3
+ background: linear-gradient(135deg, #1e3c72, #2a5298);
4
+ margin: 0;
5
+ min-height: 100vh;
6
+ display: flex;
7
+ justify-content: center;
8
+ align-items: center;
9
+ color: #333;
10
+ }
11
+
12
+ .container {
13
+ background: rgba(255, 255, 255, 0.95);
14
+ border-radius: 2rem;
15
+ padding: 2rem;
16
+ max-width: 500px;
17
+ width: 90%;
18
+ text-align: center;
19
+ box-shadow: 0 20px 35px rgba(0,0,0,0.2);
20
+ transition: transform 0.2s;
21
+ }
22
+
23
+ h1 {
24
+ margin-top: 0;
25
+ color: #1e3c72;
26
+ }
27
+
28
+ .upload-area {
29
+ margin: 1.5rem 0;
30
+ }
31
+
32
+ .file-label {
33
+ background-color: #2a5298;
34
+ color: white;
35
+ padding: 0.8rem 1.5rem;
36
+ border-radius: 2rem;
37
+ cursor: pointer;
38
+ display: inline-block;
39
+ transition: background 0.3s;
40
+ }
41
+
42
+ .file-label:hover {
43
+ background-color: #1e3c72;
44
+ }
45
+
46
+ input[type="file"] {
47
+ display: none;
48
+ }
49
+
50
+ .preview {
51
+ margin: 1rem 0;
52
+ min-height: 200px;
53
+ display: flex;
54
+ justify-content: center;
55
+ }
56
+
57
+ #previewImg {
58
+ max-width: 100%;
59
+ max-height: 250px;
60
+ border-radius: 1rem;
61
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
62
+ display: none;
63
+ }
64
+
65
+ button {
66
+ background-color: #28a745;
67
+ color: white;
68
+ border: none;
69
+ padding: 0.8rem 1.8rem;
70
+ font-size: 1rem;
71
+ border-radius: 2rem;
72
+ cursor: pointer;
73
+ transition: all 0.2s;
74
+ margin-top: 1rem;
75
+ }
76
+
77
+ button:disabled {
78
+ background-color: #6c757d;
79
+ cursor: not-allowed;
80
+ }
81
+
82
+ button:hover:not(:disabled) {
83
+ background-color: #218838;
84
+ transform: scale(1.02);
85
+ }
86
+
87
+ .result {
88
+ margin-top: 1.5rem;
89
+ font-size: 1.6rem;
90
+ font-weight: bold;
91
+ padding: 1rem;
92
+ border-radius: 1rem;
93
+ background: #f8f9fa;
94
+ }
95
+
96
+ .result.hombre {
97
+ color: #28a745;
98
+ background: #e6f4ea;
99
+ }
100
+
101
+ .result.mujer {
102
+ color: #28a745;
103
+ background: #fce8e6;
104
+ }
105
+
106
+ footer {
107
+ margin-top: 2rem;
108
+ font-size: 0.8rem;
109
+ color: #6c757d;
110
+ }
templates/index.html ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Clasificador de sexo</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <h1>📸 Clasificador de sexo</h1>
12
+ <p>Sube una foto de una cara y te diré si es <strong>mujer</strong> o <strong>hombre</strong>.</p>
13
+
14
+ <div class="upload-area">
15
+ <input type="file" id="imageInput" accept="image/*" capture="environment">
16
+ <label for="imageInput" class="file-label">📁 Seleccionar imagen</label>
17
+ </div>
18
+
19
+ <div class="preview">
20
+ <img id="previewImg" alt="Vista previa">
21
+ </div>
22
+
23
+ <button id="classifyBtn" disabled>🔍 Clasificar</button>
24
+
25
+ <div id="result" class="result"></div>
26
+ </div>
27
+
28
+ <script src="{{ url_for('static', filename='script.js') }}"></script>
29
+ </body>
30
+ </html>