Spaces:
Sleeping
Sleeping
| 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) # Clamp between 15-90% | |
| pixel_density = (width * height) / 1000000 # Megapixels | |
| if pixel_density > 2.0: # High resolution image | |
| area_multiplier = 1.2 | |
| elif pixel_density > 1.0: # Medium resolution | |
| area_multiplier = 1.0 | |
| else: # Low resolution | |
| area_multiplier = 0.8 | |
| estimated_roof_area_m2 = (usable_percent / 100) * area_multiplier * (50 + (total_area / 50000)) | |
| panel_area = 1.65 # m² per panel | |
| max_panels = int(estimated_roof_area_m2 / panel_area) | |
| system_kw = max_panels * 0.4 # 400W per panel | |
| if condition == "excellent": | |
| system_kw *= 1.1 | |
| elif condition == "poor": | |
| system_kw *= 0.7 | |
| system_kw = max(min(system_kw, 20), 2) | |
| confidence = 40 | |
| # Sharpness contribution (0-30 points) | |
| if laplacian_var > 800: | |
| confidence += 30 | |
| elif laplacian_var > 500: | |
| confidence += 25 | |
| elif laplacian_var > 200: | |
| confidence += 15 | |
| elif laplacian_var > 100: | |
| confidence += 8 | |
| # Brightness contribution (0-20 points) | |
| if 80 < brightness < 180: | |
| confidence += 20 | |
| elif 60 < brightness < 200: | |
| confidence += 12 | |
| elif 40 < brightness < 220: | |
| confidence += 5 | |
| # Contrast contribution (0-15 points) | |
| 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") | |
| # CSS for proper display | |
| 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") | |
| # Recommendations | |
| 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'])}") | |
| # Download | |
| 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() | |