How to Detect Scratches with OpenCV Python: Complete Tutorial

Tutorial Computer Vision

Hardware Used

USB webcam or industrial camera LED or controlled lighting Standard PC or Raspberry Pi

Software Stack

Python 3.7+ OpenCV NumPy Matplotlib scikit-image

Use Cases

Metal surface inspection Automotive paint QC Glass defect detection Polished surface inspection Scratch depth measurement

Introduction

Scratch detection is critical for quality control in manufacturing industries including automotive, electronics, glass, and metal fabrication. In this tutorial, you’ll learn multiple approaches to detect scratches using OpenCV Python, from traditional image processing to modern deep learning methods.

What You’ll Learn

  • Preprocessing techniques for scratch detection
  • Edge detection methods (Canny, Sobel, Laplacian)
  • Morphological operations for noise removal
  • Hough Line Transform for linear scratches
  • Contour analysis for irregular scratches
  • Combining multiple techniques for robust detection

Applications

  • Metal surface inspection
  • Glass quality control
  • Plastic part inspection
  • Smartphone screen defect detection
  • Automotive paint inspection

Environment Setup

1
2
3
4
pip install opencv-python
pip install numpy
pip install matplotlib
pip install scikit-image
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import cv2
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path

# Helper function to display images
def show_images(images, titles, figsize=(15, 5)):
    """Display multiple images side by side"""
    n = len(images)
    fig, axes = plt.subplots(1, n, figsize=figsize)
    if n == 1:
        axes = [axes]

    for ax, img, title in zip(axes, images, titles):
        if len(img.shape) == 2:
            ax.imshow(img, cmap='gray')
        else:
            ax.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        ax.set_title(title)
        ax.axis('off')

    plt.tight_layout()
    plt.show()

Method 1: Edge Detection Approach

This method works well for linear scratches on uniform surfaces.

Step 1: Load and Preprocess Image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def load_and_preprocess(image_path):
    """Load image and convert to grayscale"""
    # Read image
    img = cv2.imread(image_path)
    if img is None:
        raise ValueError(f"Could not load image: {image_path}")

    # Convert to grayscale
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    return img, gray

# Load test image
img, gray = load_and_preprocess('metal_surface.jpg')
show_images([img, gray], ['Original', 'Grayscale'])

Step 2: Denoise the Image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def denoise_image(gray, method='gaussian'):
    """
    Remove noise while preserving edges

    Args:
        gray: Grayscale input image
        method: 'gaussian', 'bilateral', or 'median'
    """
    if method == 'gaussian':
        # Gaussian blur - fast but blurs edges
        denoised = cv2.GaussianBlur(gray, (5, 5), 0)

    elif method == 'bilateral':
        # Bilateral filter - preserves edges better
        denoised = cv2.bilateralFilter(gray, 9, 75, 75)

    elif method == 'median':
        # Median filter - good for salt-and-pepper noise
        denoised = cv2.medianBlur(gray, 5)

    return denoised

# Compare denoising methods
gaussian = denoise_image(gray, 'gaussian')
bilateral = denoise_image(gray, 'bilateral')
median = denoise_image(gray, 'median')

show_images(
    [gray, gaussian, bilateral, median],
    ['Original', 'Gaussian', 'Bilateral', 'Median'],
    figsize=(20, 5)
)

Step 3: Edge Detection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def detect_edges(img, method='canny'):
    """
    Detect edges using various methods

    Args:
        img: Grayscale input image
        method: 'canny', 'sobel', 'laplacian', 'scharr'
    """
    if method == 'canny':
        # Canny edge detection - best for clean edges
        edges = cv2.Canny(img, 50, 150)

    elif method == 'sobel':
        # Sobel operator - directional gradients
        sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
        sobely = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
        edges = np.sqrt(sobelx**2 + sobely**2)
        edges = np.uint8(edges / edges.max() * 255)

    elif method == 'laplacian':
        # Laplacian - detects edges in all directions
        laplacian = cv2.Laplacian(img, cv2.CV_64F)
        edges = np.uint8(np.absolute(laplacian))

    elif method == 'scharr':
        # Scharr - more accurate than Sobel
        scharrx = cv2.Scharr(img, cv2.CV_64F, 1, 0)
        scharry = cv2.Scharr(img, cv2.CV_64F, 0, 1)
        edges = np.sqrt(scharrx**2 + scharry**2)
        edges = np.uint8(edges / edges.max() * 255)

    return edges

# Apply denoising then edge detection
denoised = denoise_image(gray, 'bilateral')
canny = detect_edges(denoised, 'canny')
sobel = detect_edges(denoised, 'sobel')
laplacian = detect_edges(denoised, 'laplacian')

show_images(
    [denoised, canny, sobel, laplacian],
    ['Denoised', 'Canny', 'Sobel', 'Laplacian'],
    figsize=(20, 5)
)

Step 4: Morphological Operations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def morphological_cleanup(edges, kernel_size=3, operation='close'):
    """
    Clean up edge detection results

    Args:
        edges: Binary edge image
        kernel_size: Size of morphological kernel
        operation: 'close', 'open', 'dilate', 'erode'
    """
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))

    if operation == 'close':
        # Close gaps in scratches
        result = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)

    elif operation == 'open':
        # Remove small noise
        result = cv2.morphologyEx(edges, cv2.MORPH_OPEN, kernel)

    elif operation == 'dilate':
        # Thicken edges
        result = cv2.dilate(edges, kernel, iterations=1)

    elif operation == 'erode':
        # Thin edges
        result = cv2.erode(edges, kernel, iterations=1)

    return result

# Clean up Canny edges
closed = morphological_cleanup(canny, kernel_size=3, operation='close')
opened = morphological_cleanup(closed, kernel_size=3, operation='open')

show_images(
    [canny, closed, opened],
    ['Canny Edges', 'After Closing', 'After Opening']
)

Method 2: Hough Line Transform (for Linear Scratches)

Perfect for detecting straight line scratches.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def detect_scratches_hough(img, edges, min_line_length=50, max_line_gap=10):
    """
    Detect linear scratches using Hough Line Transform

    Args:
        img: Original color image
        edges: Edge-detected binary image
        min_line_length: Minimum line length to detect
        max_line_gap: Maximum gap between line segments

    Returns:
        Image with detected scratches drawn
    """
    # Probabilistic Hough Line Transform
    lines = cv2.HoughLinesP(
        edges,
        rho=1,              # Distance resolution in pixels
        theta=np.pi/180,    # Angular resolution in radians
        threshold=50,       # Minimum votes
        minLineLength=min_line_length,
        maxLineGap=max_line_gap
    )

    # Draw detected lines
    result = img.copy()
    scratch_count = 0

    if lines is not None:
        for line in lines:
            x1, y1, x2, y2 = line[0]

            # Calculate line angle and length
            length = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
            angle = np.abs(np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi)

            # Filter by angle and length (customize for your use case)
            # Example: detect mostly horizontal or vertical scratches
            if length > min_line_length:
                cv2.line(result, (x1, y1), (x2, y2), (0, 0, 255), 2)
                scratch_count += 1

    return result, scratch_count

# Detect scratches
denoised = denoise_image(gray, 'bilateral')
edges = detect_edges(denoised, 'canny')
result, count = detect_scratches_hough(img, edges, min_line_length=30, max_line_gap=10)

print(f"Detected {count} potential scratches")
show_images([img, edges, result], ['Original', 'Edges', f'Detected ({count} scratches)'])

Method 3: Contour Detection (for Irregular Scratches)

Works for non-linear scratches and complex shapes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def detect_scratches_contours(img, edges, min_area=50, max_area=10000):
    """
    Detect scratches using contour analysis

    Args:
        img: Original color image
        edges: Binary edge image
        min_area: Minimum contour area to consider
        max_area: Maximum contour area to consider

    Returns:
        Image with detected scratches, list of scratch contours
    """
    # Find contours
    contours, hierarchy = cv2.findContours(
        edges,
        cv2.RETR_EXTERNAL,
        cv2.CHAIN_APPROX_SIMPLE
    )

    result = img.copy()
    scratch_contours = []

    for contour in contours:
        area = cv2.contourArea(contour)

        # Filter by area
        if min_area < area < max_area:
            # Calculate aspect ratio
            x, y, w, h = cv2.boundingRect(contour)
            aspect_ratio = max(w, h) / min(w, h)

            # Scratches are typically elongated (high aspect ratio)
            if aspect_ratio > 3:  # Adjust threshold as needed
                scratch_contours.append(contour)

                # Draw bounding box
                cv2.rectangle(result, (x, y), (x + w, y + h), (0, 255, 0), 2)

                # Draw contour
                cv2.drawContours(result, [contour], -1, (0, 0, 255), 2)

    return result, scratch_contours

# Detect scratches using contours
denoised = denoise_image(gray, 'bilateral')
edges = detect_edges(denoised, 'canny')
cleaned = morphological_cleanup(edges, kernel_size=3, operation='close')

result, scratches = detect_scratches_contours(img, cleaned, min_area=100, max_area=5000)

print(f"Detected {len(scratches)} scratches")
show_images([img, cleaned, result], ['Original', 'Edges', f'Detected ({len(scratches)} scratches)'])

Method 4: Adaptive Thresholding + Analysis

For varying lighting conditions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def detect_scratches_adaptive(img, block_size=11, C=2):
    """
    Detect scratches using adaptive thresholding

    Args:
        img: Grayscale image
        block_size: Size of neighborhood for threshold calculation
        C: Constant subtracted from mean

    Returns:
        Binary image with potential scratches
    """
    # Apply adaptive thresholding
    binary = cv2.adaptiveThreshold(
        img,
        255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY_INV,
        block_size,
        C
    )

    # Remove small noise
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    cleaned = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)

    # Close gaps in scratches
    kernel_close = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 1))
    result = cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, kernel_close)

    return result

# Test adaptive approach
denoised = denoise_image(gray, 'bilateral')
adaptive = detect_scratches_adaptive(denoised, block_size=15, C=3)

show_images([gray, denoised, adaptive], ['Original', 'Denoised', 'Adaptive Threshold'])

Complete Scratch Detection Pipeline

Combining all methods for robust detection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
class ScratchDetector:
    """
    Complete scratch detection pipeline
    """

    def __init__(self,
                 denoise_method='bilateral',
                 edge_method='canny',
                 min_length=30,
                 min_area=100,
                 min_aspect_ratio=3.0):

        self.denoise_method = denoise_method
        self.edge_method = edge_method
        self.min_length = min_length
        self.min_area = min_area
        self.min_aspect_ratio = min_aspect_ratio

    def preprocess(self, img):
        """Preprocess image"""
        if len(img.shape) == 3:
            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        else:
            gray = img

        # Denoise
        if self.denoise_method == 'bilateral':
            denoised = cv2.bilateralFilter(gray, 9, 75, 75)
        elif self.denoise_method == 'gaussian':
            denoised = cv2.GaussianBlur(gray, (5, 5), 0)
        else:
            denoised = gray

        return denoised

    def detect_edges(self, img):
        """Detect edges"""
        if self.edge_method == 'canny':
            edges = cv2.Canny(img, 50, 150)
        elif self.edge_method == 'sobel':
            sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
            sobely = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
            edges = np.sqrt(sobelx**2 + sobely**2)
            edges = np.uint8(edges / edges.max() * 255)
        else:
            edges = cv2.Canny(img, 50, 150)

        return edges

    def cleanup_edges(self, edges):
        """Morphological cleanup"""
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
        closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)
        opened = cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel)
        return opened

    def find_scratches(self, img, edges):
        """Find scratch contours"""
        contours, _ = cv2.findContours(
            edges,
            cv2.RETR_EXTERNAL,
            cv2.CHAIN_APPROX_SIMPLE
        )

        scratches = []

        for contour in contours:
            area = cv2.contourArea(contour)

            if area < self.min_area:
                continue

            x, y, w, h = cv2.boundingRect(contour)
            aspect_ratio = max(w, h) / (min(w, h) + 1e-5)

            if aspect_ratio >= self.min_aspect_ratio:
                scratches.append({
                    'contour': contour,
                    'bbox': (x, y, w, h),
                    'area': area,
                    'aspect_ratio': aspect_ratio
                })

        return scratches

    def detect(self, img):
        """
        Main detection pipeline

        Args:
            img: Input image (BGR or grayscale)

        Returns:
            Dictionary with results
        """
        # Preprocess
        preprocessed = self.preprocess(img)

        # Detect edges
        edges = self.detect_edges(preprocessed)

        # Cleanup
        cleaned = self.cleanup_edges(edges)

        # Find scratches
        scratches = self.find_scratches(img, cleaned)

        # Create visualization
        result = img.copy() if len(img.shape) == 3 else cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

        for scratch in scratches:
            x, y, w, h = scratch['bbox']
            cv2.rectangle(result, (x, y), (x + w, y + h), (0, 255, 0), 2)
            cv2.drawContours(result, [scratch['contour']], -1, (0, 0, 255), 1)

        return {
            'scratches': scratches,
            'count': len(scratches),
            'preprocessed': preprocessed,
            'edges': edges,
            'cleaned': cleaned,
            'visualization': result
        }

# Use the detector
detector = ScratchDetector(
    denoise_method='bilateral',
    edge_method='canny',
    min_length=30,
    min_area=150,
    min_aspect_ratio=3.5
)

# Detect scratches
results = detector.detect(img)

print(f"✅ Detected {results['count']} scratches")

# Show results
show_images(
    [img, results['edges'], results['visualization']],
    ['Original', 'Edges', f"Detected ({results['count']} scratches)"],
    figsize=(18, 6)
)

# Print scratch details
for i, scratch in enumerate(results['scratches']):
    print(f"\nScratch {i+1}:")
    print(f"  Area: {scratch['area']:.0f} pixels")
    print(f"  Aspect Ratio: {scratch['aspect_ratio']:.2f}")
    print(f"  BBox: {scratch['bbox']}")

Advanced Techniques

1. Directional Filtering

Detect scratches in specific directions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def directional_scratch_detection(img, angle=0, tolerance=15):
    """
    Detect scratches in a specific direction

    Args:
        img: Grayscale image
        angle: Target angle in degrees (0=horizontal, 90=vertical)
        tolerance: Angular tolerance in degrees
    """
    # Detect edges
    edges = cv2.Canny(img, 50, 150)

    # Find lines
    lines = cv2.HoughLinesP(edges, 1, np.pi/180, 50, minLineLength=30, maxLineGap=10)

    directional_scratches = []

    if lines is not None:
        for line in lines:
            x1, y1, x2, y2 = line[0]
            line_angle = np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi
            line_angle = abs(line_angle)

            # Check if within tolerance
            if abs(line_angle - angle) <= tolerance or abs(line_angle - angle - 180) <= tolerance:
                directional_scratches.append(line[0])

    return directional_scratches

# Detect horizontal scratches
horizontal = directional_scratch_detection(gray, angle=0, tolerance=20)
print(f"Detected {len(horizontal)} horizontal scratches")

# Detect vertical scratches
vertical = directional_scratch_detection(gray, angle=90, tolerance=20)
print(f"Detected {len(vertical)} vertical scratches")

2. Multi-Scale Detection

Detect scratches at different scales:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def multiscale_scratch_detection(img, scales=[0.5, 1.0, 1.5]):
    """
    Detect scratches at multiple scales

    Args:
        img: Input grayscale image
        scales: List of scale factors

    Returns:
        Combined detection results
    """
    all_scratches = []

    for scale in scales:
        # Resize image
        h, w = img.shape[:2]
        new_h, new_w = int(h * scale), int(w * scale)
        resized = cv2.resize(img, (new_w, new_h))

        # Detect scratches
        detector = ScratchDetector()
        results = detector.detect(resized)

        # Scale coordinates back
        for scratch in results['scratches']:
            x, y, w, h = scratch['bbox']
            scaled_bbox = (
                int(x / scale),
                int(y / scale),
                int(w / scale),
                int(h / scale)
            )
            scratch['bbox'] = scaled_bbox
            all_scratches.append(scratch)

    # Remove duplicates (non-maximum suppression)
    # ... implement NMS here ...

    return all_scratches

3. Background Subtraction

For images with known good reference:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def reference_based_detection(test_img, reference_img, threshold=30):
    """
    Detect scratches by comparing to reference image

    Args:
        test_img: Image to inspect
        reference_img: Known good reference
        threshold: Difference threshold
    """
    # Ensure same size
    test_img = cv2.resize(test_img, (reference_img.shape[1], reference_img.shape[0]))

    # Compute absolute difference
    diff = cv2.absdiff(test_img, reference_img)

    # Threshold difference
    _, binary = cv2.threshold(diff, threshold, 255, cv2.THRESH_BINARY)

    # Remove noise
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    cleaned = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)

    return cleaned, diff

Parameter Tuning Tips

Finding Optimal Canny Thresholds

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def tune_canny_thresholds(img, low_range=(30, 100), high_range=(100, 200)):
    """Interactive Canny threshold tuning"""

    best_low, best_high = 50, 150

    # Try different combinations
    fig, axes = plt.subplots(3, 3, figsize=(15, 15))

    low_values = np.linspace(low_range[0], low_range[1], 3, dtype=int)
    high_values = np.linspace(high_range[0], high_range[1], 3, dtype=int)

    for i, low in enumerate(low_values):
        for j, high in enumerate(high_values):
            edges = cv2.Canny(img, low, high)
            axes[i, j].imshow(edges, cmap='gray')
            axes[i, j].set_title(f'Low={low}, High={high}')
            axes[i, j].axis('off')

    plt.tight_layout()
    plt.show()

    return best_low, best_high

# Tune parameters
low, high = tune_canny_thresholds(gray)

Determining Optimal Morphology Kernel Size

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def tune_morphology_kernel(edges, sizes=[3, 5, 7, 9]):
    """Compare different kernel sizes"""

    fig, axes = plt.subplots(2, len(sizes), figsize=(20, 8))

    for i, size in enumerate(sizes):
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (size, size))

        closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)
        opened = cv2.morphologyEx(edges, cv2.MORPH_OPEN, kernel)

        axes[0, i].imshow(closed, cmap='gray')
        axes[0, i].set_title(f'Close (k={size})')
        axes[0, i].axis('off')

        axes[1, i].imshow(opened, cmap='gray')
        axes[1, i].set_title(f'Open (k={size})')
        axes[1, i].axis('off')

    plt.tight_layout()
    plt.show()

Real-World Example: Metal Surface Inspection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def inspect_metal_surface(image_path, save_path=None):
    """
    Complete metal surface inspection pipeline

    Args:
        image_path: Path to metal surface image
        save_path: Optional path to save results

    Returns:
        Inspection report
    """
    # Load image
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # Initialize detector
    detector = ScratchDetector(
        denoise_method='bilateral',
        edge_method='canny',
        min_area=100,
        min_aspect_ratio=3.0
    )

    # Detect scratches
    results = detector.detect(img)

    # Generate report
    report = {
        'image_path': image_path,
        'total_scratches': results['count'],
        'status': 'FAIL' if results['count'] > 0 else 'PASS',
        'scratches': []
    }

    for i, scratch in enumerate(results['scratches']):
        x, y, w, h = scratch['bbox']
        report['scratches'].append({
            'id': i + 1,
            'location': (x, y),
            'size': (w, h),
            'area': scratch['area'],
            'severity': 'HIGH' if scratch['area'] > 500 else 'MEDIUM' if scratch['area'] > 200 else 'LOW'
        })

    # Save visualization
    if save_path:
        cv2.imwrite(save_path, results['visualization'])

    return report, results['visualization']

# Inspect image
report, viz = inspect_metal_surface('metal_part.jpg', 'inspection_result.jpg')

print("\n" + "="*50)
print("INSPECTION REPORT")
print("="*50)
print(f"Status: {report['status']}")
print(f"Total Scratches: {report['total_scratches']}")
print("\nDetails:")
for scratch in report['scratches']:
    print(f"  Scratch {scratch['id']}: {scratch['severity']} severity, area={scratch['area']:.0f}px²")
print("="*50)

Performance Optimization

Speed Comparison

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import time

def benchmark_methods(img, iterations=10):
    """Compare speed of different methods"""

    methods = {
        'Canny': lambda: cv2.Canny(img, 50, 150),
        'Sobel': lambda: cv2.Sobel(img, cv2.CV_64F, 1, 1, ksize=3),
        'Laplacian': lambda: cv2.Laplacian(img, cv2.CV_64F),
        'Complete Pipeline': lambda: ScratchDetector().detect(cv2.cvtColor(img, cv2.COLOR_GRAY2BGR))
    }

    results = {}

    for name, func in methods.items():
        start = time.time()
        for _ in range(iterations):
            func()
        elapsed = (time.time() - start) / iterations * 1000  # ms
        results[name] = elapsed

    print("\nPerformance Benchmark:")
    print("-" * 40)
    for name, time_ms in sorted(results.items(), key=lambda x: x[1]):
        print(f"{name:20s}: {time_ms:6.2f} ms")

    return results

# Run benchmark
benchmark_results = benchmark_methods(gray)

Troubleshooting Common Issues

Issue 1: False Positives from Texture

Problem: Detecting surface texture as scratches

Solution:

1
2
3
4
5
6
7
8
9
# Use stronger denoising
denoised = cv2.bilateralFilter(gray, 15, 100, 100)

# Increase minimum scratch length/area
detector = ScratchDetector(min_area=200, min_aspect_ratio=5.0)

# Apply texture filter
texture_removed = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT,
                                   cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15)))

Issue 2: Missing Small Scratches

Problem: Small scratches not detected

Solution:

1
2
3
4
5
6
7
8
# Increase image resolution
upscaled = cv2.resize(gray, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)

# Use more sensitive edge detection
edges = cv2.Canny(upscaled, 30, 100)  # Lower thresholds

# Reduce minimum area
detector = ScratchDetector(min_area=50)

Issue 3: Poor Performance on Curved Surfaces

Problem: Scratches on curved/reflective surfaces

Solution:

1
2
3
4
5
6
7
# Use adaptive thresholding
adaptive = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                 cv2.THRESH_BINARY, 15, 2)

# Apply CLAHE for better contrast
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
equalized = clahe.apply(gray)

Integration with Deep Learning

For even better results, combine with neural networks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Pseudo-code for hybrid approach
def hybrid_scratch_detection(img):
    """
    Combine traditional CV with deep learning
    """
    # Step 1: Traditional CV for candidate regions
    detector = ScratchDetector()
    results = detector.detect(img)

    # Step 2: Extract ROIs
    rois = []
    for scratch in results['scratches']:
        x, y, w, h = scratch['bbox']
        roi = img[y:y+h, x:x+w]
        rois.append(roi)

    # Step 3: Classify with CNN (pseudo-code)
    # model = load_model('scratch_classifier.h5')
    # predictions = model.predict(rois)

    # Step 4: Filter false positives
    # confirmed_scratches = [r for r, p in zip(results['scratches'], predictions) if p > 0.8]

    return results

Conclusion

You now have multiple approaches to detect scratches using OpenCV Python:

  1. Edge Detection - Fast, good for clean backgrounds
  2. Hough Transform - Best for linear scratches
  3. Contour Analysis - Works for irregular shapes
  4. Adaptive Methods - Handles varying lighting

Best Practices:

  • Always denoise before edge detection
  • Tune parameters for your specific surface type
  • Use morphological operations to clean results
  • Combine multiple methods for robust detection
  • Consider deep learning for complex cases

Next Steps:


Hardware:

Books:

Courses:


Questions? Issues? Drop a comment below or contact us!

Related Posts:

Don't Miss the Next Insight

Weekly updates on computer vision, defect detection, and practical AI implementation.

Was this article helpful?

Your feedback helps improve future content

James Lions

James Lions

AI & Computer Vision enthusiast exploring the future of automated defect detection

Discussion