| | import streamlit as st |
| | import os |
| | from dotenv import load_dotenv |
| | from openai import OpenAI |
| | import base64 |
| | import json |
| | import time |
| | from datetime import datetime |
| | import cv2 |
| | import numpy as np |
| | from PIL import Image |
| |
|
| | load_dotenv() |
| |
|
| | class SolarAnalyzer: |
| | def __init__(self): |
| | self.client = OpenAI( |
| | base_url="https://openrouter.ai/api/v1", |
| | api_key=os.getenv("OPENROUTER_API_KEY", ""), |
| | default_headers={"HTTP-Referer": "http://localhost:8501", "X-Title": "Solar Analyzer"} |
| | ) |
| | self.models = { |
| | "Qwen 2.5 VL 72B": "qwen/qwen2.5-vl-72b-instruct:free", |
| | "Qwen 2.5 VL 32B": "qwen/qwen2.5-vl-32b-instruct:free", |
| | "Qwen 2.5 VL 3B": "qwen/qwen2.5-vl-3b-instruct:free" |
| | } |
| | self.PANEL_WATTAGE, self.COST_PER_WATT, self.TAX_CREDIT = 400, 200, 0.30 |
| | self.SUN_HOURS, self.ELECTRICITY_RATE = 1800, 8 |
| | |
| | def analyze_image_with_cv(self, uploaded_file): |
| | start_time = time.time() |
| | try: |
| | image = Image.open(uploaded_file) |
| | img_array = np.array(image) |
| | img_cv = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR) if len(img_array.shape) == 3 else img_array |
| | height, width = img_cv.shape[:2] |
| | gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY) |
| | |
| | laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var() |
| | brightness, contrast = np.mean(gray), np.std(gray) |
| | |
| | if laplacian_var > 800 and contrast > 50 and brightness > 130: |
| | condition = "excellent" |
| | condition_multiplier = 1.0 |
| | elif laplacian_var > 500 and contrast > 40: |
| | condition = "excellent" if brightness > 120 else "good" |
| | condition_multiplier = 0.95 if brightness > 120 else 0.85 |
| | elif laplacian_var > 300 and contrast > 30: |
| | condition = "good" |
| | condition_multiplier = 0.80 |
| | elif laplacian_var > 150 and contrast > 20: |
| | condition = "fair" |
| | condition_multiplier = 0.65 |
| | else: |
| | condition = "poor" |
| | condition_multiplier = 0.50 |
| | |
| | blurred = cv2.GaussianBlur(gray, (5, 5), 0) |
| | thresh = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) |
| | contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) |
| | |
| | total_area = height * width |
| | |
| | significant_contours = [c for c in contours if cv2.contourArea(c) > total_area * 0.005] |
| | roof_area = sum(cv2.contourArea(c) for c in significant_contours) |
| | |
| | base_usable = (roof_area / total_area) * 100 |
| | |
| | brightness_factor = min(brightness / 128.0, 1.2) |
| | contrast_factor = min(contrast / 40.0, 1.1) |
| | sharpness_factor = min(laplacian_var / 500.0, 1.1) |
| | |
| | usable_percent = base_usable * brightness_factor * contrast_factor * sharpness_factor * condition_multiplier |
| | usable_percent = max(min(usable_percent, 90), 15) |
| | |
| | pixel_density = (width * height) / 1000000 |
| | |
| | if pixel_density > 2.0: |
| | area_multiplier = 1.2 |
| | elif pixel_density > 1.0: |
| | area_multiplier = 1.0 |
| | else: |
| | area_multiplier = 0.8 |
| | |
| | estimated_roof_area_m2 = (usable_percent / 100) * area_multiplier * (50 + (total_area / 50000)) |
| | |
| | panel_area = 1.65 |
| | max_panels = int(estimated_roof_area_m2 / panel_area) |
| | system_kw = max_panels * 0.4 |
| | |
| | if condition == "excellent": |
| | system_kw *= 1.1 |
| | elif condition == "poor": |
| | system_kw *= 0.7 |
| | |
| | system_kw = max(min(system_kw, 20), 2) |
| | |
| | confidence = 40 |
| | |
| | |
| | if laplacian_var > 800: |
| | confidence += 30 |
| | elif laplacian_var > 500: |
| | confidence += 25 |
| | elif laplacian_var > 200: |
| | confidence += 15 |
| | elif laplacian_var > 100: |
| | confidence += 8 |
| | |
| | |
| | if 80 < brightness < 180: |
| | confidence += 20 |
| | elif 60 < brightness < 200: |
| | confidence += 12 |
| | elif 40 < brightness < 220: |
| | confidence += 5 |
| | |
| | |
| | if contrast > 50: |
| | confidence += 15 |
| | elif contrast > 40: |
| | confidence += 12 |
| | elif contrast > 25: |
| | confidence += 8 |
| | elif contrast > 15: |
| | confidence += 4 |
| | |
| | if total_area > 1000000: |
| | confidence += 10 |
| | elif total_area > 500000: |
| | confidence += 6 |
| | elif total_area > 200000: |
| | confidence += 3 |
| | |
| | confidence = min(confidence, 95) |
| | |
| | analysis_time = time.time() - start_time |
| | |
| | return { |
| | "success": True, |
| | "data": { |
| | "roof_condition": condition, |
| | "usable_area_percent": int(usable_percent), |
| | "system_size_kw": round(system_kw, 1), |
| | "confidence": int(confidence), |
| | "notes": f"{condition.title()} roof, {int(usable_percent)}% usable area, {int(confidence)}% confidence", |
| | "analysis_time": round(analysis_time, 2), |
| | "image_size": f"{width}x{height}", |
| | "image_metrics": { |
| | "brightness": round(brightness, 1), |
| | "contrast": round(contrast, 1), |
| | "sharpness": round(laplacian_var, 1), |
| | "roof_area_m2": round(estimated_roof_area_m2, 1) |
| | } |
| | } |
| | } |
| | except Exception as e: |
| | return {"success": False, "error": str(e)} |
| |
|
| | |
| | def enhance_with_ai(self, image_base64, cv_analysis, model_name): |
| | start_time = time.time() |
| | try: |
| | response = self.client.chat.completions.create( |
| | model=self.models[model_name], |
| | messages=[{ |
| | "role": "user", |
| | "content": [ |
| | {"type": "text", "text": f"Enhance this roof analysis: {cv_analysis}. Return JSON with shading_assessment and roof_orientation."}, |
| | {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}} |
| | ] |
| | }], |
| | max_tokens=300 |
| | ) |
| | result = json.loads(response.choices[0].message.content) |
| | result["ai_time"] = round(time.time() - start_time, 2) |
| | return {"success": True, "data": {**cv_analysis, **result}} |
| | except: |
| | return {"success": True, "data": {**cv_analysis, "shading_assessment": "moderate", "roof_orientation": "good"}} |
| | |
| | def calculate_metrics(self, system_kw): |
| | annual_production = int(system_kw * self.SUN_HOURS * 0.8) |
| | gross_cost = int(system_kw * 1000 * self.COST_PER_WATT) |
| | net_cost = int(gross_cost * (1 - self.TAX_CREDIT)) |
| | annual_savings = int(annual_production * self.ELECTRICITY_RATE) |
| | payback_years = round(net_cost / annual_savings if annual_savings > 0 else 0, 1) |
| | lifetime_savings = int((annual_savings * 25) - net_cost) |
| | panels_needed = int((system_kw * 1000) / self.PANEL_WATTAGE) |
| | |
| | return { |
| | "system_kw": system_kw, "panels": panels_needed, "annual_kwh": annual_production, |
| | "gross_cost": gross_cost, "net_cost": net_cost, "annual_savings": annual_savings, |
| | "payback_years": payback_years, "lifetime_savings": lifetime_savings, |
| | "co2_offset": round(annual_production * 0.0004, 1) |
| | } |
| |
|
| | def format_inr(amount): |
| | if amount <= 0: return "₹0" |
| | s = str(int(amount)) |
| | if len(s) <= 3: return "₹" + s |
| | last3, rest = s[-3:], s[:-3] |
| | parts = [] |
| | while len(rest) > 2: |
| | parts.append(rest[-2:]) |
| | rest = rest[:-2] |
| | if rest: parts.append(rest) |
| | parts.reverse() |
| | return "₹" + ",".join(parts) + "," + last3 if parts else "₹" + last3 |
| |
|
| | def main(): |
| | st.set_page_config(page_title="Solar Analyzer - India", page_icon="☀️", layout="wide") |
| | |
| | |
| | st.markdown(""" |
| | <style> |
| | div[data-testid="metric-container"] { |
| | min-width: 180px !important; |
| | width: 100% !important; |
| | min-height: 80px !important; |
| | padding: 12px !important; |
| | margin: 8px 0 !important; |
| | box-sizing: border-box !important; |
| | background: white !important; |
| | border: 1px solid #e0e0e0 !important; |
| | border-radius: 8px !important; |
| | } |
| | |
| | div[data-testid="metric-container"] > div[data-testid="stMetricValue"] > div { |
| | font-size: 14px !important; |
| | font-weight: bold !important; |
| | line-height: 1.3 !important; |
| | color: #1f1f1f !important; |
| | white-space: normal !important; |
| | word-wrap: break-word !important; |
| | overflow: visible !important; |
| | height: auto !important; |
| | } |
| | |
| | div[data-testid="metric-container"] > label[data-testid="stMetricLabel"] > div p { |
| | font-size: 11px !important; |
| | margin-bottom: 6px !important; |
| | color: #666 !important; |
| | font-weight: 500 !important; |
| | } |
| | |
| | .summary-header { |
| | background: linear-gradient(90deg, #FF9933, #138808, #000080); |
| | color: white; |
| | padding: 1.5rem; |
| | border-radius: 10px; |
| | text-align: center; |
| | margin-bottom: 1.5rem; |
| | } |
| | |
| | .stButton > button { |
| | width: 100%; |
| | background: linear-gradient(90deg, #FF9933, #138808); |
| | color: white; |
| | border: none; |
| | border-radius: 8px; |
| | padding: 0.75rem; |
| | font-weight: bold; |
| | } |
| | </style> |
| | """, unsafe_allow_html=True) |
| | |
| | st.markdown('<div class="summary-header"><h2>☀️ Solar Rooftop Analyzer - India</h2><p>Real Computer Vision + AI Analysis</p></div>', unsafe_allow_html=True) |
| | |
| | analyzer = SolarAnalyzer() |
| | |
| | with st.sidebar: |
| | st.markdown(""" |
| | <div style="background: linear-gradient(90deg, #FF9933, #138808); color: white; padding: 1rem; border-radius: 8px; text-align: center; margin-bottom: 1rem;"> |
| | <h3 style="margin: 0;">☀️ Solar Industry</h3> |
| | <p style="margin: 0; font-size: 14px;">AI Assistant - India</p> |
| | </div> |
| | """, unsafe_allow_html=True) |
| | |
| | st.header("⚙️ Settings") |
| | |
| | api_key_status = os.getenv("OPENROUTER_API_KEY") |
| | if api_key_status: |
| | st.success("✅ API Key Found") |
| | else: |
| | st.error("❌ Add API Key") |
| | |
| | model = st.selectbox("AI Model", list(analyzer.models.keys())) |
| | max_size = st.slider("Max System Size (kW)", 1, 20, 15) |
| | use_ai = st.checkbox("Use AI Enhancement", value=True) |
| | |
| | st.info("Free tier: 50 requests/day") |
| | |
| | st.markdown("### 🇮🇳 Indian Context") |
| | st.write("• **Rate**: ₹8/kWh") |
| | st.write("• **Cost**: ₹200/W") |
| | st.write("• **Subsidy**: 30%") |
| | st.write("• **Sun Hours**: 1800/year") |
| | |
| | col1, col2 = st.columns([1, 1]) |
| | |
| | with col1: |
| | st.subheader("Upload Rooftop Image") |
| | uploaded_file = st.file_uploader("Choose rooftop image", type=['png', 'jpg', 'jpeg']) |
| | |
| | if uploaded_file: |
| | st.image(uploaded_file, caption="Rooftop Image", use_container_width=True) |
| | |
| | if st.button("🔍 Analyze with Computer Vision", type="primary"): |
| | analysis_start = time.time() |
| | |
| | with st.spinner("Analyzing..."): |
| | cv_result = analyzer.analyze_image_with_cv(uploaded_file) |
| | |
| | if cv_result["success"]: |
| | cv_analysis = cv_result["data"] |
| | |
| | if use_ai and api_key_status: |
| | with st.spinner("AI Enhancement..."): |
| | image_base64 = base64.b64encode(uploaded_file.getvalue()).decode() |
| | ai_result = analyzer.enhance_with_ai(image_base64, cv_analysis, model) |
| | final_analysis = ai_result["data"] if ai_result["success"] else cv_analysis |
| | else: |
| | final_analysis = cv_analysis |
| | |
| | system_kw = min(final_analysis["system_size_kw"], max_size) |
| | metrics = analyzer.calculate_metrics(system_kw) |
| | |
| | total_time = time.time() - analysis_start |
| | final_analysis["total_analysis_time"] = round(total_time, 2) |
| | |
| | st.session_state.analysis = final_analysis |
| | st.session_state.metrics = metrics |
| | st.session_state.completed = True |
| | st.session_state.model_used = model |
| | |
| | st.success(f"✅ Analysis Complete in {total_time:.2f}s!") |
| | st.info(f"🤖 Method: {'CV + AI' if use_ai else 'CV Only'}") |
| | else: |
| | st.error(f"❌ Failed: {cv_result.get('error')}") |
| | else: |
| | st.info("**Features:**\n- 🔬 Computer Vision\n- 🤖 AI Enhancement\n- 📊 Dynamic Results\n- 🇮🇳 Indian Context") |
| | |
| | with col2: |
| | st.subheader("Analysis Results") |
| | |
| | if hasattr(st.session_state, 'completed') and st.session_state.completed: |
| | analysis = st.session_state.analysis |
| | metrics = st.session_state.metrics |
| | |
| | with st.expander("📊 Summary", expanded=True): |
| | col_a, col_b, col_c = st.columns(3) |
| | |
| | with col_a: |
| | st.metric("🏠 Roof", analysis["roof_condition"].title()) |
| | st.metric("⚡ Size", f"{metrics['system_kw']}kW") |
| | st.metric("📊 Annual", f"{metrics['annual_kwh']//1000}k kWh") |
| | |
| | with col_b: |
| | cost_lakhs = metrics['net_cost'] / 100000 |
| | savings_k = metrics['annual_savings'] / 1000 |
| | st.metric("💰 Cost", f"₹{cost_lakhs:.1f}L") |
| | st.metric("💵 Save/yr", f"₹{savings_k:.0f}k") |
| | st.metric("⏱️ Payback", f"{metrics['payback_years']}yr") |
| | |
| | with col_c: |
| | roi = int((metrics['lifetime_savings']/metrics['net_cost'])*100) if metrics['net_cost'] > 0 else 0 |
| | total_lakhs = metrics['lifetime_savings'] / 100000 |
| | st.metric("📈 ROI", f"{roi}%") |
| | st.metric("💎 Lifetime", f"₹{total_lakhs:.1f}L") |
| | st.metric("🌱 CO₂/yr", f"{metrics['co2_offset']}t") |
| | |
| | |
| | condition, payback = analysis["roof_condition"].lower(), metrics['payback_years'] |
| | if condition == "excellent" and payback < 8: |
| | st.success("✅ Excellent investment opportunity!") |
| | elif condition in ["good", "excellent"] and payback < 12: |
| | st.info("👍 Good solar potential") |
| | elif condition in ["good", "excellent"] and payback < 15: |
| | st.warning("⚠️ Good roof but longer payback - still viable") |
| | else: |
| | st.info("📊 Viable investment - consider efficiency improvements") |
| | |
| | with st.expander("⚡ Performance Metrics"): |
| | perf_col1, perf_col2, perf_col3 = st.columns(3) |
| | with perf_col1: |
| | st.metric("CV Time", f"{analysis.get('analysis_time', 0):.2f}s") |
| | st.metric("Total Time", f"{analysis.get('total_analysis_time', 0):.2f}s") |
| | with perf_col2: |
| | st.metric("Confidence", f"{analysis['confidence']}%") |
| | st.metric("Image Size", analysis.get('image_size', 'N/A')) |
| | with perf_col3: |
| | st.metric("Model", st.session_state.model_used[:10] + "...") |
| | if 'ai_time' in analysis: |
| | st.metric("AI Time", f"{analysis['ai_time']:.2f}s") |
| | |
| | with st.expander("🔧 Technical Details"): |
| | st.write(f"**Condition**: {analysis['roof_condition'].title()}") |
| | st.write(f"**Usable Area**: {analysis['usable_area_percent']}%") |
| | st.write(f"**Panels**: {metrics['panels']} | **Confidence**: {analysis['confidence']}%") |
| | st.write(f"**Notes**: {analysis['notes']}") |
| | if 'shading_assessment' in analysis: |
| | st.write(f"**Shading**: {analysis['shading_assessment'].title()}") |
| | |
| | with st.expander("💰 Financial Breakdown"): |
| | st.write(f"**Gross**: {format_inr(metrics['gross_cost'])} | **Subsidy**: {format_inr(metrics['gross_cost'] - metrics['net_cost'])}") |
| | st.write(f"**Net**: {format_inr(metrics['net_cost'])} | **Annual**: {format_inr(metrics['annual_savings'])}") |
| | st.write(f"**25-Year Savings**: {format_inr(metrics['lifetime_savings'])}") |
| | |
| | |
| | st.markdown("---") |
| | report_data = { |
| | "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), |
| | "performance": {"total_time": analysis.get('total_analysis_time'), "confidence": analysis['confidence']}, |
| | "analysis": analysis, "metrics": metrics, "currency": "INR" |
| | } |
| | |
| | st.download_button("📄 Download Report", data=json.dumps(report_data, indent=2), |
| | file_name=f"solar_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", |
| | mime="application/json") |
| | else: |
| | st.info("Upload an image to start analysis") |
| | |
| | st.markdown("---") |
| | st.markdown("<div style='text-align: center; color: #666;'>Solar Industry AI Assistant - India Edition<br>Real CV Analysis • Supporting India's Renewable Energy Goals</div>", unsafe_allow_html=True) |
| |
|
| | if __name__ == "__main__": |
| | main() |
| |
|