SISTEM PIANO VIRTUAL BERBASIS PICAM

  SISTEM PIANO VIRTUAL

 

Adriel Fernanda Mardiansyah1, Firmansyah Trijaya2, Muhammad Ridho Apriansyah3, Rifqi Khoirul Lathif4

 

Jurusan Teknik Elektro, Prodi Teknologi Rekayasa Elektronika, Politeknik Negeri Semarang

Jl. Prof. Soedarto, Tembalang, Kec, Tembalang, Kota Semarang, Jawa Tengah, 50275

 1adrielmardiansyah123@gmail.com

2 firmansyahtrijaya9@gmail.com

3ridhoapriansyah936@gmail.com

4rfqkhoirul@gmail.com

 

 

INTISARI - Sistem Piano Virtual ini dikembangkan dengan memanfaatkan teknologi computer vision untuk menciptakan alternatif alat musik yang interaktif dan higienis. Berbekal komponen Raspberry Pi 4 dan kamera Pi Cam, sistem mampu mendeteksi dan melacak 21 titik anatomi fisik tangan pengguna secara real-time menggunakan pustaka MediaPipe. Gerakan jari pada sumbu X dan Y kemudian dipetakan ke dalam 8 zona nada utama, yang akan menginstruksikan buzzer untuk mengeluarkan frekuensi suara Pulse Width Modulation (PWM) yang sesuai dengan gestur tangan, menjadikannya sebuah sistem alat musik tanpa sentuhan fisik langsung.

Kata KunciSistem Piano Virtual, Computer Vision, Raspberry Pi 4, Mediapipe. Pelacakan Tangan, Pemetaan Gestur, Zona Nada, PWM, Alat Musik Interaktif, Tanpa Sentuhan.


I. PENDAHULUAN

A. Latar Belakang

Kemajuan teknologi kecerdasan buatan (AI) dan computer vision modern saat ini memungkinkan dilakukannya pelacakan titik anatomi fisik secara waktu nyata (real-time). Menariknya, pelacakan tingkat lanjut ini dapat dilakukan hanya dengan menggunakan kamera standar, tanpa perlu mengandalkan perangkat sensor fisik yang mahal.

Pemanfaatan teknologi interaksi tanpa sentuh (touchless) ini membuka peluang untuk menciptakan alternatif alat musik yang lebih higienis, interaktif, dan menyenangkan. Sistem Piano Virtual ini dirancang sebagai implementasi dari konsep tersebut, di mana pengguna dapat menghasilkan nada layaknya bermain piano murni melalui gestur tangan di udara.

II. METODOLOGI DAN KOMPONEN

Untuk menjalankan pemrosesan AI dengan latensi rendah, sistem ini memadukan perangkat keras berkinerja tinggi dengan pustaka perangkat lunak pengolahan citra. Berikut adalah alat dan pustaka yang digunakan:

  • Raspberry Pi 4: Berfungsi sebagai pusat komputasi tinggi yang menjalankan seluruh proses sistem; mulai dari pengambilan gambar dengan kamera, deteksi gestur tangan, hingga pengendalian buzzer melalui sinyal PWM.
Gambar 1 Raspberry Pi 4

  • Pi Cam: Berperan sebagai masukan sensor visual untuk menangkap frame video gerakan tangan pengguna dan memetakan koordinat titik penanda tangan secara presisi.
 Gambar 2. Pi Cam

  • Buzzer: Komponen piezoelektrik ini memberikan luaran audio dengan menggunakan PWM (Pulse Width Modulation) untuk menghasilkan frekuensi nada yang sesuai.
Gambar 3 Buzzer Pasif

  • OpenCV & Python: Python merupakan bahasa pemrograman utama yang mengintegrasikan seluruh komponen sistem. OpenCV digunakan untuk menangkap frame dari Pi Cam, melakukan konversi format warna, dan menggambar titik penanda pada tangan.
  • MediaPipe Hands: Pustaka kecerdasan buatan dari Google yang secara spesifik mampu mendeteksi dan mengekstraksi 21 titik landmark tangan dalam waktu nyata.
  • Flask Web Server: Berfungsi sebagai web server lokal untuk menampilkan streaming video dari kamera secara langsung ke web browser.

III. HASIL DAN PEMBAHASAN

A. Cara Kerja Sistem

Logika sistem Piano Virtual ini bekerja melalui empat tahapan utama:

  1. Tangkapan: Modul Pi Cam secara terus-menerus menangkap frame video dari gerakan tangan pengguna.
  2. Ekstraksi: Pustaka MediaPipe mendeteksi dan memetakan 21 koordinat landmark tangan dari frame yang ditangkap.
  3. Klasifikasi: Sistem memproses bentuk gestur menggunakan logika Sumbu X dan Y, didukung oleh kalibrasi jarak otomatis dan algoritma penyaringan noise.
  4. Bunyi: Berdasarkan hasil klasifikasi zona, Raspberry Pi akan mengirimkan sinyal PWM dengan frekuensi tertentu ke komponen Buzzer aktif.

B. Diagram Blok Sistem

Alur kerja perangkat keras dikelompokkan dalam blok masukan, proses, dan keluaran sebagai berikut:

Gambar 4 Blok Diagram

  • MASUKAN: Proses deteksi jari menggunakan Pi Cam.
  • PROSES: Pemrosesan data citra dan logika kendali dieksekusi oleh Raspberry Pi 4.
  • LUARAN: Menghasilkan Suara (Buzzer Piezoelektrik) dan Tampilan visual melalui Web Browser.

C. Diagram Alir

  

Gambar 5 Flowchart

D. Pemetaan Nada Musik

Sistem mendeteksi tangan di dalam ruang 3D, di mana setiap pergerakan vertikal pada sumbu Y dipetakan menjadi 8 zona nada utama. Berdasarkan konfigurasi program, pemetaan frekuensinya adalah:

  • Nada Do = 261 Hz
  • Nada Re = 294 Hz
  • Nada Mi = 330 Hz
  • Nada Fa = 349 Hz
  • Nada So / Sol = 392 Hz
  • Nada La = 440 Hz
  • Nada Si = 494 Hz
  • Nada Do' = 523 Hz

E. Kode Program Utama

Berikut adalah bagian utama dari program Python yang mengonfigurasi zona nada, proses deteksi, dan aktivasi PWM pada aktuator:

"""============================================================================
 Pemrogram      : Kelompok RE-3B /3
  1. 03-Adriel Fernanda M.      NIM:4.34.23.1.03
  2. 09-Firmansyah Trijaya      NIM:4.34.23.1.10
  3. 15-Muhammad Ridho A.       NIM:4.34.23.1.16
  4. 21-Rifqi Khoirul Lathif    NIM:4.34.23.1.22
Tgl.Praktikum  : Minggu, 14 Juni 2026
===============================================================================
Sistem Piano Virtual
  program piano virtual berbasis computer vision
-------------------------------------------------------------------------------
Materi baru:
- pengenalan computer vision (deteksi tangan dengan MediaPipe)
- luaran PWM (Pulse Width Modulation) untuk kendali frekuensi nada
- streaming video lewat web server (Flask)
-------------------------------------------------------------------------------
Komponen:
- 1x Raspberry Pi 4
- 1x Kamera (PiCamera / Pi Cam)
- 1x Buzzer pasif
=========================================================================== """

import time, threading, cv2                       # modul waktu, thread, dan olah citra
import mediapipe as mp                             # modul deteksi tangan
import RPi.GPIO as GPIO                             # modul kendali pin GPIO Raspberry Pi
from flask import Flask, Response                  # modul web server untuk streaming video
from picamera2 import Picamera2                    # modul kendali kamera Raspberry Pi

# Konfigurasi
PIN_BUZZER, LEBAR_FRAME, TINGGI_FRAME = 18, 640, 480  # pin buzzer GPIO18, ukuran frame 640x480 px
ZONA_NADA = [                                       # daftar 8 zona nada: nama, frekuensi (Hz), warna BGR
  {"nama": "Do",  "frekuensi": 261, "warna": (200,  80, 255)},  # zona 1: nada Do
  {"nama": "Re",  "frekuensi": 294, "warna": (255, 140,  50)},  # zona 2: nada Re
  {"nama": "Mi",  "frekuensi": 330, "warna": ( 50, 200,  80)},  # zona 3: nada Mi
  {"nama": "Fa",  "frekuensi": 349, "warna": ( 30, 210, 230)},  # zona 4: nada Fa
  {"nama": "Sol", "frekuensi": 392, "warna": ( 50, 100, 230)},  # zona 5: nada Sol
  {"nama": "La",  "frekuensi": 440, "warna": (200,  60, 200)},  # zona 6: nada La
  {"nama": "Si",  "frekuensi": 494, "warna": ( 80, 210, 160)},  # zona 7: nada Si
  {"nama": "Do'", "frekuensi": 523, "warna": (200,  80, 255)},  # zona 8: nada Do oktaf atas
]
LEBAR_ZONA = LEBAR_FRAME // len(ZONA_NADA)          # lebar tiap zona = 640 / 8 = 80 px

# GPIO & PWM
GPIO.setmode(GPIO.BCM)                              # gunakan penomoran pin mode BCM
GPIO.setup(PIN_BUZZER, GPIO.OUT)                    # set pin buzzer sebagai output
pwm = GPIO.PWM(PIN_BUZZER, 440)                     # buat objek PWM, frekuensi awal 440 Hz

# Status global (dipakai bersama thread kamera & buzzer)
nada_aktif = nada_sebelumnya = None                 # nada yang sedang ditunjuk jari & nada sebelumnya
buzzer_nyala = False                                # status nyala/mati buzzer saat ini
lock = threading.Lock()                             # kunci agar antar-thread tidak bentrok baca/tulis data

# Kamera & MediaPipe
kamera = Picamera2()                                # inisialisasi objek kamera
kamera.configure(kamera.create_preview_configuration(main={"size": (LEBAR_FRAME, TINGGI_FRAME)}))  # atur resolusi
kamera.start()                                      # mulai streaming dari kamera
time.sleep(1)                                       # beri waktu 1 detik agar kamera siap

mp_hands, mp_drawing = mp.solutions.hands, mp.solutions.drawing_utils  # modul deteksi & gambar landmark tangan
hands = mp_hands.Hands(max_num_hands=1, min_detection_confidence=0.7, min_tracking_confidence=0.7)  # deteksi 1 tangan


def dapatkan_zona(x):
  """Index zona dari posisi X jari."""
  return min(x // LEBAR_ZONA, len(ZONA_NADA) - 1), ZONA_NADA[min(x // LEBAR_ZONA, len(ZONA_NADA) - 1)]
  # bagi posisi X piksel dengan lebar zona untuk dapat nomor zona, lalu ambil data nadanya


def gambar_zona(frame, idx_aktif):
  for i, nada in enumerate(ZONA_NADA):              # ulangi untuk setiap zona nada
    x0, xc = i * LEBAR_ZONA, i * LEBAR_ZONA + LEBAR_ZONA // 2  # titik awal & titik tengah zona ke-i
    if i == idx_aktif:                              # jika zona ini sedang aktif/ditunjuk jari
      overlay = frame.copy()                        # salin frame untuk dibuat lapisan transparan
      cv2.rectangle(overlay, (x0, 0), (x0 + LEBAR_ZONA, TINGGI_FRAME), nada["warna"], -1)  # gambar kotak warna penuh
      cv2.addWeighted(overlay, 0.35, frame, 0.65, 0, frame)  # gabungkan jadi semi-transparan ke frame asli
      cv2.putText(frame, nada["nama"], (xc - 18, 38), cv2.FONT_HERSHEY_SIMPLEX, 0.9, nada["warna"], 3)  # label besar
    else:                                            # jika zona ini tidak aktif
      cv2.line(frame, (x0, 0), (x0, TINGGI_FRAME), (180, 180, 180), 1)  # gambar garis pembatas zona
      cv2.putText(frame, nada["nama"], (xc - 15, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (180, 180, 180), 1)  # label redup


def thread_buzzer():
  """Cek perubahan nada tiap 50ms, kendalikan PWM buzzer."""
  global buzzer_nyala, nada_sebelumnya             # pakai variabel global, bukan variabel lokal baru
  while True:                                       # ulangi terus selama program berjalan
    with lock:                                      # kunci agar aman dibaca, tidak bentrok dengan thread kamera
      nada_sekarang = nada_aktif                    # baca status nada yang sedang aktif saat ini

    if nada_sekarang is not None:                   # jika ada jari yang menunjuk sebuah zona nada
      if nada_sekarang != nada_sebelumnya:          # jika nadanya berbeda dari sebelumnya (ada perubahan)
        pwm.ChangeFrequency(nada_sekarang["frekuensi"])  # ubah frekuensi PWM sesuai nada baru
        if not buzzer_nyala:                        # jika buzzer belum menyala
          pwm.start(50)                             # nyalakan PWM dengan duty cycle 50%
          buzzer_nyala = True                       # tandai buzzer sedang menyala
        print(f"♪ {nada_sekarang['nama']} — {nada_sekarang['frekuensi']} Hz")  # cetak info nada ke terminal
        nada_sebelumnya = nada_sekarang             # simpan nada ini sebagai nada sebelumnya untuk loop berikutnya
    elif buzzer_nyala:                               # jika tidak ada nada aktif tapi buzzer masih menyala
      pwm.stop()                                    # matikan PWM
      buzzer_nyala = False                          # tandai buzzer sudah mati
      nada_sebelumnya = None                        # reset nada sebelumnya

    time.sleep(0.05)                                # tunggu 50 milidetik sebelum mengecek lagi


app = Flask(__name__)                                # buat objek aplikasi web Flask

def generate_frames():
  global nada_aktif                                 # pakai variabel global nada_aktif
  while True:                                        # ulangi terus mengambil frame video
    frame = kamera.capture_array()                  # ambil satu frame dari kamera
    frame.flags.writeable = False                   # set frame read-only agar proses MediaPipe lebih cepat
    hasil = hands.process(frame)                     # deteksi tangan pada frame
    frame.flags.writeable = True                    # kembalikan frame jadi bisa ditulis
    frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)  # ubah format warna RGB ke BGR (standar OpenCV)
    idx_aktif = -1                                   # default: tidak ada zona yang aktif

    if hasil.multi_hand_landmarks:                   # jika ada tangan yang terdeteksi
      for hand_landmarks in hasil.multi_hand_landmarks:  # ulangi untuk setiap tangan terdeteksi (maks 1)
        mp_drawing.draw_landmarks(frame, hand_landmarks, mp_hands.HAND_CONNECTIONS)  # gambar titik & garis tangan
        h, w, _ = frame.shape                       # ambil tinggi & lebar frame dalam piksel
        lm8 = hand_landmarks.landmark[8]            # ambil landmark nomor 8 (ujung jari telunjuk)
        x8, y8 = int(lm8.x * w), int(lm8.y * h)     # ubah koordinat relatif jadi koordinat piksel asli

        idx_aktif, nada_zona = dapatkan_zona(x8)    # tentukan zona & nada berdasarkan posisi X jari
        with lock:                                   # kunci agar aman ditulis, tidak bentrok dengan thread buzzer
          nada_aktif = nada_zona                    # simpan nada aktif terbaru untuk dibaca thread buzzer

        cv2.circle(frame, (x8, y8), 15, (0, 0, 255), -1)        # gambar lingkaran merah di ujung jari
        cv2.circle(frame, (x8, y8), 15, (255, 255, 255), 2)     # gambar garis tepi putih pada lingkaran
        cv2.putText(frame, f"{nada_zona['nama']}  {nada_zona['frekuensi']} Hz",
                    (10, TINGGI_FRAME - 20), cv2.FONT_HERSHEY_SIMPLEX, 1.2, nada_zona["warna"], 3)  # info nada di layar
    else:                                            # jika tidak ada tangan terdeteksi
      with lock:                                     # kunci sebelum menulis variabel global
        nada_aktif = None                           # reset nada aktif jadi tidak ada

    gambar_zona(frame, idx_aktif)                    # gambar ulang kotak 8 zona nada di atas frame
    ok, buffer = cv2.imencode('.jpg', frame)         # encode frame jadi format JPEG
    if not ok:                                       # jika proses encode gagal
      continue                                       # lewati frame ini, lanjut ke frame berikutnya
    yield b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n'  # kirim frame ke browser


@app.route('/')
def index():
  return '''
    <html>
    <head>
      <title>Piano Virtual RE3B</title>
      <style>
        body { background:#111; color:#eee; font-family:sans-serif; text-align:center; padding:20px; }
        h2   { color:#7cc; margin-bottom:10px; }
        img  { border: 2px solid #444; border-radius: 8px; }
        p    { color:#888; font-size:13px; margin-top:10px; }
      </style>
    </head>
    <body>
      <h2>Piano Virtual Berbasis Computer Vision — RE3B</h2>
      <img src="/video" width="640">
      <p>Arahkan jari telunjuk ke zona nada untuk membunyikan piano</p>
    </body>
    </html>
    '''
  # halaman utama: tampilkan judul dan elemen <img> yang menarik video dari endpoint /video

@app.route('/video')
def video():
  return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')
  # endpoint streaming: kirim frame video terus-menerus dalam format multipart MJPEG


if __name__ == '__main__':                           # jalankan blok ini hanya jika file dieksekusi langsung
  threading.Thread(target=thread_buzzer, daemon=True).start()  # jalankan thread buzzer di background
  print("Thread buzzer berjalan...")                 # info ke terminal bahwa thread buzzer sudah aktif
  print("Piano Virtual siap!")                       # info ke terminal bahwa sistem siap digunakan
  print("Buka browser: http://<IP-Raspi>:5000")      # info alamat untuk diakses lewat browser
  print("Tekan Ctrl+C untuk berhenti\n")              # info cara menghentikan program
  try:
    app.run(host='0.0.0.0', port=5000, debug=False)  # jalankan web server Flask di port 5000
  except KeyboardInterrupt:                           # jika program dihentikan manual lewat Ctrl+C
    print("\nDihentikan pengguna")                    # info bahwa program dihentikan oleh pengguna
  finally:
    pwm.stop()                                        # matikan PWM buzzer
    GPIO.cleanup()                                    # bersihkan semua konfigurasi pin GPIO
    kamera.stop()                                     # hentikan streaming kamera
    print("Semua perangkat dibersihkan. Selesai.")    # info bahwa proses cleanup sudah selesai

Komentar

Postingan populer dari blog ini

SISTEM KONVEYOR OTOMATIS DENGAN SENSOR INFRARED DAN KONTROL MANUAL

Pompa Air Otomatis Berbasis ATMega8535

SISTEM PEMANTAUAN SUHU DAN KELEMBABAN PADA SUATU RUANGAN MENGGUNAKAN SENSOR DHT22 BERBASIS MIKROKONTROLLER ARDUINO UNO ATMEGA328P