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
4rfqkhoirul@gmail.com
INTISARI - Sistem Piano Virtual ini dikembangkan dengan memanfaatkan teknologi computer vision untuk menciptakan alternatif alat musik yang interaktif dan higienis
Kata Kunci: Sistem 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.
- Pi Cam: Berperan sebagai masukan sensor visual untuk menangkap frame video gerakan tangan pengguna dan memetakan koordinat titik penanda tangan secara presisi.
- Buzzer: Komponen piezoelektrik ini memberikan luaran audio dengan menggunakan PWM (Pulse Width Modulation) untuk menghasilkan frekuensi nada yang sesuai.
- 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:
- Tangkapan: Modul Pi Cam secara terus-menerus menangkap frame video dari gerakan tangan pengguna.
- Ekstraksi: Pustaka MediaPipe mendeteksi dan memetakan 21 koordinat landmark tangan dari frame yang ditangkap.
- Klasifikasi: Sistem memproses bentuk gestur menggunakan logika Sumbu X dan Y, didukung oleh kalibrasi jarak otomatis dan algoritma penyaringan noise.
- 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:
- 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
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
Posting Komentar