import streamlit as st
import cv2
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
import pandas as pd
import io
import base64
from PIL import Image
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.express as px
# --- Session state initialization ---
def initialize_session_state():
"""Initialize all session state variables"""
if 'uploaded_image' not in st.session_state:
st.session_state['uploaded_image'] = None
if 'analysis_complete' not in st.session_state:
st.session_state['analysis_complete'] = False
if 'analysis_results' not in st.session_state:
st.session_state['analysis_results'] = {}
if 'processing' not in st.session_state:
st.session_state['processing'] = False
# Fix for Hugging Face Spaces permissions
import os
import tempfile
os.environ['STREAMLIT_BROWSER_GATHER_USAGE_STATS'] = 'false'
os.environ['MPLCONFIGDIR'] = tempfile.gettempdir()
# Initialize session state
initialize_session_state()
# Page configuration
st.set_page_config(
page_title="Kolam Design Analyzer",
page_icon="🎨",
layout="wide",
initial_sidebar_state="expanded"
)
# Custom CSS for professional styling
st.markdown("""
""", unsafe_allow_html=True)
# Title and header
st.markdown("""
🎨 Kolam Design Analyzer
Smart India Hackathon 2025 - AI-Powered Traditional Art Analysis
Discover the mathematical principles and geometric patterns behind traditional Kolam designs
""", unsafe_allow_html=True)
# Sidebar with consistent parameters
with st.sidebar:
st.markdown("### 🔧 Analysis Parameters")
# Use session state for parameters to prevent re-runs
if 'params' not in st.session_state:
st.session_state['params'] = {
'image_size': 256,
'threshold_value': 127,
'canny_low': 30,
'canny_high': 100,
'max_corners': 100,
'min_line_length': 5
}
image_size = st.slider("Image Processing Size", 128, 512,
st.session_state['params']['image_size'], step=64)
threshold_value = st.slider("Binary Threshold", 50, 200,
st.session_state['params']['threshold_value'])
canny_low = st.slider("Canny Low Threshold", 10, 100,
st.session_state['params']['canny_low'])
canny_high = st.slider("Canny High Threshold", 50, 200,
st.session_state['params']['canny_high'])
max_corners = st.slider("Maximum Corners", 50, 200,
st.session_state['params']['max_corners'])
min_line_length = st.slider("Minimum Line Length", 3, 20,
st.session_state['params']['min_line_length'])
# Update parameters in session state
st.session_state['params'].update({
'image_size': image_size,
'threshold_value': threshold_value,
'canny_low': canny_low,
'canny_high': canny_high,
'max_corners': max_corners,
'min_line_length': min_line_length
})
st.markdown("---")
st.markdown("### 📊 About This Tool")
st.info("This application uses computer vision and graph theory to analyze traditional Kolam designs, extracting geometric patterns and design principles.")
# Reset button
if st.button("🔄 Reset Analysis"):
st.session_state['analysis_complete'] = False
st.session_state['uploaded_image'] = None
st.session_state['analysis_results'] = {}
st.session_state['processing'] = False
st.rerun()
class KolamAnalyzer:
def __init__(self):
self.cipher = None
self.encryption_key = None
def generate_encryption_key(self):
"""Generate encryption key for graph data"""
try:
from cryptography.fernet import Fernet
self.encryption_key = Fernet.generate_key()
self.cipher = Fernet(self.encryption_key)
return self.encryption_key.decode()
except ImportError:
return "Encryption not available"
def preprocess_image(self, image, size, threshold_val, canny_low, canny_high):
"""Preprocess uploaded image"""
try:
# Convert PIL image to OpenCV format
img_array = np.array(image)
if len(img_array.shape) == 3:
img_gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
else:
img_gray = img_array
# Resize image
img_resized = cv2.resize(img_gray, (size, size))
# Apply binary threshold
_, thresh = cv2.threshold(img_resized, threshold_val, 255, cv2.THRESH_BINARY_INV)
# Edge detection
edges = cv2.Canny(thresh, canny_low, canny_high)
return img_resized, thresh, edges
except Exception as e:
st.error(f"Error in image preprocessing: {str(e)}")
return None, None, None
def detect_nodes(self, edges, max_corners):
"""Detect corner points as graph nodes"""
try:
corners = cv2.goodFeaturesToTrack(
edges,
maxCorners=max_corners,
qualityLevel=0.01,
minDistance=5
)
if corners is None:
return []
return [tuple(pt.ravel()) for pt in corners.astype(int)]
except Exception as e:
st.error(f"Error in node detection: {str(e)}")
return []
def detect_edges(self, edges, nodes, min_line_length):
"""Detect lines and create graph edges"""
try:
lines = cv2.HoughLinesP(
edges,
1,
np.pi/180,
threshold=30,
minLineLength=min_line_length,
maxLineGap=10
)
graph_edges = []
if lines is not None and len(nodes) > 0:
for x1, y1, x2, y2 in lines[:,0]:
n1 = min(range(len(nodes)),
key=lambda i: np.linalg.norm(np.array(nodes[i]) - np.array([x1,y1])))
n2 = min(range(len(nodes)),
key=lambda i: np.linalg.norm(np.array(nodes[i]) - np.array([x2,y2])))
if n1 != n2 and (n1, n2) not in graph_edges and (n2, n1) not in graph_edges:
graph_edges.append((n1, n2))
# Fallback: connect nearby nodes if no lines detected
if len(graph_edges) == 0:
graph_edges = self.connect_nearby_nodes(nodes, max_distance=30)
return graph_edges
except Exception as e:
st.error(f"Error in edge detection: {str(e)}")
return []
def connect_nearby_nodes(self, nodes, max_distance=30):
"""Connect nearby nodes as fallback"""
edges = []
for i, (x1, y1) in enumerate(nodes):
for j, (x2, y2) in enumerate(nodes):
if i < j:
distance = np.linalg.norm(np.array([x1, y1]) - np.array([x2, y2]))
if distance <= max_distance:
edges.append((i, j))
return edges
def build_graph(self, nodes, edges):
"""Build NetworkX graph from nodes and edges"""
try:
G = nx.Graph()
for idx, node in enumerate(nodes):
G.add_node(idx, pos=node)
for n1, n2 in edges:
G.add_edge(n1, n2)
return G
except Exception as e:
st.error(f"Error in graph building: {str(e)}")
return nx.Graph()
def extract_graph_features(self, G):
"""Extract mathematical features from the graph"""
try:
num_nodes = G.number_of_nodes()
num_edges = G.number_of_edges()
degrees = [d for _, d in G.degree()]
avg_degree = np.mean(degrees) if degrees else 0
max_degree = max(degrees) if degrees else 0
min_degree = min(degrees) if degrees else 0
# Calculate cycles
try:
num_cycles = sum(1 for c in nx.cycle_basis(G))
except:
num_cycles = 0
# Calculate connectivity
is_connected = nx.is_connected(G) if num_nodes > 0 else False
num_components = nx.number_connected_components(G)
# Calculate centrality measures
try:
betweenness = nx.betweenness_centrality(G)
avg_betweenness = np.mean(list(betweenness.values())) if betweenness else 0
closeness = nx.closeness_centrality(G)
avg_closeness = np.mean(list(closeness.values())) if closeness else 0
except:
avg_betweenness = 0
avg_closeness = 0
return {
"num_nodes": num_nodes,
"num_edges": num_edges,
"avg_degree": round(avg_degree, 2),
"max_degree": max_degree,
"min_degree": min_degree,
"num_cycles": num_cycles,
"is_connected": is_connected,
"num_components": num_components,
"avg_betweenness": round(avg_betweenness, 4),
"avg_closeness": round(avg_closeness, 4),
"density": round(nx.density(G), 4) if num_nodes > 1 else 0
}
except Exception as e:
st.error(f"Error in feature extraction: {str(e)}")
return {}
def encrypt_graph(self, G):
"""Encrypt graph data for security"""
try:
if not self.cipher:
self.generate_encryption_key()
adj_matrix = nx.to_numpy_array(G)
adj_bytes = adj_matrix.tobytes()
encrypted = self.cipher.encrypt(adj_bytes)
return encrypted
except Exception as e:
return None
def create_interactive_graph(self, G):
"""Create interactive graph visualization using Plotly"""
try:
pos = nx.get_node_attributes(G, 'pos')
if not pos:
# If no positions, use spring layout
pos = nx.spring_layout(G)
# Extract edges
edge_x = []
edge_y = []
for edge in G.edges():
x0, y0 = pos[edge[0]]
x1, y1 = pos[edge[1]]
edge_x.extend([x0, x1, None])
edge_y.extend([y0, y1, None])
# Create edge trace
edge_trace = go.Scatter(
x=edge_x, y=edge_y,
line=dict(width=2, color='#FF6B35'),
hoverinfo='none',
mode='lines'
)
# Extract nodes
node_x = []
node_y = []
node_text = []
node_degree = []
for node in G.nodes():
x, y = pos[node]
node_x.append(x)
node_y.append(y)
degree = G.degree(node)
node_degree.append(degree)
node_text.append(f'Node {node}
Kolam Design Analyzer | Smart India Hackathon 2025
Preserving traditional art through modern technology 🎨✨
""", unsafe_allow_html=True)