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:
- Edge Detection - Fast, good for clean backgrounds
- Hough Transform - Best for linear scratches
- Contour Analysis - Works for irregular shapes
- 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:
- Try these methods on your own images
- Tune parameters for your use case
- Combine with YOLOv8 for end-to-end detection
- Deploy on Jetson Nano for real-time inspection
Recommended Resources
Hardware:
- Industrial USB 3.0 Cameras - High-quality 1080p imaging for consistent defect detection
- LED Ring Light for Microscope - Uniform illumination eliminates shadows
- LED Panel Lights - For larger surface inspections
- Camera Mounting Arms - Stable positioning for repeatability
Books:
- Learning OpenCV 4 Computer Vision with Python - Complete OpenCV guide
- Practical Python and OpenCV - Beginner-friendly introduction
- Programming Computer Vision with Python - Advanced algorithms and techniques
Courses:
- OpenCV Python Tutorial - Official docs
- PyImageSearch - Excellent CV tutorials
Questions? Issues? Drop a comment below or contact us!
Related Posts:
Discussion