BODDUSWATHISREE commited on
Commit
8998636
Β·
verified Β·
1 Parent(s): fc4acbe

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +370 -350
src/streamlit_app.py CHANGED
@@ -2,6 +2,8 @@ import streamlit as st
2
  import cv2
3
  import numpy as np
4
  import networkx as nx
 
 
5
  import matplotlib.pyplot as plt
6
  import pandas as pd
7
  import io
@@ -10,18 +12,7 @@ from PIL import Image
10
  import plotly.graph_objects as go
11
  from plotly.subplots import make_subplots
12
  import plotly.express as px
13
-
14
- # --- Session state initialization ---
15
- def initialize_session_state():
16
- """Initialize all session state variables"""
17
- if 'uploaded_image' not in st.session_state:
18
- st.session_state['uploaded_image'] = None
19
- if 'analysis_complete' not in st.session_state:
20
- st.session_state['analysis_complete'] = False
21
- if 'analysis_results' not in st.session_state:
22
- st.session_state['analysis_results'] = {}
23
- if 'processing' not in st.session_state:
24
- st.session_state['processing'] = False
25
 
26
  # Fix for Hugging Face Spaces permissions
27
  import os
@@ -29,10 +20,7 @@ import tempfile
29
  os.environ['STREAMLIT_BROWSER_GATHER_USAGE_STATS'] = 'false'
30
  os.environ['MPLCONFIGDIR'] = tempfile.gettempdir()
31
 
32
- # Initialize session state
33
- initialize_session_state()
34
-
35
- # Page configuration
36
  st.set_page_config(
37
  page_title="Kolam Design Analyzer",
38
  page_icon="🎨",
@@ -40,7 +28,29 @@ st.set_page_config(
40
  initial_sidebar_state="expanded"
41
  )
42
 
43
- # Custom CSS for professional styling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  st.markdown("""
45
  <style>
46
  .main-header {
@@ -61,22 +71,6 @@ st.markdown("""
61
  margin: 0.5rem 0;
62
  }
63
 
64
- .analysis-section {
65
- background: #f8f9fa;
66
- padding: 1.5rem;
67
- border-radius: 10px;
68
- margin: 1rem 0;
69
- }
70
-
71
- .upload-section {
72
- border: 2px dashed #FF6B35;
73
- padding: 2rem;
74
- border-radius: 10px;
75
- text-align: center;
76
- margin: 1rem 0;
77
- background: #fff9f7;
78
- }
79
-
80
  .stButton > button {
81
  background: linear-gradient(90deg, #FF6B35 0%, #F7931E 100%);
82
  color: white;
@@ -87,15 +81,34 @@ st.markdown("""
87
  transition: all 0.3s;
88
  }
89
 
90
- .stButton > button:hover {
91
- transform: translateY(-2px);
92
- box-shadow: 0 4px 8px rgba(0,0,0,0.2);
93
- }
94
-
95
- /* Prevent page jumping */
96
  .main .block-container {
97
  padding-top: 1rem;
98
  padding-bottom: 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
  </style>
101
  """, unsafe_allow_html=True)
@@ -109,56 +122,6 @@ st.markdown("""
109
  </div>
110
  """, unsafe_allow_html=True)
111
 
112
- # Sidebar with consistent parameters
113
- with st.sidebar:
114
- st.markdown("### πŸ”§ Analysis Parameters")
115
-
116
- # Use session state for parameters to prevent re-runs
117
- if 'params' not in st.session_state:
118
- st.session_state['params'] = {
119
- 'image_size': 256,
120
- 'threshold_value': 127,
121
- 'canny_low': 30,
122
- 'canny_high': 100,
123
- 'max_corners': 100,
124
- 'min_line_length': 5
125
- }
126
-
127
- image_size = st.slider("Image Processing Size", 128, 512,
128
- st.session_state['params']['image_size'], step=64)
129
- threshold_value = st.slider("Binary Threshold", 50, 200,
130
- st.session_state['params']['threshold_value'])
131
- canny_low = st.slider("Canny Low Threshold", 10, 100,
132
- st.session_state['params']['canny_low'])
133
- canny_high = st.slider("Canny High Threshold", 50, 200,
134
- st.session_state['params']['canny_high'])
135
- max_corners = st.slider("Maximum Corners", 50, 200,
136
- st.session_state['params']['max_corners'])
137
- min_line_length = st.slider("Minimum Line Length", 3, 20,
138
- st.session_state['params']['min_line_length'])
139
-
140
- # Update parameters in session state
141
- st.session_state['params'].update({
142
- 'image_size': image_size,
143
- 'threshold_value': threshold_value,
144
- 'canny_low': canny_low,
145
- 'canny_high': canny_high,
146
- 'max_corners': max_corners,
147
- 'min_line_length': min_line_length
148
- })
149
-
150
- st.markdown("---")
151
- st.markdown("### πŸ“Š About This Tool")
152
- st.info("This application uses computer vision and graph theory to analyze traditional Kolam designs, extracting geometric patterns and design principles.")
153
-
154
- # Reset button
155
- if st.button("πŸ”„ Reset Analysis"):
156
- st.session_state['analysis_complete'] = False
157
- st.session_state['uploaded_image'] = None
158
- st.session_state['analysis_results'] = {}
159
- st.session_state['processing'] = False
160
- st.rerun()
161
-
162
  class KolamAnalyzer:
163
  def __init__(self):
164
  self.cipher = None
@@ -211,7 +174,6 @@ class KolamAnalyzer:
211
  return []
212
  return [tuple(pt.ravel()) for pt in corners.astype(int)]
213
  except Exception as e:
214
- st.error(f"Error in node detection: {str(e)}")
215
  return []
216
 
217
  def detect_edges(self, edges, nodes, min_line_length):
@@ -242,7 +204,6 @@ class KolamAnalyzer:
242
 
243
  return graph_edges
244
  except Exception as e:
245
- st.error(f"Error in edge detection: {str(e)}")
246
  return []
247
 
248
  def connect_nearby_nodes(self, nodes, max_distance=30):
@@ -266,7 +227,6 @@ class KolamAnalyzer:
266
  G.add_edge(n1, n2)
267
  return G
268
  except Exception as e:
269
- st.error(f"Error in graph building: {str(e)}")
270
  return nx.Graph()
271
 
272
  def extract_graph_features(self, G):
@@ -314,168 +274,249 @@ class KolamAnalyzer:
314
  "density": round(nx.density(G), 4) if num_nodes > 1 else 0
315
  }
316
  except Exception as e:
317
- st.error(f"Error in feature extraction: {str(e)}")
318
  return {}
319
-
320
- def encrypt_graph(self, G):
321
- """Encrypt graph data for security"""
322
- try:
323
- if not self.cipher:
324
- self.generate_encryption_key()
325
-
326
- adj_matrix = nx.to_numpy_array(G)
327
- adj_bytes = adj_matrix.tobytes()
328
- encrypted = self.cipher.encrypt(adj_bytes)
329
- return encrypted
330
- except Exception as e:
331
- return None
332
-
333
- def create_interactive_graph(self, G):
334
- """Create interactive graph visualization using Plotly"""
335
- try:
336
- pos = nx.get_node_attributes(G, 'pos')
337
-
338
- if not pos:
339
- # If no positions, use spring layout
340
- pos = nx.spring_layout(G)
341
-
342
- # Extract edges
343
- edge_x = []
344
- edge_y = []
345
- for edge in G.edges():
346
- x0, y0 = pos[edge[0]]
347
- x1, y1 = pos[edge[1]]
348
- edge_x.extend([x0, x1, None])
349
- edge_y.extend([y0, y1, None])
350
-
351
- # Create edge trace
352
- edge_trace = go.Scatter(
353
- x=edge_x, y=edge_y,
354
- line=dict(width=2, color='#FF6B35'),
355
- hoverinfo='none',
356
- mode='lines'
357
- )
358
-
359
- # Extract nodes
360
- node_x = []
361
- node_y = []
362
- node_text = []
363
- node_degree = []
364
-
365
- for node in G.nodes():
366
- x, y = pos[node]
367
- node_x.append(x)
368
- node_y.append(y)
369
- degree = G.degree(node)
370
- node_degree.append(degree)
371
- node_text.append(f'Node {node}<br>Degree: {degree}')
372
-
373
- # Create node trace
374
- node_trace = go.Scatter(
375
- x=node_x, y=node_y,
376
- mode='markers',
377
- hoverinfo='text',
378
- text=node_text,
379
- marker=dict(
380
- size=[max(10, d*3) for d in node_degree],
381
- color=node_degree,
382
- colorscale='Viridis',
383
- colorbar=dict(
384
- thickness=15,
385
- len=0.5,
386
- x=1.02,
387
- title="Node Degree"
388
- ),
389
- line=dict(width=2, color='white')
390
- )
391
- )
392
-
393
- # Create figure
394
- fig = go.Figure(data=[edge_trace, node_trace],
395
- layout=go.Layout(
396
- title='Interactive Kolam Graph Structure',
397
- titlefont_size=16,
398
- showlegend=False,
399
- hovermode='closest',
400
- margin=dict(b=20,l=5,r=5,t=40),
401
- annotations=[ dict(
402
- text="Node size represents degree centrality",
403
- showarrow=False,
404
- xref="paper", yref="paper",
405
- x=0.005, y=-0.002,
406
- xanchor="left", yanchor="bottom",
407
- font=dict(size=12)
408
- )],
409
- xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
410
- yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
411
- plot_bgcolor='white'
412
- ))
413
-
414
- return fig
415
- except Exception as e:
416
- st.error(f"Error creating interactive graph: {str(e)}")
417
- return None
418
 
419
- # Initialize analyzer
420
  @st.cache_resource
421
  def get_analyzer():
422
  return KolamAnalyzer()
423
 
424
  analyzer = get_analyzer()
425
 
426
- # Main content area
427
- col1, col2 = st.columns([1, 2])
428
-
429
  # Check library availability
 
 
 
 
430
  try:
431
  import pandas as pd
432
- PANDAS_AVAILABLE = True
433
- except ImportError as e:
434
- st.warning("⚠️ Pandas not available due to NumPy compatibility. Using basic data structures.")
435
  PANDAS_AVAILABLE = False
436
 
437
  try:
438
  import plotly.graph_objects as go
439
  import plotly.express as px
440
- PLOTLY_AVAILABLE = True
441
- except ImportError as e:
442
- st.warning("⚠️ Plotly not available due to NumPy compatibility. Using matplotlib for visualizations.")
443
  PLOTLY_AVAILABLE = False
444
 
445
  try:
446
  from cryptography.fernet import Fernet
447
- CRYPTO_AVAILABLE = True
448
- except ImportError as e:
449
- st.warning("⚠️ Cryptography not available. Encryption features disabled.")
450
  CRYPTO_AVAILABLE = False
451
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  with col1:
453
  st.markdown("### πŸ“€ Upload Kolam Image")
454
 
455
  uploaded_file = st.file_uploader(
456
  "Choose a Kolam image...",
457
  type=["png", "jpg", "jpeg"],
458
- help="Upload a clear image of a Kolam design for analysis",
459
- key="file_uploader"
460
  )
461
 
462
- # Handle file upload
463
  if uploaded_file is not None:
464
- # Only process if it's a new file
465
- if (st.session_state['uploaded_image'] is None or
466
- uploaded_file.name != getattr(st.session_state.get('uploaded_file'), 'name', None)):
 
467
  st.session_state['uploaded_image'] = Image.open(uploaded_file)
468
- st.session_state['uploaded_file'] = uploaded_file
469
  st.session_state['analysis_complete'] = False
470
  st.session_state['analysis_results'] = {}
 
471
 
472
  # Display uploaded image
473
  if st.session_state['uploaded_image'] is not None:
474
  st.image(st.session_state['uploaded_image'], caption="Uploaded Kolam", use_column_width=True)
475
 
476
  # Analysis button
477
- if st.button("πŸ” Analyze Kolam Design", key="analyze_btn", disabled=st.session_state.get('processing', False)):
 
 
 
 
478
  st.session_state['processing'] = True
 
479
 
480
  with st.spinner("Analyzing Kolam design..."):
481
  try:
@@ -527,106 +568,80 @@ with col1:
527
  with col2:
528
  st.markdown("### πŸ“Š Analysis Results")
529
 
530
- if st.session_state['analysis_complete'] and st.session_state['analysis_results']:
531
  results = st.session_state['analysis_results']
532
 
533
- # Create tabs for different visualizations
534
  tab1, tab2, tab3, tab4 = st.tabs(["πŸ–ΌοΈ Image Processing", "πŸ“ˆ Graph Analysis", "πŸ“Š Features", "πŸ” Security"])
535
 
536
  with tab1:
537
  st.markdown("#### Image Processing Pipeline")
538
 
539
- try:
540
- # Create subplot for processed images
541
- fig, axes = plt.subplots(1, 3, figsize=(15, 5))
542
-
543
- axes[0].imshow(results['original_img'], cmap='gray')
544
- axes[0].set_title('Original Grayscale', fontsize=12, fontweight='bold')
545
- axes[0].axis('off')
546
-
547
- axes[1].imshow(results['thresh_img'], cmap='gray')
548
- axes[1].set_title('Binary Threshold', fontsize=12, fontweight='bold')
549
- axes[1].axis('off')
550
-
551
- axes[2].imshow(results['edges_img'], cmap='gray')
552
- axes[2].set_title('Edge Detection', fontsize=12, fontweight='bold')
553
- axes[2].axis('off')
554
-
555
- plt.tight_layout()
556
- st.pyplot(fig)
557
- plt.close()
558
-
559
- # Show detected nodes
560
- st.markdown("#### Detected Corner Points")
561
- img_with_nodes = results['original_img'].copy()
562
- for x, y in results['nodes']:
563
- cv2.circle(img_with_nodes, (int(x), int(y)), 3, (255), -1)
564
-
565
- fig_nodes, ax_nodes = plt.subplots(1, 1, figsize=(8, 8))
566
- ax_nodes.imshow(img_with_nodes, cmap='gray')
567
- ax_nodes.set_title(f'Detected Nodes: {len(results["nodes"])}',
568
- fontsize=14, fontweight='bold')
569
- ax_nodes.axis('off')
570
- st.pyplot(fig_nodes)
571
- plt.close()
572
- except Exception as e:
573
- st.error(f"Error displaying image processing results: {str(e)}")
574
 
575
  with tab2:
576
  st.markdown("#### Interactive Graph Visualization")
577
 
578
- try:
579
- # Create interactive graph
580
- if results['graph'].number_of_nodes() > 0:
581
- fig_interactive = analyzer.create_interactive_graph(results['graph'])
582
- if fig_interactive:
583
- st.plotly_chart(fig_interactive, use_container_width=True)
584
- else:
585
- st.warning("No graph structure detected in the image.")
586
-
587
- # Graph statistics
588
- col_a, col_b = st.columns(2)
589
- with col_a:
590
- st.metric("Total Nodes", results['features'].get('num_nodes', 0))
591
- st.metric("Total Edges", results['features'].get('num_edges', 0))
592
- st.metric("Graph Density", results['features'].get('density', 0))
593
-
594
- with col_b:
595
- st.metric("Average Degree", results['features'].get('avg_degree', 0))
596
- st.metric("Number of Cycles", results['features'].get('num_cycles', 0))
597
- st.metric("Connected Components", results['features'].get('num_components', 0))
598
- except Exception as e:
599
- st.error(f"Error displaying graph analysis: {str(e)}")
600
 
601
  with tab3:
602
  st.markdown("#### Mathematical Properties")
603
 
604
- try:
605
- # Create metrics dataframe
606
- if PANDAS_AVAILABLE:
607
- features_df = pd.DataFrame([
608
- {"Property": "Nodes", "Value": results['features'].get('num_nodes', 0)},
609
- {"Property": "Edges", "Value": results['features'].get('num_edges', 0)},
610
- {"Property": "Average Degree", "Value": results['features'].get('avg_degree', 0)},
611
- {"Property": "Maximum Degree", "Value": results['features'].get('max_degree', 0)},
612
- {"Property": "Minimum Degree", "Value": results['features'].get('min_degree', 0)},
613
- {"Property": "Cycles", "Value": results['features'].get('num_cycles', 0)},
614
- {"Property": "Graph Density", "Value": results['features'].get('density', 0)},
615
- {"Property": "Average Betweenness", "Value": results['features'].get('avg_betweenness', 0)},
616
- {"Property": "Average Closeness", "Value": results['features'].get('avg_closeness', 0)},
617
- {"Property": "Connected", "Value": "Yes" if results['features'].get('is_connected', False) else "No"},
618
- {"Property": "Components", "Value": results['features'].get('num_components', 0)}
619
- ])
620
-
621
- st.dataframe(features_df, use_container_width=True)
622
- else:
623
- # Display as simple table without pandas
624
- for key, value in results['features'].items():
625
- st.write(f"**{key.replace('_', ' ').title()}**: {value}")
626
 
627
- # Visualize degree distribution
628
- if results['graph'].number_of_nodes() > 0 and PLOTLY_AVAILABLE:
629
- degrees = [d for _, d in results['graph'].degree()]
 
 
 
 
 
 
 
 
630
  fig_hist = px.histogram(
631
  x=degrees,
632
  title="Degree Distribution",
@@ -635,69 +650,74 @@ with col2:
635
  )
636
  fig_hist.update_layout(
637
  plot_bgcolor='white',
638
- paper_bgcolor='white'
 
639
  )
640
- st.plotly_chart(fig_hist, use_container_width=True)
641
- except Exception as e:
642
- st.error(f"Error displaying features: {str(e)}")
643
 
644
  with tab4:
645
  st.markdown("#### Security & Data Protection")
646
 
647
- try:
648
- if CRYPTO_AVAILABLE:
649
- # Encrypt graph
650
- encrypted_data = analyzer.encrypt_graph(results['graph'])
651
-
652
- if encrypted_data:
653
- col_x, col_y = st.columns(2)
654
- with col_x:
655
- st.success("πŸ” Graph data encrypted successfully!")
656
- st.info(f"Encrypted data size: {len(encrypted_data)} bytes")
657
-
658
- with col_y:
659
- if results.get('encryption_key'):
660
- st.code(f"Encryption Key:\n{results['encryption_key']}", language="text")
661
- else:
662
- st.error("Failed to encrypt graph data")
663
- else:
664
- st.warning("πŸ”’ Encryption not available due to package compatibility issues.")
665
- st.info("Graph data will be stored in plain text format.")
666
-
667
- # Download options
668
- st.markdown("#### πŸ“₯ Download Results")
669
 
670
- col_dl1, col_dl2 = st.columns(2)
671
- with col_dl1:
672
- # Prepare features for download
673
- if PANDAS_AVAILABLE:
674
- features_json = pd.DataFrame([results['features']]).to_json(orient='records')
675
- else:
676
- import json
677
- features_json = json.dumps([results['features']], indent=2)
678
 
679
- st.download_button(
680
- "πŸ“Š Download Features (JSON)",
681
- data=features_json,
682
- file_name="kolam_features.json",
683
- mime="application/json"
684
- )
 
 
685
 
686
- with col_dl2:
687
- # Prepare adjacency matrix
688
- adj_matrix = nx.to_numpy_array(results['graph'])
689
- adj_buffer = io.BytesIO()
690
- np.save(adj_buffer, adj_matrix)
691
- st.download_button(
692
- "πŸ”’ Download Adjacency Matrix",
693
- data=adj_buffer.getvalue(),
694
- file_name="kolam_adjacency.npy",
695
- mime="application/octet-stream"
696
- )
697
- except Exception as e:
698
- st.error(f"Error in security section: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
699
  else:
700
  st.info("πŸ‘† Please upload a Kolam image and click 'Analyze' to see results")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
 
702
  # Footer
703
  st.markdown("---")
 
2
  import cv2
3
  import numpy as np
4
  import networkx as nx
5
+ import matplotlib
6
+ matplotlib.use('Agg') # Use non-interactive backend
7
  import matplotlib.pyplot as plt
8
  import pandas as pd
9
  import io
 
12
  import plotly.graph_objects as go
13
  from plotly.subplots import make_subplots
14
  import plotly.express as px
15
+ import json
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  # Fix for Hugging Face Spaces permissions
18
  import os
 
20
  os.environ['STREAMLIT_BROWSER_GATHER_USAGE_STATS'] = 'false'
21
  os.environ['MPLCONFIGDIR'] = tempfile.gettempdir()
22
 
23
+ # Page configuration - MUST be first Streamlit command
 
 
 
24
  st.set_page_config(
25
  page_title="Kolam Design Analyzer",
26
  page_icon="🎨",
 
28
  initial_sidebar_state="expanded"
29
  )
30
 
31
+ # --- Session state initialization ---
32
+ def initialize_session_state():
33
+ """Initialize all session state variables"""
34
+ defaults = {
35
+ 'uploaded_image': None,
36
+ 'analysis_complete': False,
37
+ 'analysis_results': {},
38
+ 'processing': False,
39
+ 'image_uploaded': False,
40
+ 'analysis_hash': None,
41
+ 'cached_figures': {},
42
+ 'params_changed': False,
43
+ 'file_hash': None
44
+ }
45
+
46
+ for key, value in defaults.items():
47
+ if key not in st.session_state:
48
+ st.session_state[key] = value
49
+
50
+ # Initialize session state
51
+ initialize_session_state()
52
+
53
+ # Custom CSS for professional styling and anti-flicker
54
  st.markdown("""
55
  <style>
56
  .main-header {
 
71
  margin: 0.5rem 0;
72
  }
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  .stButton > button {
75
  background: linear-gradient(90deg, #FF6B35 0%, #F7931E 100%);
76
  color: white;
 
81
  transition: all 0.3s;
82
  }
83
 
84
+ /* Anti-flicker CSS */
 
 
 
 
 
85
  .main .block-container {
86
  padding-top: 1rem;
87
  padding-bottom: 1rem;
88
+ max-width: 100%;
89
+ }
90
+
91
+ .stTabs [data-baseweb="tab-list"] {
92
+ gap: 2px;
93
+ }
94
+
95
+ .stTabs [data-baseweb="tab"] {
96
+ height: 50px;
97
+ }
98
+
99
+ .element-container {
100
+ width: 100% !important;
101
+ }
102
+
103
+ /* Prevent layout shifts */
104
+ .stPlotlyChart, .stPyplot {
105
+ width: 100%;
106
+ min-height: 400px;
107
+ }
108
+
109
+ /* Stabilize metrics */
110
+ [data-testid="metric-container"] {
111
+ min-height: 80px;
112
  }
113
  </style>
114
  """, unsafe_allow_html=True)
 
122
  </div>
123
  """, unsafe_allow_html=True)
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  class KolamAnalyzer:
126
  def __init__(self):
127
  self.cipher = None
 
174
  return []
175
  return [tuple(pt.ravel()) for pt in corners.astype(int)]
176
  except Exception as e:
 
177
  return []
178
 
179
  def detect_edges(self, edges, nodes, min_line_length):
 
204
 
205
  return graph_edges
206
  except Exception as e:
 
207
  return []
208
 
209
  def connect_nearby_nodes(self, nodes, max_distance=30):
 
227
  G.add_edge(n1, n2)
228
  return G
229
  except Exception as e:
 
230
  return nx.Graph()
231
 
232
  def extract_graph_features(self, G):
 
274
  "density": round(nx.density(G), 4) if num_nodes > 1 else 0
275
  }
276
  except Exception as e:
 
277
  return {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
+ # Initialize analyzer - cached to prevent recreation
280
  @st.cache_resource
281
  def get_analyzer():
282
  return KolamAnalyzer()
283
 
284
  analyzer = get_analyzer()
285
 
 
 
 
286
  # Check library availability
287
+ PANDAS_AVAILABLE = True
288
+ PLOTLY_AVAILABLE = True
289
+ CRYPTO_AVAILABLE = True
290
+
291
  try:
292
  import pandas as pd
293
+ except ImportError:
 
 
294
  PANDAS_AVAILABLE = False
295
 
296
  try:
297
  import plotly.graph_objects as go
298
  import plotly.express as px
299
+ except ImportError:
 
 
300
  PLOTLY_AVAILABLE = False
301
 
302
  try:
303
  from cryptography.fernet import Fernet
304
+ except ImportError:
 
 
305
  CRYPTO_AVAILABLE = False
306
 
307
+ # Helper function to create stable matplotlib figures
308
+ @st.cache_data(hash_funcs={np.ndarray: lambda x: x.tobytes()})
309
+ def create_processing_figure(original_img, thresh_img, edges_img):
310
+ """Create cached matplotlib figure for image processing"""
311
+ fig, axes = plt.subplots(1, 3, figsize=(15, 5))
312
+
313
+ axes[0].imshow(original_img, cmap='gray')
314
+ axes[0].set_title('Original Grayscale', fontsize=12, fontweight='bold')
315
+ axes[0].axis('off')
316
+
317
+ axes[1].imshow(thresh_img, cmap='gray')
318
+ axes[1].set_title('Binary Threshold', fontsize=12, fontweight='bold')
319
+ axes[1].axis('off')
320
+
321
+ axes[2].imshow(edges_img, cmap='gray')
322
+ axes[2].set_title('Edge Detection', fontsize=12, fontweight='bold')
323
+ axes[2].axis('off')
324
+
325
+ plt.tight_layout()
326
+ return fig
327
+
328
+ @st.cache_data(hash_funcs={np.ndarray: lambda x: x.tobytes()})
329
+ def create_nodes_figure(original_img, nodes):
330
+ """Create cached matplotlib figure for detected nodes"""
331
+ img_with_nodes = original_img.copy()
332
+ for x, y in nodes:
333
+ cv2.circle(img_with_nodes, (int(x), int(y)), 3, (255), -1)
334
+
335
+ fig, ax = plt.subplots(1, 1, figsize=(8, 8))
336
+ ax.imshow(img_with_nodes, cmap='gray')
337
+ ax.set_title(f'Detected Nodes: {len(nodes)}', fontsize=14, fontweight='bold')
338
+ ax.axis('off')
339
+ return fig
340
+
341
+ @st.cache_data(hash_funcs={nx.Graph: lambda g: str(sorted(g.edges()))})
342
+ def create_interactive_graph(G):
343
+ """Create cached interactive graph visualization"""
344
+ if G.number_of_nodes() == 0:
345
+ return None
346
+
347
+ pos = nx.get_node_attributes(G, 'pos')
348
+
349
+ if not pos:
350
+ pos = nx.spring_layout(G, seed=42) # Fixed seed for consistency
351
+
352
+ # Extract edges
353
+ edge_x = []
354
+ edge_y = []
355
+ for edge in G.edges():
356
+ x0, y0 = pos[edge[0]]
357
+ x1, y1 = pos[edge[1]]
358
+ edge_x.extend([x0, x1, None])
359
+ edge_y.extend([y0, y1, None])
360
+
361
+ # Create edge trace
362
+ edge_trace = go.Scatter(
363
+ x=edge_x, y=edge_y,
364
+ line=dict(width=2, color='#FF6B35'),
365
+ hoverinfo='none',
366
+ mode='lines',
367
+ name='Edges'
368
+ )
369
+
370
+ # Extract nodes
371
+ node_x = []
372
+ node_y = []
373
+ node_text = []
374
+ node_degree = []
375
+
376
+ for node in G.nodes():
377
+ x, y = pos[node]
378
+ node_x.append(x)
379
+ node_y.append(y)
380
+ degree = G.degree(node)
381
+ node_degree.append(degree)
382
+ node_text.append(f'Node {node}<br>Degree: {degree}')
383
+
384
+ # Create node trace
385
+ node_trace = go.Scatter(
386
+ x=node_x, y=node_y,
387
+ mode='markers',
388
+ hoverinfo='text',
389
+ text=node_text,
390
+ name='Nodes',
391
+ marker=dict(
392
+ size=[max(10, d*3) for d in node_degree],
393
+ color=node_degree,
394
+ colorscale='Viridis',
395
+ colorbar=dict(
396
+ thickness=15,
397
+ len=0.5,
398
+ x=1.02,
399
+ title="Node Degree"
400
+ ),
401
+ line=dict(width=2, color='white')
402
+ )
403
+ )
404
+
405
+ # Create figure
406
+ fig = go.Figure(
407
+ data=[edge_trace, node_trace],
408
+ layout=go.Layout(
409
+ title='Interactive Kolam Graph Structure',
410
+ titlefont_size=16,
411
+ showlegend=False,
412
+ hovermode='closest',
413
+ margin=dict(b=20,l=5,r=5,t=40),
414
+ annotations=[dict(
415
+ text="Node size represents degree centrality",
416
+ showarrow=False,
417
+ xref="paper", yref="paper",
418
+ x=0.005, y=-0.002,
419
+ xanchor="left", yanchor="bottom",
420
+ font=dict(size=12)
421
+ )],
422
+ xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
423
+ yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
424
+ plot_bgcolor='white',
425
+ height=500 # Fixed height to prevent layout shifts
426
+ )
427
+ )
428
+
429
+ return fig
430
+
431
+ # Sidebar with parameters
432
+ with st.sidebar:
433
+ st.markdown("### πŸ”§ Analysis Parameters")
434
+
435
+ # Initialize default parameters
436
+ if 'params' not in st.session_state:
437
+ st.session_state['params'] = {
438
+ 'image_size': 256,
439
+ 'threshold_value': 127,
440
+ 'canny_low': 30,
441
+ 'canny_high': 100,
442
+ 'max_corners': 100,
443
+ 'min_line_length': 5
444
+ }
445
+
446
+ # Get current parameters
447
+ current_params = st.session_state['params'].copy()
448
+
449
+ # Parameter sliders
450
+ image_size = st.slider("Image Processing Size", 128, 512, current_params['image_size'], step=64)
451
+ threshold_value = st.slider("Binary Threshold", 50, 200, current_params['threshold_value'])
452
+ canny_low = st.slider("Canny Low Threshold", 10, 100, current_params['canny_low'])
453
+ canny_high = st.slider("Canny High Threshold", 50, 200, current_params['canny_high'])
454
+ max_corners = st.slider("Maximum Corners", 50, 200, current_params['max_corners'])
455
+ min_line_length = st.slider("Minimum Line Length", 3, 20, current_params['min_line_length'])
456
+
457
+ # Update parameters and check if changed
458
+ new_params = {
459
+ 'image_size': image_size,
460
+ 'threshold_value': threshold_value,
461
+ 'canny_low': canny_low,
462
+ 'canny_high': canny_high,
463
+ 'max_corners': max_corners,
464
+ 'min_line_length': min_line_length
465
+ }
466
+
467
+ if new_params != st.session_state['params']:
468
+ st.session_state['params'] = new_params
469
+ st.session_state['params_changed'] = True
470
+
471
+ st.markdown("---")
472
+ st.markdown("### πŸ“Š About This Tool")
473
+ st.info("This application uses computer vision and graph theory to analyze traditional Kolam designs, extracting geometric patterns and design principles.")
474
+
475
+ # Reset button
476
+ if st.button("πŸ”„ Reset Analysis"):
477
+ for key in ['analysis_complete', 'analysis_results', 'uploaded_image',
478
+ 'processing', 'analysis_hash', 'cached_figures', 'file_hash']:
479
+ if key in st.session_state:
480
+ del st.session_state[key]
481
+ st.cache_data.clear()
482
+ st.rerun()
483
+
484
+ # Main content area
485
+ col1, col2 = st.columns([1, 2], gap="medium")
486
+
487
  with col1:
488
  st.markdown("### πŸ“€ Upload Kolam Image")
489
 
490
  uploaded_file = st.file_uploader(
491
  "Choose a Kolam image...",
492
  type=["png", "jpg", "jpeg"],
493
+ help="Upload a clear image of a Kolam design for analysis"
 
494
  )
495
 
496
+ # Handle file upload with hash checking
497
  if uploaded_file is not None:
498
+ file_hash = hash(uploaded_file.read())
499
+ uploaded_file.seek(0) # Reset file pointer
500
+
501
+ if st.session_state['file_hash'] != file_hash:
502
  st.session_state['uploaded_image'] = Image.open(uploaded_file)
503
+ st.session_state['file_hash'] = file_hash
504
  st.session_state['analysis_complete'] = False
505
  st.session_state['analysis_results'] = {}
506
+ st.cache_data.clear() # Clear cache for new image
507
 
508
  # Display uploaded image
509
  if st.session_state['uploaded_image'] is not None:
510
  st.image(st.session_state['uploaded_image'], caption="Uploaded Kolam", use_column_width=True)
511
 
512
  # Analysis button
513
+ analyze_disabled = (st.session_state.get('processing', False) or
514
+ (st.session_state.get('analysis_complete', False) and
515
+ not st.session_state.get('params_changed', False)))
516
+
517
+ if st.button("πŸ” Analyze Kolam Design", disabled=analyze_disabled):
518
  st.session_state['processing'] = True
519
+ st.session_state['params_changed'] = False
520
 
521
  with st.spinner("Analyzing Kolam design..."):
522
  try:
 
568
  with col2:
569
  st.markdown("### πŸ“Š Analysis Results")
570
 
571
+ if st.session_state.get('analysis_complete', False) and st.session_state.get('analysis_results'):
572
  results = st.session_state['analysis_results']
573
 
574
+ # Create stable tabs
575
  tab1, tab2, tab3, tab4 = st.tabs(["πŸ–ΌοΈ Image Processing", "πŸ“ˆ Graph Analysis", "πŸ“Š Features", "πŸ” Security"])
576
 
577
  with tab1:
578
  st.markdown("#### Image Processing Pipeline")
579
 
580
+ # Use cached figure creation
581
+ fig = create_processing_figure(
582
+ results['original_img'],
583
+ results['thresh_img'],
584
+ results['edges_img']
585
+ )
586
+ st.pyplot(fig, clear_figure=True)
587
+
588
+ # Show detected nodes with cached figure
589
+ st.markdown("#### Detected Corner Points")
590
+ fig_nodes = create_nodes_figure(results['original_img'], results['nodes'])
591
+ st.pyplot(fig_nodes, clear_figure=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
 
593
  with tab2:
594
  st.markdown("#### Interactive Graph Visualization")
595
 
596
+ # Create interactive graph with caching
597
+ fig_interactive = create_interactive_graph(results['graph'])
598
+ if fig_interactive:
599
+ st.plotly_chart(fig_interactive, use_container_width=True, key="main_graph")
600
+ else:
601
+ st.warning("No graph structure detected in the image.")
602
+
603
+ # Stable graph statistics
604
+ col_a, col_b = st.columns(2)
605
+ with col_a:
606
+ st.metric("Total Nodes", results['features'].get('num_nodes', 0))
607
+ st.metric("Total Edges", results['features'].get('num_edges', 0))
608
+ st.metric("Graph Density", results['features'].get('density', 0))
609
+
610
+ with col_b:
611
+ st.metric("Average Degree", results['features'].get('avg_degree', 0))
612
+ st.metric("Number of Cycles", results['features'].get('num_cycles', 0))
613
+ st.metric("Connected Components", results['features'].get('num_components', 0))
 
 
 
 
614
 
615
  with tab3:
616
  st.markdown("#### Mathematical Properties")
617
 
618
+ # Create stable dataframe
619
+ if PANDAS_AVAILABLE:
620
+ features_data = [
621
+ {"Property": "Nodes", "Value": results['features'].get('num_nodes', 0)},
622
+ {"Property": "Edges", "Value": results['features'].get('num_edges', 0)},
623
+ {"Property": "Average Degree", "Value": results['features'].get('avg_degree', 0)},
624
+ {"Property": "Maximum Degree", "Value": results['features'].get('max_degree', 0)},
625
+ {"Property": "Minimum Degree", "Value": results['features'].get('min_degree', 0)},
626
+ {"Property": "Cycles", "Value": results['features'].get('num_cycles', 0)},
627
+ {"Property": "Graph Density", "Value": results['features'].get('density', 0)},
628
+ {"Property": "Average Betweenness", "Value": results['features'].get('avg_betweenness', 0)},
629
+ {"Property": "Average Closeness", "Value": results['features'].get('avg_closeness', 0)},
630
+ {"Property": "Connected", "Value": "Yes" if results['features'].get('is_connected', False) else "No"},
631
+ {"Property": "Components", "Value": results['features'].get('num_components', 0)}
632
+ ]
 
 
 
 
 
 
 
633
 
634
+ features_df = pd.DataFrame(features_data)
635
+ st.dataframe(features_df, use_container_width=True, hide_index=True)
636
+ else:
637
+ # Display as table without pandas
638
+ for key, value in results['features'].items():
639
+ st.write(f"**{key.replace('_', ' ').title()}**: {value}")
640
+
641
+ # Degree distribution with fixed height
642
+ if results['graph'].number_of_nodes() > 0 and PLOTLY_AVAILABLE:
643
+ degrees = [d for _, d in results['graph'].degree()]
644
+ if degrees:
645
  fig_hist = px.histogram(
646
  x=degrees,
647
  title="Degree Distribution",
 
650
  )
651
  fig_hist.update_layout(
652
  plot_bgcolor='white',
653
+ paper_bgcolor='white',
654
+ height=400 # Fixed height
655
  )
656
+ st.plotly_chart(fig_hist, use_container_width=True, key="degree_hist")
 
 
657
 
658
  with tab4:
659
  st.markdown("#### Security & Data Protection")
660
 
661
+ if CRYPTO_AVAILABLE:
662
+ encrypted_data = analyzer.encrypt_graph(results['graph'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
663
 
664
+ if encrypted_data:
665
+ col_x, col_y = st.columns(2)
666
+ with col_x:
667
+ st.success("πŸ” Graph data encrypted successfully!")
668
+ st.info(f"Encrypted data size: {len(encrypted_data)} bytes")
 
 
 
669
 
670
+ with col_y:
671
+ if results.get('encryption_key'):
672
+ st.code(f"Encryption Key:\n{results['encryption_key'][:50]}...", language="text")
673
+ else:
674
+ st.error("Failed to encrypt graph data")
675
+ else:
676
+ st.warning("πŸ”’ Encryption not available.")
677
+ st.info("Graph data will be stored in plain text format.")
678
 
679
+ # Download options
680
+ st.markdown("#### πŸ“₯ Download Results")
681
+
682
+ col_dl1, col_dl2 = st.columns(2)
683
+ with col_dl1:
684
+ # Features JSON
685
+ features_json = json.dumps([results['features']], indent=2)
686
+ st.download_button(
687
+ "πŸ“Š Download Features (JSON)",
688
+ data=features_json,
689
+ file_name="kolam_features.json",
690
+ mime="application/json"
691
+ )
692
+
693
+ with col_dl2:
694
+ # Adjacency matrix
695
+ adj_matrix = nx.to_numpy_array(results['graph'])
696
+ adj_buffer = io.BytesIO()
697
+ np.save(adj_buffer, adj_matrix)
698
+ st.download_button(
699
+ "πŸ”’ Download Adjacency Matrix",
700
+ data=adj_buffer.getvalue(),
701
+ file_name="kolam_adjacency.npy",
702
+ mime="application/octet-stream"
703
+ )
704
  else:
705
  st.info("πŸ‘† Please upload a Kolam image and click 'Analyze' to see results")
706
+
707
+ # Show placeholder content to maintain layout stability
708
+ tab1, tab2, tab3, tab4 = st.tabs(["πŸ–ΌοΈ Image Processing", "πŸ“ˆ Graph Analysis", "πŸ“Š Features", "πŸ” Security"])
709
+
710
+ with tab1:
711
+ st.write("Upload an image and run analysis to see image processing results.")
712
+
713
+ with tab2:
714
+ st.write("Upload an image and run analysis to see graph visualization.")
715
+
716
+ with tab3:
717
+ st.write("Upload an image and run analysis to see mathematical features.")
718
+
719
+ with tab4:
720
+ st.write("Upload an image and run analysis to see security options.")
721
 
722
  # Footer
723
  st.markdown("---")