AI Structural Defect Detection from Photographs System

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1 servicesAll 1566 services
AI Structural Defect Detection from Photographs System
Medium
~1-2 weeks
FAQ
AI Development Areas
AI Solution Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822

AI-based detection of structural defects from photographs

Cracks in concrete, corrosion of reinforcement, delamination of masonry, deformation of load-bearing elements—early detection is critical for building safety. Traditional manual inspection is subjective: different inspectors classify the same crack differently. The AI system provides a reproducible assessment with quantitative metrics.

Task: Classification and segmentation of defects

Structural defects require pixel-level precision, not just bbox resolution—we care about crack length, width, and orientation. This is the task of semantic segmentation.

import torch
import numpy as np
import cv2
import segmentation_models_pytorch as smp
from PIL import Image
from torchvision import transforms
from dataclasses import dataclass
from typing import Optional

@dataclass
class DefectAnalysis:
    defect_type: str
    severity: str          # 'hairline', 'minor', 'moderate', 'severe', 'critical'
    area_px: int
    area_ratio: float
    max_width_px: Optional[float]
    max_length_px: Optional[float]
    orientation: Optional[float]  # градусы от вертикали
    bounding_box: list

class StructuralDefectDetector:
    def __init__(self, model_path: str):
        """
        UNet++ с EfficientNet-B5 энкодером.
        Дообучен на Concrete Crack Images Dataset (40k изображений)
        + собственный датасет с коррозией и расслоением.
        """
        self.model = smp.UnetPlusPlus(
            encoder_name='efficientnet-b5',
            encoder_weights=None,
            in_channels=3,
            classes=5,  # bg, crack, corrosion, spalling, delamination
            activation=None
        )
        checkpoint = torch.load(model_path, map_location='cpu')
        self.model.load_state_dict(checkpoint['model'])
        self.model.eval()

        self.class_names = {
            0: 'background',
            1: 'crack',
            2: 'corrosion',
            3: 'spalling',
            4: 'delamination'
        }

        self.transform = transforms.Compose([
            transforms.Resize((512, 512)),
            transforms.ToTensor(),
            transforms.Normalize([0.485, 0.456, 0.406],
                                  [0.229, 0.224, 0.225])
        ])

    @torch.no_grad()
    def analyze(self, image: np.ndarray,
                 gsd_mm_per_pixel: Optional[float] = None) -> list[DefectAnalysis]:
        """
        gsd_mm_per_pixel: масштаб (из метаданных съёмки с дрона или лазера).
        Позволяет давать размеры в мм, а не пикселях.
        """
        h, w = image.shape[:2]
        pil_img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
        tensor = self.transform(pil_img).unsqueeze(0)

        logits = self.model(tensor)  # (1, 5, 512, 512)
        mask = logits.argmax(dim=1)[0].numpy()  # (512, 512)

        # Масштабируем маску обратно к исходному размеру
        mask_full = cv2.resize(
            mask.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST
        )

        defects = []
        for cls_id in range(1, 5):
            cls_mask = (mask_full == cls_id).astype(np.uint8)
            if cls_mask.sum() < 100:  # фильтр шума
                continue

            contours, _ = cv2.findContours(cls_mask, cv2.RETR_EXTERNAL,
                                            cv2.CHAIN_APPROX_SIMPLE)

            for cnt in contours:
                area = int(cv2.contourArea(cnt))
                if area < 50:
                    continue

                x, y, cw, ch = cv2.boundingRect(cnt)
                area_ratio = area / (w * h)

                # Для трещин — скелетонизация для длины/ширины
                max_width = None
                max_length = None
                orientation = None

                if cls_id == 1:  # crack
                    max_width, max_length, orientation = self._analyze_crack(
                        cls_mask[y:y+ch, x:x+cw]
                    )

                defects.append(DefectAnalysis(
                    defect_type=self.class_names[cls_id],
                    severity=self._classify_severity(cls_id, area_ratio,
                                                      max_width, gsd_mm_per_pixel),
                    area_px=area,
                    area_ratio=area_ratio,
                    max_width_px=max_width,
                    max_length_px=max_length,
                    orientation=orientation,
                    bounding_box=[x, y, x+cw, y+ch]
                ))

        return defects

    def _analyze_crack(self, crack_roi: np.ndarray) -> tuple:
        """Скелетонизация трещины для измерения ширины и длины"""
        from skimage.morphology import skeletonize

        skeleton = skeletonize(crack_roi > 0)
        length = float(skeleton.sum())  # пикселей скелета ≈ длина

        # Ширина через distance transform
        dist = cv2.distanceTransform(crack_roi, cv2.DIST_L2, 5)
        max_width = float(dist.max() * 2) if dist.max() > 0 else 0

        # Ориентация через PCA
        pts = np.column_stack(np.where(skeleton))
        if len(pts) > 10:
            mean = pts.mean(axis=0)
            centered = pts - mean
            _, _, vt = np.linalg.svd(centered)
            angle = np.degrees(np.arctan2(vt[0, 0], vt[0, 1]))
        else:
            angle = 0.0

        return max_width, length, angle

    def _classify_severity(self, cls_id: int, area_ratio: float,
                             width_px: Optional[float],
                             gsd: Optional[float]) -> str:
        if cls_id == 1:  # crack severity по ширине (мм)
            width_mm = (width_px * gsd) if (width_px and gsd) else None
            if width_mm:
                if width_mm < 0.2:   return 'hairline'
                if width_mm < 0.5:   return 'minor'
                if width_mm < 1.5:   return 'moderate'
                if width_mm < 5.0:   return 'severe'
                return 'critical'

        # Для остальных — по площади
        if area_ratio < 0.005: return 'minor'
        if area_ratio < 0.02:  return 'moderate'
        if area_ratio < 0.05:  return 'severe'
        return 'critical'

Standards for assessing defects

Type of defect Severity criterion Standard (GOST/SP)
Crack in concrete Opening width > 0.3 mm SP 20.13330
Crack in reinforced concrete (bending) > 0.2mm normal, > 0.1mm oblique GOST R 55961
Corrosion of reinforcement Area > 10% of cross-section SP 28.13330
Spalling/chipping of concrete Depth > 20 mm

Case: Inspection of 120 overpass supports

Objective: to assess the technical condition of the overpass using drone photography.

  • 120 supports, 3500 photos with GSD 0.5–1.5 mm/pixel
  • Processing: 2.5 hours on RTX 3090
  • Found: 847 cracks (including 23 critical, width > 1 mm), 156 corrosion zones
  • Manual verification of 5% random results: 94% severity classification accuracy
Project type Term
Crack detector (segmentation) 4–6 weeks
Complete system (4 types of defects + metrics) 7–12 weeks
With measurements in mm and standard assessment 10–16 weeks