Amadeo_Michi_Webcam_Calculator_MIDI

Hi,
I developed this software in Python that opens a MIDI file, an algorithmic file; with the detection of hands in a webcam and the use of a calculator in Python.

I donnot if some one is interested in increase the copmplexity of the software?

I did it partially with the aid of AI:

import cv2
import mediapipe as mp
import time
import tkinter as tk
import random
import os

from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from mido import MidiFile, MidiTrack, Message, MetaMessage


# ============================================================
# CONFIGURACIÓN GENERAL
# ============================================================

# Tiempo que deben permanecer las manos frente a la cámara
HAND_SECONDS = 10

# Cámara que vamos a utilizar
# 0 normalmente corresponde a la cámara integrada
CAMERA_INDEX = 0

# Ubicación del modelo de MediaPipe
MODEL_PATH = os.path.expanduser(
    "~/modelos_mediapipe/hand_landmarker.task"
)

# Nombre y ubicación del archivo MIDI
MIDI_FILENAME = os.path.expanduser(
    "~/resultado_midi.mid"
)


# ============================================================
# COMPROBAR EL MODELO DE MEDIAPIPE
# ============================================================

# Comprobamos que el archivo del modelo existe antes
# de iniciar MediaPipe.

if not os.path.isfile(MODEL_PATH):

    print()
    print("ERROR: No se encuentra el modelo de MediaPipe.")
    print()
    print("Se está buscando en:")
    print(MODEL_PATH)
    print()
    print("Comprueba que existe el archivo:")
    print("hand_landmarker.task")
    print()
    
    raise SystemExit


# ============================================================
# CONFIGURAR MEDIAPIPE
# ============================================================

# Opciones básicas de MediaPipe.
# Aquí indicamos dónde está el modelo de detección de manos.

base_options = python.BaseOptions(
    model_asset_path=MODEL_PATH
)


# Configuración del detector de manos.

options = vision.HandLandmarkerOptions(
    base_options=base_options,

    # VIDEO permite procesar imágenes consecutivas
    # procedentes de la cámara.

    running_mode=vision.RunningMode.VIDEO,

    # Detectar hasta dos manos.
    num_hands=2,

    # Confianza mínima para detectar una mano.
    min_hand_detection_confidence=0.7,

    # Confianza mínima para considerar válida
    # la presencia de una mano.
    min_hand_presence_confidence=0.7,

    # Confianza utilizada para seguir la mano
    # entre diferentes imágenes.
    min_tracking_confidence=0.7
)


# Crear el detector.

detector = vision.HandLandmarker.create_from_options(
    options
)


# ============================================================
# FUNCIÓN PARA DETECTAR LAS MANOS
# ============================================================

def detectar_manos():

    print()
    print("========================================")
    print("       DETECCIÓN DE MANOS")
    print("========================================")
    print()
    print("Muestra tus manos durante 10 segundos.")
    print("Pulsa Q para cancelar.")
    print()

    # Abrir la cámara.

    cap = cv2.VideoCapture(
        CAMERA_INDEX
    )

    # Comprobar que la cámara se abrió correctamente.

    if not cap.isOpened():

        print("ERROR: No se pudo abrir la cámara.")

        return False

    # Aquí guardaremos el momento en que
    # aparecen las manos por primera vez.

    inicio_manos = None

    # MediaPipe necesita un timestamp creciente
    # para el modo VIDEO.

    timestamp_ms = 0

    while True:

        # Leer una imagen de la cámara.

        ret, frame = cap.read()

        if not ret:

            print("ERROR: No se pudo leer la cámara.")

            break

        # Voltear horizontalmente la imagen.
        # Esto hace que funcione como un espejo.

        frame = cv2.flip(
            frame,
            1
        )

        # OpenCV utiliza BGR.
        # MediaPipe necesita RGB.

        rgb = cv2.cvtColor(
            frame,
            cv2.COLOR_BGR2RGB
        )

        # Convertir la imagen de OpenCV
        # a una imagen compatible con MediaPipe.

        mp_image = mp.Image(
            image_format=mp.ImageFormat.SRGB,
            data=rgb
        )

        # Aumentar el timestamp.

        timestamp_ms += 33

        # Detectar las manos.

        resultado = detector.detect_for_video(
            mp_image,
            timestamp_ms
        )

        # ====================================================
        # SI HAY MANOS
        # ====================================================

        if resultado.hand_landmarks:

            # Si es la primera vez que vemos las manos,
            # iniciar el contador.

            if inicio_manos is None:

                inicio_manos = time.time()

            # Calcular cuánto tiempo llevan las manos
            # delante de la cámara.

            tiempo = time.time() - inicio_manos

            segundos = int(tiempo)

            # Mostrar el contador en la ventana.

            cv2.putText(
                frame,
                f"Manos: {segundos}/{HAND_SECONDS}",
                (20, 40),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.9,
                (0, 255, 0),
                2
            )

            # =================================================
            # DIBUJAR LOS PUNTOS DE LAS MANOS
            # =================================================

            for mano in resultado.hand_landmarks:

                # Cada mano contiene 21 puntos.

                for punto in mano:

                    # Convertir coordenadas relativas
                    # de MediaPipe a píxeles.

                    x = int(
                        punto.x *
                        frame.shape[1]
                    )

                    y = int(
                        punto.y *
                        frame.shape[0]
                    )

                    # Dibujar cada punto.

                    cv2.circle(
                        frame,
                        (x, y),
                        5,
                        (0, 255, 0),
                        -1
                    )

            # =================================================
            # COMPROBAR LOS 10 SEGUNDOS
            # =================================================

            if tiempo >= HAND_SECONDS:

                cv2.putText(
                    frame,
                    "Abriendo calculadora...",
                    (20, 80),
                    cv2.FONT_HERSHEY_SIMPLEX,
                    0.8,
                    (0, 255, 0),
                    2
                )

                cv2.imshow(
                    "Camara",
                    frame
                )

                # Mostrar el mensaje durante un segundo.

                cv2.waitKey(1000)

                # Cerrar la cámara.

                cap.release()

                cv2.destroyAllWindows()

                return True

        # ====================================================
        # SI NO HAY MANOS
        # ====================================================

        else:

            # Si las manos desaparecen,
            # el contador vuelve a cero.

            inicio_manos = None

            cv2.putText(
                frame,
                "Muestra tus manos",
                (20, 40),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.9,
                (0, 0, 255),
                2
            )

        # Mostrar la cámara.

        cv2.imshow(
            "Camara",
            frame
        )

        # Leer el teclado.

        tecla = cv2.waitKey(1) & 0xFF

        # La tecla Q permite cancelar.

        if tecla == ord("q"):

            cap.release()

            cv2.destroyAllWindows()

            return False

    # Cerrar cámara si salimos del bucle.

    cap.release()

    cv2.destroyAllWindows()

    return False


# ============================================================
# FUNCIÓN PARA CREAR EL ARCHIVO MIDI
# ============================================================

def crear_midi():

    print()
    print("========================================")
    print("          GENERANDO ARCHIVO MIDI")
    print("========================================")
    print()

    # Crear archivo MIDI.
    # 480 ticks representan una negra.

    midi = MidiFile(
        ticks_per_beat=480
    )

    # Crear una pista.

    track = MidiTrack()

    midi.tracks.append(
        track
    )

    # ========================================================
    # SELECCIONAR PIANO
    # ========================================================

    # Programa 0 corresponde normalmente
    # al piano acústico en General MIDI.

    track.append(
        Message(
            "program_change",
            program=0,
            time=0
        )
    )

    # ========================================================
    # ESTABLECER TEMPO
    # ========================================================

    # 1.000.000 microsegundos por negra
    # equivale aproximadamente a 60 BPM.

    track.append(
        MetaMessage(
            "set_tempo",
            tempo=1000000,
            time=0
        )
    )

    # ========================================================
    # NOTAS BASE
    # ========================================================

    # Notas MIDI:
    #
    # 60 = Do
    # 62 = Re
    # 64 = Mi
    # 65 = Fa
    # 67 = Sol
    # 69 = La
    # 71 = Si
    # 72 = Do

    notas = [
        60,
        62,
        64,
        65,
        67,
        69,
        71,
        72
    ]

    # ========================================================
    # INTERVALOS DE TERCERA MENOR
    # ========================================================

    # Una tercera menor son 3 semitonos.
    #
    # Los demás valores permiten crear
    # diferentes desplazamientos relacionados
    # con esa idea.

    terceras_menores = [
        3,
        6,
        9,
        12,
        15,
        18
    ]

    # ========================================================
    # INTERVALOS DE SÉPTIMA MAYOR
    # ========================================================

    septimas_mayores = [
        11,
        14,
        17,
        20,
        23
    ]

    # ========================================================
    # DURACIONES
    # ========================================================

    # Duraciones expresadas en ticks.

    duraciones = [
        240,
        480,
        720,
        960
    ]

    # ========================================================
    # GENERAR 30 INTERVALOS DE TERCERA
    # ========================================================

    for _ in range(30):

        # Elegir nota inicial.

        nota1 = random.choice(
            notas
        )

        # Elegir intervalo.

        intervalo = random.choice(
            terceras_menores
        )

        # Calcular segunda nota.

        nota2 = nota1 + intervalo

        # Evitar superar el máximo MIDI.

        if nota2 > 127:

            nota2 = 127

        # Elegir duración.

        duracion = random.choice(
            duraciones
        )

        # Encender primera nota.

        track.append(
            Message(
                "note_on",
                note=nota1,
                velocity=64,
                time=0
            )
        )

        # Apagar primera nota.

        track.append(
            Message(
                "note_off",
                note=nota1,
                velocity=64,
                time=duracion
            )
        )

        # Encender segunda nota.

        track.append(
            Message(
                "note_on",
                note=nota2,
                velocity=64,
                time=0
            )
        )

        # Apagar segunda nota.

        track.append(
            Message(
                "note_off",
                note=nota2,
                velocity=64,
                time=duracion
            )
        )

    # ========================================================
    # GENERAR 40 INTERVALOS DE SÉPTIMA
    # ========================================================

    for _ in range(40):

        # Elegir nota inicial.

        nota1 = random.choice(
            notas
        )

        # Elegir intervalo.

        intervalo = random.choice(
            septimas_mayores
        )

        # Calcular segunda nota.

        nota2 = nota1 + intervalo

        # Evitar superar el rango MIDI.

        if nota2 > 127:

            nota2 = 127

        # Elegir duración.

        duracion = random.choice(
            duraciones
        )

        # Encender primera nota.

        track.append(
            Message(
                "note_on",
                note=nota1,
                velocity=64,
                time=0
            )
        )

        # Apagar primera nota.

        track.append(
            Message(
                "note_off",
                note=nota1,
                velocity=64,
                time=duracion
            )
        )

        # Encender segunda nota.

        track.append(
            Message(
                "note_on",
                note=nota2,
                velocity=64,
                time=0
            )
        )

        # Apagar segunda nota.

        track.append(
            Message(
                "note_off",
                note=nota2,
                velocity=64,
                time=duracion
            )
        )

    # ========================================================
    # COMPLETAR EL MIDI HASTA 7 MINUTOS
    # ========================================================

    # 7 minutos = 420 segundos.
    #
    # Con 60 BPM y 480 ticks por negra:
    #
    # 420 x 480 = 201600 ticks.

    objetivo_ticks = 420 * 480

    # Calcular el tiempo que ya tiene la pista.

    tiempo_total = sum(
        mensaje.time
        for mensaje in track
    )

    # Agregar notas hasta alcanzar aproximadamente
    # los 7 minutos.

    while tiempo_total < objetivo_ticks:

        # Elegir una nota.

        nota = random.choice(
            notas
        )

        # Calcular cuánto falta.

        restante = (
            objetivo_ticks -
            tiempo_total
        )

        # Elegir duración.

        duracion = random.choice(
            duraciones
        )

        # No superar el objetivo.

        if duracion > restante:

            duracion = restante

        if duracion <= 0:

            break

        # Encender nota.

        track.append(
            Message(
                "note_on",
                note=nota,
                velocity=64,
                time=0
            )
        )

        # Apagar nota.

        track.append(
            Message(
                "note_off",
                note=nota,
                velocity=64,
                time=duracion
            )
        )

        # Actualizar tiempo.

        tiempo_total += duracion

    # ========================================================
    # GUARDAR ARCHIVO
    # ========================================================

    midi.save(
        MIDI_FILENAME
    )

    print()
    print("MIDI generado correctamente.")
    print()
    print("Archivo creado:")
    print(MIDI_FILENAME)
    print()
    print("Duración aproximada: 7 minutos")
    print()


# ============================================================
# CALCULADORA
# ============================================================

def abrir_calculadora():

    # Crear ventana.

    root = tk.Tk()

    root.title(
        "Calculadora MIDI"
    )

    root.geometry(
        "330x450"
    )

    root.configure(
        bg="#333333"
    )

    # Variable que contiene lo escrito
    # en la pantalla.

    entrada = tk.StringVar()

    # ========================================================
    # AGREGAR CARÁCTER
    # ========================================================

    def agregar(valor):

        entrada.set(
            entrada.get() +
            str(valor)
        )

    # ========================================================
    # LIMPIAR
    # ========================================================

    def limpiar():

        entrada.set("")

    # ========================================================
    # CALCULAR
    # ========================================================

    def calcular():

        expresion = entrada.get()

        # No hacer nada si está vacío.

        if not expresion:

            return

        # Caracteres permitidos.

        permitidos = (
            "0123456789+-*/.() "
        )

        # Comprobar que solamente se introducen
        # caracteres matemáticos.

        for caracter in expresion:

            if caracter not in permitidos:

                entrada.set(
                    "Error"
                )

                return

        try:

            # Calcular la expresión.

            resultado = eval(
                expresion,
                {
                    "__builtins__": {}
                },
                {}
            )

            # Mostrar resultado.

            entrada.set(
                str(resultado)
            )

            # =================================================
            # GENERAR MIDI
            # =================================================

            # Si la operación contiene una suma,
            # generar el archivo MIDI.

            if "+" in expresion:

                crear_midi()

        except Exception:

            entrada.set(
                "Error"
            )

    # ========================================================
    # PANTALLA
    # ========================================================

    display = tk.Entry(
        root,
        textvariable=entrada,
        font=("Arial", 24),
        bd=10,
        relief="sunken",
        justify="right",
        bg="black",
        fg="#00FF00"
    )

    display.grid(
        row=0,
        column=0,
        columnspan=4,
        padx=5,
        pady=10
    )

    # ========================================================
    # BOTONES
    # ========================================================

    botones = [

        ("7", 1, 0),
        ("8", 1, 1),
        ("9", 1, 2),
        ("/", 1, 3),

        ("4", 2, 0),
        ("5", 2, 1),
        ("6", 2, 2),
        ("*", 2, 3),

        ("1", 3, 0),
        ("2", 3, 1),
        ("3", 3, 2),
        ("-", 3, 3),

        ("0", 4, 0),
        (".", 4, 1),
        ("+", 4, 2),
        ("=", 4, 3),

        ("C", 5, 0)
    ]

    # Crear todos los botones.

    for texto, fila, columna in botones:

        # Botón C.

        if texto == "C":

            comando = limpiar

        # Botón igual.

        elif texto == "=":

            comando = calcular

        # Botones numéricos y operadores.

        else:

            comando = (
                lambda valor=texto:
                agregar(valor)
            )

        # Crear botón.

        boton = tk.Button(
            root,
            text=texto,
            font=("Arial", 18),
            width=5,
            height=2,
            command=comando,
            bg="#333333",
            fg="white",
            relief="raised"
        )

        boton.grid(
            row=fila,
            column=columna,
            padx=2,
            pady=2
        )

    # ========================================================
    # USAR ENTER
    # ========================================================

    # También podemos pulsar Enter
    # en lugar del botón "=".

    root.bind(
        "<Return>",
        lambda evento: calcular()
    )

    # ========================================================
    # INICIAR CALCULADORA
    # ========================================================

    root.mainloop()


# ============================================================
# PROGRAMA PRINCIPAL
# ============================================================

# Primero detectamos las manos.
#
# Si están presentes durante 10 segundos,
# abrimos la calculadora.

try:

    manos_detectadas = detectar_manos()

    if manos_detectadas:

        print()
        print("Manos detectadas durante 10 segundos.")
        print("Abriendo calculadora...")
        print()

        abrir_calculadora()

    else:

        print()
        print("La detección de manos fue cancelada.")
        print()

finally:

    # Cerrar MediaPipe correctamente.

    detector.close()