[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6FJVWQC7D189RYPDTC3WDD8":3,"skill-same:01M6FJVWQC7D189RYPDTC3WDD8":65},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":32,"linkOnly":33,"markdownContent":34,"markdownBody":35,"frontMatter":36,"refName":38,"commitSha":39,"skillDirectory":40,"forksCount":41,"repositoryHtmlUrl":42,"repositoryHomepageUrl":6,"ownerHtmlUrl":43,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":44,"install":45,"resources":58},"01M6FJVWQC7D189RYPDTC3WDD8","drone-site-survey",null,"Process drone survey data for construction sites. Generate orthomosaics, DEMs, point clouds, calculate volumes, track progress, and integrate with BIM models for comparison.","kbarbel640-del\u002Fskills","kbarbel640-del","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F255315490?v=4","01M6CJPPQGCCESXMG3K7BBGQ5J","01M6CJPPX2PB2BCZERFVM2C2B3",1,"MIT","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fdatadrivenconstruction\u002Fdrone-site-survey\u002FSKILL.md",[],[18,19,20,21,22,23,24,25,26,27,28,29,30,31],"site","survey","drone","drone site survey","generate","data","calculate","process","integrate","models","sites","progress","comparison","with","2026-11-10 10:12:30",false,"---\r\nslug: \"drone-site-survey\"\r\ndisplay_name: \"Drone Site Survey\"\r\ndescription: \"Process drone survey data for construction sites. Generate orthomosaics, DEMs, point clouds, calculate volumes, track progress, and integrate with BIM models for comparison.\"\r\n---\r\n\r\n# Drone Site Survey Processing\r\n\r\n## Overview\r\n\r\nThis skill implements drone data processing for construction site monitoring. Process aerial imagery to generate maps, measure volumes, track progress, and compare with design models.\r\n\r\n**Capabilities:**\r\n- Orthomosaic generation\r\n- Digital Elevation Model (DEM) creation\r\n- Point cloud processing\r\n- Volume calculations\r\n- Progress monitoring\r\n- BIM comparison\r\n- Stockpile measurement\r\n\r\n## Quick Start\r\n\r\n```python\r\nfrom dataclasses import dataclass\r\nfrom typing import List, Dict, Tuple, Optional\r\nfrom datetime import datetime\r\nimport numpy as np\r\n\r\n@dataclass\r\nclass DroneImage:\r\n    filename: str\r\n    timestamp: datetime\r\n    latitude: float\r\n    longitude: float\r\n    altitude: float\r\n    heading: float\r\n    pitch: float\r\n    roll: float\r\n    camera_model: str\r\n\r\n@dataclass\r\nclass PointCloud:\r\n    points: np.ndarray  # Nx3 array\r\n    colors: Optional[np.ndarray] = None  # Nx3 RGB\r\n    normals: Optional[np.ndarray] = None  # Nx3\r\n\r\n@dataclass\r\nclass VolumeResult:\r\n    volume_m3: float\r\n    area_m2: float\r\n    method: str\r\n    reference_plane: str\r\n    confidence: float\r\n\r\ndef calculate_volume_simple(point_cloud: PointCloud,\r\n                           reference_z: float = None) -> VolumeResult:\r\n    \"\"\"Simple volume calculation from point cloud\"\"\"\r\n    points = point_cloud.points\r\n\r\n    if reference_z is None:\r\n        reference_z = np.min(points[:, 2])\r\n\r\n    # Grid-based volume calculation\r\n    x_min, x_max = np.min(points[:, 0]), np.max(points[:, 0])\r\n    y_min, y_max = np.min(points[:, 1]), np.max(points[:, 1])\r\n\r\n    grid_size = 0.5  # 50cm grid\r\n    x_bins = np.arange(x_min, x_max + grid_size, grid_size)\r\n    y_bins = np.arange(y_min, y_max + grid_size, grid_size)\r\n\r\n    volume = 0\r\n    cell_area = grid_size ** 2\r\n\r\n    for i in range(len(x_bins) - 1):\r\n        for j in range(len(y_bins) - 1):\r\n            mask = (\r\n                (points[:, 0] >= x_bins[i]) & (points[:, 0] \u003C x_bins[i + 1]) &\r\n                (points[:, 1] >= y_bins[j]) & (points[:, 1] \u003C y_bins[j + 1])\r\n            )\r\n            cell_points = points[mask]\r\n            if len(cell_points) > 0:\r\n                max_z = np.max(cell_points[:, 2])\r\n                height = max_z - reference_z\r\n                if height > 0:\r\n                    volume += height * cell_area\r\n\r\n    area = (x_max - x_min) * (y_max - y_min)\r\n\r\n    return VolumeResult(\r\n        volume_m3=volume,\r\n        area_m2=area,\r\n        method='grid_based',\r\n        reference_plane=f'z={reference_z:.2f}',\r\n        confidence=0.9\r\n    )\r\n\r\n# Example usage\r\nsample_points = np.random.rand(10000, 3) * [100, 100, 10]  # 100x100m, 10m height\r\npoint_cloud = PointCloud(points=sample_points)\r\nresult = calculate_volume_simple(point_cloud)\r\nprint(f\"Volume: {result.volume_m3:.2f} m³, Area: {result.area_m2:.2f} m²\")\r\n```\r\n\r\n## Comprehensive Drone Survey System\r\n\r\n### Image Processing Pipeline\r\n\r\n```python\r\nfrom dataclasses import dataclass, field\r\nfrom typing import List, Dict, Tuple, Optional\r\nfrom datetime import datetime\r\nimport numpy as np\r\nfrom pathlib import Path\r\nimport json\r\n\r\n@dataclass\r\nclass CameraParameters:\r\n    focal_length_mm: float\r\n    sensor_width_mm: float\r\n    sensor_height_mm: float\r\n    image_width_px: int\r\n    image_height_px: int\r\n\r\n@dataclass\r\nclass GeoReference:\r\n    crs: str  # Coordinate Reference System (e.g., \"EPSG:4326\")\r\n    origin: Tuple[float, float, float]  # lat, lon, alt\r\n    rotation: Tuple[float, float, float]  # heading, pitch, roll\r\n\r\n@dataclass\r\nclass SurveyFlight:\r\n    flight_id: str\r\n    date: datetime\r\n    site_name: str\r\n    images: List[DroneImage]\r\n    camera: CameraParameters\r\n    geo_reference: GeoReference\r\n    flight_altitude: float\r\n    overlap_forward: float = 0.8\r\n    overlap_side: float = 0.7\r\n    gsd: float = 0  # Ground Sample Distance (cm\u002Fpixel)\r\n\r\n    def __post_init__(self):\r\n        if self.gsd == 0 and self.camera:\r\n            # Calculate GSD\r\n            sensor_width = self.camera.sensor_width_mm\r\n            focal_length = self.camera.focal_length_mm\r\n            image_width = self.camera.image_width_px\r\n            altitude = self.flight_altitude\r\n\r\n            self.gsd = (altitude * sensor_width) \u002F (focal_length * image_width) * 100  # cm\r\n\r\n@dataclass\r\nclass ProcessingResult:\r\n    orthomosaic_path: Optional[str] = None\r\n    dem_path: Optional[str] = None\r\n    dsm_path: Optional[str] = None\r\n    point_cloud_path: Optional[str] = None\r\n    report_path: Optional[str] = None\r\n    statistics: Dict = field(default_factory=dict)\r\n\r\nclass DroneDataProcessor:\r\n    \"\"\"Process drone survey data\"\"\"\r\n\r\n    def __init__(self, output_dir: str):\r\n        self.output_dir = Path(output_dir)\r\n        self.output_dir.mkdir(parents=True, exist_ok=True)\r\n\r\n    def process_survey(self, flight: SurveyFlight,\r\n                      generate_ortho: bool = True,\r\n                      generate_dem: bool = True,\r\n                      generate_pointcloud: bool = True) -> ProcessingResult:\r\n        \"\"\"Process drone survey data\"\"\"\r\n        result = ProcessingResult()\r\n        result.statistics['flight_id'] = flight.flight_id\r\n        result.statistics['image_count'] = len(flight.images)\r\n        result.statistics['gsd_cm'] = flight.gsd\r\n        result.statistics['flight_date'] = flight.date.isoformat()\r\n\r\n        # In production, these would call actual photogrammetry libraries\r\n        # like OpenDroneMap, Pix4D API, or custom SfM pipeline\r\n\r\n        if generate_ortho:\r\n            result.orthomosaic_path = str(self.output_dir \u002F f\"{flight.flight_id}_ortho.tif\")\r\n            result.statistics['ortho_resolution'] = flight.gsd\r\n\r\n        if generate_dem:\r\n            result.dem_path = str(self.output_dir \u002F f\"{flight.flight_id}_dem.tif\")\r\n            result.dsm_path = str(self.output_dir \u002F f\"{flight.flight_id}_dsm.tif\")\r\n\r\n        if generate_pointcloud:\r\n            result.point_cloud_path = str(self.output_dir \u002F f\"{flight.flight_id}_pointcloud.las\")\r\n\r\n        # Generate report\r\n        result.report_path = str(self.output_dir \u002F f\"{flight.flight_id}_report.json\")\r\n        with open(result.report_path, 'w') as f:\r\n            json.dump(result.statistics, f, indent=2)\r\n\r\n        return result\r\n\r\n    def extract_point_cloud(self, las_path: str) -> PointCloud:\r\n        \"\"\"Extract point cloud from LAS file\"\"\"\r\n        # In production, use laspy or similar\r\n        # Simulated point cloud for demonstration\r\n        n_points = 100000\r\n        points = np.random.rand(n_points, 3) * [100, 100, 20]\r\n        colors = np.random.randint(0, 255, (n_points, 3), dtype=np.uint8)\r\n\r\n        return PointCloud(points=points, colors=colors)\r\n\r\n    def compare_surveys(self, survey1: ProcessingResult,\r\n                       survey2: ProcessingResult) -> Dict:\r\n        \"\"\"Compare two surveys for change detection\"\"\"\r\n        # Load point clouds\r\n        pc1 = self.extract_point_cloud(survey1.point_cloud_path)\r\n        pc2 = self.extract_point_cloud(survey2.point_cloud_path)\r\n\r\n        # Calculate elevation differences\r\n        # In production, use proper point cloud registration and comparison\r\n\r\n        comparison = {\r\n            'survey1_date': survey1.statistics.get('flight_date'),\r\n            'survey2_date': survey2.statistics.get('flight_date'),\r\n            'point_count_diff': len(pc2.points) - len(pc1.points),\r\n            'changes_detected': []\r\n        }\r\n\r\n        return comparison\r\n```\r\n\r\n### Volume Calculation Engine\r\n\r\n```python\r\nfrom scipy.spatial import Delaunay\r\nfrom scipy.interpolate import griddata\r\nimport numpy as np\r\n\r\nclass VolumeCalculator:\r\n    \"\"\"Advanced volume calculations from drone data\"\"\"\r\n\r\n    def __init__(self, point_cloud: PointCloud):\r\n        self.points = point_cloud.points\r\n        self.colors = point_cloud.colors\r\n\r\n    def calculate_cut_fill(self, design_surface: np.ndarray,\r\n                          grid_size: float = 0.5) -> Dict:\r\n        \"\"\"Calculate cut and fill volumes compared to design surface\"\"\"\r\n        # Create grid\r\n        x_min, x_max = np.min(self.points[:, 0]), np.max(self.points[:, 0])\r\n        y_min, y_max = np.min(self.points[:, 1]), np.max(self.points[:, 1])\r\n\r\n        x_grid = np.arange(x_min, x_max, grid_size)\r\n        y_grid = np.arange(y_min, y_max, grid_size)\r\n        xx, yy = np.meshgrid(x_grid, y_grid)\r\n\r\n        # Interpolate actual surface\r\n        actual_z = griddata(\r\n            self.points[:, :2],\r\n            self.points[:, 2],\r\n            (xx, yy),\r\n            method='linear'\r\n        )\r\n\r\n        # Interpolate design surface\r\n        design_z = griddata(\r\n            design_surface[:, :2],\r\n            design_surface[:, 2],\r\n            (xx, yy),\r\n            method='linear'\r\n        )\r\n\r\n        # Calculate differences\r\n        diff = actual_z - design_z\r\n        cell_area = grid_size ** 2\r\n\r\n        cut_volume = np.nansum(diff[diff > 0]) * cell_area\r\n        fill_volume = np.nansum(np.abs(diff[diff \u003C 0])) * cell_area\r\n        net_volume = cut_volume - fill_volume\r\n\r\n        return {\r\n            'cut_volume_m3': float(cut_volume),\r\n            'fill_volume_m3': float(fill_volume),\r\n            'net_volume_m3': float(net_volume),\r\n            'balance': 'cut' if net_volume > 0 else 'fill',\r\n            'grid_size_m': grid_size,\r\n            'area_m2': float((x_max - x_min) * (y_max - y_min))\r\n        }\r\n\r\n    def calculate_stockpile_volume(self, base_method: str = 'lowest_perimeter') -> VolumeResult:\r\n        \"\"\"Calculate stockpile volume\"\"\"\r\n        # Find boundary points\r\n        from scipy.spatial import ConvexHull\r\n        hull = ConvexHull(self.points[:, :2])\r\n        boundary_points = self.points[hull.vertices]\r\n\r\n        if base_method == 'lowest_perimeter':\r\n            reference_z = np.min(boundary_points[:, 2])\r\n        elif base_method == 'average_perimeter':\r\n            reference_z = np.mean(boundary_points[:, 2])\r\n        elif base_method == 'triangulated':\r\n            # Create base triangulation from boundary\r\n            reference_z = np.min(self.points[:, 2])\r\n        else:\r\n            reference_z = np.min(self.points[:, 2])\r\n\r\n        # Calculate volume using triangulated surface\r\n        volume = self._triangulated_volume(reference_z)\r\n\r\n        hull_area = self._calculate_hull_area(boundary_points[:, :2])\r\n\r\n        return VolumeResult(\r\n            volume_m3=volume,\r\n            area_m2=hull_area,\r\n            method='triangulated_' + base_method,\r\n            reference_plane=f'z={reference_z:.2f}',\r\n            confidence=0.95\r\n        )\r\n\r\n    def _triangulated_volume(self, reference_z: float) -> float:\r\n        \"\"\"Calculate volume using Delaunay triangulation\"\"\"\r\n        # Create 2D triangulation\r\n        points_2d = self.points[:, :2]\r\n        tri = Delaunay(points_2d)\r\n\r\n        volume = 0\r\n        for simplex in tri.simplices:\r\n            # Get triangle vertices\r\n            p1 = self.points[simplex[0]]\r\n            p2 = self.points[simplex[1]]\r\n            p3 = self.points[simplex[2]]\r\n\r\n            # Calculate prism volume\r\n            avg_height = (p1[2] + p2[2] + p3[2]) \u002F 3 - reference_z\r\n            if avg_height > 0:\r\n                # Triangle area\r\n                area = 0.5 * abs(\r\n                    (p2[0] - p1[0]) * (p3[1] - p1[1]) -\r\n                    (p3[0] - p1[0]) * (p2[1] - p1[1])\r\n                )\r\n                volume += area * avg_height\r\n\r\n        return volume\r\n\r\n    def _calculate_hull_area(self, points_2d: np.ndarray) -> float:\r\n        \"\"\"Calculate area of convex hull\"\"\"\r\n        hull = ConvexHull(points_2d)\r\n        return hull.volume  # In 2D, volume is actually area\r\n\r\n    def generate_contours(self, interval: float = 1.0) -> List[Dict]:\r\n        \"\"\"Generate elevation contours\"\"\"\r\n        z_min = np.min(self.points[:, 2])\r\n        z_max = np.max(self.points[:, 2])\r\n\r\n        contour_levels = np.arange(\r\n            np.floor(z_min \u002F interval) * interval,\r\n            np.ceil(z_max \u002F interval) * interval + interval,\r\n            interval\r\n        )\r\n\r\n        contours = []\r\n        for level in contour_levels:\r\n            # In production, use proper contouring algorithm\r\n            contours.append({\r\n                'elevation': float(level),\r\n                'type': 'major' if level % 5 == 0 else 'minor'\r\n            })\r\n\r\n        return contours\r\n```\r\n\r\n### Progress Monitoring\r\n\r\n```python\r\nfrom datetime import date\r\nfrom typing import List, Dict\r\n\r\n@dataclass\r\nclass ProgressPoint:\r\n    date: date\r\n    point_cloud: PointCloud\r\n    orthomosaic_path: str\r\n    annotations: Dict = field(default_factory=dict)\r\n\r\nclass ConstructionProgressMonitor:\r\n    \"\"\"Monitor construction progress from drone surveys\"\"\"\r\n\r\n    def __init__(self, project_name: str):\r\n        self.project_name = project_name\r\n        self.surveys: List[ProgressPoint] = []\r\n        self.volume_calculator = None\r\n\r\n    def add_survey(self, survey_date: date, point_cloud: PointCloud,\r\n                  orthomosaic_path: str):\r\n        \"\"\"Add survey for progress tracking\"\"\"\r\n        self.surveys.append(ProgressPoint(\r\n            date=survey_date,\r\n            point_cloud=point_cloud,\r\n            orthomosaic_path=orthomosaic_path\r\n        ))\r\n        self.surveys.sort(key=lambda x: x.date)\r\n\r\n    def calculate_earthwork_progress(self, design_surface: np.ndarray) -> List[Dict]:\r\n        \"\"\"Calculate earthwork progress over time\"\"\"\r\n        progress = []\r\n\r\n        for i, survey in enumerate(self.surveys):\r\n            calc = VolumeCalculator(survey.point_cloud)\r\n            cut_fill = calc.calculate_cut_fill(design_surface)\r\n\r\n            progress.append({\r\n                'date': survey.date.isoformat(),\r\n                'survey_index': i + 1,\r\n                'cut_volume_m3': cut_fill['cut_volume_m3'],\r\n                'fill_volume_m3': cut_fill['fill_volume_m3'],\r\n                'net_volume_m3': cut_fill['net_volume_m3']\r\n            })\r\n\r\n        # Calculate progress percentages\r\n        if len(progress) > 1:\r\n            initial = progress[0]\r\n            for p in progress[1:]:\r\n                if initial['net_volume_m3'] != 0:\r\n                    p['progress_pct'] = (\r\n                        (initial['net_volume_m3'] - p['net_volume_m3']) \u002F\r\n                        abs(initial['net_volume_m3']) * 100\r\n                    )\r\n                else:\r\n                    p['progress_pct'] = 0\r\n\r\n        return progress\r\n\r\n    def detect_changes(self, survey1_idx: int, survey2_idx: int,\r\n                      threshold_m: float = 0.5) -> Dict:\r\n        \"\"\"Detect changes between two surveys\"\"\"\r\n        pc1 = self.surveys[survey1_idx].point_cloud\r\n        pc2 = self.surveys[survey2_idx].point_cloud\r\n\r\n        # Create grids for comparison\r\n        grid_size = 1.0  # 1m grid\r\n\r\n        x_min = min(np.min(pc1.points[:, 0]), np.min(pc2.points[:, 0]))\r\n        x_max = max(np.max(pc1.points[:, 0]), np.max(pc2.points[:, 0]))\r\n        y_min = min(np.min(pc1.points[:, 1]), np.min(pc2.points[:, 1]))\r\n        y_max = max(np.max(pc1.points[:, 1]), np.max(pc2.points[:, 1]))\r\n\r\n        x_bins = np.arange(x_min, x_max + grid_size, grid_size)\r\n        y_bins = np.arange(y_min, y_max + grid_size, grid_size)\r\n\r\n        changes = {\r\n            'increased': [],  # Areas where elevation increased\r\n            'decreased': [],  # Areas where elevation decreased\r\n            'unchanged': 0\r\n        }\r\n\r\n        for i in range(len(x_bins) - 1):\r\n            for j in range(len(y_bins) - 1):\r\n                # Get points in cell for both surveys\r\n                cell_x = (x_bins[i] + x_bins[i + 1]) \u002F 2\r\n                cell_y = (y_bins[j] + y_bins[j + 1]) \u002F 2\r\n\r\n                mask1 = (\r\n                    (pc1.points[:, 0] >= x_bins[i]) & (pc1.points[:, 0] \u003C x_bins[i + 1]) &\r\n                    (pc1.points[:, 1] >= y_bins[j]) & (pc1.points[:, 1] \u003C y_bins[j + 1])\r\n                )\r\n                mask2 = (\r\n                    (pc2.points[:, 0] >= x_bins[i]) & (pc2.points[:, 0] \u003C x_bins[i + 1]) &\r\n                    (pc2.points[:, 1] >= y_bins[j]) & (pc2.points[:, 1] \u003C y_bins[j + 1])\r\n                )\r\n\r\n                if np.sum(mask1) > 0 and np.sum(mask2) > 0:\r\n                    z1 = np.mean(pc1.points[mask1, 2])\r\n                    z2 = np.mean(pc2.points[mask2, 2])\r\n                    diff = z2 - z1\r\n\r\n                    if diff > threshold_m:\r\n                        changes['increased'].append({\r\n                            'x': cell_x, 'y': cell_y, 'change_m': diff\r\n                        })\r\n                    elif diff \u003C -threshold_m:\r\n                        changes['decreased'].append({\r\n                            'x': cell_x, 'y': cell_y, 'change_m': diff\r\n                        })\r\n                    else:\r\n                        changes['unchanged'] += 1\r\n\r\n        changes['summary'] = {\r\n            'increased_cells': len(changes['increased']),\r\n            'decreased_cells': len(changes['decreased']),\r\n            'unchanged_cells': changes['unchanged'],\r\n            'date_from': self.surveys[survey1_idx].date.isoformat(),\r\n            'date_to': self.surveys[survey2_idx].date.isoformat()\r\n        }\r\n\r\n        return changes\r\n\r\n    def generate_progress_report(self, output_path: str,\r\n                                design_surface: np.ndarray = None) -> str:\r\n        \"\"\"Generate progress report\"\"\"\r\n        import pandas as pd\r\n\r\n        report_data = {\r\n            'project': self.project_name,\r\n            'surveys': len(self.surveys),\r\n            'date_range': {\r\n                'start': self.surveys[0].date.isoformat() if self.surveys else None,\r\n                'end': self.surveys[-1].date.isoformat() if self.surveys else None\r\n            }\r\n        }\r\n\r\n        if design_surface is not None:\r\n            report_data['earthwork_progress'] = self.calculate_earthwork_progress(design_surface)\r\n\r\n        with pd.ExcelWriter(output_path, engine='openpyxl') as writer:\r\n            # Summary\r\n            pd.DataFrame([report_data]).to_excel(writer, sheet_name='Summary', index=False)\r\n\r\n            # Survey list\r\n            survey_data = [{\r\n                'Date': s.date,\r\n                'Orthomosaic': s.orthomosaic_path\r\n            } for s in self.surveys]\r\n            pd.DataFrame(survey_data).to_excel(writer, sheet_name='Surveys', index=False)\r\n\r\n            # Progress (if available)\r\n            if 'earthwork_progress' in report_data:\r\n                pd.DataFrame(report_data['earthwork_progress']).to_excel(\r\n                    writer, sheet_name='Progress', index=False\r\n                )\r\n\r\n        return output_path\r\n```\r\n\r\n### BIM Comparison\r\n\r\n```python\r\nclass BIMDroneComparator:\r\n    \"\"\"Compare drone survey with BIM model\"\"\"\r\n\r\n    def __init__(self, bim_surface: np.ndarray, drone_pointcloud: PointCloud):\r\n        self.bim = bim_surface\r\n        self.drone = drone_pointcloud\r\n\r\n    def compare_elevations(self, grid_size: float = 1.0) -> Dict:\r\n        \"\"\"Compare drone elevations with BIM design\"\"\"\r\n        # Create comparison grid\r\n        x_min = max(np.min(self.bim[:, 0]), np.min(self.drone.points[:, 0]))\r\n        x_max = min(np.max(self.bim[:, 0]), np.max(self.drone.points[:, 0]))\r\n        y_min = max(np.min(self.bim[:, 1]), np.min(self.drone.points[:, 1]))\r\n        y_max = min(np.max(self.bim[:, 1]), np.max(self.drone.points[:, 1]))\r\n\r\n        x_grid = np.arange(x_min, x_max, grid_size)\r\n        y_grid = np.arange(y_min, y_max, grid_size)\r\n        xx, yy = np.meshgrid(x_grid, y_grid)\r\n\r\n        # Interpolate both surfaces\r\n        bim_z = griddata(self.bim[:, :2], self.bim[:, 2], (xx, yy), method='linear')\r\n        drone_z = griddata(\r\n            self.drone.points[:, :2],\r\n            self.drone.points[:, 2],\r\n            (xx, yy),\r\n            method='linear'\r\n        )\r\n\r\n        # Calculate differences\r\n        diff = drone_z - bim_z\r\n\r\n        valid_mask = ~np.isnan(diff)\r\n        valid_diff = diff[valid_mask]\r\n\r\n        return {\r\n            'mean_diff_m': float(np.mean(valid_diff)),\r\n            'std_diff_m': float(np.std(valid_diff)),\r\n            'max_diff_m': float(np.max(valid_diff)),\r\n            'min_diff_m': float(np.min(valid_diff)),\r\n            'rmse_m': float(np.sqrt(np.mean(valid_diff ** 2))),\r\n            'within_5cm': float(np.sum(np.abs(valid_diff) \u003C 0.05) \u002F len(valid_diff) * 100),\r\n            'within_10cm': float(np.sum(np.abs(valid_diff) \u003C 0.10) \u002F len(valid_diff) * 100),\r\n            'comparison_points': int(len(valid_diff))\r\n        }\r\n\r\n    def find_deviations(self, tolerance_m: float = 0.1) -> List[Dict]:\r\n        \"\"\"Find areas with significant deviations\"\"\"\r\n        deviations = []\r\n        grid_size = 1.0\r\n\r\n        # Grid comparison\r\n        x_min = max(np.min(self.bim[:, 0]), np.min(self.drone.points[:, 0]))\r\n        x_max = min(np.max(self.bim[:, 0]), np.max(self.drone.points[:, 0]))\r\n        y_min = max(np.min(self.bim[:, 1]), np.min(self.drone.points[:, 1]))\r\n        y_max = min(np.max(self.bim[:, 1]), np.max(self.drone.points[:, 1]))\r\n\r\n        for x in np.arange(x_min, x_max, grid_size):\r\n            for y in np.arange(y_min, y_max, grid_size):\r\n                # Get average elevations\r\n                bim_mask = (\r\n                    (self.bim[:, 0] >= x) & (self.bim[:, 0] \u003C x + grid_size) &\r\n                    (self.bim[:, 1] >= y) & (self.bim[:, 1] \u003C y + grid_size)\r\n                )\r\n                drone_mask = (\r\n                    (self.drone.points[:, 0] >= x) & (self.drone.points[:, 0] \u003C x + grid_size) &\r\n                    (self.drone.points[:, 1] >= y) & (self.drone.points[:, 1] \u003C y + grid_size)\r\n                )\r\n\r\n                if np.sum(bim_mask) > 0 and np.sum(drone_mask) > 0:\r\n                    bim_z = np.mean(self.bim[bim_mask, 2])\r\n                    drone_z = np.mean(self.drone.points[drone_mask, 2])\r\n                    diff = drone_z - bim_z\r\n\r\n                    if abs(diff) > tolerance_m:\r\n                        deviations.append({\r\n                            'x': x + grid_size \u002F 2,\r\n                            'y': y + grid_size \u002F 2,\r\n                            'bim_elevation': bim_z,\r\n                            'actual_elevation': drone_z,\r\n                            'deviation_m': diff,\r\n                            'type': 'high' if diff > 0 else 'low'\r\n                        })\r\n\r\n        return deviations\r\n```\r\n\r\n## Quick Reference\r\n\r\n| Measurement | Method | Accuracy |\r\n|-------------|--------|----------|\r\n| Stockpile Volume | Triangulated | ±2-5% |\r\n| Cut\u002FFill Volume | Grid comparison | ±5% |\r\n| Area Measurement | Orthomosaic | \u003C1cm GSD |\r\n| Elevation (DEM) | Photogrammetry | ±2-5cm |\r\n| Progress Tracking | Multi-temporal | Relative |\r\n\r\n## Resources\r\n\r\n- **OpenDroneMap**: https:\u002F\u002Fwww.opendronemap.org\r\n- **Pix4D**: https:\u002F\u002Fwww.pix4d.com\r\n- **LAStools**: https:\u002F\u002Frapidlasso.com\u002Flastools\u002F\r\n- **DDC Website**: https:\u002F\u002Fdatadrivenconstruction.io\r\n\r\n## Next Steps\r\n\r\n- See `progress-monitoring-cv` for image-based progress\r\n- See `bim-validation-pipeline` for model comparison\r\n- See `data-visualization` for 3D visualization\r\n","# Drone Site Survey Processing\r\n\r\n## Overview\r\n\r\nThis skill implements drone data processing for construction site monitoring. Process aerial imagery to generate maps, measure volumes, track progress, and compare with design models.\r\n\r\n**Capabilities:**\r\n- Orthomosaic generation\r\n- Digital Elevation Model (DEM) creation\r\n- Point cloud processing\r\n- Volume calculations\r\n- Progress monitoring\r\n- BIM comparison\r\n- Stockpile measurement\r\n\r\n## Quick Start\r\n\r\n```python\r\nfrom dataclasses import dataclass\r\nfrom typing import List, Dict, Tuple, Optional\r\nfrom datetime import datetime\r\nimport numpy as np\r\n\r\n@dataclass\r\nclass DroneImage:\r\n    filename: str\r\n    timestamp: datetime\r\n    latitude: float\r\n    longitude: float\r\n    altitude: float\r\n    heading: float\r\n    pitch: float\r\n    roll: float\r\n    camera_model: str\r\n\r\n@dataclass\r\nclass PointCloud:\r\n    points: np.ndarray  # Nx3 array\r\n    colors: Optional[np.ndarray] = None  # Nx3 RGB\r\n    normals: Optional[np.ndarray] = None  # Nx3\r\n\r\n@dataclass\r\nclass VolumeResult:\r\n    volume_m3: float\r\n    area_m2: float\r\n    method: str\r\n    reference_plane: str\r\n    confidence: float\r\n\r\ndef calculate_volume_simple(point_cloud: PointCloud,\r\n                           reference_z: float = None) -> VolumeResult:\r\n    \"\"\"Simple volume calculation from point cloud\"\"\"\r\n    points = point_cloud.points\r\n\r\n    if reference_z is None:\r\n        reference_z = np.min(points[:, 2])\r\n\r\n    # Grid-based volume calculation\r\n    x_min, x_max = np.min(points[:, 0]), np.max(points[:, 0])\r\n    y_min, y_max = np.min(points[:, 1]), np.max(points[:, 1])\r\n\r\n    grid_size = 0.5  # 50cm grid\r\n    x_bins = np.arange(x_min, x_max + grid_size, grid_size)\r\n    y_bins = np.arange(y_min, y_max + grid_size, grid_size)\r\n\r\n    volume = 0\r\n    cell_area = grid_size ** 2\r\n\r\n    for i in range(len(x_bins) - 1):\r\n        for j in range(len(y_bins) - 1):\r\n            mask = (\r\n                (points[:, 0] >= x_bins[i]) & (points[:, 0] \u003C x_bins[i + 1]) &\r\n                (points[:, 1] >= y_bins[j]) & (points[:, 1] \u003C y_bins[j + 1])\r\n            )\r\n            cell_points = points[mask]\r\n            if len(cell_points) > 0:\r\n                max_z = np.max(cell_points[:, 2])\r\n                height = max_z - reference_z\r\n                if height > 0:\r\n                    volume += height * cell_area\r\n\r\n    area = (x_max - x_min) * (y_max - y_min)\r\n\r\n    return VolumeResult(\r\n        volume_m3=volume,\r\n        area_m2=area,\r\n        method='grid_based',\r\n        reference_plane=f'z={reference_z:.2f}',\r\n        confidence=0.9\r\n    )\r\n\r\n# Example usage\r\nsample_points = np.random.rand(10000, 3) * [100, 100, 10]  # 100x100m, 10m height\r\npoint_cloud = PointCloud(points=sample_points)\r\nresult = calculate_volume_simple(point_cloud)\r\nprint(f\"Volume: {result.volume_m3:.2f} m³, Area: {result.area_m2:.2f} m²\")\r\n```\r\n\r\n## Comprehensive Drone Survey System\r\n\r\n### Image Processing Pipeline\r\n\r\n```python\r\nfrom dataclasses import dataclass, field\r\nfrom typing import List, Dict, Tuple, Optional\r\nfrom datetime import datetime\r\nimport numpy as np\r\nfrom pathlib import Path\r\nimport json\r\n\r\n@dataclass\r\nclass CameraParameters:\r\n    focal_length_mm: float\r\n    sensor_width_mm: float\r\n    sensor_height_mm: float\r\n    image_width_px: int\r\n    image_height_px: int\r\n\r\n@dataclass\r\nclass GeoReference:\r\n    crs: str  # Coordinate Reference System (e.g., \"EPSG:4326\")\r\n    origin: Tuple[float, float, float]  # lat, lon, alt\r\n    rotation: Tuple[float, float, float]  # heading, pitch, roll\r\n\r\n@dataclass\r\nclass SurveyFlight:\r\n    flight_id: str\r\n    date: datetime\r\n    site_name: str\r\n    images: List[DroneImage]\r\n    camera: CameraParameters\r\n    geo_reference: GeoReference\r\n    flight_altitude: float\r\n    overlap_forward: float = 0.8\r\n    overlap_side: float = 0.7\r\n    gsd: float = 0  # Ground Sample Distance (cm\u002Fpixel)\r\n\r\n    def __post_init__(self):\r\n        if self.gsd == 0 and self.camera:\r\n            # Calculate GSD\r\n            sensor_width = self.camera.sensor_width_mm\r\n            focal_length = self.camera.focal_length_mm\r\n            image_width = self.camera.image_width_px\r\n            altitude = self.flight_altitude\r\n\r\n            self.gsd = (altitude * sensor_width) \u002F (focal_length * image_width) * 100  # cm\r\n\r\n@dataclass\r\nclass ProcessingResult:\r\n    orthomosaic_path: Optional[str] = None\r\n    dem_path: Optional[str] = None\r\n    dsm_path: Optional[str] = None\r\n    point_cloud_path: Optional[str] = None\r\n    report_path: Optional[str] = None\r\n    statistics: Dict = field(default_factory=dict)\r\n\r\nclass DroneDataProcessor:\r\n    \"\"\"Process drone survey data\"\"\"\r\n\r\n    def __init__(self, output_dir: str):\r\n        self.output_dir = Path(output_dir)\r\n        self.output_dir.mkdir(parents=True, exist_ok=True)\r\n\r\n    def process_survey(self, flight: SurveyFlight,\r\n                      generate_ortho: bool = True,\r\n                      generate_dem: bool = True,\r\n                      generate_pointcloud: bool = True) -> ProcessingResult:\r\n        \"\"\"Process drone survey data\"\"\"\r\n        result = ProcessingResult()\r\n        result.statistics['flight_id'] = flight.flight_id\r\n        result.statistics['image_count'] = len(flight.images)\r\n        result.statistics['gsd_cm'] = flight.gsd\r\n        result.statistics['flight_date'] = flight.date.isoformat()\r\n\r\n        # In production, these would call actual photogrammetry libraries\r\n        # like OpenDroneMap, Pix4D API, or custom SfM pipeline\r\n\r\n        if generate_ortho:\r\n            result.orthomosaic_path = str(self.output_dir \u002F f\"{flight.flight_id}_ortho.tif\")\r\n            result.statistics['ortho_resolution'] = flight.gsd\r\n\r\n        if generate_dem:\r\n            result.dem_path = str(self.output_dir \u002F f\"{flight.flight_id}_dem.tif\")\r\n            result.dsm_path = str(self.output_dir \u002F f\"{flight.flight_id}_dsm.tif\")\r\n\r\n        if generate_pointcloud:\r\n            result.point_cloud_path = str(self.output_dir \u002F f\"{flight.flight_id}_pointcloud.las\")\r\n\r\n        # Generate report\r\n        result.report_path = str(self.output_dir \u002F f\"{flight.flight_id}_report.json\")\r\n        with open(result.report_path, 'w') as f:\r\n            json.dump(result.statistics, f, indent=2)\r\n\r\n        return result\r\n\r\n    def extract_point_cloud(self, las_path: str) -> PointCloud:\r\n        \"\"\"Extract point cloud from LAS file\"\"\"\r\n        # In production, use laspy or similar\r\n        # Simulated point cloud for demonstration\r\n        n_points = 100000\r\n        points = np.random.rand(n_points, 3) * [100, 100, 20]\r\n        colors = np.random.randint(0, 255, (n_points, 3), dtype=np.uint8)\r\n\r\n        return PointCloud(points=points, colors=colors)\r\n\r\n    def compare_surveys(self, survey1: ProcessingResult,\r\n                       survey2: ProcessingResult) -> Dict:\r\n        \"\"\"Compare two surveys for change detection\"\"\"\r\n        # Load point clouds\r\n        pc1 = self.extract_point_cloud(survey1.point_cloud_path)\r\n        pc2 = self.extract_point_cloud(survey2.point_cloud_path)\r\n\r\n        # Calculate elevation differences\r\n        # In production, use proper point cloud registration and comparison\r\n\r\n        comparison = {\r\n            'survey1_date': survey1.statistics.get('flight_date'),\r\n            'survey2_date': survey2.statistics.get('flight_date'),\r\n            'point_count_diff': len(pc2.points) - len(pc1.points),\r\n            'changes_detected': []\r\n        }\r\n\r\n        return comparison\r\n```\r\n\r\n### Volume Calculation Engine\r\n\r\n```python\r\nfrom scipy.spatial import Delaunay\r\nfrom scipy.interpolate import griddata\r\nimport numpy as np\r\n\r\nclass VolumeCalculator:\r\n    \"\"\"Advanced volume calculations from drone data\"\"\"\r\n\r\n    def __init__(self, point_cloud: PointCloud):\r\n        self.points = point_cloud.points\r\n        self.colors = point_cloud.colors\r\n\r\n    def calculate_cut_fill(self, design_surface: np.ndarray,\r\n                          grid_size: float = 0.5) -> Dict:\r\n        \"\"\"Calculate cut and fill volumes compared to design surface\"\"\"\r\n        # Create grid\r\n        x_min, x_max = np.min(self.points[:, 0]), np.max(self.points[:, 0])\r\n        y_min, y_max = np.min(self.points[:, 1]), np.max(self.points[:, 1])\r\n\r\n        x_grid = np.arange(x_min, x_max, grid_size)\r\n        y_grid = np.arange(y_min, y_max, grid_size)\r\n        xx, yy = np.meshgrid(x_grid, y_grid)\r\n\r\n        # Interpolate actual surface\r\n        actual_z = griddata(\r\n            self.points[:, :2],\r\n            self.points[:, 2],\r\n            (xx, yy),\r\n            method='linear'\r\n        )\r\n\r\n        # Interpolate design surface\r\n        design_z = griddata(\r\n            design_surface[:, :2],\r\n            design_surface[:, 2],\r\n            (xx, yy),\r\n            method='linear'\r\n        )\r\n\r\n        # Calculate differences\r\n        diff = actual_z - design_z\r\n        cell_area = grid_size ** 2\r\n\r\n        cut_volume = np.nansum(diff[diff > 0]) * cell_area\r\n        fill_volume = np.nansum(np.abs(diff[diff \u003C 0])) * cell_area\r\n        net_volume = cut_volume - fill_volume\r\n\r\n        return {\r\n            'cut_volume_m3': float(cut_volume),\r\n            'fill_volume_m3': float(fill_volume),\r\n            'net_volume_m3': float(net_volume),\r\n            'balance': 'cut' if net_volume > 0 else 'fill',\r\n            'grid_size_m': grid_size,\r\n            'area_m2': float((x_max - x_min) * (y_max - y_min))\r\n        }\r\n\r\n    def calculate_stockpile_volume(self, base_method: str = 'lowest_perimeter') -> VolumeResult:\r\n        \"\"\"Calculate stockpile volume\"\"\"\r\n        # Find boundary points\r\n        from scipy.spatial import ConvexHull\r\n        hull = ConvexHull(self.points[:, :2])\r\n        boundary_points = self.points[hull.vertices]\r\n\r\n        if base_method == 'lowest_perimeter':\r\n            reference_z = np.min(boundary_points[:, 2])\r\n        elif base_method == 'average_perimeter':\r\n            reference_z = np.mean(boundary_points[:, 2])\r\n        elif base_method == 'triangulated':\r\n            # Create base triangulation from boundary\r\n            reference_z = np.min(self.points[:, 2])\r\n        else:\r\n            reference_z = np.min(self.points[:, 2])\r\n\r\n        # Calculate volume using triangulated surface\r\n        volume = self._triangulated_volume(reference_z)\r\n\r\n        hull_area = self._calculate_hull_area(boundary_points[:, :2])\r\n\r\n        return VolumeResult(\r\n            volume_m3=volume,\r\n            area_m2=hull_area,\r\n            method='triangulated_' + base_method,\r\n            reference_plane=f'z={reference_z:.2f}',\r\n            confidence=0.95\r\n        )\r\n\r\n    def _triangulated_volume(self, reference_z: float) -> float:\r\n        \"\"\"Calculate volume using Delaunay triangulation\"\"\"\r\n        # Create 2D triangulation\r\n        points_2d = self.points[:, :2]\r\n        tri = Delaunay(points_2d)\r\n\r\n        volume = 0\r\n        for simplex in tri.simplices:\r\n            # Get triangle vertices\r\n            p1 = self.points[simplex[0]]\r\n            p2 = self.points[simplex[1]]\r\n            p3 = self.points[simplex[2]]\r\n\r\n            # Calculate prism volume\r\n            avg_height = (p1[2] + p2[2] + p3[2]) \u002F 3 - reference_z\r\n            if avg_height > 0:\r\n                # Triangle area\r\n                area = 0.5 * abs(\r\n                    (p2[0] - p1[0]) * (p3[1] - p1[1]) -\r\n                    (p3[0] - p1[0]) * (p2[1] - p1[1])\r\n                )\r\n                volume += area * avg_height\r\n\r\n        return volume\r\n\r\n    def _calculate_hull_area(self, points_2d: np.ndarray) -> float:\r\n        \"\"\"Calculate area of convex hull\"\"\"\r\n        hull = ConvexHull(points_2d)\r\n        return hull.volume  # In 2D, volume is actually area\r\n\r\n    def generate_contours(self, interval: float = 1.0) -> List[Dict]:\r\n        \"\"\"Generate elevation contours\"\"\"\r\n        z_min = np.min(self.points[:, 2])\r\n        z_max = np.max(self.points[:, 2])\r\n\r\n        contour_levels = np.arange(\r\n            np.floor(z_min \u002F interval) * interval,\r\n            np.ceil(z_max \u002F interval) * interval + interval,\r\n            interval\r\n        )\r\n\r\n        contours = []\r\n        for level in contour_levels:\r\n            # In production, use proper contouring algorithm\r\n            contours.append({\r\n                'elevation': float(level),\r\n                'type': 'major' if level % 5 == 0 else 'minor'\r\n            })\r\n\r\n        return contours\r\n```\r\n\r\n### Progress Monitoring\r\n\r\n```python\r\nfrom datetime import date\r\nfrom typing import List, Dict\r\n\r\n@dataclass\r\nclass ProgressPoint:\r\n    date: date\r\n    point_cloud: PointCloud\r\n    orthomosaic_path: str\r\n    annotations: Dict = field(default_factory=dict)\r\n\r\nclass ConstructionProgressMonitor:\r\n    \"\"\"Monitor construction progress from drone surveys\"\"\"\r\n\r\n    def __init__(self, project_name: str):\r\n        self.project_name = project_name\r\n        self.surveys: List[ProgressPoint] = []\r\n        self.volume_calculator = None\r\n\r\n    def add_survey(self, survey_date: date, point_cloud: PointCloud,\r\n                  orthomosaic_path: str):\r\n        \"\"\"Add survey for progress tracking\"\"\"\r\n        self.surveys.append(ProgressPoint(\r\n            date=survey_date,\r\n            point_cloud=point_cloud,\r\n            orthomosaic_path=orthomosaic_path\r\n        ))\r\n        self.surveys.sort(key=lambda x: x.date)\r\n\r\n    def calculate_earthwork_progress(self, design_surface: np.ndarray) -> List[Dict]:\r\n        \"\"\"Calculate earthwork progress over time\"\"\"\r\n        progress = []\r\n\r\n        for i, survey in enumerate(self.surveys):\r\n            calc = VolumeCalculator(survey.point_cloud)\r\n            cut_fill = calc.calculate_cut_fill(design_surface)\r\n\r\n            progress.append({\r\n                'date': survey.date.isoformat(),\r\n                'survey_index': i + 1,\r\n                'cut_volume_m3': cut_fill['cut_volume_m3'],\r\n                'fill_volume_m3': cut_fill['fill_volume_m3'],\r\n                'net_volume_m3': cut_fill['net_volume_m3']\r\n            })\r\n\r\n        # Calculate progress percentages\r\n        if len(progress) > 1:\r\n            initial = progress[0]\r\n            for p in progress[1:]:\r\n                if initial['net_volume_m3'] != 0:\r\n                    p['progress_pct'] = (\r\n                        (initial['net_volume_m3'] - p['net_volume_m3']) \u002F\r\n                        abs(initial['net_volume_m3']) * 100\r\n                    )\r\n                else:\r\n                    p['progress_pct'] = 0\r\n\r\n        return progress\r\n\r\n    def detect_changes(self, survey1_idx: int, survey2_idx: int,\r\n                      threshold_m: float = 0.5) -> Dict:\r\n        \"\"\"Detect changes between two surveys\"\"\"\r\n        pc1 = self.surveys[survey1_idx].point_cloud\r\n        pc2 = self.surveys[survey2_idx].point_cloud\r\n\r\n        # Create grids for comparison\r\n        grid_size = 1.0  # 1m grid\r\n\r\n        x_min = min(np.min(pc1.points[:, 0]), np.min(pc2.points[:, 0]))\r\n        x_max = max(np.max(pc1.points[:, 0]), np.max(pc2.points[:, 0]))\r\n        y_min = min(np.min(pc1.points[:, 1]), np.min(pc2.points[:, 1]))\r\n        y_max = max(np.max(pc1.points[:, 1]), np.max(pc2.points[:, 1]))\r\n\r\n        x_bins = np.arange(x_min, x_max + grid_size, grid_size)\r\n        y_bins = np.arange(y_min, y_max + grid_size, grid_size)\r\n\r\n        changes = {\r\n            'increased': [],  # Areas where elevation increased\r\n            'decreased': [],  # Areas where elevation decreased\r\n            'unchanged': 0\r\n        }\r\n\r\n        for i in range(len(x_bins) - 1):\r\n            for j in range(len(y_bins) - 1):\r\n                # Get points in cell for both surveys\r\n                cell_x = (x_bins[i] + x_bins[i + 1]) \u002F 2\r\n                cell_y = (y_bins[j] + y_bins[j + 1]) \u002F 2\r\n\r\n                mask1 = (\r\n                    (pc1.points[:, 0] >= x_bins[i]) & (pc1.points[:, 0] \u003C x_bins[i + 1]) &\r\n                    (pc1.points[:, 1] >= y_bins[j]) & (pc1.points[:, 1] \u003C y_bins[j + 1])\r\n                )\r\n                mask2 = (\r\n                    (pc2.points[:, 0] >= x_bins[i]) & (pc2.points[:, 0] \u003C x_bins[i + 1]) &\r\n                    (pc2.points[:, 1] >= y_bins[j]) & (pc2.points[:, 1] \u003C y_bins[j + 1])\r\n                )\r\n\r\n                if np.sum(mask1) > 0 and np.sum(mask2) > 0:\r\n                    z1 = np.mean(pc1.points[mask1, 2])\r\n                    z2 = np.mean(pc2.points[mask2, 2])\r\n                    diff = z2 - z1\r\n\r\n                    if diff > threshold_m:\r\n                        changes['increased'].append({\r\n                            'x': cell_x, 'y': cell_y, 'change_m': diff\r\n                        })\r\n                    elif diff \u003C -threshold_m:\r\n                        changes['decreased'].append({\r\n                            'x': cell_x, 'y': cell_y, 'change_m': diff\r\n                        })\r\n                    else:\r\n                        changes['unchanged'] += 1\r\n\r\n        changes['summary'] = {\r\n            'increased_cells': len(changes['increased']),\r\n            'decreased_cells': len(changes['decreased']),\r\n            'unchanged_cells': changes['unchanged'],\r\n            'date_from': self.surveys[survey1_idx].date.isoformat(),\r\n            'date_to': self.surveys[survey2_idx].date.isoformat()\r\n        }\r\n\r\n        return changes\r\n\r\n    def generate_progress_report(self, output_path: str,\r\n                                design_surface: np.ndarray = None) -> str:\r\n        \"\"\"Generate progress report\"\"\"\r\n        import pandas as pd\r\n\r\n        report_data = {\r\n            'project': self.project_name,\r\n            'surveys': len(self.surveys),\r\n            'date_range': {\r\n                'start': self.surveys[0].date.isoformat() if self.surveys else None,\r\n                'end': self.surveys[-1].date.isoformat() if self.surveys else None\r\n            }\r\n        }\r\n\r\n        if design_surface is not None:\r\n            report_data['earthwork_progress'] = self.calculate_earthwork_progress(design_surface)\r\n\r\n        with pd.ExcelWriter(output_path, engine='openpyxl') as writer:\r\n            # Summary\r\n            pd.DataFrame([report_data]).to_excel(writer, sheet_name='Summary', index=False)\r\n\r\n            # Survey list\r\n            survey_data = [{\r\n                'Date': s.date,\r\n                'Orthomosaic': s.orthomosaic_path\r\n            } for s in self.surveys]\r\n            pd.DataFrame(survey_data).to_excel(writer, sheet_name='Surveys', index=False)\r\n\r\n            # Progress (if available)\r\n            if 'earthwork_progress' in report_data:\r\n                pd.DataFrame(report_data['earthwork_progress']).to_excel(\r\n                    writer, sheet_name='Progress', index=False\r\n                )\r\n\r\n        return output_path\r\n```\r\n\r\n### BIM Comparison\r\n\r\n```python\r\nclass BIMDroneComparator:\r\n    \"\"\"Compare drone survey with BIM model\"\"\"\r\n\r\n    def __init__(self, bim_surface: np.ndarray, drone_pointcloud: PointCloud):\r\n        self.bim = bim_surface\r\n        self.drone = drone_pointcloud\r\n\r\n    def compare_elevations(self, grid_size: float = 1.0) -> Dict:\r\n        \"\"\"Compare drone elevations with BIM design\"\"\"\r\n        # Create comparison grid\r\n        x_min = max(np.min(self.bim[:, 0]), np.min(self.drone.points[:, 0]))\r\n        x_max = min(np.max(self.bim[:, 0]), np.max(self.drone.points[:, 0]))\r\n        y_min = max(np.min(self.bim[:, 1]), np.min(self.drone.points[:, 1]))\r\n        y_max = min(np.max(self.bim[:, 1]), np.max(self.drone.points[:, 1]))\r\n\r\n        x_grid = np.arange(x_min, x_max, grid_size)\r\n        y_grid = np.arange(y_min, y_max, grid_size)\r\n        xx, yy = np.meshgrid(x_grid, y_grid)\r\n\r\n        # Interpolate both surfaces\r\n        bim_z = griddata(self.bim[:, :2], self.bim[:, 2], (xx, yy), method='linear')\r\n        drone_z = griddata(\r\n            self.drone.points[:, :2],\r\n            self.drone.points[:, 2],\r\n            (xx, yy),\r\n            method='linear'\r\n        )\r\n\r\n        # Calculate differences\r\n        diff = drone_z - bim_z\r\n\r\n        valid_mask = ~np.isnan(diff)\r\n        valid_diff = diff[valid_mask]\r\n\r\n        return {\r\n            'mean_diff_m': float(np.mean(valid_diff)),\r\n            'std_diff_m': float(np.std(valid_diff)),\r\n            'max_diff_m': float(np.max(valid_diff)),\r\n            'min_diff_m': float(np.min(valid_diff)),\r\n            'rmse_m': float(np.sqrt(np.mean(valid_diff ** 2))),\r\n            'within_5cm': float(np.sum(np.abs(valid_diff) \u003C 0.05) \u002F len(valid_diff) * 100),\r\n            'within_10cm': float(np.sum(np.abs(valid_diff) \u003C 0.10) \u002F len(valid_diff) * 100),\r\n            'comparison_points': int(len(valid_diff))\r\n        }\r\n\r\n    def find_deviations(self, tolerance_m: float = 0.1) -> List[Dict]:\r\n        \"\"\"Find areas with significant deviations\"\"\"\r\n        deviations = []\r\n        grid_size = 1.0\r\n\r\n        # Grid comparison\r\n        x_min = max(np.min(self.bim[:, 0]), np.min(self.drone.points[:, 0]))\r\n        x_max = min(np.max(self.bim[:, 0]), np.max(self.drone.points[:, 0]))\r\n        y_min = max(np.min(self.bim[:, 1]), np.min(self.drone.points[:, 1]))\r\n        y_max = min(np.max(self.bim[:, 1]), np.max(self.drone.points[:, 1]))\r\n\r\n        for x in np.arange(x_min, x_max, grid_size):\r\n            for y in np.arange(y_min, y_max, grid_size):\r\n                # Get average elevations\r\n                bim_mask = (\r\n                    (self.bim[:, 0] >= x) & (self.bim[:, 0] \u003C x + grid_size) &\r\n                    (self.bim[:, 1] >= y) & (self.bim[:, 1] \u003C y + grid_size)\r\n                )\r\n                drone_mask = (\r\n                    (self.drone.points[:, 0] >= x) & (self.drone.points[:, 0] \u003C x + grid_size) &\r\n                    (self.drone.points[:, 1] >= y) & (self.drone.points[:, 1] \u003C y + grid_size)\r\n                )\r\n\r\n                if np.sum(bim_mask) > 0 and np.sum(drone_mask) > 0:\r\n                    bim_z = np.mean(self.bim[bim_mask, 2])\r\n                    drone_z = np.mean(self.drone.points[drone_mask, 2])\r\n                    diff = drone_z - bim_z\r\n\r\n                    if abs(diff) > tolerance_m:\r\n                        deviations.append({\r\n                            'x': x + grid_size \u002F 2,\r\n                            'y': y + grid_size \u002F 2,\r\n                            'bim_elevation': bim_z,\r\n                            'actual_elevation': drone_z,\r\n                            'deviation_m': diff,\r\n                            'type': 'high' if diff > 0 else 'low'\r\n                        })\r\n\r\n        return deviations\r\n```\r\n\r\n## Quick Reference\r\n\r\n| Measurement | Method | Accuracy |\r\n|-------------|--------|----------|\r\n| Stockpile Volume | Triangulated | ±2-5% |\r\n| Cut\u002FFill Volume | Grid comparison | ±5% |\r\n| Area Measurement | Orthomosaic | \u003C1cm GSD |\r\n| Elevation (DEM) | Photogrammetry | ±2-5cm |\r\n| Progress Tracking | Multi-temporal | Relative |\r\n\r\n## Resources\r\n\r\n- **OpenDroneMap**: https:\u002F\u002Fwww.opendronemap.org\r\n- **Pix4D**: https:\u002F\u002Fwww.pix4d.com\r\n- **LAStools**: https:\u002F\u002Frapidlasso.com\u002Flastools\u002F\r\n- **DDC Website**: https:\u002F\u002Fdatadrivenconstruction.io\r\n\r\n## Next Steps\r\n\r\n- See `progress-monitoring-cv` for image-based progress\r\n- See `bim-validation-pipeline` for model comparison\r\n- See `data-visualization` for 3D visualization\r\n",{"slug":5,"description":7,"display_name":37},"Drone Site Survey","main","e7e336ac628c3e3bd771aff1aaa469c93778f94a","skills\u002Fdatadrivenconstruction\u002Fdrone-site-survey",0,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del","2026-08-23 22:39:37",{"source":8,"skillName":5,"commands":46,"prompt":56,"zipAvailable":57},[47,50,53],{"tool":48,"command":49},"npx","npx skills add kbarbel640-del\u002Fskills --skill drone-site-survey",{"tool":51,"command":52},"bunx","bunx skills add kbarbel640-del\u002Fskills --skill drone-site-survey",{"tool":54,"command":55},"pnpm","pnpm dlx skills add kbarbel640-del\u002Fskills --skill drone-site-survey","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fdatadrivenconstruction\u002Fdrone-site-survey\u002FSKILL.md",true,[59],{"path":60,"resourceType":61,"resourceTypeDesc":62,"sizeBytes":63,"sourceHtmlUrl":64},"_meta.json","REFERENCE","参考文档",302,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fdatadrivenconstruction\u002Fdrone-site-survey\u002F_meta.json",[66,96,125,146,152,172],{"skillNo":67,"name":68,"nameZh":6,"description":69,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":70,"categories":71,"keywords":77,"publishedAt":32},"01M6G8J55967D6281ACMKAKTEQ","crowd-prompting","A marketplace where AI agents improve prompts, system instructions, tool descriptions, and other text-based content with domain expertise from real-world operations — and earn tokens for valuable contributions.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzzadrian\u002Fcrowd-prompting\u002FSKILL.md",[72],{"categoryNo":73,"categoryName":74,"taxonomyType":75,"taxonomyTypeDesc":76},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[78,79,80,31,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95],"crowd prompting","prompting","api","tokens","domain","world","prompts","operations","instructions","text","tool","system","improve","productivity","marketplace","https","content","ai agents",{"skillNo":97,"name":98,"nameZh":6,"description":99,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":100,"categories":101,"keywords":107,"publishedAt":32},"01M6G8J0PKPAYVET6BWE8W4C26","dilemm-ai","Compete in DilemmAI, the prisoner's dilemma AI arena at dilemm.ai. Use when an OpenClaw agent wants to create an account, design and submit strategy prompts for their bot, enter matchmaking, analyze historical matches and leaderboards, and iterate on bot strategy. Requires headless browser automation (no API available yet).","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzyori\u002Fdilemm-ai\u002FSKILL.md",[102],{"categoryNo":103,"categoryName":104,"taxonomyType":105,"taxonomyTypeDesc":106},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[108,80,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,84,124],"dilemm ai","arena","analyze","browser","openclaw agent","create","automation","browser automation","bot","openclaw","design","strategy","headless","compete","submit","matchmaking","use",{"skillNo":126,"name":127,"nameZh":6,"description":128,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":129,"categories":130,"keywords":134,"publishedAt":32},"01M6G8HWAF3K9SW7NP3Z83CPJ6","arxiv-paper-reviews","Interact with arXiv Crawler API to fetch papers, read reviews, and submit comments. Use when working with arXiv papers, fetching paper lists by date\u002Fcategory\u002Finterest, viewing paper details with comments, or submitting paper reviews via API at http:\u002F\u002F150.158.152.82:8000.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzxrys\u002Fweak-accept\u002FSKILL.md",[131],{"categoryNo":132,"categoryName":133,"taxonomyType":105,"taxonomyTypeDesc":106},"01JBSEEDCAT000000000000021","资料检索与研究",[135,136,137,138,139,140,141,31,142,122,124,143,80,144,145],"arxiv paper reviews","reviews","arxiv","paper","crawler","papers","comments","fetch","read","http","via",{"skillNo":147,"name":127,"nameZh":6,"description":128,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":148,"categories":149,"keywords":151,"publishedAt":32},"01M6G8HQJ7N7WMY6YV7D7SSJ16","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzxrys\u002Farxiv-paper-reviews\u002FSKILL.md",[150],{"categoryNo":132,"categoryName":133,"taxonomyType":105,"taxonomyTypeDesc":106},[138,137,136,135,145,140,141,31,139,122,124,142,143,80,144],{"skillNo":153,"name":154,"nameZh":6,"description":155,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":156,"categories":157,"keywords":158,"publishedAt":32},"01M6G8HM95Y64JNVFC2HN4E2BG","git-summary","Get a quick summary of the current Git repository including status, recent commits, branches, and contributors.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzweack\u002Fgit-summary\u002FSKILL.md",[],[159,160,161,162,163,164,165,166,167,168,169,170,171],"git","summary","git summary","commits","repository","win32","status","linux","contributors","true","quick","get","branches",{"skillNo":173,"name":174,"nameZh":6,"description":175,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":176,"categories":177,"keywords":182,"publishedAt":32},"01M6G8HGEGF05CNEJCGVYFSC4R","agent-constitution","Interact with AgentConstitution governance contracts on Base Sepolia. Check compliance, read rules, log actions, query governance state.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fztsalexey\u002Fagent-constitution\u002FSKILL.md",[178,179],{"categoryNo":103,"categoryName":104,"taxonomyType":105,"taxonomyTypeDesc":106},{"categoryNo":180,"categoryName":181,"taxonomyType":75,"taxonomyTypeDesc":76},"01JBSEEDCAT000000000000037","律师与法务",[183,184,185,186,31,187,188,38,189,143,190,191,192,193,194,93,195,196,197],"agent constitution","constitution","contracts","tree","query","base","log","check","Rules","projects","compliance","actions","state","governance","github"]