Yaz Hobooti commited on
Commit
fed2115
·
1 Parent(s): 191c79a

Implement ChatGPT barcode improvements: ZXing hints, tiling fallback, GS1 parsing

Browse files
Files changed (1) hide show
  1. app.py +118 -50
app.py CHANGED
@@ -852,6 +852,36 @@ import fitz # PyMuPDF
852
  try:
853
  import zxingcpp; HAS_ZXING=True
854
  except Exception: HAS_ZXING=False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
855
  try:
856
  from pyzbar.pyzbar import decode as zbar_decode, ZBarSymbol; HAS_ZBAR=True
857
  except Exception: HAS_ZBAR=False; ZBarSymbol=None
@@ -891,47 +921,58 @@ def _validate(sym: str, payload: str) -> bool:
891
  s, norm = _normalize_upc_ean(sym, payload)
892
  return _ean_checksum_ok(norm) if s in ("EAN13","EAN-13","EAN8","EAN-8","UPCA","UPC-A") else bool(payload)
893
 
 
 
 
 
 
 
 
 
 
 
 
 
 
894
  def _decode_zxing(pil: Image.Image) -> List[Dict[str,Any]]:
895
  if not HAS_ZXING: return []
896
  arr = np.asarray(pil.convert("L"))
897
  out=[]
898
- for r in zxingcpp.read_barcodes(arr): # try_harder is default True in recent builds; otherwise supply options
899
- # zxingcpp.Position may be iterable (sequence of points) or an object with corner attributes
 
 
 
 
 
900
  x1=y1=x2=y2=w=h=0
901
  pos = getattr(r, "position", None)
902
  pts: List[Any] = []
903
  if pos is not None:
904
  try:
905
- pts = list(pos) # works if iterable
906
  except TypeError:
907
- # Fall back to known corner attribute names across versions
908
  corner_names = (
909
- "top_left", "topLeft",
910
- "top_right", "topRight",
911
- "bottom_left", "bottomLeft",
912
- "bottom_right", "bottomRight",
913
- "point1", "point2", "point3", "point4",
914
  )
915
  seen=set()
916
  for name in corner_names:
917
  if hasattr(pos, name):
918
  p = getattr(pos, name)
919
- # avoid duplicates
920
- if id(p) not in seen and hasattr(p, "x") and hasattr(p, "y"):
921
- pts.append(p)
922
- seen.add(id(p))
923
  if pts:
924
- xs=[int(getattr(p, "x", 0)) for p in pts]
925
- ys=[int(getattr(p, "y", 0)) for p in pts]
926
- x1,x2=min(xs),max(xs); y1,y2=min(ys),max(ys)
927
- w,h=x2-x1,y2-y1
 
928
  out.append({
929
- "type": str(r.format),
930
  "data": r.text or "",
931
- "left": x1,
932
- "top": y1,
933
- "width": w,
934
- "height": h,
935
  })
936
  return out
937
 
@@ -971,38 +1012,62 @@ def _decode_cv2_qr(pil: Image.Image) -> List[Dict[str,Any]]:
971
 
972
  def _decode_variants(pil: Image.Image) -> List[Dict[str,Any]]:
973
  """
974
- Try a few light variants. If we upscale, scale detections back to the original size.
975
- We avoid rotations here to keep coordinates aligned with the original image.
976
  """
977
- variants = []
978
- w, h = pil.size
979
-
980
- # base variants @1.0x
981
- variants.append(("orig", pil, 1.0))
982
- variants.append(("gray", ImageOps.grayscale(pil).convert("RGB"), 1.0))
983
- variants.append(("bin", _binarize(pil).convert("RGB"), 1.0))
984
-
985
- # upsample small pages, then scale back coords
986
- if max(w, h) < 1600:
987
- up2 = pil.resize((w*2, h*2), resample=Image.NEAREST)
988
- variants.append(("up2", up2, 2.0))
989
- variants.append(("up2_bin", _binarize(up2).convert("RGB"), 2.0))
990
-
991
- for tag, vimg, sc in variants:
992
- # Prefer ZXing, then ZBar, then DMTX, then OpenCV-QR
993
- res = _decode_zxing(vimg) or _decode_zbar(vimg) or _decode_dmtx(vimg) or _decode_cv2_qr(vimg)
994
- if not res:
995
- continue
996
-
997
- # Scale results back to original size when needed
998
- if sc != 1.0:
999
  for r in res:
1000
- r["left"] = int(round(r.get("left", 0) / sc))
1001
- r["top"] = int(round(r.get("top", 0) / sc))
1002
- r["width"] = int(round(r.get("width", 0) / sc))
1003
- r["height"] = int(round(r.get("height",0) / sc))
1004
  return res
1005
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
  return []
1007
 
1008
  def _pix_to_pil(pix) -> Image.Image:
@@ -1417,6 +1482,9 @@ def find_barcode_boxes_and_info_from_pdf(pdf_path: str, image_size: Optional[Tup
1417
  "page": page_idx + 1,
1418
  "source": f"page@dpi{int(effective_dpi)}"
1419
  })
 
 
 
1420
 
1421
  y_offset += ph
1422
  doc.close()
 
852
  try:
853
  import zxingcpp; HAS_ZXING=True
854
  except Exception: HAS_ZXING=False
855
+
856
+ def _zxing_hints_all():
857
+ if not HAS_ZXING:
858
+ return None
859
+ hints = zxingcpp.DecodeHints()
860
+ # Work harder + allow rotated orientations internally (keeps coords correct)
861
+ try: hints.try_harder = True
862
+ except Exception: pass
863
+ try: hints.try_rotate = True
864
+ except Exception: pass
865
+ # GS1 interpretation (FNC1)
866
+ try: hints.is_gs1 = True
867
+ except Exception: pass
868
+
869
+ # Enable as many formats as the wrapper exposes (covers GS1 DataBar incl. stacked/expanded)
870
+ BF = getattr(zxingcpp, "BarcodeFormat", None)
871
+ mask = 0
872
+ for nm in [
873
+ "QR_CODE", "AZTEC", "PDF417", "DATA_MATRIX", "MAXICODE",
874
+ "EAN_13", "EAN_8", "UPC_A", "UPC_E",
875
+ "CODE_39", "CODE_93", "CODE_128", "ITF", "CODABAR",
876
+ "RSS_14", "RSS_EXPANDED", "RSS_LIMITED", # AKA GS1 DataBar family
877
+ "GS1_DATABAR", "GS1_DATABAR_EXPANDED", "GS1_DATABAR_LIMITED" # some wheels expose these names
878
+ ]:
879
+ val = getattr(BF, nm, None)
880
+ if val is not None:
881
+ mask |= int(val)
882
+ if mask:
883
+ hints.formats = mask
884
+ return hints
885
  try:
886
  from pyzbar.pyzbar import decode as zbar_decode, ZBarSymbol; HAS_ZBAR=True
887
  except Exception: HAS_ZBAR=False; ZBarSymbol=None
 
921
  s, norm = _normalize_upc_ean(sym, payload)
922
  return _ean_checksum_ok(norm) if s in ("EAN13","EAN-13","EAN8","EAN-8","UPCA","UPC-A") else bool(payload)
923
 
924
+ def parse_gs1(text: str) -> Optional[dict]:
925
+ if not text: return None
926
+ # ZXing returns FNC1 as ASCII 29 (\x1D) for GS1-128/QR/DM
927
+ s = text.replace("\x1D", ")(") # visual separator
928
+ # Very lightweight AI parser for common AIs; extend as needed
929
+ import re as _re
930
+ ai_pat = _re.compile(r"\((\d{2,4})\)([^()]+)")
931
+ out = {}
932
+ for m in ai_pat.finditer(s):
933
+ ai, val = m.group(1), m.group(2)
934
+ out[ai] = val
935
+ return out or None
936
+
937
  def _decode_zxing(pil: Image.Image) -> List[Dict[str,Any]]:
938
  if not HAS_ZXING: return []
939
  arr = np.asarray(pil.convert("L"))
940
  out=[]
941
+ hints = _zxing_hints_all()
942
+ try:
943
+ res = zxingcpp.read_barcodes(arr, hints=hints) if hints is not None else zxingcpp.read_barcodes(arr)
944
+ except Exception:
945
+ res = []
946
+
947
+ for r in res or []:
948
  x1=y1=x2=y2=w=h=0
949
  pos = getattr(r, "position", None)
950
  pts: List[Any] = []
951
  if pos is not None:
952
  try:
953
+ pts = list(pos)
954
  except TypeError:
 
955
  corner_names = (
956
+ "top_left","topLeft","top_right","topRight",
957
+ "bottom_left","bottomLeft","bottom_right","bottomRight",
958
+ "point1","point2","point3","point4",
 
 
959
  )
960
  seen=set()
961
  for name in corner_names:
962
  if hasattr(pos, name):
963
  p = getattr(pos, name)
964
+ if id(p) not in seen and hasattr(p,"x") and hasattr(p,"y"):
965
+ pts.append(p); seen.add(id(p))
 
 
966
  if pts:
967
+ xs=[int(getattr(p,"x",0)) for p in pts]
968
+ ys=[int(getattr(p,"y",0)) for p in pts]
969
+ x1,x2=min(xs),max(xs); y1,y2=min(ys),max(ys); w,h=x2-x1,y2-y1
970
+
971
+ fmt = getattr(getattr(r,"format",None),"name", None) or str(getattr(r,"format",""))
972
  out.append({
973
+ "type": fmt,
974
  "data": r.text or "",
975
+ "left": x1, "top": y1, "width": w, "height": h,
 
 
 
976
  })
977
  return out
978
 
 
1012
 
1013
  def _decode_variants(pil: Image.Image) -> List[Dict[str,Any]]:
1014
  """
1015
+ Multi-variant decode with coord-safe upscales and a tiling fallback.
1016
+ We rely on ZXing's internal rotation search via hints (so no manual rotate).
1017
  """
1018
+ def _decode_and_rescale(img: Image.Image, scale: float) -> List[Dict[str,Any]]:
1019
+ res = _decode_zxing(img) or _decode_zbar(img) or _decode_dmtx(img) or _decode_cv2_qr(img)
1020
+ if not res: return []
1021
+ if scale != 1.0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1022
  for r in res:
1023
+ r["left"] = int(round(r.get("left", 0) / scale))
1024
+ r["top"] = int(round(r.get("top", 0) / scale))
1025
+ r["width"] = int(round(r.get("width", 0) / scale))
1026
+ r["height"] = int(round(r.get("height",0) / scale))
1027
  return res
1028
 
1029
+ # 1) Whole-page variants
1030
+ W,H = pil.size
1031
+ variants = [
1032
+ (pil, 1.0),
1033
+ (ImageOps.grayscale(pil).convert("RGB"), 1.0),
1034
+ (_binarize(pil).convert("RGB"), 1.0),
1035
+ ]
1036
+ if max(W,H) < 1800:
1037
+ up2 = pil.resize((W*2, H*2), resample=Image.NEAREST)
1038
+ variants += [
1039
+ (up2, 2.0),
1040
+ (_binarize(up2).convert("RGB"), 2.0),
1041
+ ]
1042
+ for vimg, sc in variants:
1043
+ res = _decode_and_rescale(vimg, sc)
1044
+ if res:
1045
+ return res
1046
+
1047
+ # 2) Tiled fallback (helps tiny or stacked GS1)
1048
+ # Overlapping 3x3 grid
1049
+ grid = 3
1050
+ step_x = W // grid
1051
+ step_y = H // grid
1052
+ ovx, ovy = step_x // 6, step_y // 6
1053
+ hits: List[Dict[str,Any]] = []
1054
+ for iy in range(grid):
1055
+ for ix in range(grid):
1056
+ x0 = max(ix*step_x - ovx, 0)
1057
+ y0 = max(iy*step_y - ovy, 0)
1058
+ x1 = min((ix+1)*step_x + ovx, W)
1059
+ y1 = min((iy+1)*step_y + ovy, H)
1060
+ tile = pil.crop((x0,y0,x1,y1))
1061
+ # light variants per tile
1062
+ for vimg, sc in [(tile,1.0), (_binarize(tile).convert("RGB"),1.0)]:
1063
+ res = _decode_and_rescale(vimg, sc)
1064
+ for r in res:
1065
+ r["left"] += x0
1066
+ r["top"] += y0
1067
+ # width/height already scaled
1068
+ hits.append(r)
1069
+ if hits:
1070
+ return hits
1071
  return []
1072
 
1073
  def _pix_to_pil(pix) -> Image.Image:
 
1482
  "page": page_idx + 1,
1483
  "source": f"page@dpi{int(effective_dpi)}"
1484
  })
1485
+ # Add GS1 parsing if available
1486
+ gs1 = parse_gs1(payload)
1487
+ if gs1: infos[-1]["gs1"] = gs1
1488
 
1489
  y_offset += ph
1490
  doc.close()