[testXJIG QR 코드 인식 고도화 #3-3] 구현 부분

2023. 7. 21. 20:09프로젝트 로그/테스트x솔루션 JIG 개발기

반응형

본 포스팅에서는 testXJIG가 카메라에서 영상을 추출하여 QR 코드를 검출하는 과정을 위한 SW 로직과 코드들을 공유 한다.

 

SW 로직

testXJIG가 카메라에서 영상을 추출하고, 이를 채널 별(QR 코드 갯수 별) 이미지를 잘라 전처리하는 절차는 아래 다이어그램과 같다.

 

testXJIG에서는 카메라 제어를 통해 영상을 추출하는 Video Thread와 이미지를 받아 채널별 이미지를 자르고 QR 코드를 검출하는 QR Detect Thread 두개의 Thread로 동작 한다.

( 처음에는 Video Thread에서 Loop를 돌려 QR Detect Thread 동작을 해 보았는데 속도 문제가 있어 Video Thread와 QR Detect Thread를 분리 하였다. )

 

Video Thread와 QR Detect Thread 간 이미지 전달은 Queue를 통해 전달하며, Video Thread는 Queue에 이미지가 한개라도 있으면( QR Detect Thread에서 이미지를 꺼내가지 않았다면 ) 이미지를 Queue에 넣지 않고 화면에만 출력한다.

 

 

 

카메라 제어를 통해 영상을 추출하는 Thread, [VideoThread]

 

QR Detect Thread에서는 Optional 하게 이미지 전-처리 과정을 수행 할 수 있다.

( 예, Blur 수행 여부, 이미지 반전 사용 여부, adaptive Thread 파라미터 값 변경 )

이미지를 받아 QR을 검출하는 Thread, [QR Detect Thread]

코드

[Video Thread] 이미지 Load 코드(getImageRGB32FromCamera)

IDS 카메라에서 영상을 추출하여 QImage 객체로 만들어 리턴하는 함수이다.

카메라 설정으로 영상을 추출하는 과정에서 Gamma corrector( 밝기 조절 ), EdgeEnhancement ( Edge 강조 ) 기능을 사용할 수 있으며, JIG S/W에서는 Optional 하게 사용 할 수 있도록 함수를 만들었다.

    def getImageRGB32FromCamera(self, gamma=0.4, edgeFactor=0):
        if self.__acquisition_running == False:
            return False, None

        try:
            # Get buffer from device's datastream
            buffer = self.__datastream.WaitForFinishedBuffer(5000)

            # Create IDS peak IPL image for debayering and convert it to RGBa8 format
            ipl_image = ids_peak_ipl_extension.BufferToImage(buffer)

            # Gamma Corrector
            gamma_crtn = ids_peak_ipl.GammaCorrector()
            gamma_crtn.SetGammaCorrectionValue(gamma)
            gamma_crtn.ProcessInPlace(ipl_image)

            converted_ipl_image = ipl_image.ConvertTo(ids_peak_ipl.PixelFormatName_BGRa8)

            edgeEnhanced = ids_peak_ipl.EdgeEnhancement()
            edgeEnhanced.SetFactor(edgeFactor)
            edgeEnhanced.ProcessInPlace(converted_ipl_image)

            
            # Queue buffer so that it can be used again
            self.__datastream.QueueBuffer(buffer)
        except Exception as ex:
            VideoManagerLogger.instance().logger().error("Exception : {0}".format(ex))
            VideoManagerLogger.instance().logger().error(traceback.format_exc())
            
            return False, None

        # Get raw image data from converted image and construct a QImage from it
        image_np_array = converted_ipl_image.get_numpy_1D()
        image = QImage(image_np_array,
                    converted_ipl_image.Width(), converted_ipl_image.Height(), 
                    QImage.Format_RGB32)

        return True, image.copy()

 

[Video Thread] 실행 부

위에서 설명한 로직과 같이 카메라에서 이미지를 추출하고 QR Detect Thread로 이미지를 전달할 Queue에 이미지를 전달하고 추출된 이미지를 화면에 보여주는 기능을 수행한다.

만약 QR 코드가 검출되었다면, self.drawQrRect() 함수를 통해 QR 코드에서 검출된 값과 QR 코드 영역을 녹색 네모로 그려 준다.

    def run(self):
        while self.alive:
            time.sleep(self.__acquisition_timeIntervalS)

            if self.isPause == True:
                continue

            currentTimeStamp = round(time.time() * 1000)

            ######################################################################
            # JIG Operation 중에는 설정 된 시간 만큼만 카메라 동작을 수행하고 멈춤
            if self.isCalibrationMode == False:
                if( (currentTimeStamp - self.startTimeStampResume) > self.CONFIG.VIDEO_SCAN_TIME_MS ):
                    VideoManagerLogger.instance().logger().debug("Timeout : {0}".format((currentTimeStamp - self.startTimeStampResume)))
                    self.setPause()
            ######################################################################

            result, originQImage = self.getImageRGB32FromCamera(gamma=1)
            if result == False:
                continue

            if self.imageQueue.qsize() < 1:
                self.imageQueue.put(originQImage.copy())

            originFrame = self.QImageToNpArray(image=originQImage, channelCnt=4)
            height, width, channels = originFrame.shape

            ''' draw QRCodes Rectangles '''
            self.drawQrRect(originFrame, widthRatio=self.widthRatio, heightRatio=self.heightRatio)

            ''' draw Region Dividing Lines'''
            self.drawRegionRectPerChannel(originFrame, widthRatio=self.widthRatio, heightRatio=self.heightRatio)

            ''' draw Frame Image '''
            self.drawFinalImage(frame=originFrame, width=width, height=height, channels=channels)

            # DrawFinalImage를 그린 후, Pause 동작 수행
            if self.idsQrDetectTh.getFoundedQrCodeCount() >= len(self.Region.rects):
                self.setPause()

 

[QR Detect Thread] 이미지 전처리 함수

Video Thread로 부터 전달 받은 이미지를 QR 검출에 용이하게 이미지 전처리를 수행하는 함수이다.

위 로직에서 설명했듯이 Blur, Invert(반전), adaptiveThreadhold의 파라미터를 Optionally 하게 사용 할 수 있도록 구현 하였다.

 

    def preProcessingMethod1(self, image:QImage, rectIndex, enableInvert=False, thresholdBlockSize=15, isDetailView=False, useGrayScale=False, useBlur=False):
        croppedQImg = self.cutSubImageFromQImage(index=rectIndex, image=image, widthRatio=self.widthRatio, heightRatio=self.heightRatio)

        croppedWidth = croppedQImg.width()
        croppedHeight = croppedQImg.height()

        if useGrayScale == True:
            croppedQImg = croppedQImg.convertToFormat(QImage.Format_Grayscale8)
            croppedFrame = qimage2ndarray.raw_view(croppedQImg)

            if enableInvert == True:
                invertedFrame = cv2.bitwise_not(croppedFrame)
            else:
                invertedFrame = croppedFrame
            
            #resized_croppedFrame = cv2.resize(invertedFrame, (croppedWidth*self.CONFIG.SCALE, croppedHeight*self.CONFIG.SCALE), interpolation=cv2.INTER_LINEAR )
            #resized_croppedFrame = cv2.resize(invertedFrame, (croppedWidth, croppedHeight), interpolation=cv2.INTER_LINEAR )

            resized_croppedFrame = invertedFrame

            if useBlur == True:
                blur = cv2.GaussianBlur(resized_croppedFrame, (5, 5), 0)
                thresholdframe = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, thresholdBlockSize, 2)
            else:
                #thresholdframe = cv2.adaptiveThreshold(resized_croppedFrame, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, thresholdBlockSize, 2)
                thresholdframe = cv2.adaptiveThreshold(resized_croppedFrame, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, thresholdBlockSize, 2)

            preProcessingFrame = thresholdframe
        else:
            #croppedFrame = self.QImageToNpArray(image=croppedQImg, channelCnt=4)
            croppedFrame = qimage2ndarray.rgb_view(croppedQImg)
            #resized_croppedFrame = cv2.resize(croppedFrame, (croppedWidth*self.CONFIG.SCALE, croppedHeight*self.CONFIG.SCALE), interpolation=cv2.INTER_LINEAR )
            #resized_croppedFrame = cv2.resize(croppedFrame, (croppedWidth, croppedHeight), interpolation=cv2.INTER_LINEAR )
            resized_croppedFrame = croppedFrame

            preProcessingFrame = resized_croppedFrame

        
        if isDetailView == True:
            chIdx = rectIndex + 1
            viewerHeight = self.detailViewDict[chIdx].height()
            viewerWidth = self.detailViewDict[chIdx].width()

            preProcessingQImg = qimage2ndarray.array2qimage(array=preProcessingFrame, normalize=True)
            
            resizedQImg = self.resizeQImage(preProcessingQImg, viwerHeight=viewerHeight, viwerWidth=viewerWidth)
            pixmap = QPixmap.fromImage(resizedQImg)
            self.detailViewDict[chIdx].setPixmap(pixmap)

        return True, preProcessingFrame

 

[QR Detect Thread] QR 코드 검출 함수

전처리된 이미지를 기반으로 QR 코드를 찾아 QR 코드의 값과 영역을 추출하는 함수이다.

QR 코드 검출을 위해서는 pyzbar와 openCV의 qrcodedetector를 사용하였으며, 성능 향상을 위해 두 가지 라이브러리를 동시에 수행 한다.

( 두개의 QR 코드 검출 라이브러리가 검출 특성이 달라, pyzbar가 못하는 것을 qrcodedetector가 검출하고 qrcodedetector가 검출 못하는 것을 pyzbar가 검출하는 경우가 있었음. )

    def detectBarcode(self, frame:np.ndarray, rectIndex):
        try:
            barcodes = pyzbar.decode(frame, symbols=[ZBarSymbol.QRCODE])
            if len(barcodes) > 0:
                channelIdx = rectIndex + 1      # 채널인덱스는 1부터 시작, Rect인덱스는 0부터 시작
                # Crop된 이미지를 사용하기 때문에 검색된 바코드는 1개임.
                result, existIdx = self.detectedBarcodeDictObj.appendBarcodeFromPyZbar(channelIdx = channelIdx, pyzbarObj=barcodes[0])
                if result == False:
                    VideoManagerLogger.instance().logger().error("PyZbar. Exist Idx : {0}, Wanted Idx : {1}".format(existIdx, rectIndex))
                    return False
                
                VideoManagerLogger.instance().logger().debug("PyZbar. Found Barcode : {0}".format(barcodes))
                return True

        except Exception as ex:
            VideoManagerLogger.instance().logger().error("Exception(PyZbar) : {0}".format(ex))
            VideoManagerLogger.instance().logger().error(traceback.format_exc())
        

        try:
            data, bbox, rectifiedImage = self.qrDecoder.detectAndDecode(frame)
            if len(data) > 0:
                channelIdx = rectIndex + 1      # 채널인덱스는 1부터 시작, Rect인덱스는 0부터 시작
                result, existIdx = self.detectedBarcodeDictObj.appendBarcodeFromQrDetector(channelIdx = channelIdx, barcodeData=data, boxCoordinates=bbox)
                if result == False:
                    VideoManagerLogger.instance().logger().error("QrDecoder. Exist Idx : {0}, Wanted Idx : {1}".format(existIdx, rectIndex))
                    return False
                
                VideoManagerLogger.instance().logger().debug("QrDecoder. Found Barcode : {0}".format(data))
                return True

        except Exception as ex:
            VideoManagerLogger.instance().logger().error("Exception(QrDecoder) : {0}".format(ex))
            VideoManagerLogger.instance().logger().error(traceback.format_exc())
        
        return False

 

[QR Detect Thread] 실행부

Queue에 Video Thread가 보낸 이미지가 있으면 채널 갯수 만큼 이미지 전처리, QR 코드 검출을 수행 한다.

QR 코드 검출 속도를 높이기 위해, 검출이 성공 했을 때의 이미지 전처리 파라미터 값들을 저장하고 있다가, 다음 전처리에 해당 파라미터 값들을 재 사용한다. ( 재사용한 파라미터로 특정 시간이 넘도록(self.CAMERA_PARAM_REMOVE_TIMEOUT_MS) QR 검출에 실패하면 파라미터 값을 삭제하고 다시 파라미터 값들을 변경해 가면서 QR 코드를 검출 한다. )

 

    def run(self):
        self.alive = True

        originQImage = QImage()
        while self.alive:
            time.sleep(0.001)
            if self.isPause == True:
                self.startResumeTimeStampMs = round(time.time() * 1000)
                self.useGrayScaleFlag = False
                self.useInvertFlag = False
                self.foundQrCodeCount = 0
                continue

            # VideoThread에서 Image를 보낼 때 까지 Block 되어 있음.
            # Queue에 Image가 쌓여 있는 경우, 가장 먼저 수신한 Image를 처리하고 나머지 Image들은 지움
            result, recvedOriginQimage = self.getImageFromQueueBlock(emptyLeftImages=True, timeoutS=0.001)
            if result == True:
                originQImage = recvedOriginQimage
            
            if originQImage.isNull() == True:
                continue

            currentTimeStamp = round(time.time() * 1000)
            if ( (currentTimeStamp - self.startResumeTimeStampMs) > 500 ):
                self.useGrayScaleFlag = True
            
            for rectIdx in range(len(self.Region.rects)):
                channelIdx = rectIdx + 1
                if self.detectedBarcodeDictObj.isExistData(channelIdx=channelIdx):
                    continue

                if( (currentTimeStamp - self.startResumeTimeStampMs) > self.CAMERA_PARAM_REMOVE_TIMEOUT_MS ):
                    self.imageParamManager.removeImageParam(channelIdx=channelIdx)

                imageParamDict = self.imageParamManager.getImageParamDict()
                
                if channelIdx in imageParamDict.keys():
                    useInvertFlag = imageParamDict[channelIdx].invertFlag            
                    useGrayScale = imageParamDict[channelIdx].grayScaleFlag            
                    useBlur = imageParamDict[channelIdx].blurFlag
                    thresholdBlockSize = imageParamDict[channelIdx].thresholdBlockSize
                    #VideoManagerLogger.instance().logger().debug("CH : {0}. Use Exist Param. ThresholdBlockSize : {1}".format(channelIdx, self.thresholdBlockSize))
                else:
                    useInvertFlag = True
                    useGrayScale = True
                    useBlur = True
                    thresholdBlockSize = self.thresholdBlockSize

                result, preProcessedFrame = self.preProcessingBeforeDecodeQr(image = originQImage, rectIndex=rectIdx,
                                                                            useGrayScale=useGrayScale,
                                                                            enableInvert=useInvertFlag, thresholdBlockSize=thresholdBlockSize,
                                                                            useBlur=useBlur)

                if result == False:
                    continue

                result = self.detectBarcode(frame=preProcessedFrame, rectIndex=rectIdx)
                if result == True:
                    self.foundQrCodeCount = self.foundQrCodeCount + 1
                    VideoManagerLogger.instance().logger().debug("Invert Flag : {0}, Threshold Block Size : {1}".format(useInvertFlag, thresholdBlockSize))
                    
                    successDecodeQrImageParam = ImageParam()
                    
                    successDecodeQrImageParam.invertFlag = useInvertFlag
                    successDecodeQrImageParam.grayScaleFlag = useGrayScale
                    successDecodeQrImageParam.blurFlag = useBlur
                    successDecodeQrImageParam.thresholdBlockSize = thresholdBlockSize

                    self.imageParamManager.saveImageParam(channelIdx = channelIdx, imageParam = successDecodeQrImageParam)
                    
            # Invert 된 이미지와, Invert 되지 않은 이미지에 같은 ThresholdBlockSize를 적용하기 위해
            # ThresholdBlockSize는 useInvertFlag가 True일 때만 값을 변경 한다.
            self.thresholdBlockSize = self.thresholdBlockSize + 2
            if self.thresholdBlockSize > self.DEFAULT_MAX_THRESHOLD_BLOCK_SIZE:
                self.thresholdBlockSize = self.DEFAULT_START_THRESHOLD_BLOCK_SIZE
반응형