Spaces:
Runtime error
Runtime error
File size: 14,159 Bytes
3abe264 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | import os
import cv2
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from sklearn.cluster import KMeans
from PIL import Image
import gradio as gr
def rgb_to_hex(r, g, b):
"""Converts RGB integers to a hex string."""
return f"#{int(r):02x}{int(g):02x}{int(b):02x}"
def analyze_palette(img_numpy, num_colors=5):
"""Clusters pixels using K-Means to extract dominant color palette."""
try:
# Resize image to speed up K-Means significantly
img_small = cv2.resize(img_numpy, (150, 150), interpolation=cv2.INTER_AREA)
pixels = img_small.reshape(-1, 3)
# Run K-Means
kmeans = KMeans(n_clusters=num_colors, random_state=42, n_init=10)
kmeans.fit(pixels)
colors = kmeans.cluster_centers_
labels = kmeans.labels_
# Calculate percentages
counts = np.bincount(labels)
total = len(labels)
percentages = counts / total
# Sort colors by dominance (percentage)
sorted_indices = np.argsort(percentages)[::-1]
colors = colors[sorted_indices]
percentages = percentages[sorted_indices]
# Compile color data
palette_data = []
warm_percentage = 0.0
cool_percentage = 0.0
for i in range(num_colors):
r, g, b = colors[i]
hex_val = rgb_to_hex(r, g, b)
pct = percentages[i]
# Simple heuristic for color temperature
# Warm: R > B. Cool: B >= R
is_warm = r > b
if is_warm:
warm_percentage += pct
else:
cool_percentage += pct
# Basic visual psychology association maps
vibes = "Neutral/Balanced"
if r > 150 and g < 100 and b < 100:
vibes = "Urgency, Excitement, Passion, or Danger"
elif b > 150 and r < 100 and g < 150:
vibes = "Trust, Stability, Calm, or Professionalism"
elif g > 150 and r < 120 and b < 120:
vibes = "Nature, Growth, Health, or Balance"
elif r > 180 and g > 150 and b < 100:
vibes = "Energy, Optimism, Warmth, or Caution"
elif r > 120 and g < 80 and b > 120:
vibes = "Luxury, Creative, Mystery, or Dignity"
elif r > 200 and g > 200 and b > 200:
vibes = "Clarity, Minimalism, Openness, or Light"
elif r < 60 and g < 60 and b < 60:
vibes = "Authority, Sophistication, Drama, or Mystery"
palette_data.append({
"Hex": hex_val,
"RGB": f"({int(r)}, {int(g)}, {int(b)})",
"Dominance": pct,
"Vibes & Framing Role": vibes
})
# Draw Plotly stacked bar chart
fig = go.Figure()
for item in palette_data:
fig.add_trace(go.Bar(
name=item["Hex"],
y=["Palette"],
x=[item["Dominance"]],
orientation='h',
marker=dict(color=item["Hex"]),
hovertemplate=f"Color: {item['Hex']}<br>Dominance: {item['Dominance']:.1%}<br>{item['Vibes & Framing Role']}<extra></extra>"
))
fig.update_layout(
barmode='stack',
showlegend=False,
height=120,
template="plotly_dark",
plot_bgcolor="#111827",
paper_bgcolor="#0d0f12",
margin=dict(l=10, r=10, t=10, b=10),
xaxis=dict(showticklabels=False, showgrid=False, zeroline=False),
yaxis=dict(showticklabels=False, showgrid=False, zeroline=False)
)
# Temp analysis text
temp_status = f"Visual Temperature: **{'Warm' if warm_percentage > cool_percentage else 'Cool'}** " \
f"({warm_percentage:.1%} Warm vs. {cool_percentage:.1%} Cool tones dominant)."
df_palette = pd.DataFrame(palette_data)
return fig, df_palette, temp_status
except Exception as e:
print(f"Palette analysis error: {e}")
return go.Figure(), pd.DataFrame(), f"Error running palette clustering: {e}"
def analyze_composition(img_numpy):
"""Draws Rule-of-Thirds grid lines and measures edge texture centers of gravity."""
try:
h, w, _ = img_numpy.shape
img_grid = img_numpy.copy()
# Draw Rule-of-Thirds grid lines (high-contrast cyan)
grid_color = (0, 255, 255) # Cyan
line_w = max(2, int(w * 0.003))
# Horizontal lines
h1, h2 = int(h / 3), int(2 * h / 3)
cv2.line(img_grid, (0, h1), (w, h1), grid_color, line_w)
cv2.line(img_grid, (0, h2), (w, h2), grid_color, line_w)
# Vertical lines
w1, w2 = int(w / 3), int(2 * w / 3)
cv2.line(img_grid, (w1, 0), (w1, h), grid_color, line_w)
cv2.line(img_grid, (w2, 0), (w2, h), grid_color, line_w)
# Draw intersection circles
intersections = [(w1, h1), (w2, h1), (w1, h2), (w2, h2)]
circle_r = max(5, int(w * 0.01))
for (ix, iy) in intersections:
cv2.circle(img_grid, (ix, iy), circle_r, (255, 112, 67), -1) # Coral dots
cv2.circle(img_grid, (ix, iy), circle_r + 2, (255, 255, 255), max(1, int(w * 0.001)))
# Calculate visual texture density center (Canny edges)
gray = cv2.cvtColor(img_numpy, cv2.COLOR_RGB2GRAY)
edges = cv2.Canny(gray, 50, 150)
# Get coordinates of all edge pixels
edge_coords = np.argwhere(edges > 0)
if len(edge_coords) > 0:
# edge_coords holds (y, x)
avg_y, avg_x = np.mean(edge_coords, axis=0)
# Determine proximity to nearest intersection
distances = [np.hypot(avg_x - ix, avg_y - iy) for (ix, iy) in intersections]
min_dist = min(distances)
max_possible_dist = np.hypot(w, h)
proximity = 1 - (min_dist / (max_possible_dist * 0.25))
proximity = max(0.0, min(1.0, proximity))
# Label alignment
if proximity > 0.70:
align_text = f"Rule-of-Thirds Alignment: **Strong Alignment** ({proximity:.1%} visual intersection score). " \
"The main subject focus resides directly on one of the four power intersections, pulling viewer attention immediately."
else:
align_text = f"Rule-of-Thirds Alignment: **Centered/Diffuse Composition** ({proximity:.1%} visual intersection score). " \
"Visual weight is either balanced in the center or scattered across the frame, standard for documentarians or landscapes."
else:
align_text = "No strong visual edges found. Flat or uniform composition."
return img_grid, align_text
except Exception as e:
print(f"Composition error: {e}")
return img_numpy, f"Error processing geometry: {e}"
def analyze_lighting(img_numpy):
"""Computes a Plotly brightness distribution histogram and classifies visual lighting key."""
try:
gray = cv2.cvtColor(img_numpy, cv2.COLOR_RGB2GRAY)
h, w = gray.shape
total_pixels = h * w
# Calculate luminance histogram
hist = cv2.calcHist([gray], [0], None, [256], [0, 256]).flatten()
# Metrics
mean_brightness = np.mean(gray)
std_brightness = np.std(gray)
# Classify lighting key
# High Key: Bright backgrounds, high mean, lower variance
# Low Key: Shadow dominated, low mean, high variance (chiascuro)
if mean_brightness >= 165:
key_style = "High-Key Lighting (Bright & Open)"
summary_desc = "Features bright, fully lit environments with soft shadows. Commonly utilized in consumer commercials, corporate flyers, and optimistic political campaign media to convey transparency and positive energy."
elif mean_brightness <= 85:
key_style = "Low-Key Lighting (Dramatic & Shadow-Heavy)"
summary_desc = "Dominated by deep shadows, dark backgrounds, and stark contrast. Popular in film noir, investigative photojournalism, or negative attack advertisements to invoke mystery, tension, or critical framing."
else:
key_style = "Mid-Key / Standard Studio Lighting"
summary_desc = "Features a realistic, balanced, or moderate lighting key. Popular in objective documentary filmmaking, standard portraiture, and everyday press releases to represent authenticity and balanced focus."
# Draw Plotly Line Histogram
df_hist = pd.DataFrame({
"Luminance (0-255)": np.arange(256),
"Pixel Count": hist
})
fig = px.line(
df_hist, x="Luminance (0-255)", y="Pixel Count",
title="Luminance Distribution Histogram (0 = Pure Black, 255 = Pure White)",
template="plotly_dark"
)
fig.update_traces(line=dict(color="#f59e0b", width=3)) # Amber curve
fig.update_layout(
plot_bgcolor="#111827",
paper_bgcolor="#0d0f12",
margin=dict(l=20, r=20, t=50, b=20),
xaxis=dict(showgrid=False),
yaxis=dict(showgrid=False)
)
summary_text = f"Visual Key: **{key_style}**\n\n* **Average Luminance**: {mean_brightness:.1f} / 255\n* **Contrast Spread (Std Dev)**: {std_brightness:.1f}\n\n*Description*: {summary_desc}"
return fig, summary_text
except Exception as e:
print(f"Lighting error: {e}")
return go.Figure(), f"Error analyzing luminance: {e}"
def full_analyzer_pipeline(img):
"""Triggers the full deconstruction analysis when an image is uploaded."""
if img is None:
return go.Figure(), pd.DataFrame(), "No image uploaded.", None, "No image uploaded.", go.Figure(), "No image uploaded."
# 1. Palette
fig_pal, df_pal, pal_status = analyze_palette(img)
# 2. Composition
img_grid, comp_status = analyze_composition(img)
# 3. Lighting
fig_light, light_status = analyze_lighting(img)
return fig_pal, df_pal, pal_status, img_grid, comp_status, fig_light, light_status
# Custom premium gradient CSS (Red/Yellow vibes)
custom_css = """
body { background-color: #0d0f12; color: #e3e6eb; font-family: 'Inter', sans-serif; }
.gradio-container { max-width: 1200px !important; margin: 0 auto !important; }
h1, h2, h3 { color: #ffffff !important; font-weight: 700 !important; }
.btn-primary { background: linear-gradient(135deg, #ef4444 0%, #f59e0b 100%) !important; border: none !important; color: white !important; font-weight: 600 !important; }
.btn-primary:hover { filter: brightness(1.1); }
.dataframe-container { background: #111827 !important; border: 1px solid #1f2937 !important; border-radius: 8px; }
"""
with gr.Blocks(theme=gr.themes.Monochrome(), css=custom_css) as demo:
gr.Markdown(
"""
# π¨ Visual Rhetoric & Composition Analyzer
### Deconstruct media framing, analyze pixel color palettes (K-means), test Rule-of-Thirds alignment, and map lighting profiles to audit visual bias.
"""
)
with gr.Row():
with gr.Column(scale=4):
with gr.Card():
gr.Markdown("### 1. Upload Visual Artifact")
image_input = gr.Image(label="Upload Image (Campaign Flyer, News Photo, Advertisement)", type="numpy")
analyze_btn = gr.Button("π¨ Run Rhetorical Deconstruction", variant="primary", elem_classes="btn-primary")
with gr.Card():
gr.Markdown("### π Rule-of-Thirds Grid Overlay")
image_grid_output = gr.Image(label="Compositional Lines & Power Intersections", type="numpy", interactive=False)
with gr.Column(scale=6):
with gr.Tabs():
with gr.TabItem("π¨ Color & Palette Psychology"):
palette_plot = gr.Plot(label="Dominant Pixel Palettes (K-Means)")
palette_status = gr.Markdown("Please upload an image to run analysis.")
palette_table = gr.Dataframe(
headers=["Hex", "RGB", "Dominance", "Vibes & Framing Role"],
datatype=["str", "str", "number", "str"],
label="Dominant Colors Quantitative Distribution",
interactive=False,
elem_classes="dataframe-container"
)
with gr.TabItem("π Composition & Geometry"):
comp_status = gr.Markdown("Please upload an image to run analysis.")
gr.Markdown(
"""
**Methodology**:
- **Cyan lines** demarcate the vertical and horizontal 1/3 grid markers.
- **Coral dots** highlight the 4 focal intersections where visual elements naturally pull the highest attention.
- **Texture Center**: The app runs localized Sobel edge detection to find the image's texture 'center of gravity' and evaluates how close it is to these intersections.
"""
)
with gr.TabItem("π Lighting, Contrast & Mood"):
lighting_plot = gr.Plot(label="Luminance Profile (LCC)")
lighting_status = gr.Markdown("Please upload an image to run analysis.")
# Core callback
analyze_btn.click(
fn=full_analyzer_pipeline,
inputs=[image_input],
outputs=[palette_plot, palette_table, palette_status, image_grid_output, comp_status, lighting_plot, lighting_status]
)
if __name__ == "__main__":
demo.launch()
|