[논문 리뷰]/[산학과제] 결함검출

[논문 리뷰] Fusion of multi-light source illuminated images for effective defect inspection on highly reflective surfaces

johyeongseob 2024. 9. 10. 11:37

논문: https://www.sciencedirect.com/science/article/pii/S088832702200276X

 

저자: Guizhong Fu, Shukai Jia, Wenbin Zhu, Jiangxin Yang, Yanlong Cao, ichael Ying Yang, Yanpeng Cao (Corresponding author)

 

인용: Fu, Guizhong, et al. "Fusion of multi-light source illuminated images for effective defect inspection on highly reflective surfaces." Mechanical Systems and Signal Processing 175 (2022): 109109.

 

깃허브: https://github.com/Xavierman/Fusion-of-multi-light-source-illuminated-images-for-defect-inspection

 

0. 초록 (Abstract)

  제조 과정에서 완성품에 대한 결함 검사는 주로 사람이 담당하였으나, 이는 시간, 돈, 노동이 필요하다. 저자는 이를 대신하기 위한 다중 조명 촬영 시스템 (multi-light source illumination/acquision system)을 개발하였다. 해당 시스템은 여러 입력을 받을 수 있는 (multi-stream) 단일 CNN 모델로 이루어져있으며 deep supervision과 channel attetion(CA)이라는 두 가지의 테크닉을 사용한다. 시스템은 여러 촬영 환경에서 얻은 이미지들의 특징맵 (feature map)을 합성 (fusion)하고 처리하여 더 정확한 결과를 얻었으며, 초당 20 frames이상을 처리할 수 있는 경량화 모델을 만들 수 있었다.

 

그림 1: (좌) 전문가들의 결함 탐지, (중) 제안된 다중 조명 촬영 시스템, (우) 특징 추출 및 합성(fusion) 과정

 

1. 서론 (Introduction)

  효과적인 표면 결함 검출은 제품 생산의 질을 높이기 위한 중요한 단계이다. 이전에는 전문가들이 직접 눈으로 확인했으나 최근 자동 검사 시스템이 관심을 받고 있다. 강력한 학습 능력으로 다양한 컴퓨터 비전 작업에 활용되는 CNN 모델은 결함 검출에서도 결함의 특징을 잘 학습할 수 있다. CNN모델로는 VGG나 ResNet이 존재하나, 계산의 효율성을 위해 SqueezeNet을 시스템의 특징 추출기로 사용한다. 위 시스템이 현재 결함 검출 시스템과 다른 점은 다중 조명을 사용한 점이다. 기존 표면 결함 검출 방법은 단일 조명을 사용하여 결함 이미지를 얻는다. 하지만 이는 현실적인 결함 검출 특히, 빛에 반사되는 매끄러운 재질을 가진 표면을 가진 제품들의 결함 검출에서는 적합하지 않다. 그러므로 저자들은 다중 조명 환경을 이용하여 촬영한 이미지들의 합성을 통해 표면 결함을 검출한다. 해당 방법의 동기(motive)는 인간 전문가들의 방법이다. 그들은 결함을 검출할 때, 조명을 여러 각도에서 제품을 비추며 결함을 검출하기 때문이다. (그림 1) 그러므로 저자들은 네 방향 (좌, 우, 위, 아래)으로 조명을 비추어 결함 이미지를 얻는다. 그리고 제안한 특징 추출기로 특징을 추출하고 합성한 최종 특징맵을 이용하 결함을 분류한다. 또한 높은 정확성을 얻기 위해 각 이미지에서 얻은 특징맵에 deep supervision을 적용하고, 특징맵을 재조정 (re-calibrate)하는 channel attention (CA) 두 가지 기법을 추가한다. 저자들은 표면이 매끄러우면서 여러 결함 종류를 가진 새 데이터셋도 구성하였다. (그림 2) 저자들이 제안한 결함 검출 시스템은 해당 데이터셋에 대해 좋은 결과를 보였다. 본 논문의 contribution은 다음과 같다.

 

  1. 다조명을 활용한 이미지 촬영 시스템을 제안하며, 새로운 다중 촬영 기반 결함 데이터셋 (USB connectors)을 구성하였다.
  2. 결함 검출 시스템의 각 CNN모델에 deep supervision과 channel attention (CA)를 추가하여 결함에 대한 더욱 정확한 특징맵을 추출하였다.
  3. 효과적인 결함 검출 시스템으로 결함 검출에 대한 높은 정확도를 이끌어내었으며 해당 시스템은 경령화된 모델을 사용하며 (3.5MB) 초당 20 프레임 이상을 처리할 수 있다.

 

그림 2: 빛 반사율이 높은 표면을 가진 제품의 결함 이미지 데이터셋 구성 방법

 

2. 다중 조명 촬영 시스템과 데이터셋 (Multi-light source illumination/acquisition system and dataset)

  저자들은 시스템을 이용해, 네 개의 조명을 개별로 작동하여 (single-light source illuminated images) 조명이 위, 아래, 좌, 우 로 비추어진 제품의 표면 이미지를 촬영하고, 네 개의 조명을 모두 작동하여 (uniformly illuminated one)가장 밝은 상태의 표면 이미지를 촬영하여 하나의 제품 표면에 대해 총 다섯 개의 이미지를 얻는다. (그림 3) 데이터셋은 네 가지 결함, BrightLine, Deformation, Scratch, Dent 을 포함하여 약 700개의 샘플을 만들었다. 훈련 데이터는 560개 (다섯 가지 조명 촬영 시스템으로 총 2800개)이며 다른 batch에서 구한 테스트 데이터는 240개이다.

그림 3: 네 가지 결함 종류(BrightLine, Deformation, Scratch, and Dent)에 대한 다조명 샘플 이미지들

 

  현재 데이터셋에는 단점이 하나 있다. 전체 이미지에서 불필요한 배경이 존재하여 결함 검출에서 불필요한 계산이 발생한다. 배경을 제거하기 위해 필요한 부분(800*1000)을 ROI로 지정한 후, [1] 논문에서 사용한 방법을 적용한다. 하나의 이미지에 ROI 좌표를 계산하면, 다섯 개의 이미지에 같은 ROI 좌표를 적용할 수 있다. 이후 사람 전문가가 추출된 ROI에서 결함 레이블링 및 패치화 (200 x 200)를 진행한다. (그림 4) 이를 통해 최종 데이터셋을 구성한다. 데이터셋은 표 1에서 확인할 수 있다. 훈련 데이터와 테스트 데이터의 비율은 1:4이다.

 

[1] Yang, Jiangxin, et al. "A deep learning-based surface defect inspection system using multiscale and channel-compressed features." IEEE transactions on instrumentation and measurement 69.10 (2020): 8032-8042.

 

그림 4: 추출된 ROI로부터 결함 레이블링 작업 및 패치화

 

표 1: 결함 이미지 데이터셋

 

3. 결함 분류 모델 (Defect classification model)

  결함 검출 시스템은 총 4개 (multi-stream) CNN 모델로 구성되어 있다. 각 CNN 모델은 ImageNet으로 사전 훈련된 SqueezeNet이며 이들은 결함 이미지에서 특징 맵을 추출한다. 추가로 결함 검출 정확성을 향상시키기 위해 CNN모델에는 deep supervision과 channel attention (CA) 기법이 사용된다.

 

3.1. 스퀴즈넷 기반 특징 추출 및 합성 (SqueezeNet-based feature extraction and fusion)

  일반적인 CNN 모델은 GoogLeNet, VGG, ResNet이 있으나 특정 작업 (결함 검출)을 위해서 결함 검출 시스템은 경량화 모델인 SqueezeNet architecture를 사용한다. 시스템은 조명 조건 (위, 아래, 좌, 우)에 따라 네 개의 SqueezeNet을 사용하여 각 이미지에서 특징맵을 추출한다. 이후 네 개의 특징맵을 합성한다.

\begin{align*}
    & \mathbf{M} = f(\mathbf{L}, \mathbf{R}, \mathbf{U}, \mathbf{D})
    ,
    \tag{1}
\end{align*}

여기서 f는 합성 함수이고, $\mathbf{L} \in \mathbb{R}^{C_1 \times H_1 \times W_1}, \mathbf{R} \in \mathbb{R}^{C_2 \times H_2 \times W_2}, \mathbf{U} \in \mathbb{R}^{C_3 \times H_3 \times W_3}, \mathbf{D} \in \mathbb{R}^{C_4 \times H_4 \times W_4}$ 는 각 조명 조건에서 얻은 이미지의 특징맵이다. 네 개의 이미지들은 모두 같은 해상도를 가지고 같은 CNN모델을 사용하여 특징맵을 추출하였기 때문에 각 특징맵들 또한 해상도와 차원이 같다. 네 개의 특징맵을 모두 쌓은 다음 (stack) 채널 수를 1/4로 줄이기 위해 3 x 3 커널을 가진 convolution layer를 사용한다. 전체적인 결함 검출 시스템 구조는 다음과 같다. (그림 5, 표 2)

그림 5: 결함 검출 시스템 구조
표 2: CNN모델 (SqueezeNet)의 구조, 마지막 conv 10에서 분류작업을 위해 채널 수를 5 (BrightLine, Deformation, Scratch, Dent, Normal)로 줄인다.

 

3.2. 개별 경로에 대한 Deep supervision (Deep supervision on individual streams)

  결함 검출 정확도를 향상시키기 위해, 저자들은 다중 손실함수를 사용하여 시스템 CNN 모델의 파라미터를 업데이트한다. 손실함수는 개별 경로의 피처맵과 합성 피처맵에 적용된다.

\begin{align*}
    &  \mathcal{L}_F=(\alpha_\text{L}\mathcal{L}_\text{L}+\alpha_\text{R}\mathcal{L}_\text{R}+\alpha_\text{D}\mathcal{L}_\text{D}+\alpha_\text{D}\mathcal{L}_\text{D}+\alpha_\text{M}\mathcal{L}_\text{M})
    ,
    \tag{2}
\end{align*}

위 수식에서 $\mathcal{L}$은 cross entropy loss term이다. 그리고 다섯 개의 $\alpha$는 기본값이 1이다. $\mathcal{L}$은 아래와 같이 계산된다.

\begin{align*}
    & \mathcal{L}=-\sum_{k=1}^{5}t_k[k\log \text{Pr}(y=k)+(1-k)\log (1-\text{Pr}(y=k))]
    ,
    \tag{3}
\end{align*}

$t_k=1$은 입력 이미지의 레이블이 k인 경우이고, 반대는 $t_k=0$이다. Pr(y=k)는 예측 confidence 이다.

\begin{align*}
    & \text{Pr}(y=k)=\frac{e^{G_k}}{\sum_{j=1}^{5}e^{G_j}}
    ,
    \tag{4}
\end{align*}

$G_k$는 k번째 GAP layer 값이다.

 

3.3. 채널 어텐션 기반 특징맵 재조정 (CA-based feature re-calibration)

  저자들은 특징맵 추출 계산 이후, CA 매커니즘을 CNN모델에 추가하였다. CA메커님즘은 주어진 입력 피처맵 $X \in \mathbb{R}^{C \times H \times W}$에 global average pooling (GAP)를 적용하고 아래 식과 같이 fully-connected (FC) layer와 ReLU 활성화 함수를 적용한 스칼라 값에 원래 피처맵을 곱한다. (그림 6) (주: CA-based re-calibration은 SENet 구조와 동일하다.)

\begin{align*}
    & z(c)=GAP(\text{X})=\frac{1}{H \times W}\sum_{h=1}^{H}\sum_{w=1}^{W}\text{X}(c,h,w)
    .
    \tag{5}
\end{align*}

\begin{align*}
    & \alpha=Sig(FC(Re(FC(z))))
    .
    \tag{6}
\end{align*}

\begin{align*}
    & \text{X}^{CA}(c)=\alpha \cdot \text{X}(c)
    ,
    \tag{7}
\end{align*}

채널 가중치 $\alpha$는 지도학습이나 인간의 개입 없이 장면마다 다르게 적용되며 (scene-specific) 자기 학습이 가능한 장점이 있다.

 

그림 6: CA-기반 특징맵 재조정 구조

 

4. 실험 결과 (Experiments)

4.1. 실험 세부사항 (Implementation details)

  결함 검출 시스템은 네 개의 SqueezeNet과 추가 fusion layers와 DS, CA-based 재조정 모듈로 이루어져 있으며, pre-trained SqueezNet을 제외한 파라미터는 Xavier 분포로 초기화한다. 배치 사이즈는 64이며, 훈련 iteration(epoch)은 4000이다. 초기 학습률 (learning rate)은 0.0025이며, optimizer는 Stochastic Gradient Descent (SGD)를 사용하고 momentum과 weight decay는 각각 0.9와 0.0001이다. 결함 분류 metrix는 분류 정확도 (classification accuracy %)를 사용한다.

 

4.2. 성능 분석 (Performance analysis)

4.2.1. 다중 조명으로 촬영된 이미지들의 피처맵 합성 (Multi-light source illuminated feature fusion)

  실험 결과는 다중 조명으로 쵤영된 이미지들의 피처맵 합성이 표면 결함을 검출하는데 더욱 효과적인 피처맵을 생산하고 결함 분류 성능을 높일 수 있음을 보여준다. (표 3) 그리고 SqueezeNet에서 early stage (Fire4), late stage (Fire9), final stage (Conv10)에서 각각 특징맵을 추출해 어느 stage에서 나온 특징맵이 결함 검출에 유의미한지 실험한 결과, final stage에서 나온 피처맵이 더 깊은 convolutional layer를 통과하였기 때문에 더 좋은 결함 분류 정확도를 나타낸다. (표 4)

 

4.2.2. Depp supervision and CA-based re-calibration

  저자들은 결함 검출 시스템에 Deep supervision (DS)와 channel attention (CA) 기반 특징맵 재조정이 유의미한지 ablation 실험을 진행하였다. 실험 결과, DS와 CA는 유의미함을 알 수 있다. (표 5) 또한 시각적으로 DS와 CA의 효과를 입증하였다. (그림 7, 그림 8) 특히 그림 8에서는 CA 전에는 prediction과 label이 맞지 않았으나, CA 후에는 prediction과 label이 동일함을 볼 수 있다.

표 3: 단일 조명으로 촬영한 이미지의 결함 분류 성능과 다중 조명으로 촬영한 이미지들의 합성을 통한 결함 분류 성능 비교

 

표 4: CNN 모델의 각 stage에서 추출한 피처맵을 통한 결함 분류 성능 비교

 

표 5: DS와 CA의 유무에 따른 결함 분류 성능 비교

 

그림 7: Deep supervision 전, 후의 피처맵 시각화

 

그림 8: channel attention-based re-calibration 전, 후의 피처맵 시각화

 

5. 결론 (Conclusion)

  본 논문에서 저자들은 결함 검출 시스템의 하드웨어 시스템을 구성하였고, 다중 조명으로 촬영한 이미지들의 특징맵 합성을 통한 결함 분류 모델은 빠르고 높은 결함 분류 정확도를 보였다.

 

6. Pytorch 코드

1. Pre-trained SqueezeNet on ImageNet-1K

import torch
import torch.nn as nn
from torchsummary import summary


class SqueezeNet1_0(nn.Module):
    def __init__(self):
        super(SqueezeNet1_0, self).__init__()

        # squeezenet1_0 model and pre-trained weights with ImageNet-1K
        squeezenet1_0 = models.squeezenet1_0(pretrained=True)

        # features: no use fully connected layer of SqueezeNet
        self.features = nn.Sequential(*list(squeezenet1_0.features.children()))


    def forward(self, x):
        x = self.features(x)
        return x


if __name__ == "__main__":
    model = SqueezeNet1_0().cuda()
    summary(model, input_size=(3, 224, 224))

 

Pytorch에서 제공하는 SqueezeNet이다. SqueezeNet의 Fire 모듈과 전체 구조는 아래 코드조각을 참고하면 된다.

 

class Fire(nn.Module):
    def __init__(self, inplanes: int, squeeze_planes: int, expand1x1_planes: int, expand3x3_planes: int) -> None:
        super().__init__()
        self.inplanes = inplanes
        self.squeeze = nn.Conv2d(inplanes, squeeze_planes, kernel_size=1)
        self.squeeze_activation = nn.ReLU(inplace=True)
        self.expand1x1 = nn.Conv2d(squeeze_planes, expand1x1_planes, kernel_size=1)
        self.expand1x1_activation = nn.ReLU(inplace=True)
        self.expand3x3 = nn.Conv2d(squeeze_planes, expand3x3_planes, kernel_size=3, padding=1)
        self.expand3x3_activation = nn.ReLU(inplace=True)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = self.squeeze_activation(self.squeeze(x))
        return torch.cat(
            [self.expand1x1_activation(self.expand1x1(x)), self.expand3x3_activation(self.expand3x3(x))], 1
        )


class SqueezeNet(nn.Module):
    def __init__(self, version: str = "1_0", num_classes: int = 1000, dropout: float = 0.5) -> None:
        super().__init__()
        _log_api_usage_once(self)
        self.num_classes = num_classes
        if version == "1_0":
            self.features = nn.Sequential(
                nn.Conv2d(3, 96, kernel_size=7, stride=2),
                nn.ReLU(inplace=True),
                nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
                Fire(96, 16, 64, 64),
                Fire(128, 16, 64, 64),
                Fire(128, 32, 128, 128),
                nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
                Fire(256, 32, 128, 128),
                Fire(256, 48, 192, 192),
                Fire(384, 48, 192, 192),
                Fire(384, 64, 256, 256),
                nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
                Fire(512, 64, 256, 256),
            )

        # Final convolution is initialized differently from the rest
        final_conv = nn.Conv2d(512, self.num_classes, kernel_size=1)
        self.classifier = nn.Sequential(
            nn.Dropout(p=dropout), final_conv, nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d((1, 1))
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = self.features(x)
        x = self.classifier(x)
        return torch.flatten(x, 1)

 

 

2. Channel attention re-calibration (SENet)

import torch
import torch.nn as nn

# Channel attention re-calibration
class SENet(nn.Module):
    def __init__(self, c, r=2):
        super(SENet, self).__init__()
        self.squeeze = nn.AdaptiveAvgPool2d(1)
        self.excitation = nn.Sequential(
            nn.Linear(c, c // r, bias=False),
            nn.ReLU(inplace=True),
            nn.Linear(c // r, c, bias=False),
            nn.Sigmoid()
        )

    def forward(self, input_filter):
        batch, channel, _, _ = input_filter.size()
        se = (self.squeeze(input_filter)).view(batch, channel)
        ex = (self.excitation(se))
        alpha = ex.view(batch, channel, 1, 1)
        return alpha * input_filter

 

 

3. Classifier

import torch
import torch.nn as nn
from model import SENet

# 공통 기능을 가진 BaseClassifier 클래스
class BaseClassifier(nn.Module):
    def __init__(self):
        super(BaseClassifier, self).__init__()
        in_channels = 512
        out_channels = 5

        self.SENet = SENet(in_channels)
        # self.dropout = nn.Dropout(p=0.3)

        self.Conv10 = nn.Sequential(
            nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1),
            nn.ReLU()
        )
        self.Pool10 = nn.AdaptiveAvgPool2d((1, 1))

    def process_features(self, features):
        features = self.SENet(features)
        # features = self.dropout(features)
        logits = self.Conv10(features)
        logits = self.Pool10(logits).squeeze()
        return features, logits


class SingleViewClassifier(BaseClassifier):
    def __init__(self, model):
        super(SingleViewClassifier, self).__init__()
        self.model = model

    def forward(self, x):
        feature = self.model(x)  # CNN 모델을 통해 feature 추출
        feature, logit = self.process_features(feature)  # 공통 처리
        return feature, logit


class MultiViewClassifier(BaseClassifier):
    def __init__(self):
        super(MultiViewClassifier, self).__init__()
        in_channels = 512*4
        out_channels = 512

        self.fusion_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, padding=1)

    def forward(self, outputs):
        concat = torch.cat(outputs, dim=1)
        fusion = self.fusion_conv(concat)
        _, fusion_logit = self.process_features(fusion)
        return fusion_logit