|
|
|
|
|
import gradio as gr |
|
import numpy as np |
|
import pandas as pd |
|
import joblib |
|
import matplotlib.pyplot as plt |
|
from PIL import Image |
|
import io |
|
import os |
|
|
|
|
|
image_folder = "Optical Illusion Images" |
|
|
|
|
|
enriched_folder = 'Optical Illusion Enriched Data' |
|
master_df = pd.read_csv(f'{enriched_folder}/combined_engineered_data.csv') |
|
|
|
|
|
trained_models_folder = 'Optical Illusion - Trained Models' |
|
|
|
|
|
DISPLAY_WIDTH = 1920 |
|
DISPLAY_HEIGHT = 1080 |
|
|
|
|
|
IMAGE_DESCRIPTIONS = { |
|
'duck-rabbit': 'A classic ambiguous figure that can be seen as either a duck or a rabbit', |
|
'face-vase': 'The famous Rubin\'s vase - you might see two faces in profile or a vase', |
|
'young-old': 'This image can appear as either a young woman or an old woman', |
|
'princess-oldMan': 'Can be perceived as either a princess or an old man', |
|
'lily-woman': 'This ambiguous image shows either a lily flower or a woman', |
|
'tiger-monkey': 'You might see either a tiger or a monkey in this image' |
|
} |
|
|
|
|
|
def load_all_models(): |
|
"""Load all saved models into memory""" |
|
models = {} |
|
for image_name in master_df['image_type'].unique(): |
|
try: |
|
model_path = f'{trained_models_folder}/{image_name}_models.pkl' |
|
models[image_name] = joblib.load(model_path) |
|
print(f"β Loaded model for {image_name}") |
|
except: |
|
print(f"β Could not load model for {image_name}") |
|
return models |
|
|
|
|
|
all_models = load_all_models() |
|
|
|
|
|
def load_illusion_images(image_folder): |
|
"""Load optical illusion images from a folder and resize to 1920x1080""" |
|
images = {} |
|
for image_name in all_models.keys(): |
|
image_path = f'{image_folder}/{image_name}.png' |
|
if os.path.exists(image_path): |
|
|
|
img = Image.open(image_path) |
|
img_resized = img.resize((DISPLAY_WIDTH, DISPLAY_HEIGHT), Image.Resampling.LANCZOS) |
|
images[image_name] = img_resized |
|
print(f"β Loaded and resized image for {image_name}") |
|
else: |
|
print(f"β Image not found for {image_name} at {image_path}") |
|
return images |
|
|
|
|
|
illusion_images = load_illusion_images(image_folder) |
|
|
|
|
|
def create_placeholder_image(image_name): |
|
"""Create a placeholder image with the correct dimensions""" |
|
fig, ax = plt.subplots(figsize=(19.2, 10.8), dpi=100) |
|
|
|
|
|
if image_name is None: |
|
display_text = 'πΌοΈ NO IMAGE SELECTED\n\nπ Select an image from the dropdown above' |
|
else: |
|
display_text = f'πΌοΈ {image_name.upper()}\n\nπ Click where you first look\n\nβ οΈ (Image not found)' |
|
|
|
ax.text(0.5, 0.5, display_text, |
|
transform=ax.transAxes, ha='center', va='center', |
|
fontsize=28, fontweight='bold', color='#666666') |
|
ax.set_xlim(0, DISPLAY_WIDTH) |
|
ax.set_ylim(0, DISPLAY_HEIGHT) |
|
ax.axis('off') |
|
ax.set_facecolor('#f8f9fa') |
|
|
|
buf = io.BytesIO() |
|
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight', pad_inches=0) |
|
buf.seek(0) |
|
plt.close() |
|
|
|
img = Image.open(buf) |
|
|
|
img_resized = img.resize((DISPLAY_WIDTH, DISPLAY_HEIGHT), Image.Resampling.LANCZOS) |
|
return img_resized |
|
|
|
def process_click(image_name, model_type, evt: gr.SelectData): |
|
"""Process click on image and return prediction""" |
|
|
|
if evt is None: |
|
return "β Please click on the image where you first looked!", None, None |
|
|
|
if image_name is None: |
|
return "β Please select an image first!", None, None |
|
|
|
|
|
click_x_img, click_y_img = evt.index |
|
|
|
|
|
|
|
|
|
|
|
click_x_norm = click_x_img - (DISPLAY_WIDTH / 2) |
|
click_y_norm = (DISPLAY_HEIGHT / 2) - click_y_img |
|
|
|
|
|
if image_name not in all_models: |
|
return f"β No model found for {image_name}", None, None |
|
|
|
model_data = all_models[image_name] |
|
|
|
|
|
centroid_left = np.array([model_data['centroid_left_x'], model_data['centroid_left_y']]) |
|
centroid_right = np.array([model_data['centroid_right_x'], model_data['centroid_right_y']]) |
|
fixation = np.array([click_x_norm, click_y_norm]) |
|
|
|
dist_left = np.linalg.norm(fixation - centroid_left) |
|
dist_right = np.linalg.norm(fixation - centroid_right) |
|
bias = dist_right - dist_left |
|
|
|
|
|
X = pd.DataFrame([[dist_left, dist_right, bias]], |
|
columns=['dist_to_left', 'dist_to_right', 'bias_to_left']) |
|
model = model_data[f'{model_type}_model'] |
|
prediction = model.predict(X)[0] |
|
probability = model.predict_proba(X)[0] |
|
|
|
|
|
predicted_class = model_data['label_classes'][prediction] |
|
confidence = probability[prediction] |
|
|
|
|
|
if confidence >= 0.8: |
|
confidence_level = "Very High π’" |
|
elif confidence >= 0.65: |
|
confidence_level = "High π‘" |
|
elif confidence >= 0.5: |
|
confidence_level = "Moderate π " |
|
else: |
|
confidence_level = "Low π΄" |
|
|
|
|
|
message = f""" |
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 1.5rem; border-radius: 10px; color: white; margin: 0.5rem 0;"> |
|
<h2 style="color: white; margin-top: 0;">π Prediction Results</h2> |
|
|
|
<p><strong>π Click Location:</strong> ({click_x_img}, {click_y_img}) pixels from top-left<br> |
|
<strong>π― Normalized Position:</strong> ({click_x_norm:.1f}, {click_y_norm:.1f}) from center</p> |
|
|
|
<hr style="border-color: rgba(255,255,255,0.3);"> |
|
|
|
<p><strong>π Distance to Left Region:</strong> {dist_left:.1f} pixels<br> |
|
<strong>π Distance to Right Region:</strong> {dist_right:.1f} pixels<br> |
|
<strong>βοΈ Bias Score:</strong> {bias:.1f}</p> |
|
|
|
<hr style="border-color: rgba(255,255,255,0.3);"> |
|
|
|
<h3 style="color: white;">π§ Prediction: You likely see the {predicted_class.upper()} interpretation</h3> |
|
<h3 style="color: white;">π Confidence: {confidence:.1%} ({confidence_level})</h3> |
|
""" |
|
|
|
|
|
viz = create_visualization(image_name, click_x_norm, click_y_norm, |
|
predicted_class, confidence, model_type) |
|
|
|
|
|
interpretations = { |
|
'duck-rabbit': {'left': 'Duck π¦', 'right': 'Rabbit π°'}, |
|
'face-vase': {'left': 'Two Faces π₯', 'right': 'Vase πΊ'}, |
|
'young-old': {'left': 'Young Woman π©', 'right': 'Old Woman π΅'}, |
|
'princess-oldMan': {'left': 'Princess πΈ', 'right': 'Old Man π΄'}, |
|
'lily-woman': {'left': 'Lily πΈ', 'right': 'Woman π©'}, |
|
'tiger-monkey': {'left': 'Tiger π
', 'right': 'Monkey π'} |
|
} |
|
|
|
if image_name in interpretations: |
|
specific = interpretations[image_name][predicted_class] |
|
message += f"<p><strong>π¨ What you see:</strong> {specific}</p>" |
|
|
|
message += "</div>" |
|
|
|
return message, viz, create_stats_table(image_name, model_type) |
|
|
|
def create_visualization(image_name, click_x, click_y, prediction, confidence, model_type='rf'): |
|
"""Create a visualization showing the click point, centroids, and prediction""" |
|
|
|
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6), facecolor='#f8f9fa') |
|
|
|
|
|
model_data = all_models[image_name] |
|
centroid_left = np.array([model_data['centroid_left_x'], model_data['centroid_left_y']]) |
|
centroid_right = np.array([model_data['centroid_right_x'], model_data['centroid_right_y']]) |
|
|
|
|
|
resolution = 100 |
|
x_range = np.linspace(-960, 960, resolution) |
|
y_range = np.linspace(-540, 540, resolution) |
|
xx, yy = np.meshgrid(x_range, y_range) |
|
|
|
|
|
points = np.c_[xx.ravel(), yy.ravel()] |
|
features = [] |
|
for point in points: |
|
dist_left = np.linalg.norm(point - centroid_left) |
|
dist_right = np.linalg.norm(point - centroid_right) |
|
bias = dist_right - dist_left |
|
features.append([dist_left, dist_right, bias]) |
|
|
|
X = pd.DataFrame(features, columns=['dist_to_left', 'dist_to_right', 'bias_to_left']) |
|
model = model_data[f'{model_type}_model'] |
|
Z = model.predict(X) |
|
Z = Z.reshape(xx.shape) |
|
|
|
|
|
from matplotlib.colors import ListedColormap |
|
colors = ListedColormap(['#a8d5ff', '#ffb3b3']) |
|
ax1.contourf(xx, yy, Z, alpha=0.7, cmap=colors) |
|
|
|
|
|
ax1.scatter(centroid_left[0], centroid_left[1], |
|
c='blue', marker='*', s=500, edgecolors='black', label='Left centroid') |
|
ax1.scatter(centroid_right[0], centroid_right[1], |
|
c='red', marker='*', s=500, edgecolors='black', label='Right centroid') |
|
|
|
|
|
ax1.scatter(click_x, click_y, c='green', marker='X', s=300, |
|
edgecolors='black', linewidth=2, label='Your fixation', zorder=10) |
|
|
|
|
|
ax1.plot([click_x, centroid_left[0]], [click_y, centroid_left[1]], |
|
'b--', alpha=0.5, linewidth=2) |
|
ax1.plot([click_x, centroid_right[0]], [click_y, centroid_right[1]], |
|
'r--', alpha=0.5, linewidth=2) |
|
|
|
ax1.set_xlabel('X (pixels from center)') |
|
ax1.set_ylabel('Y (pixels from center)') |
|
ax1.set_title(f'Decision Space - {model_type.upper()} Model') |
|
ax1.grid(True, alpha=0.3) |
|
ax1.legend(loc='upper right', framealpha=0.9) |
|
ax1.set_xlim(-960, 960) |
|
ax1.set_ylim(-540, 540) |
|
ax1.set_aspect('equal') |
|
ax1.set_facecolor('#f8f9fa') |
|
|
|
|
|
image_df = master_df[master_df['image_type'] == image_name] |
|
|
|
|
|
choice_counts = image_df['choice'].value_counts() |
|
bars = ax2.bar(choice_counts.index, choice_counts.values, |
|
color=['#4b86db' if x == 'left' else '#db4b4b' for x in choice_counts.index]) |
|
|
|
|
|
for bar in bars: |
|
height = bar.get_height() |
|
ax2.text(bar.get_x() + bar.get_width()/2., height + 0.5, |
|
f'{height:.0f}', |
|
ha='center', va='bottom', fontsize=10) |
|
|
|
|
|
ax2.text(0.5, 0.95, f'Your Predicted Choice: {prediction.upper()}', |
|
transform=ax2.transAxes, ha='center', va='top', |
|
fontsize=16, fontweight='bold', |
|
bbox=dict(boxstyle='round,pad=0.5', facecolor='#c2f0c2' if prediction == 'left' else '#f0c2c2', |
|
alpha=0.9, edgecolor='gray')) |
|
|
|
ax2.text(0.5, 0.85, f'Confidence: {confidence:.1%}', |
|
transform=ax2.transAxes, ha='center', va='top', fontsize=14) |
|
|
|
ax2.set_xlabel('Interpretation') |
|
ax2.set_ylabel('Number of Participants') |
|
ax2.set_title(f'Overall Distribution for {image_name}') |
|
|
|
|
|
ax2.text(0.5, 0.05, f'Model CV Accuracy: {model_data[f"cv_accuracy_{model_type}"]:.1%}', |
|
transform=ax2.transAxes, ha='center', va='bottom', fontsize=12, |
|
style='italic', alpha=0.7) |
|
|
|
ax2.set_facecolor('#f8f9fa') |
|
|
|
plt.tight_layout() |
|
|
|
|
|
buf = io.BytesIO() |
|
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight') |
|
buf.seek(0) |
|
plt.close() |
|
|
|
return Image.open(buf) |
|
|
|
def create_stats_table(image_name, model_type): |
|
"""Create a statistics table for the selected image""" |
|
model_data = all_models[image_name] |
|
image_df = master_df[master_df['image_type'] == image_name] |
|
|
|
stats = { |
|
'Metric': ['π₯ Total Participants', 'β¬
οΈ Left Choices', 'β‘οΈ Right Choices', |
|
f'π― {model_type.upper()} Accuracy', 'βοΈ Class Balance', 'π Majority Choice'], |
|
'Value': [ |
|
len(image_df), |
|
model_data['class_distribution'].get('left', 0), |
|
model_data['class_distribution'].get('right', 0), |
|
f"{model_data[f'cv_accuracy_{model_type}']:.1%}", |
|
f"{min(model_data['class_distribution'].values()) / len(image_df):.1%}", |
|
f"{image_df['choice'].mode()[0].title()} ({image_df['choice'].value_counts().max()}/{len(image_df)})" |
|
] |
|
} |
|
|
|
return pd.DataFrame(stats) |
|
|
|
|
|
css = """ |
|
.gradio-container { |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
} |
|
|
|
.main-header { |
|
text-align: center; |
|
margin-bottom: 2rem; |
|
padding: 1.5rem; |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
border-radius: 15px; |
|
color: white; |
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1); |
|
} |
|
|
|
.instruction-box { |
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); |
|
padding: 1rem; |
|
border-radius: 10px; |
|
color: white; |
|
margin: 1rem 0; |
|
} |
|
|
|
.stats-highlight { |
|
background-color: #f8f9fa; |
|
border-left: 4px solid #007bff; |
|
padding: 1rem; |
|
margin: 0.5rem 0; |
|
} |
|
""" |
|
|
|
|
|
with gr.Blocks(title="π§ Optical Illusion First Fixation Predictor", |
|
theme=gr.themes.Soft(), css=css) as demo: |
|
|
|
gr.HTML(""" |
|
<div class="main-header"> |
|
<h1>π§ Optical Illusion First Fixation Predictor</h1> |
|
<h3>Can we predict what you see based on where you look?</h3> |
|
<p>This AI-powered tool analyzes your first fixation point to predict which interpretation of an ambiguous image you'll perceive!</p> |
|
</div> |
|
""") |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=2): |
|
|
|
available_images = list(all_models.keys()) if all_models else [] |
|
default_image = available_images[0] if available_images else None |
|
|
|
image_choice = gr.Dropdown( |
|
choices=available_images, |
|
value=default_image, |
|
label="πΌοΈ Select Optical Illusion", |
|
info="Choose which ambiguous image to analyze" |
|
) |
|
|
|
|
|
image_description = gr.Markdown( |
|
value=IMAGE_DESCRIPTIONS.get(default_image, "Select an image to see its description.") if default_image else "Select an image to see its description.", |
|
label="π Image Description" |
|
) |
|
|
|
|
|
model_type = gr.Radio( |
|
choices=[("Random Forest (Recommended)", "rf"), ("Logistic Regression", "lr")], |
|
value="rf", |
|
label="π Prediction Model", |
|
info="Random Forest typically provides better accuracy for this task", |
|
container=True |
|
) |
|
|
|
|
|
image_display = gr.Image( |
|
label="π Click where your eyes first landed on the image", |
|
interactive=True, |
|
type="pil", |
|
height=540, |
|
width=960, |
|
elem_classes="main-image" |
|
) |
|
|
|
with gr.Column(scale=1): |
|
|
|
prediction_output = gr.Markdown( |
|
label="π§ Prediction Results", |
|
value="""<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 1rem; border-radius: 10px; color: white;"> |
|
<strong>π Click on the image to get your prediction!</strong><br><br> |
|
The AI will analyze where you looked first and predict what you're likely to see. |
|
</div>""", |
|
elem_classes="stats-highlight" |
|
) |
|
stats_table = gr.DataFrame(label="π Image Statistics") |
|
|
|
|
|
with gr.Row(): |
|
visualization_output = gr.Image( |
|
label="π Analysis Visualization", |
|
type="pil" |
|
) |
|
|
|
|
|
with gr.Accordion("βΉοΈ How It Works", open=False): |
|
gr.Markdown(""" |
|
### π€ The Science Behind the Prediction |
|
|
|
**π― Feature Extraction:** |
|
- We calculate the distance from your click point to the centroid of each interpretation region |
|
- A "bias score" measures which region you're closer to |
|
|
|
**π§ Machine Learning Models:** |
|
- **Random Forest:** Uses multiple decision trees for robust predictions |
|
- **Logistic Regression:** A linear approach that's fast and interpretable |
|
|
|
**π Training Process:** |
|
- Trained on eye-tracking data from multiple participants |
|
- Uses Leave-One-Participant-Out Cross-Validation for unbiased evaluation |
|
- Ensures the model generalizes to new users |
|
|
|
**π¨ Coordinate System:** |
|
- Center of image = (0, 0) |
|
- X-axis: -960 to +960 pixels (left to right) |
|
- Y-axis: -540 to +540 pixels (bottom to top) |
|
""") |
|
|
|
with gr.Accordion("π Model Performance", open=False): |
|
if all_models: |
|
summary_data = [] |
|
for img_name, model_data in all_models.items(): |
|
summary_data.append({ |
|
'Image': img_name.replace('-', ' ').title(), |
|
'RF Accuracy': f"{model_data['cv_accuracy_rf']:.1%}", |
|
'LR Accuracy': f"{model_data['cv_accuracy_lr']:.1%}", |
|
'Participants': model_data['total_samples'], |
|
'Best Model': 'RF' if model_data['cv_accuracy_rf'] > model_data['cv_accuracy_lr'] else 'LR' |
|
}) |
|
|
|
gr.DataFrame( |
|
value=pd.DataFrame(summary_data), |
|
label="Cross-Validation Performance Summary" |
|
) |
|
|
|
|
|
def update_image_and_description(image_name): |
|
|
|
if image_name is None: |
|
empty_stats = pd.DataFrame({ |
|
'Metric': ['Select an image to see statistics'], |
|
'Value': [''] |
|
}) |
|
return (create_placeholder_image(None), |
|
"Select an image to see its description.", |
|
"""<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 1rem; border-radius: 10px; color: white;"> |
|
<strong>π Please select an image first!</strong> |
|
</div>""", |
|
empty_stats) |
|
|
|
|
|
description = IMAGE_DESCRIPTIONS.get(image_name, "Description not available.") |
|
|
|
|
|
if image_name in illusion_images: |
|
|
|
model_data = all_models[image_name] |
|
image_df = master_df[master_df['image_type'] == image_name] |
|
|
|
stats = { |
|
'Metric': ['π₯ Total Participants', 'β¬
οΈ Left Choices', 'β‘οΈ Right Choices', |
|
'π― RF Accuracy', 'βοΈ Class Balance', 'π Majority Choice'], |
|
'Value': [ |
|
len(image_df), |
|
model_data['class_distribution'].get('left', 0), |
|
model_data['class_distribution'].get('right', 0), |
|
f"{model_data['cv_accuracy_rf']:.1%}", |
|
f"{min(model_data['class_distribution'].values()) / len(image_df):.1%}", |
|
f"{image_df['choice'].mode()[0].title()} ({image_df['choice'].value_counts().max()}/{len(image_df)})" |
|
] |
|
} |
|
return (illusion_images[image_name], |
|
f"**{description}**", |
|
"""<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 1rem; border-radius: 10px; color: white;"> |
|
<strong>π Click on the image to get your prediction!</strong><br><br> |
|
The AI will analyze where you looked first and predict what you're likely to see. |
|
</div>""", |
|
pd.DataFrame(stats)) |
|
else: |
|
empty_stats = pd.DataFrame({ |
|
'Metric': ['Image not found'], |
|
'Value': [''] |
|
}) |
|
return (create_placeholder_image(image_name), |
|
f"**{description}**", |
|
"""<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 1rem; border-radius: 10px; color: white;"> |
|
<strong>β οΈ Image file not found!</strong> |
|
</div>""", |
|
empty_stats) |
|
|
|
|
|
image_choice.change( |
|
fn=update_image_and_description, |
|
inputs=[image_choice], |
|
outputs=[image_display, image_description, prediction_output, stats_table] |
|
) |
|
|
|
|
|
image_display.select( |
|
fn=process_click, |
|
inputs=[image_choice, model_type], |
|
outputs=[prediction_output, visualization_output, stats_table] |
|
) |
|
|
|
|
|
demo.load( |
|
fn=update_image_and_description, |
|
inputs=[image_choice], |
|
outputs=[image_display, image_description, prediction_output, stats_table] |
|
) |
|
|
|
|
|
if available_images: |
|
gr.Markdown("## π Quick Examples") |
|
with gr.Row(): |
|
example_list = [] |
|
for img in ["duck-rabbit", "face-vase", "young-old", "tiger-monkey"]: |
|
if img in available_images: |
|
example_list.append([img, "rf"]) |
|
|
|
if example_list: |
|
gr.Examples( |
|
examples=example_list, |
|
inputs=[image_choice, model_type], |
|
label="Try these popular illusions" |
|
) |
|
|
|
|
|
gr.HTML(""" |
|
<div style="text-align: center; margin-top: 2rem; padding: 1.5rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; color: white;"> |
|
<h4>π¬ WID2003 Cognitive Science Group Assignment - OCC 2 Group 2</h4> |
|
<p><strong>Universiti Malaya</strong> | 2025</p> |
|
<p style="font-size: 0.9em; opacity: 0.8;">Vote for Us!</p> |
|
</div> |
|
""") |
|
|
|
|
|
print(f"\nImage folder: {image_folder}") |
|
print(f"Images loaded: {list(illusion_images.keys())}") |
|
print(f"Models loaded: {list(all_models.keys())}") |
|
print(f"Image dimensions: {DISPLAY_WIDTH}x{DISPLAY_HEIGHT}") |
|
|
|
|
|
if __name__ == "__main__": |
|
demo.launch( |
|
|
|
|
|
) |