| |
|
|
| 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( |
| |
| |
| ) |