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에 넣지 않고 화면에만 출력한다.
QR Detect Thread에서는 Optional 하게 이미지 전-처리 과정을 수행 할 수 있다.
( 예, Blur 수행 여부, 이미지 반전 사용 여부, adaptive 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
'프로젝트 로그 > 테스트x솔루션 JIG 개발기' 카테고리의 다른 글
오렌지파이5B GPIO로 전원 끄기 (0) | 2024.01.11 |
---|---|
orangepi에서 root 권한으로 실행하는 명령을 비밀번호 없이 수행되도록 설정하기 (0) | 2024.01.03 |
[testXJIG QR 코드 인식 고도화 #3-2] 조명 제어 (0) | 2023.07.20 |
[testXJIG QR 코드 인식 고도화 #3-1] QR 코드 인식을 위한 S/W 로직 (0) | 2023.07.18 |
[testXJIG QR 코드 인식 고도화 #2-4] IDS 카메라 기반 QT 프로그램 죽는 문제 해결 (0) | 2023.07.17 |