Update src/streamlit_app.py

#2
by BodduSriPavan111 - opened
Files changed (1) hide show
  1. src/streamlit_app.py +475 -403
src/streamlit_app.py CHANGED
@@ -4,7 +4,6 @@ import numpy as np
4
  import networkx as nx
5
  import matplotlib.pyplot as plt
6
  import pandas as pd
7
- from cryptography.fernet import Fernet
8
  import io
9
  import base64
10
  from PIL import Image
@@ -13,10 +12,16 @@ from plotly.subplots import make_subplots
13
  import plotly.express as px
14
 
15
  # --- Session state initialization ---
16
- if 'uploaded_image' not in st.session_state:
17
- st.session_state['uploaded_image'] = None
18
- if 'analysis_complete' not in st.session_state:
19
- st.session_state['analysis_complete'] = False
 
 
 
 
 
 
20
 
21
  # Fix for Hugging Face Spaces permissions
22
  import os
@@ -24,8 +29,8 @@ import tempfile
24
  os.environ['STREAMLIT_BROWSER_GATHER_USAGE_STATS'] = 'false'
25
  os.environ['MPLCONFIGDIR'] = tempfile.gettempdir()
26
 
27
-
28
-
29
 
30
  # Page configuration
31
  st.set_page_config(
@@ -86,6 +91,12 @@ st.markdown("""
86
  transform: translateY(-2px);
87
  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
88
  }
 
 
 
 
 
 
89
  </style>
90
  """, unsafe_allow_html=True)
91
 
@@ -98,20 +109,55 @@ st.markdown("""
98
  </div>
99
  """, unsafe_allow_html=True)
100
 
101
- # Sidebar
102
  with st.sidebar:
103
  st.markdown("### πŸ”§ Analysis Parameters")
104
 
105
- image_size = st.slider("Image Processing Size", 128, 512, 256, step=64)
106
- threshold_value = st.slider("Binary Threshold", 50, 200, 127)
107
- canny_low = st.slider("Canny Low Threshold", 10, 100, 30)
108
- canny_high = st.slider("Canny High Threshold", 50, 200, 100)
109
- max_corners = st.slider("Maximum Corners", 50, 200, 100)
110
- min_line_length = st.slider("Minimum Line Length", 3, 20, 5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  st.markdown("---")
113
  st.markdown("### πŸ“Š About This Tool")
114
  st.info("This application uses computer vision and graph theory to analyze traditional Kolam designs, extracting geometric patterns and design principles.")
 
 
 
 
 
 
 
 
115
 
116
  class KolamAnalyzer:
117
  def __init__(self):
@@ -120,69 +166,84 @@ class KolamAnalyzer:
120
 
121
  def generate_encryption_key(self):
122
  """Generate encryption key for graph data"""
123
- self.encryption_key = Fernet.generate_key()
124
- self.cipher = Fernet(self.encryption_key)
125
- return self.encryption_key.decode()
 
 
 
 
126
 
127
  def preprocess_image(self, image, size, threshold_val, canny_low, canny_high):
128
  """Preprocess uploaded image"""
129
- # Convert PIL image to OpenCV format
130
- img_array = np.array(image)
131
- if len(img_array.shape) == 3:
132
- img_gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
133
- else:
134
- img_gray = img_array
 
 
 
 
135
 
136
- # Resize image
137
- img_resized = cv2.resize(img_gray, (size, size))
138
-
139
- # Apply binary threshold
140
- _, thresh = cv2.threshold(img_resized, threshold_val, 255, cv2.THRESH_BINARY_INV)
141
-
142
- # Edge detection
143
- edges = cv2.Canny(thresh, canny_low, canny_high)
144
-
145
- return img_resized, thresh, edges
146
 
147
  def detect_nodes(self, edges, max_corners):
148
  """Detect corner points as graph nodes"""
149
- corners = cv2.goodFeaturesToTrack(
150
- edges,
151
- maxCorners=max_corners,
152
- qualityLevel=0.01,
153
- minDistance=5
154
- )
155
- if corners is None:
 
 
 
 
 
156
  return []
157
- return [tuple(pt.ravel()) for pt in corners.astype(int)]
158
 
159
  def detect_edges(self, edges, nodes, min_line_length):
160
  """Detect lines and create graph edges"""
161
- lines = cv2.HoughLinesP(
162
- edges,
163
- 1,
164
- np.pi/180,
165
- threshold=30,
166
- minLineLength=min_line_length,
167
- maxLineGap=10
168
- )
169
-
170
- graph_edges = []
171
- if lines is not None:
172
- for x1, y1, x2, y2 in lines[:,0]:
173
- if len(nodes) > 0:
174
  n1 = min(range(len(nodes)),
175
  key=lambda i: np.linalg.norm(np.array(nodes[i]) - np.array([x1,y1])))
176
  n2 = min(range(len(nodes)),
177
  key=lambda i: np.linalg.norm(np.array(nodes[i]) - np.array([x2,y2])))
178
  if n1 != n2 and (n1, n2) not in graph_edges and (n2, n1) not in graph_edges:
179
  graph_edges.append((n1, n2))
180
-
181
- # Fallback: connect nearby nodes if no lines detected
182
- if len(graph_edges) == 0:
183
- graph_edges = self.connect_nearby_nodes(nodes, max_distance=30)
184
 
185
- return graph_edges
 
 
 
 
 
 
 
186
 
187
  def connect_nearby_nodes(self, nodes, max_distance=30):
188
  """Connect nearby nodes as fallback"""
@@ -197,155 +258,175 @@ class KolamAnalyzer:
197
 
198
  def build_graph(self, nodes, edges):
199
  """Build NetworkX graph from nodes and edges"""
200
- G = nx.Graph()
201
- for idx, node in enumerate(nodes):
202
- G.add_node(idx, pos=node)
203
- for n1, n2 in edges:
204
- G.add_edge(n1, n2)
205
- return G
 
 
 
 
206
 
207
  def extract_graph_features(self, G):
208
  """Extract mathematical features from the graph"""
209
- num_nodes = G.number_of_nodes()
210
- num_edges = G.number_of_edges()
211
- degrees = [d for _, d in G.degree()]
212
- avg_degree = np.mean(degrees) if degrees else 0
213
- max_degree = max(degrees) if degrees else 0
214
- min_degree = min(degrees) if degrees else 0
215
-
216
- # Calculate cycles
217
  try:
218
- num_cycles = sum(1 for c in nx.cycle_basis(G))
219
- except:
220
- num_cycles = 0
 
 
 
221
 
222
- # Calculate connectivity
223
- is_connected = nx.is_connected(G) if num_nodes > 0 else False
224
- num_components = nx.number_connected_components(G)
225
-
226
- # Calculate centrality measures
227
- try:
228
- betweenness = nx.betweenness_centrality(G)
229
- avg_betweenness = np.mean(list(betweenness.values())) if betweenness else 0
 
230
 
231
- closeness = nx.closeness_centrality(G)
232
- avg_closeness = np.mean(list(closeness.values())) if closeness else 0
233
- except:
234
- avg_betweenness = 0
235
- avg_closeness = 0
236
-
237
- return {
238
- "num_nodes": num_nodes,
239
- "num_edges": num_edges,
240
- "avg_degree": round(avg_degree, 2),
241
- "max_degree": max_degree,
242
- "min_degree": min_degree,
243
- "num_cycles": num_cycles,
244
- "is_connected": is_connected,
245
- "num_components": num_components,
246
- "avg_betweenness": round(avg_betweenness, 4),
247
- "avg_closeness": round(avg_closeness, 4),
248
- "density": round(nx.density(G), 4) if num_nodes > 1 else 0
249
- }
 
 
 
 
 
 
 
 
250
 
251
  def encrypt_graph(self, G):
252
  """Encrypt graph data for security"""
253
- if not self.cipher:
254
- self.generate_encryption_key()
255
-
256
- adj_matrix = nx.to_numpy_array(G)
257
- adj_bytes = adj_matrix.tobytes()
258
- encrypted = self.cipher.encrypt(adj_bytes)
259
- return encrypted
 
 
 
260
 
261
  def create_interactive_graph(self, G):
262
  """Create interactive graph visualization using Plotly"""
263
- pos = nx.get_node_attributes(G, 'pos')
264
-
265
- if not pos:
266
- # If no positions, use spring layout
267
- pos = nx.spring_layout(G)
268
-
269
- # Extract edges
270
- edge_x = []
271
- edge_y = []
272
- for edge in G.edges():
273
- x0, y0 = pos[edge[0]]
274
- x1, y1 = pos[edge[1]]
275
- edge_x.extend([x0, x1, None])
276
- edge_y.extend([y0, y1, None])
277
-
278
- # Create edge trace
279
- edge_trace = go.Scatter(
280
- x=edge_x, y=edge_y,
281
- line=dict(width=2, color='#FF6B35'),
282
- hoverinfo='none',
283
- mode='lines'
284
- )
285
-
286
- # Extract nodes
287
- node_x = []
288
- node_y = []
289
- node_text = []
290
- node_degree = []
291
-
292
- for node in G.nodes():
293
- x, y = pos[node]
294
- node_x.append(x)
295
- node_y.append(y)
296
- degree = G.degree(node)
297
- node_degree.append(degree)
298
- node_text.append(f'Node {node}<br>Degree: {degree}')
299
-
300
- # Create node trace
301
- node_trace = go.Scatter(
302
- x=node_x, y=node_y,
303
- mode='markers',
304
- hoverinfo='text',
305
- text=node_text,
306
- marker=dict(
307
- size=[max(10, d*3) for d in node_degree],
308
- color=node_degree,
309
- colorscale='Viridis',
310
- colorbar=dict(
311
- thickness=15,
312
- len=0.5,
313
- x=1.02,
314
- title="Node Degree"
315
- ),
316
- line=dict(width=2, color='white')
317
  )
318
- )
319
-
320
- # Create figure
321
- fig = go.Figure(data=[edge_trace, node_trace],
322
- layout=go.Layout(
323
- title='Interactive Kolam Graph Structure',
324
- titlefont_size=16,
325
- showlegend=False,
326
- hovermode='closest',
327
- margin=dict(b=20,l=5,r=5,t=40),
328
- annotations=[ dict(
329
- text="Node size represents degree centrality",
330
- showarrow=False,
331
- xref="paper", yref="paper",
332
- x=0.005, y=-0.002,
333
- xanchor="left", yanchor="bottom",
334
- font=dict(size=12)
335
- )],
336
- xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
337
- yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
338
- plot_bgcolor='white'
339
- ))
340
-
341
- return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
 
343
  # Initialize analyzer
344
- analyzer = KolamAnalyzer()
 
 
 
 
345
 
346
  # Main content area
347
  col1, col2 = st.columns([1, 2])
348
 
 
349
  try:
350
  import pandas as pd
351
  PANDAS_AVAILABLE = True
@@ -371,112 +452,83 @@ except ImportError as e:
371
  with col1:
372
  st.markdown("### πŸ“€ Upload Kolam Image")
373
 
374
- #uploaded_file = st.file_uploader(
375
- #"Choose a Kolam image...",
376
- #type=["png", "jpg", "jpeg"],
377
- #help="Upload a clear image of a Kolam design for analysis"
378
- #)
379
  uploaded_file = st.file_uploader(
380
  "Choose a Kolam image...",
381
  type=["png", "jpg", "jpeg"],
382
- help="Upload a clear image of a Kolam design for analysis"
 
383
  )
384
 
385
- # Store in session state only if a new file is uploaded
386
- #if uploaded_file is not None:
387
- #st.session_state['uploaded_image'] = Image.open(uploaded_file)
388
- # Only update session state if a new file is uploaded
389
  if uploaded_file is not None:
390
- if st.session_state['uploaded_image'] is None:
 
 
391
  st.session_state['uploaded_image'] = Image.open(uploaded_file)
392
- # Display the uploaded image if available
393
- #if st.session_state['uploaded_image'] is not None:
394
- # st.image(st.session_state['uploaded_image'], caption="Uploaded Kolam", use_column_width=True)
395
 
396
-
397
- #if uploaded_file:
398
- # Display uploaded image
399
- #image = Image.open(uploaded_file)
400
- #st.image(image, caption="Uploaded Kolam", use_column_width=True)
401
-
402
- # Analysis button
403
- #if st.button("πŸ” Analyze Kolam Design", type="primary"):
404
- #with st.spinner("Analyzing Kolam design..."):
405
- # Process image
406
- #original, thresh, edges = analyzer.preprocess_image(
407
- #image, image_size, threshold_value, canny_low, canny_high
408
- #)
409
-
410
- # Detect nodes and edges
411
- #nodes = analyzer.detect_nodes(edges, max_corners)
412
- #graph_edges = analyzer.detect_edges(edges, nodes, min_line_length)
413
-
414
- # Build graph
415
- #G = analyzer.build_graph(nodes, graph_edges)
416
-
417
- # Extract features
418
- #features = analyzer.extract_graph_features(G)
419
-
420
- # Store results in session state
421
- #st.session_state.analysis_complete = True
422
- #st.session_state.original_img = original
423
- #st.session_state.thresh_img = thresh
424
- #st.session_state.edges_img = edges
425
- #st.session_state.nodes = nodes
426
- #st.session_state.graph = G
427
- #st.session_state.features = features
428
-
429
- # Generate encryption key
430
- #encryption_key = analyzer.generate_encryption_key()
431
- #st.session_state.encryption_key = encryption_key
432
-
433
- #st.success("βœ… Analysis completed successfully!")
434
- #if st.session_state['uploaded_image'] is not None:
435
- #if st.button("πŸ” Analyze Kolam Design", type="primary"):
436
- #with st.spinner("Analyzing Kolam design..."):
437
  if st.session_state['uploaded_image'] is not None:
438
  st.image(st.session_state['uploaded_image'], caption="Uploaded Kolam", use_column_width=True)
439
 
440
  # Analysis button
441
- if st.button("πŸ” Analyze Kolam Design"):
 
 
442
  with st.spinner("Analyzing Kolam design..."):
443
- # Process image
444
- original, thresh, edges = analyzer.preprocess_image(
445
- st.session_state['uploaded_image'], image_size, threshold_value, canny_low, canny_high
446
- )
447
-
448
- # Detect nodes and edges
449
- nodes = analyzer.detect_nodes(edges, max_corners)
450
- graph_edges = analyzer.detect_edges(edges, nodes, min_line_length)
451
-
452
- # Build graph
453
- G = analyzer.build_graph(nodes, graph_edges)
454
-
455
- # Extract features
456
- features = analyzer.extract_graph_features(G)
457
-
458
- # Save results in session state
459
- st.session_state.analysis_complete = True
460
- st.session_state.original_img = original
461
- st.session_state.thresh_img = thresh
462
- st.session_state.edges_img = edges
463
- st.session_state.nodes = nodes
464
- st.session_state.graph = G
465
- st.session_state.features = features
466
-
467
- # Generate encryption key
468
- encryption_key = analyzer.generate_encryption_key()
469
- st.session_state.encryption_key = encryption_key
470
-
471
- st.success("βœ… Analysis completed successfully!")
472
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
 
474
  with col2:
475
  st.markdown("### πŸ“Š Analysis Results")
476
 
477
- #if hasattr(st.session_state, 'analysis_complete') and st.session_state.analysis_complete:
478
- if st.session_state.analysis_complete:
479
- # Use st.session_state.original_img, st.session_state.graph, etc.
480
 
481
  # Create tabs for different visualizations
482
  tab1, tab2, tab3, tab4 = st.tabs(["πŸ–ΌοΈ Image Processing", "πŸ“ˆ Graph Analysis", "πŸ“Š Features", "πŸ” Security"])
@@ -484,146 +536,166 @@ with col2:
484
  with tab1:
485
  st.markdown("#### Image Processing Pipeline")
486
 
487
- # Create subplot for processed images
488
- fig, axes = plt.subplots(1, 3, figsize=(15, 5))
489
-
490
- axes[0].imshow(st.session_state.original_img, cmap='gray')
491
- axes[0].set_title('Original Grayscale', fontsize=12, fontweight='bold')
492
- axes[0].axis('off')
493
-
494
- axes[1].imshow(st.session_state.thresh_img, cmap='gray')
495
- axes[1].set_title('Binary Threshold', fontsize=12, fontweight='bold')
496
- axes[1].axis('off')
497
-
498
- axes[2].imshow(st.session_state.edges_img, cmap='gray')
499
- axes[2].set_title('Edge Detection', fontsize=12, fontweight='bold')
500
- axes[2].axis('off')
501
-
502
- plt.tight_layout()
503
- st.pyplot(fig)
504
-
505
- # Show detected nodes
506
- st.markdown("#### Detected Corner Points")
507
- img_with_nodes = st.session_state.original_img.copy()
508
- for x, y in st.session_state.nodes:
509
- cv2.circle(img_with_nodes, (int(x), int(y)), 3, (255), -1)
510
-
511
- fig_nodes, ax_nodes = plt.subplots(1, 1, figsize=(8, 8))
512
- ax_nodes.imshow(img_with_nodes, cmap='gray')
513
- ax_nodes.set_title(f'Detected Nodes: {len(st.session_state.nodes)}',
514
- fontsize=14, fontweight='bold')
515
- ax_nodes.axis('off')
516
- st.pyplot(fig_nodes)
 
 
 
 
 
517
 
518
  with tab2:
519
  st.markdown("#### Interactive Graph Visualization")
520
 
521
- # Create interactive graph
522
- if st.session_state.graph.number_of_nodes() > 0:
523
- fig_interactive = analyzer.create_interactive_graph(st.session_state.graph)
524
- st.plotly_chart(fig_interactive, use_container_width=True)
525
- else:
526
- st.warning("No graph structure detected in the image.")
527
-
528
- # Graph statistics
529
- col_a, col_b = st.columns(2)
530
- with col_a:
531
- st.metric("Total Nodes", st.session_state.features['num_nodes'])
532
- st.metric("Total Edges", st.session_state.features['num_edges'])
533
- st.metric("Graph Density", st.session_state.features['density'])
534
-
535
- with col_b:
536
- st.metric("Average Degree", st.session_state.features['avg_degree'])
537
- st.metric("Number of Cycles", st.session_state.features['num_cycles'])
538
- st.metric("Connected Components", st.session_state.features['num_components'])
 
 
 
 
539
 
540
  with tab3:
541
  st.markdown("#### Mathematical Properties")
542
 
543
- # Create metrics dataframe
544
- features_df = pd.DataFrame([
545
- {"Property": "Nodes", "Value": st.session_state.features['num_nodes']},
546
- {"Property": "Edges", "Value": st.session_state.features['num_edges']},
547
- {"Property": "Average Degree", "Value": st.session_state.features['avg_degree']},
548
- {"Property": "Maximum Degree", "Value": st.session_state.features['max_degree']},
549
- {"Property": "Minimum Degree", "Value": st.session_state.features['min_degree']},
550
- {"Property": "Cycles", "Value": st.session_state.features['num_cycles']},
551
- {"Property": "Graph Density", "Value": st.session_state.features['density']},
552
- {"Property": "Average Betweenness", "Value": st.session_state.features['avg_betweenness']},
553
- {"Property": "Average Closeness", "Value": st.session_state.features['avg_closeness']},
554
- {"Property": "Connected", "Value": "Yes" if st.session_state.features['is_connected'] else "No"},
555
- {"Property": "Components", "Value": st.session_state.features['num_components']}
556
- ])
557
-
558
- st.dataframe(features_df, use_container_width=True)
559
-
560
- # Visualize degree distribution
561
- if st.session_state.graph.number_of_nodes() > 0:
562
- degrees = [d for _, d in st.session_state.graph.degree()]
563
- fig_hist = px.histogram(
564
- x=degrees,
565
- title="Degree Distribution",
566
- labels={'x': 'Node Degree', 'y': 'Frequency'},
567
- color_discrete_sequence=['#FF6B35']
568
- )
569
- fig_hist.update_layout(
570
- plot_bgcolor='white',
571
- paper_bgcolor='white'
572
- )
573
- st.plotly_chart(fig_hist, use_container_width=True)
 
 
 
 
 
 
 
 
574
 
575
  with tab4:
576
  st.markdown("#### Security & Data Protection")
577
 
578
- if CRYPTO_AVAILABLE:
579
- # Encrypt graph
580
- encrypted_data = analyzer.encrypt_graph(st.session_state.graph)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
 
582
- if encrypted_data:
583
- col_x, col_y = st.columns(2)
584
- with col_x:
585
- st.success(f"πŸ” Graph data encrypted successfully!")
586
- st.info(f"Encrypted data size: {len(encrypted_data)} bytes")
 
 
 
587
 
588
- with col_y:
589
- if hasattr(st.session_state, 'encryption_key'):
590
- st.code(f"Encryption Key:\n{st.session_state.encryption_key}", language="text")
591
- else:
592
- st.error("Failed to encrypt graph data")
593
- else:
594
- st.warning("πŸ”’ Encryption not available due to package compatibility issues.")
595
- st.info("Graph data will be stored in plain text format.")
596
 
597
- # Download options
598
- st.markdown("#### πŸ“₯ Download Results")
599
-
600
- col_dl1, col_dl2 = st.columns(2)
601
- with col_dl1:
602
- # Prepare features for download
603
- if PANDAS_AVAILABLE:
604
- features_json = pd.DataFrame([st.session_state.features]).to_json(orient='records')
605
- else:
606
- import json
607
- features_json = json.dumps([st.session_state.features], indent=2)
608
-
609
- st.download_button(
610
- "πŸ“Š Download Features (JSON)",
611
- data=features_json,
612
- file_name="kolam_features.json",
613
- mime="application/json"
614
- )
615
-
616
- with col_dl2:
617
- # Prepare adjacency matrix
618
- adj_matrix = nx.to_numpy_array(st.session_state.graph)
619
- adj_buffer = io.BytesIO()
620
- np.save(adj_buffer, adj_matrix)
621
- st.download_button(
622
- "πŸ”’ Download Adjacency Matrix",
623
- data=adj_buffer.getvalue(),
624
- file_name="kolam_adjacency.npy",
625
- mime="application/octet-stream"
626
- )
627
  else:
628
  st.info("πŸ‘† Please upload a Kolam image and click 'Analyze' to see results")
629
 
 
4
  import networkx as nx
5
  import matplotlib.pyplot as plt
6
  import pandas as pd
 
7
  import io
8
  import base64
9
  from PIL import Image
 
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
  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(
 
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)
102
 
 
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):
 
166
 
167
  def generate_encryption_key(self):
168
  """Generate encryption key for graph data"""
169
+ try:
170
+ from cryptography.fernet import Fernet
171
+ self.encryption_key = Fernet.generate_key()
172
+ self.cipher = Fernet(self.encryption_key)
173
+ return self.encryption_key.decode()
174
+ except ImportError:
175
+ return "Encryption not available"
176
 
177
  def preprocess_image(self, image, size, threshold_val, canny_low, canny_high):
178
  """Preprocess uploaded image"""
179
+ try:
180
+ # Convert PIL image to OpenCV format
181
+ img_array = np.array(image)
182
+ if len(img_array.shape) == 3:
183
+ img_gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
184
+ else:
185
+ img_gray = img_array
186
+
187
+ # Resize image
188
+ img_resized = cv2.resize(img_gray, (size, size))
189
 
190
+ # Apply binary threshold
191
+ _, thresh = cv2.threshold(img_resized, threshold_val, 255, cv2.THRESH_BINARY_INV)
192
+
193
+ # Edge detection
194
+ edges = cv2.Canny(thresh, canny_low, canny_high)
195
+
196
+ return img_resized, thresh, edges
197
+ except Exception as e:
198
+ st.error(f"Error in image preprocessing: {str(e)}")
199
+ return None, None, None
200
 
201
  def detect_nodes(self, edges, max_corners):
202
  """Detect corner points as graph nodes"""
203
+ try:
204
+ corners = cv2.goodFeaturesToTrack(
205
+ edges,
206
+ maxCorners=max_corners,
207
+ qualityLevel=0.01,
208
+ minDistance=5
209
+ )
210
+ if corners is None:
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):
218
  """Detect lines and create graph edges"""
219
+ try:
220
+ lines = cv2.HoughLinesP(
221
+ edges,
222
+ 1,
223
+ np.pi/180,
224
+ threshold=30,
225
+ minLineLength=min_line_length,
226
+ maxLineGap=10
227
+ )
228
+
229
+ graph_edges = []
230
+ if lines is not None and len(nodes) > 0:
231
+ for x1, y1, x2, y2 in lines[:,0]:
232
  n1 = min(range(len(nodes)),
233
  key=lambda i: np.linalg.norm(np.array(nodes[i]) - np.array([x1,y1])))
234
  n2 = min(range(len(nodes)),
235
  key=lambda i: np.linalg.norm(np.array(nodes[i]) - np.array([x2,y2])))
236
  if n1 != n2 and (n1, n2) not in graph_edges and (n2, n1) not in graph_edges:
237
  graph_edges.append((n1, n2))
 
 
 
 
238
 
239
+ # Fallback: connect nearby nodes if no lines detected
240
+ if len(graph_edges) == 0:
241
+ graph_edges = self.connect_nearby_nodes(nodes, max_distance=30)
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):
249
  """Connect nearby nodes as fallback"""
 
258
 
259
  def build_graph(self, nodes, edges):
260
  """Build NetworkX graph from nodes and edges"""
261
+ try:
262
+ G = nx.Graph()
263
+ for idx, node in enumerate(nodes):
264
+ G.add_node(idx, pos=node)
265
+ for n1, n2 in edges:
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):
273
  """Extract mathematical features from the graph"""
 
 
 
 
 
 
 
 
274
  try:
275
+ num_nodes = G.number_of_nodes()
276
+ num_edges = G.number_of_edges()
277
+ degrees = [d for _, d in G.degree()]
278
+ avg_degree = np.mean(degrees) if degrees else 0
279
+ max_degree = max(degrees) if degrees else 0
280
+ min_degree = min(degrees) if degrees else 0
281
 
282
+ # Calculate cycles
283
+ try:
284
+ num_cycles = sum(1 for c in nx.cycle_basis(G))
285
+ except:
286
+ num_cycles = 0
287
+
288
+ # Calculate connectivity
289
+ is_connected = nx.is_connected(G) if num_nodes > 0 else False
290
+ num_components = nx.number_connected_components(G)
291
 
292
+ # Calculate centrality measures
293
+ try:
294
+ betweenness = nx.betweenness_centrality(G)
295
+ avg_betweenness = np.mean(list(betweenness.values())) if betweenness else 0
296
+
297
+ closeness = nx.closeness_centrality(G)
298
+ avg_closeness = np.mean(list(closeness.values())) if closeness else 0
299
+ except:
300
+ avg_betweenness = 0
301
+ avg_closeness = 0
302
+
303
+ return {
304
+ "num_nodes": num_nodes,
305
+ "num_edges": num_edges,
306
+ "avg_degree": round(avg_degree, 2),
307
+ "max_degree": max_degree,
308
+ "min_degree": min_degree,
309
+ "num_cycles": num_cycles,
310
+ "is_connected": is_connected,
311
+ "num_components": num_components,
312
+ "avg_betweenness": round(avg_betweenness, 4),
313
+ "avg_closeness": round(avg_closeness, 4),
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
 
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:
482
+ # Process image with current parameters
483
+ params = st.session_state['params']
484
+ original, thresh, edges = analyzer.preprocess_image(
485
+ st.session_state['uploaded_image'],
486
+ params['image_size'],
487
+ params['threshold_value'],
488
+ params['canny_low'],
489
+ params['canny_high']
490
+ )
491
+
492
+ if original is not None:
493
+ # Detect nodes and edges
494
+ nodes = analyzer.detect_nodes(edges, params['max_corners'])
495
+ graph_edges = analyzer.detect_edges(edges, nodes, params['min_line_length'])
496
+
497
+ # Build graph
498
+ G = analyzer.build_graph(nodes, graph_edges)
499
+
500
+ # Extract features
501
+ features = analyzer.extract_graph_features(G)
502
+
503
+ # Store results in session state
504
+ st.session_state['analysis_results'] = {
505
+ 'original_img': original,
506
+ 'thresh_img': thresh,
507
+ 'edges_img': edges,
508
+ 'nodes': nodes,
509
+ 'graph': G,
510
+ 'features': features
511
+ }
512
+
513
+ # Generate encryption key
514
+ encryption_key = analyzer.generate_encryption_key()
515
+ st.session_state['analysis_results']['encryption_key'] = encryption_key
516
+
517
+ st.session_state['analysis_complete'] = True
518
+ st.success("βœ… Analysis completed successfully!")
519
+ else:
520
+ st.error("Failed to process the image. Please try with different parameters.")
521
+
522
+ except Exception as e:
523
+ st.error(f"Analysis failed: {str(e)}")
524
+ finally:
525
+ st.session_state['processing'] = False
526
 
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"])
 
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",
633
+ labels={'x': 'Node Degree', 'y': 'Frequency'},
634
+ color_discrete_sequence=['#FF6B35']
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