diff --git a/qreader.py b/qreader.py index 8fbba5e..50a2513 100644 --- a/qreader.py +++ b/qreader.py @@ -54,6 +54,10 @@ class DecodeQRResult: image: np.ndarray result: Decoded + cropped_bbox: np.ndarray | None = None + cropped_quad: np.ndarray | None = None + corrected_perspective_values: tuple[np.ndarray, np.ndarray] | None = None + def wrap( scale_factor: float, @@ -62,6 +66,10 @@ def wrap( blur_kernel_sizes: tuple[tuple[int, int], ...] | None, image: np.ndarray, results: typing.List[Decoded], + + cropped_bbox: np.ndarray | None = None, + cropped_quad: np.ndarray | None = None, + corrected_perspective_values: tuple[np.ndarray, np.ndarray] | None = None ) -> list[DecodeQRResult]: return [ @@ -72,6 +80,10 @@ def wrap( blur_kernel_sizes=blur_kernel_sizes, image=image, result=result, + + cropped_bbox=cropped_bbox, + cropped_quad=cropped_quad, + corrected_perspective_values = corrected_perspective_values, ) for result in results ] @@ -147,6 +159,7 @@ def detect( bounding box in absolute coordinates, while 'bbox_xyxyn' is the bounding box in normalized coordinates (from 0. to 1.). """ + return self.detector.detect(image=image, is_bgr=is_bgr) def decode( @@ -155,6 +168,7 @@ def decode( detection_result: dict[ str, np.ndarray | float | tuple[float | int, float | int] ], + return_zbar_poly: bool = False, ) -> str | None: """ This method decodes a single QR code on the given image, described by a detection_result. @@ -188,11 +202,41 @@ def decode( f"Double decoding failed for {self.reencode_to}. Returning utf-8 decoded string." ) + if return_zbar_poly: + polygon = decodeQRResult.result.polygon + if decodeQRResult.corrections == "cropped_bbox": + bbox_xyxy = decodeQRResult.cropped_bbox + x_offset, y_offset = bbox_xyxy[0], bbox_xyxy[1] + scale_factor = decodeQRResult.scale_factor + corrected_polygon = [ + (point.x / scale_factor + x_offset, point.y / scale_factor + y_offset) + for point in polygon + ] + return decoded_str, corrected_polygon + elif decodeQRResult.corrections == "corrected_perspective": + padded_quad_xy, dst_pts = decodeQRResult.corrected_perspective_values + M_inv = cv2.getPerspectiveTransform(dst_pts, padded_quad_xy) + corrected_polygon = cv2.perspectiveTransform(np.array([[(point.x, point.y) for point in polygon]], dtype=np.float32), M_inv)[0] + scale_factor = decodeQRResult.scale_factor + corrected_polygon = [ + (point[0] / scale_factor, point[1] / scale_factor) for point in corrected_polygon + ] + return decoded_str, corrected_polygon + + else: + return decoded_str, None + return decoded_str + if return_zbar_poly: + return None, None return None def detect_and_decode( - self, image: np.ndarray, return_detections: bool = False, is_bgr: bool = False + self, + image: np.ndarray, + return_detections: bool = False, + is_bgr: bool = False, + return_zbar_poly: bool = False, ) -> ( tuple[ dict[str, np.ndarray | float | tuple[float | int, float | int]], str | None @@ -217,7 +261,7 @@ def detect_and_decode( """ detections = self.detect(image=image, is_bgr=is_bgr) decoded_qrs = tuple( - self.decode(image=image, detection_result=detection) + self.decode(image=image, detection_result=detection, return_zbar_poly=return_zbar_poly) for detection in detections ) @@ -313,6 +357,7 @@ def _decode_qr_zbar( ): continue + rescaled_image = cv2.resize( src=image, dsize=None, @@ -329,7 +374,11 @@ def _decode_qr_zbar( blur_kernel_sizes=None, image=rescaled_image, results=decodedQR, + + cropped_bbox=detection_result[BBOX_XYXY] if label == "cropped_bbox" else None, + corrected_perspective_values=(detection_result[PADDED_QUAD_XY], self.__get_dst_pts(corrected_perspective)) if label == "corrected_perspective" else None, ) + # For QRs with black background and white foreground, try to invert the image inverted_image = image = 255 - rescaled_image decodedQR = decodeQR(inverted_image, symbols=[ZBarSymbol.QRCODE]) @@ -341,6 +390,9 @@ def _decode_qr_zbar( blur_kernel_sizes=None, image=inverted_image, results=decodedQR, + + cropped_bbox=detection_result[BBOX_XYXY] if label == "cropped_bbox" else None, + corrected_perspective_values=(detection_result[PADDED_QUAD_XY], self.__get_dst_pts(corrected_perspective)) if label == "corrected_perspective" else None, ) # If it not works, try to parse to grayscale (if it is not already) @@ -362,6 +414,9 @@ def _decode_qr_zbar( blur_kernel_sizes=((5, 5), (7, 7)), image=gray, results=decodedQR, + + cropped_bbox=detection_result[BBOX_XYXY] if label == "cropped_bbox" else None, + corrected_perspective_values=(detection_result[PADDED_QUAD_XY], self.__get_dst_pts(corrected_perspective)) if label == "corrected_perspective" else None, ) if len(rescaled_image.shape) == 3: @@ -387,6 +442,9 @@ def _decode_qr_zbar( blur_kernel_sizes=((3, 3),), image=sharpened_gray, results=decodedQR, + + cropped_bbox=detection_result[BBOX_XYXY] if label == "cropped_bbox" else None, + corrected_perspective_values=(detection_result[PADDED_QUAD_XY], self.__get_dst_pts(corrected_perspective)) if label == "corrected_perspective" else None, ) return [] @@ -436,6 +494,12 @@ def __correct_perspective( dst_img = cv2.warpPerspective(image, M, (N, N)) return dst_img + + def __get_dst_pts(self, corrected_perspective: np.ndarray) -> np.ndarray: + N = corrected_perspective.shape[0] + return np.array( + [[0, 0], [N - 1, 0], [N - 1, N - 1], [0, N - 1]], dtype=np.float32 + ) def __threshold_and_blur_decodings( self,