Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Monitor construction progress using computer vision. Analyze site photos and drone imagery to track work completion, detect safety issues, and compare against BIM models.
.claude/skills/datadrivenconstruction-progress-monitoring-cv/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 253% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 86% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 109% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 112% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 164% | 0% |
This skill implements computer vision for construction progress monitoring. Analyze site images automatically to track completion, detect hazards, and compare physical progress against planned work.
Applications:
pythonimport cv2 import numpy as np from PIL import Image import torch from torchvision import models, transforms # Load pre-trained model for construction scene analysis model = models.resnet50(pretrained=True) model.eval() # Preprocess image transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # Load site photo img = Image.open("site_photo.jpg") input_tensor = transform(img).unsqueeze(0) # Analyze with torch.no_grad(): output = model(input_tensor) print("Image analyzed successfully")
pythonimport cv2 import numpy as np from PIL import Image from typing import Dict, List, Tuple, Optional from dataclasses import dataclass from enum import Enum import torch from torchvision import models, transforms from torchvision.models.detection import fasterrcnn_resnet50_fpn class ConstructionPhase(Enum): EXCAVATION = "excavation" FOUNDATION = "foundation" STRUCTURE = "structure" ENCLOSURE = "enclosure" MEP_ROUGH = "mep_rough" FINISHES = "finishes" COMPLETE = "complete" @dataclass class ProgressReport: timestamp: str image_path: str detected_phase: ConstructionPhase estimated_progress: float detected_elements: List[Dict] safety_observations: List[Dict] quality_issues: List[Dict] comparison_to_plan: Optional[float] class ConstructionProgressAnalyzer: """Analyze construction progress from images""" def __init__(self, use_gpu: bool = True): self.device = torch.device('cuda' if use_gpu and torch.cuda.is_available() else 'cpu') # Load detection model self.detector = fasterrcnn_resnet50_fpn(pretrained=True) self.detector.to(self.device) self.detector.eval() # Image transform self.transform = transforms.Compose([ transforms.ToTensor() ]) # Construction element labels (would need fine-tuned model in production) self.construction_labels = { 'column': ['column', 'pillar', 'post'], 'beam': ['beam', 'girder'], 'slab': ['floor', 'slab', 'deck'], 'wall': ['wall', 'partition'], 'scaffold': ['scaffold', 'scaffolding'], 'crane': ['crane', 'tower crane'], 'equipment': ['excavator', 'loader', 'truck'], 'worker': ['person', 'worker'] } def analyze_image(self, image_path: str) -> ProgressReport: """Analyze a single construction site image""" # Load image img = Image.open(image_path).convert('RGB') img_tensor = self.transform(img).to(self.device) # Run detection with torch.no_grad(): predictions = self.detector([img_tensor]) # Process detections detected_elements = self._process_detections(predictions[0]) # Estimate phase and progress phase = self._estimate_phase(detected_elements, img) progress = self._estimate_progress(phase, detected_elements) # Safety analysis safety_obs = self._analyze_safety(img, detected_elements) # Quality check (simplified) quality_issues = self._check_quality(img) return ProgressReport( timestamp=self._get_timestamp(), image_path=image_path, detected_phase=phase, estimated_progress=progress, detected_elements=detected_elements, safety_observations=safety_obs, quality_issues=quality_issues, comparison_to_plan=None ) def _process_detections(self, predictions: Dict) -> List[Dict]: """Process model predictions into detected elements""" elements = [] boxes = predictions['boxes'].cpu().numpy() labels = predictions['labels'].cpu().numpy() scores = predictions['scores'].cpu().numpy() for box, label, score in zip(boxes, labels, scores): if score > 0.5: # Confidence threshold elements.append({ 'box': box.tolist(), 'label': label, 'score': float(score), 'area': (box[2] - box[0]) * (box[3] - box[1]) }) return elements def _estimate_phase(self, elements: List[Dict], img: Image) -> ConstructionPhase: """Estimate construction phase from detected elements""" # Convert to numpy for color analysis img_array = np.array(img) # Color-based phase estimation (simplified) hsv = cv2.cvtColor(img_array, cv2.COLOR_RGB2HSV) # Brown/earth tones indicate excavation earth_mask = cv2.inRange(hsv, (10, 50, 50), (30, 255, 255)) earth_ratio = np.sum(earth_mask > 0) / earth_mask.size # Gray tones indicate concrete gray_mask = cv2.inRange(hsv, (0, 0, 50), (180, 50, 200)) gray_ratio = np.sum(gray_mask > 0) / gray_mask.size # Steel colors steel_mask = cv2.inRange(hsv, (0, 0, 100), (180, 30, 255)) steel_ratio = np.sum(steel_mask > 0) / steel_mask.size # Simple phase logic if earth_ratio > 0.3: return ConstructionPhase.EXCAVATION elif gray_ratio > 0.2 and steel_ratio < 0.1: return ConstructionPhase.FOUNDATION elif steel_ratio > 0.1: return ConstructionPhase.STRUCTURE else: return ConstructionPhase.ENCLOSURE def _estimate_progress(self, phase: ConstructionPhase, elements: List[Dict]) -> float: """Estimate progress percentage within phase""" phase_base_progress = { ConstructionPhase.EXCAVATION: 5, ConstructionPhase.FOUNDATION: 15, ConstructionPhase.STRUCTURE: 35, ConstructionPhase.ENCLOSURE: 60, ConstructionPhase.MEP_ROUGH: 75, ConstructionPhase.FINISHES: 90, ConstructionPhase.COMPLETE: 100 } base = phase_base_progress.get(phase, 0) # Adjust based on detected elements element_count = len(elements) adjustment = min(element_count * 0.5, 10) return min(base + adjustment, 100) def _analyze_safety(self, img: Image, elements: List[Dict]) -> List[Dict]: """Analyze safety compliance""" observations = [] img_array = np.array(img) # Check for safety vest colors (orange, yellow, green) hsv = cv2.cvtColor(img_array, cv2.COLOR_RGB2HSV) # Orange vest detection orange_mask = cv2.inRange(hsv, (10, 100, 100), (25, 255, 255)) orange_pixels = np.sum(orange_mask > 0) # Yellow vest detection yellow_mask = cv2.inRange(hsv, (25, 100, 100), (35, 255, 255)) yellow_pixels = np.sum(yellow_mask > 0) if orange_pixels + yellow_pixels < 100: # Threshold observations.append({ 'type': 'PPE_VISIBILITY', 'severity': 'Medium', 'message': 'Limited high-visibility clothing detected' }) # Check for workers detected worker_count = sum(1 for e in elements if e.get('label') == 1) if worker_count > 0: observations.append({ 'type': 'WORKER_COUNT', 'severity': 'Info', 'message': f'{worker_count} workers detected on site' }) return observations def _check_quality(self, img: Image) -> List[Dict]: """Check for visible quality issues""" issues = [] img_array = np.array(img) # Edge detection for irregularities gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY) edges = cv2.Canny(gray, 50, 150) # High edge density might indicate messy work or issues edge_density = np.sum(edges > 0) / edges.size if edge_density > 0.3: issues.append({ 'type': 'VISUAL_COMPLEXITY', 'severity': 'Low', 'message': 'High visual complexity - manual review recommended' }) return issues def _get_timestamp(self) -> str: from datetime import datetime return datetime.now().isoformat() def compare_to_bim(self, image_path: str, bim_render_path: str) -> float: """Compare site photo to BIM rendering""" site_img = cv2.imread(image_path) bim_img = cv2.imread(bim_render_path) # Resize to same dimensions target_size = (800, 600) site_img = cv2.resize(site_img, target_size) bim_img = cv2.resize(bim_img, target_size) # Convert to grayscale site_gray = cv2.cvtColor(site_img, cv2.COLOR_BGR2GRAY) bim_gray = cv2.cvtColor(bim_img, cv2.COLOR_BGR2GRAY) # Calculate structural similarity from skimage.metrics import structural_similarity similarity, _ = structural_similarity(site_gray, bim_gray, full=True) return similarity def batch_analyze(self, image_paths: List[str]) -> List[ProgressReport]: """Analyze multiple images""" return [self.analyze_image(path) for path in image_paths]
pythonclass TimeLapseAnalyzer: """Analyze construction progress over time from image series""" def __init__(self, analyzer: ConstructionProgressAnalyzer): self.analyzer = analyzer self.reports: List[ProgressReport] = [] def add_image(self, image_path: str, date: str): """Add image to time series""" report = self.analyzer.analyze_image(image_path) report.timestamp = date self.reports.append(report) def get_progress_curve(self) -> pd.DataFrame: """Generate progress curve from analyzed images""" data = [{ 'date': r.timestamp, 'phase': r.detected_phase.value, 'progress': r.estimated_progress, 'element_count': len(r.detected_elements) } for r in self.reports] return pd.DataFrame(data).sort_values('date') def detect_delays(self, planned_progress: pd.DataFrame) -> List[Dict]: """Compare actual vs planned progress""" actual = self.get_progress_curve() delays = [] for _, row in actual.iterrows(): planned_row = planned_progress[ planned_progress['date'] == row['date'] ] if not planned_row.empty: planned_pct = planned_row.iloc[0]['progress'] actual_pct = row['progress'] if actual_pct < planned_pct - 5: # 5% tolerance delays.append({ 'date': row['date'], 'planned': planned_pct, 'actual': actual_pct, 'delay_pct': planned_pct - actual_pct }) return delays def generate_report(self, output_path: str): """Generate progress monitoring report""" progress_df = self.get_progress_curve() with pd.ExcelWriter(output_path, engine='openpyxl') as writer: progress_df.to_excel(writer, sheet_name='Progress', index=False) # Safety observations safety_data = [] for r in self.reports: for obs in r.safety_observations: safety_data.append({ 'Date': r.timestamp, 'Type': obs['type'], 'Severity': obs['severity'], 'Message': obs['message'] }) if safety_data: pd.DataFrame(safety_data).to_excel( writer, sheet_name='Safety', index=False ) return output_path
| Analysis Type | Method | Output | |--------------|--------|--------| | Phase Detection | Color analysis + Object detection | Construction phase | | Progress % | Element counting + Phase base | Completion percentage | | Safety Check | Color detection (PPE) + Worker count | Safety observations | | Quality Check | Edge detection + Anomaly detection | Quality issues | | BIM Comparison | Structural similarity | Similarity score |
4d-simulation for schedule comparisondata-visualization for progress dashboardsrisk-assessment-ml for delay prediction| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-17 | pass→pass | 11,902 | 2,529 | -79% | 1 | 1 | 0% | 1,940 | 3,900 | +101% | 0 | 0 | — |
case-05 | fail→fail | 27,204 | 30,326 | +11% | 1 | 1 | 0% | 5,192 | 9,650 | +86% | 0 | 0 | — |
case-06 | fail→fail | 17,279 | 17,596 | +2% | 1 | 1 | 0% | 2,803 | 6,846 | +144% | 0 | 0 | — |
case-07 | pass→pass | 10,962 | 2,242 | -80% | 1 | 1 | 0% | 1,629 | 3,893 | +139% | 0 | 0 | — |
case-01 | fail→pass | 9,630 | 14,692 | +53% | 1 | 1 | 0% | 1,902 | 6,713 | +253% | 0 | 0 | — |
case-02 | fail→fail | 10,107 | 7,380 | -27% | 1 | 1 | 0% | 1,904 | 4,915 | +158% | 0 | 0 | — |
case-03 | fail→pass | 18,994 | 18,563 | -2% | 1 | 1 | 0% | 3,950 | 7,359 | +86% | 0 | 0 | — |
case-04 | fail→fail | 19,239 | 24,730 | +29% | 1 | 1 | 0% | 3,004 | 8,403 | +180% | 0 | 0 | — |
case-08 | fail→pass | 12,356 | 3,376 | -73% | 1 | 1 | 0% | 1,926 | 4,029 | +109% | 0 | 0 | — |
case-09 | fail→pass | 12,206 | 5,858 | -52% | 1 | 1 | 0% | 2,172 | 4,615 | +112% | 0 | 0 | — |
case-10 | fail→pass | 11,025 | 3,279 | -70% | 1 | 1 | 0% | 1,538 | 4,056 | +164% | 0 | 0 | — |
case-11 | fail→pass | 20,952 | 3,910 | -81% | 1 | 1 | 0% | 1,370 | 4,262 | +211% | 0 | 0 | — |
case-12 | fail→pass | 10,866 | 2,382 | -78% | 1 | 1 | 0% | 1,708 | 3,867 | +126% | 0 | 0 | — |
case-13 | fail→pass | 9,174 | 2,140 | -77% | 1 | 1 | 0% | 1,401 | 3,861 | +176% | 0 | 0 | — |
case-14 | fail→pass | 5,419 | 2,241 | -59% | 1 | 1 | 0% | 749 | 3,859 | +415% | 0 | 0 | — |
case-15 | fail→pass | 11,784 | 2,244 | -81% | 1 | 1 | 0% | 1,856 | 3,872 | +109% | 0 | 0 | — |
case-16 | fail→pass | 12,113 | 3,904 | -68% | 1 | 1 | 0% | 1,809 | 4,182 | +131% | 0 | 0 | — |
case-18 | fail→pass | 5,412 | 1,519 | -72% | 1 | 1 | 0% | 789 | 3,766 | +377% | 0 | 0 | — |
case-19 | fail→pass | 14,082 | 4,217 | -70% | 1 | 1 | 0% | 2,220 | 4,294 | +93% | 0 | 0 | — |
case-20 | pass→pass | 5,144 | 2,738 | -47% | 1 | 1 | 0% | 877 | 3,948 | +350% | 0 | 0 | — |
case-21 | fail→pass | 7,845 | 1,820 | -77% | 1 | 1 | 0% | 1,227 | 3,782 | +208% | 0 | 0 | — |
case-22 | fail→pass | 9,527 | 2,863 | -70% | 1 | 1 | 0% | 1,435 | 4,025 | +180% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +68 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.