340 lines
14 KiB
Python
340 lines
14 KiB
Python
#3_hasil.py
|
|
import streamlit as st
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
from collections import Counter
|
|
|
|
CATEGORIES = ['Angry', 'Sad', 'Happy', 'Fearful', 'Disgust', 'Neutral', 'Surprised']
|
|
|
|
st.set_page_config(page_title="Analisis Multimodal", layout="wide")
|
|
|
|
st.markdown("""
|
|
<style>
|
|
.main-header {
|
|
color: white;
|
|
background-color: #1E1E5A;
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
border-radius: 10px 10px 0 0;
|
|
margin-bottom: 2rem;
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
}
|
|
.section-header {
|
|
color: #FFFFFF;
|
|
font-weight: bold;
|
|
margin-bottom: 20px;
|
|
border-bottom: 2px solid #3498db;
|
|
padding-bottom: 10px;
|
|
}
|
|
.positive-score {
|
|
color: #2ecc71;
|
|
font-weight: bold;
|
|
}
|
|
.negative-score {
|
|
color: #e74c3c;
|
|
font-weight: bold;
|
|
}
|
|
.stButton > button {
|
|
background-color: #007bff;
|
|
color: white;
|
|
padding: 10px 24px;
|
|
border-radius: 8px;
|
|
font-size: 1rem;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: 0.3s;
|
|
display: block;
|
|
margin: 0 auto;
|
|
width: 100%;
|
|
}
|
|
.stButton > button:hover {
|
|
background-color: #0056b3;
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
}
|
|
.custom-button-container {
|
|
margin-top: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.divider {
|
|
margin-top: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
border-top: 1px solid #eee;
|
|
}
|
|
</style>
|
|
""", unsafe_allow_html=True)
|
|
|
|
st.markdown('<div class="main-header">📊 Laporan Analisis Multimodal</div>', unsafe_allow_html=True)
|
|
|
|
emotion_mapping = {
|
|
'Happy': 'bahagia',
|
|
'Surprised': 'terkejut',
|
|
'Neutral': 'netral',
|
|
'Angry': 'marah',
|
|
'Sad': 'sedih',
|
|
'Fearful': 'takut',
|
|
'Disgust': 'jijik',
|
|
'bahagia': 'bahagia',
|
|
'terkejut': 'terkejut',
|
|
'netral': 'netral',
|
|
'marah': 'marah',
|
|
'sedih': 'sedih',
|
|
'takut': 'takut',
|
|
'jijik': 'jijik'
|
|
}
|
|
|
|
positive_emotions = ['bahagia', 'terkejut', 'netral']
|
|
negative_emotions = ['marah', 'sedih', 'takut', 'jijik']
|
|
|
|
if 'data_manager' not in st.session_state or st.session_state.data_manager is None:
|
|
st.error("Data emosi wajah tidak tersedia. Silakan lakukan analisis wajah terlebih dahulu.")
|
|
st.markdown('<div class="custom-button-container">', unsafe_allow_html=True)
|
|
if st.button("Lakukan Analisis Wajah"):
|
|
try:
|
|
st.switch_page("pages/1_analisis_wajah.py")
|
|
except FileNotFoundError:
|
|
st.error("Halaman analisis wajah tidak ditemukan.")
|
|
st.markdown('</div>', unsafe_allow_html=True)
|
|
st.stop()
|
|
|
|
if 'text_analysis_result' not in st.session_state or not st.session_state.text_analysis_result:
|
|
st.error("Data analisis teks tidak tersedia. Silakan lakukan analisis teks terlebih dahulu.")
|
|
st.markdown('<div class="custom-button-container">', unsafe_allow_html=True)
|
|
if st.button("Lakukan Analisis Teks"):
|
|
try:
|
|
st.switch_page("pages/2_analisis_teks.py")
|
|
except FileNotFoundError:
|
|
st.error("Halaman analisis teks tidak ditemukan.")
|
|
st.markdown('</div>', unsafe_allow_html=True)
|
|
st.stop()
|
|
|
|
data_manager = st.session_state.data_manager
|
|
if not data_manager.predictions or not data_manager.timestamps:
|
|
st.error("Tidak ada data emosi wajah yang valid untuk analisis.")
|
|
st.stop()
|
|
|
|
face_emotion_data = [(timestamp, CATEGORIES[pred_idx]) for pred_idx, timestamp in zip(data_manager.predictions, data_manager.timestamps)]
|
|
for timestamp, emotion in face_emotion_data:
|
|
if not isinstance(timestamp, (int, float)) or not isinstance(emotion, str):
|
|
st.error("Format data emosi wajah tidak valid.")
|
|
st.stop()
|
|
if emotion not in emotion_mapping:
|
|
st.warning(f"Emosi tidak dikenal: {emotion}. Melewati emosi ini.")
|
|
face_emotion_data = [(t, e) for t, e in face_emotion_data if e in emotion_mapping]
|
|
if not face_emotion_data:
|
|
st.error("Tidak ada data emosi wajah yang valid.")
|
|
st.stop()
|
|
|
|
text_result = st.session_state.text_analysis_result
|
|
if not isinstance(text_result, dict) or 'text' not in text_result:
|
|
st.error("Format data analisis teks tidak valid.")
|
|
st.stop()
|
|
|
|
# Process face emotions
|
|
dominant_face_emotions = {}
|
|
for _, emotion in face_emotion_data:
|
|
standardized_emotion = emotion_mapping.get(emotion)
|
|
if standardized_emotion:
|
|
dominant_face_emotions[standardized_emotion] = dominant_face_emotions.get(standardized_emotion, 0) + 1
|
|
|
|
total_face_frames = len(face_emotion_data)
|
|
if total_face_frames == 0:
|
|
st.error("Tidak ada data emosi wajah yang valid untuk analisis.")
|
|
st.stop()
|
|
|
|
face_emotion_percentages = {emotion: (count / total_face_frames) * 100
|
|
for emotion, count in dominant_face_emotions.items()}
|
|
top_face_emotions = sorted(face_emotion_percentages.items(), key=lambda x: x[1], reverse=True)[:3]
|
|
|
|
text_emotions = {}
|
|
if "emotions" in text_result:
|
|
text_emotions = {emotion_mapping.get(emotion, emotion): score * 100
|
|
for emotion, score in text_result["emotions"].items()
|
|
if emotion_mapping.get(emotion, emotion) in emotion_mapping.values()}
|
|
elif "top_emotions" in text_result:
|
|
text_emotions = {emotion_mapping.get(emotion, emotion): score * 100
|
|
for emotion, score in text_result["top_emotions"]
|
|
if emotion_mapping.get(emotion, emotion) in emotion_mapping.values()}
|
|
|
|
top_text_emotions = sorted(text_emotions.items(), key=lambda x: x[1], reverse=True)[:3]
|
|
|
|
combined_emotions = {}
|
|
for emotion, percentage in face_emotion_percentages.items():
|
|
combined_emotions[emotion] = combined_emotions.get(emotion, 0) + percentage
|
|
|
|
standardized_text_emotions = {emotion_mapping.get(e, e): score for e, score in text_emotions.items()}
|
|
for emotion, percentage in standardized_text_emotions.items():
|
|
if emotion in emotion_mapping.values():
|
|
combined_emotions[emotion] = combined_emotions.get(emotion, 0) + percentage
|
|
|
|
for emotion in combined_emotions:
|
|
appeared_in_face = emotion in face_emotion_percentages
|
|
appeared_in_text = emotion in standardized_text_emotions
|
|
divisor = 1 + (1 if appeared_in_face and appeared_in_text else 0)
|
|
combined_emotions[emotion] /= divisor
|
|
|
|
total_combined = sum(combined_emotions.values())
|
|
if total_combined > 0:
|
|
combined_emotions = {emotion: (score / total_combined) * 100
|
|
for emotion, score in combined_emotions.items()}
|
|
top_combined_emotions = sorted(combined_emotions.items(), key=lambda x: x[1], reverse=True)[:3]
|
|
|
|
face_positive_score = sum(face_emotion_percentages.get(emotion, 0) for emotion in positive_emotions)
|
|
face_negative_score = sum(face_emotion_percentages.get(emotion, 0) for emotion in negative_emotions)
|
|
total_face_score = face_positive_score + face_negative_score
|
|
if total_face_score > 0:
|
|
face_positive_score = (face_positive_score / total_face_score) * 100
|
|
face_negative_score = (face_negative_score / total_face_score) * 100
|
|
else:
|
|
face_positive_score = 0
|
|
face_negative_score = 0
|
|
st.warning("Tidak ada data emosi wajah yang valid untuk menghitung skor positif/negatif.")
|
|
|
|
if "positive_score" in text_result and "negative_score" in text_result:
|
|
text_positive_score = text_result['positive_score'] * 100
|
|
text_negative_score = text_result['negative_score'] * 100
|
|
else:
|
|
text_positive_score = sum(text_emotions.get(emotion, 0) for emotion in positive_emotions)
|
|
text_negative_score = sum(text_emotions.get(emotion, 0) for emotion in negative_emotions)
|
|
total_text_score = text_positive_score + text_negative_score
|
|
if total_text_score > 0:
|
|
text_positive_score = (text_positive_score / total_text_score) * 100
|
|
text_negative_score = (text_negative_score / total_text_score) * 100
|
|
else:
|
|
text_positive_score = 0
|
|
text_negative_score = 0
|
|
st.warning("Tidak ada data emosi teks yang valid untuk menghitung skor positif/negatif.")
|
|
|
|
avg_positive_score = (face_positive_score + text_positive_score) / 2
|
|
avg_negative_score = (face_negative_score + text_negative_score) / 2
|
|
|
|
emotion_changes = {}
|
|
for i in range(1, len(face_emotion_data)):
|
|
if face_emotion_data[i][1] != face_emotion_data[i-1][1]:
|
|
key = f"{face_emotion_data[i-1][1]} → {face_emotion_data[i][1]}"
|
|
time_diff = face_emotion_data[i][0] - face_emotion_data[i-1][0]
|
|
emotion_changes[key] = time_diff
|
|
|
|
st.markdown('<h2 class="section-header">📝 Ringkasan Umum</h2>', unsafe_allow_html=True)
|
|
st.write(f"**Durasi Analisis:** {face_emotion_data[-1][0]:.2f} detik")
|
|
st.write(f"**Jumlah Perubahan Emosi:** {len(emotion_changes)}")
|
|
st.markdown("### Skor Rata-rata Emosi (Wajah & Teks)")
|
|
st.write(f"🟢 Rata-rata Emosi Positif: <span class='positive-score'>{avg_positive_score:.1f}%</span>", unsafe_allow_html=True)
|
|
st.write(f"🔴 Rata-rata Emosi Negatif: <span class='negative-score'>{avg_negative_score:.1f}%</span>", unsafe_allow_html=True)
|
|
|
|
if avg_positive_score > 0 or avg_negative_score > 0:
|
|
fig, ax = plt.subplots(figsize=(10, 3))
|
|
labels = ['Positif', 'Negatif']
|
|
values = [avg_positive_score, avg_negative_score]
|
|
colors = ['#2ecc71', '#e74c3c']
|
|
|
|
bars = ax.barh(labels, values, color=colors, height=0.5)
|
|
|
|
for bar in bars:
|
|
width = bar.get_width()
|
|
ax.text(width + 1, bar.get_y() + bar.get_height()/2, f'{width:.1f}%',
|
|
va='center', fontweight='bold')
|
|
|
|
ax.set_xlim(0, 100)
|
|
ax.set_xlabel('Persentase (%)')
|
|
ax.spines['top'].set_visible(False)
|
|
ax.spines['right'].set_visible(False)
|
|
ax.spines['bottom'].set_color('#DDDDDD')
|
|
ax.spines['left'].set_color('#DDDDDD')
|
|
ax.tick_params(bottom=False, left=False)
|
|
ax.set_axisbelow(True)
|
|
ax.grid(axis='x', linestyle='-', alpha=0.2)
|
|
|
|
st.pyplot(fig)
|
|
else:
|
|
st.warning("Tidak cukup data untuk menampilkan grafik skor positif/negatif.")
|
|
|
|
st.write("### Top 3 Emosi Gabungan (Wajah & Teks)")
|
|
for emotion, score in top_combined_emotions:
|
|
st.write(f"- {emotion.capitalize()}: {score:.1f}%")
|
|
|
|
st.markdown('</div>', unsafe_allow_html=True)
|
|
st.markdown('<h2 class="section-header">😀 Analisis Ekspresi Wajah</h2>', unsafe_allow_html=True)
|
|
st.write("### Top 3 Emosi Wajah")
|
|
for emotion, percentage in top_face_emotions:
|
|
count = dominant_face_emotions[emotion]
|
|
st.write(f"- {emotion.capitalize()}: {percentage:.1f}% ({count} kali)")
|
|
|
|
st.write(f"### Skor Emosi Wajah")
|
|
st.write(f"🟢 Skor Positif: <span class='positive-score'>{face_positive_score:.1f}%</span>", unsafe_allow_html=True)
|
|
st.write(f"🔴 Skor Negatif: <span class='negative-score'>{face_negative_score:.1f}%</span>", unsafe_allow_html=True)
|
|
|
|
if len(face_emotion_data) > 1:
|
|
fig, ax = plt.subplots(figsize=(12, 6))
|
|
fig.patch.set_facecolor('#f4f4f7')
|
|
ax.set_facecolor('#f4f4f7')
|
|
|
|
timestamps = [data[0] for data in face_emotion_data]
|
|
emotions = [data[1] for data in face_emotion_data]
|
|
|
|
ax.scatter(timestamps, emotions, c='#3a7aff', alpha=0.7, s=30)
|
|
ax.plot(timestamps, emotions, color='#3a7aff', alpha=0.5, linewidth=1)
|
|
|
|
ax.set_xlabel("Waktu (detik)", color='#111c4e', fontsize=12)
|
|
ax.set_ylabel("Ekspresi", color='#111c4e', fontsize=12)
|
|
ax.set_title("Timeline Perubahan Ekspresi", color='#111c4e', fontsize=14, fontweight='bold')
|
|
|
|
ax.tick_params(axis='both', colors='#111c4e', labelsize=10)
|
|
for spine in ax.spines.values():
|
|
spine.set_color('#111c4e')
|
|
|
|
ax.grid(True, alpha=0.3)
|
|
|
|
plt.tight_layout()
|
|
|
|
st.pyplot(fig)
|
|
plt.close(fig)
|
|
|
|
if emotion_changes:
|
|
st.write("### Perubahan Emosi Tercepat")
|
|
for change, time in sorted(emotion_changes.items(), key=lambda x: x[1])[:3]:
|
|
st.write(f"- {change} dalam {time:.2f} detik")
|
|
|
|
st.markdown('</div>', unsafe_allow_html=True)
|
|
st.markdown('<h2 class="section-header">📝 Analisis Teks Jurnaling</h2>', unsafe_allow_html=True)
|
|
st.write("### Top 3 Emosi Terdeteksi")
|
|
for emotion, percentage in top_text_emotions:
|
|
st.write(f"- {emotion.capitalize()}: {percentage:.1f}%")
|
|
|
|
st.write(f"### Skor Emosi")
|
|
st.write(f"🟢 Skor Positif: <span class='positive-score'>{text_positive_score:.1f}%</span>", unsafe_allow_html=True)
|
|
st.write(f"🔴 Skor Negatif: <span class='negative-score'>{text_negative_score:.1f}%</span>", unsafe_allow_html=True)
|
|
st.write("### Teks Jurnal")
|
|
st.write(text_result['text'])
|
|
|
|
st.markdown('</div>', unsafe_allow_html=True)
|
|
st.markdown('<h2 class="section-header">🔍 Kesimpulan Akhir</h2>', unsafe_allow_html=True)
|
|
st.markdown("### Fusion dengan Majority Voting")
|
|
st.write("Metode ini menggabungkan keputusan dari setiap modalitas dengan menerapkan prinsip voting mayoritas.")
|
|
|
|
face_classification = 1 if face_negative_score >= face_positive_score else 0
|
|
text_classification = 1 if text_negative_score >= text_positive_score else 0
|
|
fused_score = np.mean([face_classification, text_classification])
|
|
|
|
if avg_negative_score >= 50:
|
|
st.error("⚠️ Analisis menunjukkan potensi indikasi depresi.")
|
|
st.write("Disarankan untuk berbicara dengan konselor atau psikolog.")
|
|
elif avg_negative_score < 49:
|
|
st.success("✅ Analisis menunjukkan kondisi emosi stabil.")
|
|
st.write("Tetap jaga kesehatan mental dan lanjutkan kegiatan positif.")
|
|
else:
|
|
st.warning("⚖️ Analisis menunjukkan kondisi emosi netral (skor negatif 49%).")
|
|
st.write("Pertimbangkan konsultasi jika merasa perlu.")
|
|
|
|
st.write(f"- Klasifikasi dari analisis wajah: {'Indikasi Negatif' if face_classification == 1 else 'Indikasi Positif'}")
|
|
st.write(f"- Klasifikasi dari analisis teks: {'Indikasi Negatif' if text_classification == 1 else 'Indikasi Positif'}")
|
|
|
|
st.markdown('<div class="custom-button-container">', unsafe_allow_html=True)
|
|
if st.button("Ulangi Analisis"):
|
|
st.session_state.pop('data_manager', None)
|
|
st.session_state.pop('text_analysis_result', None)
|
|
try:
|
|
st.switch_page("pages/1_analisis_wajah.py")
|
|
except FileNotFoundError:
|
|
st.error("Halaman analisis wajah tidak ditemukan.")
|
|
st.markdown('</div>', unsafe_allow_html=True) |