¿Dónde empieza lo real? Cuando un gesto activa una máquina

Levanto la mano frente a la cámara. El dedo índice apunta. Y entonces, la banda transportadora virtual comienza a moverse. Parece magia. Pero no lo es.

Ese gesto –tan simple, tan humano– es el resultado de semanas de pruebas, errores y ajustes. De líneas de código que no responden, de modelos que no entienden, de señales que no llegan. Lo que hoy parece fluido, antes fue una secuencia caótica de fallos intermitentes y frustraciones técnicas.

La idea era clara desde el inicio: construir un puente entre lo humano y lo automatizado. Que un gesto pudiera activar una máquina. Que la intención bastara para que algo se moviera, sin botones, sin contacto físico, solo con una señal emitida por el cuerpo.

Lo que desarrollé fue una arquitectura distribuida:

  • Python con visión artificial usando MediaPipe detecta los gestos.
  • Unity simula la banda transportadora y los sensores.
  • Una ESP32 recibe comandos por red para activar indicadores físicos y lo envía a un PLC

En el papel, todo estaba claro. En la práctica, fue otra historia.
Los gestos no se reconocían.
Los sockets se caían.
La ESP o python no recibía.
Unity se congelaba.
Y yo… repetía la prueba.

Una y otra vez. Cambié el código, ajusté retardos, probé diferentes protocolos, capturé logs. Las preguntas se acumulaban:

  • ¿Dónde está el cuello de botella?
  • ¿Qué tan rápido es lo “rápido suficiente”?
  • ¿Cómo evito una activación falsa sin perder sensibilidad?
  • ¿Puede coexistir el control visual con botones físicos?
  • ¿Qué significa que el sistema “entienda” mi gesto?

Fueron muchas horas de repetición obsesiva. Días enteros donde solo conseguía que el sistema hiciera “casi lo que debía”. Pero en medio de cada error, se afianzaba una idea: lo real no empieza cuando todo funciona, sino cuando algo cobra sentido.

Hoy puedo decir que el sistema responde. Que un gesto puede iniciar el flujo. Que el cuerpo puede ser interfaz. No porque sea perfecto, sino porque se adaptó. Y porque yo también me adapté a él.

Cuando el gesto se vuelve comando, la máquina obedece. Pero más importante aún: la tecnología se convierte en extensión de la intención humana.

Y ahí, justo ahí, es donde para mí empieza lo real.

Me queda claro a partir de todos mis errores hacía donde puedo ir y como siempre me quedo con todo lo aprendido :).

Cómo diseñé un sistema de ojos animados controlados por visión artificial

Hace un tiempo que vengo experimentando la combinación de hardware básico con visión artificial. La idea, era construir un sistema simple donde una matriz de LEDs representara dos ojos que se abrieran o cerraran dependiendo del parpadeo detectado en tiempo real mediante una cámara.

El primer intento consistió en recorrer la matriz LED de 8×32 pixel por pixel, encendiendo o apagando cada LED individualmente según el patrón deseado. Aunque técnicamente funcionaba, el resultado no era satisfactorio: el dibujo tardaba en actualizarse y el cambio de estado de los ojos se percibía lento.

Buscando optimizar el rendimiento, cambié el enfoque. En lugar de manejar cada LED de forma independiente, diseñé patrones binarios donde cada fila de la matriz correspondía a un byte de información. Con esto, el sistema podía actualizar toda la figura del ojo de manera instantánea, reduciendo los tiempos de actualización perceptibles.

Una vez que la animación en la matriz fue optimizada, el siguiente paso fue integrar la detección de parpadeo. Utilicé Python junto con OpenCV y MediaPipe para capturar video en tiempo real y analizar las posiciones de los ojos. Calcule el Eye Aspect Ratio (EAR) para determinar el grado de apertura de los párpados. Si el EAR bajaba por debajo de un umbral definido, se consideraba que el ojo estaba cerrado.

Cada evento detectado en Python enviaba un comando simple por comunicación serial al microcontrolador: «OPEN» si los ojos estaban abiertos, o «CLOSE» si se detectaba parpadeo. Arduino recibía estos comandos y actualizaba la matriz de LEDs en consecuencia.

Durante el proceso surgieron varios problemas adicionales, como la necesidad de afinar el umbral del EAR para evitar falsos positivos, optimizar la lectura de comandos seriales para evitar retrasos, y mejorar la fluidez de las animaciones para que los cambios de estado se sintieran naturales.

El resultado final fue un sistema que detecta el parpadeo de una persona en tiempo real y actualiza una matriz de LEDs para representar esa acción. Además, si no detecta eventos durante cierto tiempo, el sistema genera parpadeos automáticos programados, simulando actividad natural.

Esta idea fue una buena práctica para integrar visión computacional con control físico de hardware en tiempo real. También sirvió para explorar la importancia de optimizar flujos de procesamiento en sistemas de recursos limitados, aún hay muchas cosas que mejorar pero es una base con la que puedo seguir explorando nuevas ideas.

Preparando un servicio que retorna texto a partir de un audio

Pues hoy tocó preparar el entorno para ejecutar en FastApi un servicio que reciba un audio y regrese un texto.

Como es necesario procesar el audio que se graba en web y se envía al servidor, se necesitan algunas librerias para convertir entre formatos de audio

en mi caso utilicé home brew para instalar la librería ffmpeg para que funcione correctamente algunos módulos de python, el detalle es que al tener varias versiones de python tuve algunos problemas por lo que finalmente encontré una solución, que fue, instalar las versiones compiladas de las librerías que se pueden descargar aquí.

Para la prueba utilicé vue, en el cuál existe un método para iniciar y detener la grabación y finalmente el método que envía a la api.

y bueno este es parte del código de la api, como se observa ya que instalé ffmpeg y ffprobe con una versión compilada, los ejecutables los pasé al directorio /Applications para que python los pudiera encontrar.

En otras pruebas se pueden ver los resultados

ahora a probar con modelos preentrenados que se ejecuten localmente.

¿Qué relación tiene un mapa conceptual y un chatbot?

Una alternativa que tenemos para construir un chatbot son los mapas conceptuales, que nos ayudan a conectar conceptos y definir relaciones entre ellos. Esta práctica fue parte de la temática expuesta en la materia de Inteligencia Artificial, enfocada en la aplicación de modelos de representación del conocimiento en el desarrollo de asistentes virtuales.

Los mapas conceptuales o grafos de conocimiento permiten modelar información en forma de nodos y relaciones, facilitando la organización y el acceso a los datos. En este contexto, podemos crear un chatbot que responda preguntas basadas en la estructura de un grafo. Los conceptos utilizados son referentes a la temática de Inteligencia Artificial, aprovechando los temas propios de la materia de IA que imparto, en donde utilicé el ejemplo para explicar la relación.

El primer ejercicio consistió en construir un grafo dirigido que representa la relación entre distintos subcampos de la Inteligencia Artificial. Con la biblioteca networkx, al ejecutar el código se puede observar el grafo.

import networkx as nx
import matplotlib.pyplot as plt

G = nx.DiGraph()
G.add_edges_from([
    ("IA", "Machine Learning"),
    ("IA", "Deep Learning"),
    ("Machine Learning", "Redes Neuronales"),
    ("Machine Learning", "Árboles de Decisión"),
    ("Deep Learning", "CNN"),
    ("Deep Learning", "RNN")
])

plt.figure(figsize=(8,6))
nx.draw(G, with_labels=True, node_color='lightblue', edge_color='gray', node_size=3000, font_size=10)
plt.show()

Este ejercicio ayudó a visualizar cómo los conceptos de IA están conectados y cómo se pueden representar jerárquicamente mediante grafos.

y el chat bot?

Ahora toca integrar el mapa en un chatbot que pueda responder preguntas. Para esto se implementó un sistema de preguntas y respuestas que interpreta consultas sobre conceptos en el grafo y devuelve respuestas basadas en las conexiones definidas.

def responder_pregunta(pregunta):
    pregunta = pregunta.lower()
    
    # Pregunta: ¿Qué es <concepto>?
    for nodo in G.nodes:
        if f"qué es {nodo.lower()}" in pregunta:
            subtemas = list(G.successors(nodo))
            if subtemas:
                return f"{nodo} es un área de la Inteligencia Artificial que incluye conceptos como: {', '.join(subtemas)}."
            else:
                return f"{nodo} es un concepto en Inteligencia Artificial, pero no tiene subcategorías en este modelo."
    
    # Pregunta: ¿Qué relación tiene <concepto1> con <concepto2>?
    conceptos = [nodo for nodo in G.nodes if nodo.lower() in pregunta]
    
    if len(conceptos) == 2:
        nodo1, nodo2 = conceptos
        if G.has_edge(nodo1, nodo2):
            return f"{nodo1} es una subcategoría de {nodo2}."
        elif G.has_edge(nodo2, nodo1):
            return f"{nodo2} es una subcategoría de {nodo1}."
        else:
            return f"{nodo1} y {nodo2} están relacionadas con Inteligencia Artificial, pero no directamente en este grafo."
    
    return "Lo siento, no tengo información sobre eso."

Aquí algunas de las preguntas y la salida.

qué sigue?

El reto no termina aquí. Ahora se espera que los estudiantes puedan aplicar lo aprendido en otro contexto, utilizando otro tipo de conocimientos. La idea es ver los resultados más adelante y explorar cómo se pueden extender estos modelos a otras áreas de conocimiento.

Desplegando apps y servicios CI4 en hosting con cPanel

Esta guía rápida también aplica para subdominios y servicios, en esta entrada no encontrarás cómo hacer la app o servicios, simplemente cómo publicarla teniendo como prerrequisito un dominio con acceso a cpanel.

1. Preparar Archivos

  • Borra lo innecesario (vendor/ si usaste Composer, lo regenerarás en el servidor).
  • Configura .env, ajustando la base de datos y estableciendo CI_ENVIRONMENT = production. #con esto evitas que se muestren los errores y expongas información
  • Comprime el proyecto en un .zip, dejando public/ fuera del archivo.

2. Subir Archivos

  1. En cPanel > Administrador de Archivos, sube el .zip a public_html (o a la carpeta del subdominio/servicio) y extráelo.
  2. Asegúrate de que la estructura de archivos quede bien organizada.

3. Ajustar Directorios

  • Mueve el contenido de public/ a la raíz de public_html o la carpeta del subdominio.
  • Edita index.php para corregir la ruta: $pathsPath = realpath(FCPATH . '../Config/Paths.php'); #elimina los dos puntos
  • Revisa .htaccess y ajusta las reglas si es necesario.

4. Base de Datos

  1. Desde cPanel > MySQL Databases, crea la base de datos.
  2. Genera un usuario, asigna permisos completos y anota las credenciales.
  3. Exporta la BD desde tu entorno local usando phpMyAdmin o mysqldump.
  4. Importa la BD en phpMyAdmin del servidor.
  5. Edita .env con los datos correctos de la BD.

5. Si tienes algún error por la versión de php

  • En cPanel > Seleccionar Versión de PHP, elige una compatible con la versión de tu CI4.
  • Activa mod_rewrite si es necesario para URLs limpias.
  • Prueba accediendo al dominio, subdominio o servicio y verifica que todo funcione.

Creando una app de reconocimiento de patrones con Tkinter y TensorFlow

La inteligencia artificial (IA) es una disciplina que permite a las computadoras aprender y tomar decisiones basadas en datos. Utilizando modelos de aprendizaje automático, es posible entrenar algoritmos para reconocer patrones y hacer predicciones. En esta breve entrada, se muestra cómo desarrollar una aplicación en Python que permita el reconocimiento de patrones, para este caso, reconocer los números del 0 – 9 utilizando una red neuronal entrenada con TensorFlow. y para la interfaz gráfica, Tkinter, en python.

Librerias

  • Tkinter: Para la interfaz gráfica.
  • NumPy: Para manejar la matriz de datos.
  • TensorFlow/Keras: Para cargar y utilizar un modelo de reconocimiento de patrones.

Carga del Modelo de Reconocimiento

import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.utils import to_categorical

# Ejemplos simples para 0 al 9
digits = {
    0: [
        [0,1,1,0],
        [1,0,0,1],
        [1,0,0,1],
        [1,0,0,1],
        [1,0,0,1],
        [0,1,1,0]
    ],
    1: [
        [0,0,1,0],
        [0,1,1,0],
        [0,0,1,0],
        [0,0,1,0],
        [0,0,1,0],
        [0,1,1,1]
    ],
    2: [
        [0,1,1,0],
        [1,0,0,1],
        [0,0,1,0],
        [0,1,0,0],
        [1,0,0,0],
        [1,1,1,1]
    ],
    3: [
        [1,1,1,0],
        [0,0,0,1],
        [0,1,1,0],
        [0,0,0,1],
        [0,0,0,1],
        [1,1,1,0]
    ],
    4: [
        [0,0,1,0],
        [0,1,1,0],
        [1,0,1,0],
        [1,1,1,1],
        [0,0,1,0],
        [0,0,1,0]
    ],
    5: [
        [1,1,1,1],
        [1,0,0,0],
        [1,1,1,0],
        [0,0,0,1],
        [1,0,0,1],
        [0,1,1,0]
    ],
    6: [
        [0,1,1,0],
        [1,0,0,0],
        [1,1,1,0],
        [1,0,0,1],
        [1,0,0,1],
        [0,1,1,0]
    ],
    7: [
        [1,1,1,1],
        [0,0,0,1],
        [0,0,1,0],
        [0,1,0,0],
        [1,0,0,0],
        [1,0,0,0]
    ],
    8: [
        [0,1,1,0],
        [1,0,0,1],
        [0,1,1,0],
        [1,0,0,1],
        [1,0,0,1],
        [0,1,1,0]
    ],
    9: [
        [0,1,1,0],
        [1,0,0,1],
        [1,0,0,1],
        [0,1,1,1],
        [0,0,0,1],
        [0,1,1,0]
    ]
}

# Preparación de los datos
X = np.array([np.array(digits[d]).flatten() for d in digits])  # Convertir a vectores de 24 elementos
y = np.array(list(digits.keys()))

# Codificación one-hot para las etiquetas
y_categorical = to_categorical(y, num_classes=10)

# Definir el modelo de red neuronal
model = Sequential([
    Dense(32, activation='relu', input_shape=(24,)),
    Dense(16, activation='relu'),
    Dense(10, activation='softmax')  # 10 clases (0 al 9)
])

# Compilar el modelo
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])

# Entrenar el modelo
model.fit(X, y_categorical, epochs=1000, verbose=1)

# Guardar el modelo entrenado
model.save("modeloReconocimiento0-9.keras")

print("Modelo guardado exitosamente como 'modelo_numeros.keras")

El primer paso es cargar el modelo previamente entrenado. En este caso, el modelo debe haber sido guardado en un archivo modelo0-9.keras.

import tensorflow as tf
import numpy as np
import tkinter as tk

# Cargar el modelo
try:
    model = tf.keras.models.load_model('modelo0-9.keras')  
except:
    print(" No se encontró el archivo 'modelo0-9.keras'. Asegúrate de entrenar y guardar el modelo.")
    exit()

Creación de la Interfaz Gráfica con Tkinter

La interfaz se basa en un lienzo donde el usuario puede dibujar un patrón de 6×4 celdas. Cada celda puede activarse o desactivarse haciendo clic en ella.

# Dimensiones de la matriz
ROWS = 6
COLS = 4
CELL_SIZE = 50  

matrix = np.zeros((ROWS, COLS), dtype=int)

root = tk.Tk()
root.title("Reconocimiento de Patrones (Canvas)")
root.configure(bg="white")

canvas = tk.Canvas(root, width=COLS * CELL_SIZE, height=ROWS * CELL_SIZE, bg="white")
canvas.grid(row=0, column=0, columnspan=4, padx=10, pady=10)

Funcionalidad de Dibujo

Cada clic del usuario alterna el estado de la celda, cambiando su color de blanco a negro y viceversa.

rectangles = {}

def toggle_cell(event):
    col = event.x // CELL_SIZE
    row = event.y // CELL_SIZE  
    if 0 <= row < ROWS and 0 <= col < COLS:
        matrix[row, col] = 1 if matrix[row, col] == 0 else 0
        color = "black" if matrix[row, col] == 1 else "white"
        canvas.itemconfig(rectangles[(row, col)], fill=color)

Creación de la Cuadrícula

for r in range(ROWS):
    for c in range(COLS):
        x1, y1 = c * CELL_SIZE, r * CELL_SIZE
        x2, y2 = x1 + CELL_SIZE, y1 + CELL_SIZE
        rect = canvas.create_rectangle(x1, y1, x2, y2, fill="white", outline="gray")
        rectangles[(r, c)] = rect

canvas.bind("<Button-1>", toggle_cell)

Reconocimiento del Patrón

Al presionar el botón «Reconocer», la matriz se procesa y se pasa al modelo para obtener una predicción.

def recognize_number():
    flattened_matrix = matrix.flatten().reshape(1, -1)
    prediction = model.predict(flattened_matrix)
    result_label.config(text=f"Número reconocido: {np.argmax(prediction)}")

Reinicio del Dibujo

def reset_matrix():
    global matrix
    matrix = np.zeros((ROWS, COLS), dtype=int)
    for (r, c), rect in rectangles.items():
        canvas.itemconfig(rect, fill="white")  

Botones de Interacción

recognize_btn = tk.Button(root, text="Reconocer", command=recognize_number, font=("Arial", 12), bg="lightblue")
recognize_btn.grid(row=1, column=0, columnspan=2, pady=10)

reset_btn = tk.Button(root, text="Reiniciar", command=reset_matrix, font=("Arial", 12), bg="lightblue", fg="black")
reset_btn.grid(row=1, column=2, columnspan=2, pady=10)

result_label = tk.Label(root, text="Dibuja un número y presiona 'Reconocer'", font=("Arial", 12), bg="white")
result_label.grid(row=2, column=0, columnspan=4)

Finalmente, iniciamos el bucle de Tkinter:

root.mainloop()

Esta aplicación es un ejemplo práctico de cómo integrar modelos de inteligencia artificial en interfaces gráficas de usuario. Puedes mejorarla entrenando modelos más precisos o ampliando la resolución de la cuadrícula para mejorar la precisión en el reconocimiento.

Publicaciones vacías y el anonimato en la red

Hoy en día, compartir opiniones en internet es más fácil que nunca, pero también ha traído consigo un problema creciente: la difusión de publicaciones vacías, sin fundamentos sólidos y muchas veces cargadas de difamación. Lo preocupante no es solo que existan, sino la rapidez con la que se propagan y la cantidad de personas que las toman como verdad sin cuestionarlas.

El anonimato en la red ha dado pie a que muchos se sientan con la libertad de atacar sin consecuencias, escondiéndose tras perfiles sin rostro para lanzar acusaciones sin pruebas. Lo peor es que en muchos casos, estas publicaciones logran gran alcance, dañando la reputación de personas y empresas sin motivo real.

Plataformas como Facebook, Twitter y grupos en WhatsApp o Telegram han amplificado este fenómeno. En estos espacios, la información se comparte sin filtros, permitiendo que los rumores y la desinformación se propaguen a gran velocidad. Especialmente en algunos grupos de Facebook, se ha vuelto común ver cómo los usuarios difunden contenido sin verificarlo, generando una cultura de ataque sin reflexión.

Ayer, por ejemplo, me topé con un caso que ilustra esto perfectamente. En un grupo de Facebook, varias personas estaban criticando y difamando a una empresa sin aportar ninguna prueba concreta. Me dieron ganas de intervenir con argumentos y hechos verificables, pero me di cuenta de que no había espacio para el diálogo. La mayoría de los participantes solo buscaban sumarse a la indignación sin cuestionar lo que leían. Situaciones como esta son una clara muestra de la importancia de fomentar el pensamiento crítico y no dejarnos llevar por la información superficial.

¿Por qué tanta gente se suma a esta dinámica? En parte, porque es más fácil creer en algo llamativo que tomarse el tiempo de investigar la verdad. Además, compartir información que refuerza ciertas ideas genera una sensación de pertenencia, sin considerar el impacto que puede tener en la vida real.

Es fundamental desarrollar una actitud más responsable al consumir y compartir información. No basta con leer y dar por hecho lo que nos dicen; hay que cuestionar, verificar fuentes y ser conscientes del efecto que nuestras palabras pueden tener en los demás.

!de vuelta!

¡Estamos de vuelta en e-icus.net!

Han pasado varios años desde que inicié este blog bajo el nombre de «abdielicus», y con el tiempo evolucionó a «e-icus.net». Sin embargo, por diversas razones, el proyecto quedó en pausa. Hoy, con energía renovada y muchas ideas en mente, es momento de retomarlo.

En esta nueva etapa, quiero compartir contenido sobre temas que siempre me han apasionado: educación, programación y actividades diarias. Desde artículos técnicos y guías de desarrollo hasta reflexiones sobre el aprendizaje y la tecnología, este espacio será un punto de encuentro para el conocimiento y la experimentación.

Es momento de cambiar la esencia del blog, me propongo mejorar la frecuencia de publicaciones y la interacción con quienes se sumen a esta comunidad. Leeré sus opiniones, sugerencias y temas de interés para hacer de este sitio un espacio más enriquecedor para todos.

Nos leemos pronto.

Problemas con Google Chrome, resultados de búsqueda mal intencionados

Hace unos días noté que los resultados de búsqueda que hacía desde google Chrome se veían distintos, de manera regular aparece resultados de las páginas oficiales, si busco algún término por ejemplo sale wikipedia e imágenes a la derecha, digamos que el comportamiento normal.

El comportamiento anómalo, se da al momento de hacer una búsqueda, al inicio aparece los resultados comunes pero después de 1 o 2 segundos se insertan resultados con anuncios y páginas que no suelo ver.

El primer intento para corregir el error fue borrando los datos de navegación, cookies, etc; esto no funcionó.

El segundo intento fue desinstalar el programa, al hacerlo y volverlo a instalar el resultado de la misma manera no funcionó, la desinstalación no elimina varios archivos que son los responsables de dicho comportamiento, de alguna manera una cookie, o algún script pudo vulnerar los archivos de Crhome.

La solución

Por lo que me di a la tarea de eliminar los siguientes elementos en modo consola.


rm -r ~/Library/Application\ Support/Google/Chrome/
rm ~/Library/Preferences/com.google.Chrome*
rm ~/Library/Application\ Support/CrashReporter/Google\ Chrome*
rm -r ~/Library/Caches/com.google.Chrome*
rm ~/Library/Preferences/Google\ Chrome*
rm -r ~/Library/Saved\ Application\ State/com.google.Chrome.savedState/
rm ~/Library/Google/GoogleSoftwareUpdate/Actives/com.google.Chrome
rm ~/Library/Google/Google\ Chrome*
rm -r /Applications/Google\ Chrome.app/

A lo mejor le pueda servir a alguien o si me vuelve a ocurrir, aquí tengo la solución.

Luz

There is a crack in everthing, that’s how the light gets in»

Leonard Cohen