Sanjayraju30 commited on
Commit
d373620
·
verified ·
1 Parent(s): 32de3b7

Update ocr_engine.py

Browse files
Files changed (1) hide show
  1. ocr_engine.py +285 -115
ocr_engine.py CHANGED
@@ -3,72 +3,145 @@ import numpy as np
3
  import cv2
4
  import re
5
  import logging
 
 
 
6
 
7
- # Set up logging for debugging
8
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
9
 
10
- # Initialize EasyOCR
11
  easyocr_reader = easyocr.Reader(['en'], gpu=False)
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  def estimate_brightness(img):
14
  """Estimate image brightness to detect illuminated displays"""
15
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
16
- return np.mean(gray)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  def detect_roi(img):
19
  """Detect and crop the region of interest (likely the digital display)"""
20
  try:
21
- gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
 
 
 
 
22
  brightness = estimate_brightness(img)
23
- thresh_value = 230 if brightness > 100 else 190
24
- _, thresh = cv2.threshold(gray, thresh_value, 255, cv2.THRESH_BINARY)
25
- kernel = np.ones((9, 9), np.uint8)
26
- dilated = cv2.dilate(thresh, kernel, iterations=3)
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
 
28
  if contours:
29
- valid_contours = [c for c in contours if cv2.contourArea(c) > 500]
 
 
 
 
 
 
 
 
 
30
  if valid_contours:
31
- for contour in sorted(valid_contours, key=cv2.contourArea, reverse=True):
32
- x, y, w, h = cv2.boundingRect(contour)
33
- aspect_ratio = w / h
34
- if 1.5 <= aspect_ratio <= 4.0 and w > 50 and h > 30:
35
- x, y = max(0, x-40), max(0, y-40)
36
- w, h = min(w+80, img.shape[1]-x), min(h+80, img.shape[0]-y)
37
- return img[y:y+h, x:x+w], (x, y, w, h)
 
 
 
 
 
38
  return img, None
39
  except Exception as e:
40
  logging.error(f"ROI detection failed: {str(e)}")
 
41
  return img, None
42
 
43
  def detect_segments(digit_img):
44
  """Detect seven-segment patterns in a digit image"""
45
  h, w = digit_img.shape
46
- if h < 10 or w < 10:
 
47
  return None
48
 
49
- # Define segment regions (top, middle, bottom, left-top, left-bottom, right-top, right-bottom)
50
  segments = {
51
- 'top': (0, w, 0, h//5),
52
- 'middle': (0, w, 2*h//5, 3*h//5),
53
- 'bottom': (0, w, 4*h//5, h),
54
- 'left_top': (0, w//5, 0, h//2),
55
- 'left_bottom': (0, w//5, h//2, h),
56
- 'right_top': (4*w//5, w, 0, h//2),
57
- 'right_bottom': (4*w//5, w, h//2, h)
58
  }
59
 
60
  segment_presence = {}
61
  for name, (x1, x2, y1, y2) in segments.items():
 
 
62
  region = digit_img[y1:y2, x1:x2]
63
  if region.size == 0:
64
- return None
65
- # Count white pixels in the region
66
  pixel_count = np.sum(region == 255)
67
  total_pixels = region.size
68
- # Segment is present if more than 50% of the region is white
69
- segment_presence[name] = pixel_count > total_pixels * 0.5
70
 
71
- # Seven-segment digit patterns
72
  digit_patterns = {
73
  '0': ('top', 'bottom', 'left_top', 'left_bottom', 'right_top', 'right_bottom'),
74
  '1': ('right_top', 'right_bottom'),
@@ -83,140 +156,237 @@ def detect_segments(digit_img):
83
  }
84
 
85
  best_match = None
86
- max_matches = 0
87
  for digit, pattern in digit_patterns.items():
88
  matches = sum(1 for segment in pattern if segment_presence.get(segment, False))
89
- non_matches = sum(1 for segment in segment_presence if segment not in pattern and segment_presence[segment])
90
- score = matches - non_matches
91
- if score > max_matches:
92
- max_matches = score
 
 
93
  best_match = digit
94
-
 
 
 
 
 
 
 
95
  return best_match
96
 
97
  def custom_seven_segment_ocr(img, roi_bbox):
98
  """Perform custom OCR for seven-segment displays"""
99
  try:
100
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
101
- _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
- # Use EasyOCR to get bounding boxes for digits
104
  results = easyocr_reader.readtext(thresh, detail=1, paragraph=False,
105
- contrast_ths=0.1, adjust_contrast=0.7,
106
- text_threshold=0.9, mag_ratio=1.5,
107
- allowlist='0123456789.')
108
-
 
109
  if not results:
 
110
  return None
111
 
112
- # Sort bounding boxes left to right
113
- digits = []
114
- for (bbox, _, _) in results:
115
  (x1, y1), (x2, y2), (x3, y3), (x4, y4) = bbox
116
- x_min, x_max = min(x1, x4), max(x2, x3)
117
- y_min, y_max = min(y1, y2), max(y3, y4)
118
- digits.append((x_min, x_max, y_min, y_max))
119
-
120
- digits.sort(key=lambda x: x[0]) # Sort by x_min (left to right)
121
 
122
- # Extract and recognize each digit
123
  recognized_text = ""
124
- for x_min, x_max, y_min, y_max in digits:
125
- x_min, y_min = max(0, int(x_min)), max(0, int(y_min))
126
- x_max, y_max = min(thresh.shape[1], int(x_max)), min(thresh.shape[0], int(y_max))
127
  if x_max <= x_min or y_max <= y_min:
128
  continue
129
- digit_img = thresh[y_min:y_max, x_min:x_max]
130
- digit = detect_segments(digit_img)
131
- if digit:
132
- recognized_text += digit
133
-
134
- # Validate the recognized text
135
- text = recognized_text
136
- text = re.sub(r"[^\d\.]", "", text)
137
- if re.fullmatch(r"\d{1,4}(\.\d{0,3})?", text):
138
- return text
 
 
 
 
 
 
139
  return None
140
  except Exception as e:
141
  logging.error(f"Custom seven-segment OCR failed: {str(e)}")
142
  return None
143
 
144
  def extract_weight_from_image(pil_img):
 
145
  try:
146
  img = np.array(pil_img)
147
  img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
 
148
 
149
  brightness = estimate_brightness(img)
150
- conf_threshold = 0.9 if brightness > 100 else 0.7
151
 
152
- # Detect ROI
153
  roi_img, roi_bbox = detect_roi(img)
154
-
155
- # Try custom seven-segment OCR first
156
  custom_result = custom_seven_segment_ocr(roi_img, roi_bbox)
157
  if custom_result:
158
- # Format the custom result
159
- if "." in custom_result:
160
- int_part, dec_part = custom_result.split(".")
161
- int_part = int_part.lstrip("0") or "0"
162
- custom_result = f"{int_part}.{dec_part.rstrip('0')}"
163
- else:
164
- custom_result = custom_result.lstrip('0') or "0"
165
- return custom_result, 100.0 # High confidence for custom OCR
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- # Fallback to EasyOCR if custom OCR fails
168
- images_to_process = [
169
- ("raw", roi_img, {'contrast_ths': 0.1, 'adjust_contrast': 0.7, 'text_threshold': 0.9, 'mag_ratio': 1.5, 'allowlist': '0123456789.'}),
170
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
  best_weight = None
173
  best_conf = 0.0
174
  best_score = 0.0
175
-
176
- for mode, proc_img, ocr_params in images_to_process:
177
- if mode == "raw":
178
- proc_img = cv2.cvtColor(proc_img, cv2.COLOR_BGR2GRAY)
179
- results = easyocr_reader.readtext(proc_img, detail=1, paragraph=False, **ocr_params)
180
-
181
- for (bbox, text, conf) in results:
182
- text = text.lower().strip()
183
- text = text.replace(",", ".").replace(";", ".")
184
- text = text.replace("o", "0").replace("O", "0")
185
- text = text.replace("s", "5").replace("S", "5")
186
- text = text.replace("g", "9").replace("G", "6")
187
- text = text.replace("l", "1").replace("I", "1")
188
- text = text.replace("b", "8").replace("B", "8")
189
- text = text.replace("z", "2").replace("Z", "2")
190
- text = text.replace("q", "9").replace("Q", "9")
191
- text = text.replace("kgs", "").replace("kg", "").replace("k", "")
192
- text = re.sub(r"[^\d\.]", "", text)
193
-
194
- if re.fullmatch(r"\d{1,4}(\.\d{0,3})?", text):
195
- try:
196
- weight = float(text)
197
- range_score = 1.0 if 0.1 <= weight <= 500 else 0.3
198
- digit_score = 1.5 if 10 <= weight < 100 else 1.0
199
- score = conf * range_score * digit_score
200
- if score > best_score and conf > conf_threshold:
201
- best_weight = text
202
- best_conf = conf
203
- best_score = score
204
- except ValueError:
205
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
  if not best_weight:
208
- logging.info("No valid weight detected")
209
  return "Not detected", 0.0
210
 
211
  if "." in best_weight:
212
  int_part, dec_part = best_weight.split(".")
213
  int_part = int_part.lstrip("0") or "0"
214
- best_weight = f"{int_part}.{dec_part.rstrip('0')}"
 
 
 
 
 
 
215
  else:
216
  best_weight = best_weight.lstrip('0') or "0"
217
 
 
 
 
 
 
 
 
 
 
 
218
  return best_weight, round(best_conf * 100, 2)
219
 
220
  except Exception as e:
221
- logging.error(f"Weight extraction failed: {str(e)}")
222
  return "Not detected", 0.0
 
3
  import cv2
4
  import re
5
  import logging
6
+ from datetime import datetime
7
+ import os
8
+ from PIL import Image, ImageEnhance
9
 
10
+ # Set up logging for detailed debugging
11
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
12
 
13
+ # Initialize EasyOCR with English and GPU disabled (enable if you have a compatible GPU)
14
  easyocr_reader = easyocr.Reader(['en'], gpu=False)
15
 
16
+ # Directory for debug images
17
+ DEBUG_DIR = "debug_images"
18
+ os.makedirs(DEBUG_DIR, exist_ok=True)
19
+
20
+ def save_debug_image(img, filename_suffix, prefix=""):
21
+ """Saves an image to the debug directory with a timestamp."""
22
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
23
+ filename = os.path.join(DEBUG_DIR, f"{prefix}{timestamp}_{filename_suffix}.png")
24
+ if len(img.shape) == 3: # Color image
25
+ cv2.imwrite(filename, img)
26
+ else: # Grayscale image
27
+ cv2.imwrite(filename, img)
28
+ logging.debug(f"Saved debug image: {filename}")
29
+
30
  def estimate_brightness(img):
31
  """Estimate image brightness to detect illuminated displays"""
32
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
33
+ brightness = np.mean(gray)
34
+ logging.debug(f"Estimated brightness: {brightness}")
35
+ return brightness
36
+
37
+ def preprocess_image(img):
38
+ """Enhance contrast, brightness, and reduce noise for better digit detection"""
39
+ # Convert to PIL for initial enhancement
40
+ pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
41
+ pil_img = ImageEnhance.Contrast(pil_img).enhance(2.0) # Stronger contrast
42
+ pil_img = ImageEnhance.Brightness(pil_img).enhance(1.3) # Moderate brightness boost
43
+ img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
44
+ save_debug_image(img, "00_preprocessed_pil")
45
+
46
+ # Apply CLAHE to enhance local contrast
47
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
48
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
49
+ enhanced = clahe.apply(gray)
50
+ save_debug_image(enhanced, "00_clahe_enhanced")
51
+
52
+ # Apply bilateral filter to reduce noise while preserving edges
53
+ filtered = cv2.bilateralFilter(enhanced, d=11, sigmaColor=100, sigmaSpace=100)
54
+ save_debug_image(filtered, "00_bilateral_filtered")
55
+ return filtered
56
 
57
  def detect_roi(img):
58
  """Detect and crop the region of interest (likely the digital display)"""
59
  try:
60
+ save_debug_image(img, "01_original")
61
+ gray = preprocess_image(img)
62
+ save_debug_image(gray, "02_preprocessed_grayscale")
63
+
64
+ # Try multiple thresholding methods
65
  brightness = estimate_brightness(img)
66
+ if brightness > 150:
67
+ thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
68
+ cv2.THRESH_BINARY, 31, 5)
69
+ save_debug_image(thresh, "03_roi_adaptive_threshold_high")
70
+ else:
71
+ _, thresh = cv2.threshold(gray, 40, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
72
+ save_debug_image(thresh, "03_roi_otsu_threshold_low")
73
+
74
+ # Morphological operations to clean up noise and connect digits
75
+ kernel = np.ones((5, 5), np.uint8)
76
+ thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)
77
+ save_debug_image(thresh, "03_roi_morph_cleaned")
78
+
79
+ kernel = np.ones((11, 11), np.uint8)
80
+ dilated = cv2.dilate(thresh, kernel, iterations=5)
81
+ save_debug_image(dilated, "04_roi_dilated")
82
+
83
  contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
84
+
85
  if contours:
86
+ img_area = img.shape[0] * img.shape[1]
87
+ valid_contours = []
88
+ for c in contours:
89
+ area = cv2.contourArea(c)
90
+ if 200 < area < (img_area * 0.99): # Very relaxed area filter
91
+ x, y, w, h = cv2.boundingRect(c)
92
+ aspect_ratio = w / h if h > 0 else 0
93
+ if 0.5 <= aspect_ratio <= 10.0 and w > 30 and h > 20: # Very relaxed filters
94
+ valid_contours.append(c)
95
+
96
  if valid_contours:
97
+ contour = max(valid_contours, key=cv2.contourArea) # Largest contour
98
+ x, y, w, h = cv2.boundingRect(contour)
99
+ padding = 100 # Generous padding
100
+ x, y = max(0, x - padding), max(0, y - padding)
101
+ w, h = min(w + 2 * padding, img.shape[1] - x), min(h + 2 * padding, img.shape[0] - y)
102
+ roi_img = img[y:y+h, x:x+w]
103
+ save_debug_image(roi_img, "05_detected_roi")
104
+ logging.info(f"Detected ROI with dimensions: ({x}, {y}, {w}, {h})")
105
+ return roi_img, (x, y, w, h)
106
+
107
+ logging.info("No suitable ROI found, returning preprocessed image.")
108
+ save_debug_image(img, "05_no_roi_original_fallback")
109
  return img, None
110
  except Exception as e:
111
  logging.error(f"ROI detection failed: {str(e)}")
112
+ save_debug_image(img, "05_roi_detection_error_fallback")
113
  return img, None
114
 
115
  def detect_segments(digit_img):
116
  """Detect seven-segment patterns in a digit image"""
117
  h, w = digit_img.shape
118
+ if h < 8 or w < 4: # Very relaxed size constraints
119
+ logging.debug(f"Digit image too small: {w}x{h}")
120
  return None
121
 
 
122
  segments = {
123
+ 'top': (int(w*0.1), int(w*0.9), 0, int(h*0.25)),
124
+ 'middle': (int(w*0.1), int(w*0.9), int(h*0.35), int(h*0.65)),
125
+ 'bottom': (int(w*0.1), int(w*0.9), int(h*0.75), h),
126
+ 'left_top': (0, int(w*0.3), int(h*0.05), int(h*0.55)),
127
+ 'left_bottom': (0, int(w*0.3), int(h*0.45), int(h*0.95)),
128
+ 'right_top': (int(w*0.7), w, int(h*0.05), int(h*0.55)),
129
+ 'right_bottom': (int(w*0.7), w, int(h*0.45), int(h*0.95))
130
  }
131
 
132
  segment_presence = {}
133
  for name, (x1, x2, y1, y2) in segments.items():
134
+ x1, y1 = max(0, x1), max(0, y1)
135
+ x2, y2 = min(w, x2), min(h, y2)
136
  region = digit_img[y1:y2, x1:x2]
137
  if region.size == 0:
138
+ segment_presence[name] = False
139
+ continue
140
  pixel_count = np.sum(region == 255)
141
  total_pixels = region.size
142
+ segment_presence[name] = pixel_count / total_pixels > 0.3 # Very low threshold
143
+ logging.debug(f"Segment {name}: {pixel_count}/{total_pixels} = {pixel_count/total_pixels:.2f}")
144
 
 
145
  digit_patterns = {
146
  '0': ('top', 'bottom', 'left_top', 'left_bottom', 'right_top', 'right_bottom'),
147
  '1': ('right_top', 'right_bottom'),
 
156
  }
157
 
158
  best_match = None
159
+ max_score = -1
160
  for digit, pattern in digit_patterns.items():
161
  matches = sum(1 for segment in pattern if segment_presence.get(segment, False))
162
+ non_matches_penalty = sum(1 for segment in segment_presence if segment not in pattern and segment_presence[segment])
163
+ current_score = matches - non_matches_penalty
164
+ if all(segment_presence.get(s, False) for s in pattern):
165
+ current_score += 0.5
166
+ if current_score > max_score:
167
+ max_score = current_score
168
  best_match = digit
169
+ elif current_score == max_score and best_match is not None:
170
+ current_digit_non_matches = sum(1 for segment in segment_presence if segment not in pattern and segment_presence[segment])
171
+ best_digit_pattern = digit_patterns[best_match]
172
+ best_digit_non_matches = sum(1 for segment in segment_presence if segment not in best_digit_pattern and segment_presence[segment])
173
+ if current_digit_non_matches < best_digit_non_matches:
174
+ best_match = digit
175
+
176
+ logging.debug(f"Segment presence: {segment_presence}, Detected digit: {best_match}")
177
  return best_match
178
 
179
  def custom_seven_segment_ocr(img, roi_bbox):
180
  """Perform custom OCR for seven-segment displays"""
181
  try:
182
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
183
+ brightness = estimate_brightness(img)
184
+ # Try multiple thresholding approaches
185
+ if brightness > 150:
186
+ _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
187
+ save_debug_image(thresh, "06_roi_otsu_threshold")
188
+ else:
189
+ _, thresh = cv2.threshold(gray, 30, 255, cv2.THRESH_BINARY)
190
+ save_debug_image(thresh, "06_roi_simple_threshold")
191
+
192
+ # Morphological cleaning
193
+ kernel = np.ones((3, 3), np.uint8)
194
+ thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1)
195
+ save_debug_image(thresh, "06_roi_morph_cleaned")
196
 
 
197
  results = easyocr_reader.readtext(thresh, detail=1, paragraph=False,
198
+ contrast_ths=0.1, adjust_contrast=1.0,
199
+ text_threshold=0.3, mag_ratio=4.0,
200
+ allowlist='0123456789.-', y_ths=0.6)
201
+
202
+ logging.info(f"Custom OCR EasyOCR results: {results}")
203
  if not results:
204
+ logging.info("Custom OCR EasyOCR found no digits.")
205
  return None
206
 
207
+ digits_info = []
208
+ for (bbox, text, conf) in results:
 
209
  (x1, y1), (x2, y2), (x3, y3), (x4, y4) = bbox
210
+ h_bbox = max(y1, y2, y3, y4) - min(y1, y2, y3, y4)
211
+ if len(text) == 1 and (text.isdigit() or text in '.-') and h_bbox > 4:
212
+ x_min, x_max = int(min(x1, x4)), int(max(x2, x3))
213
+ y_min, y_max = int(min(y1, y2)), int(max(y3, y4))
214
+ digits_info.append((x_min, x_max, y_min, y_max, text, conf))
215
 
216
+ digits_info.sort(key=lambda x: x[0])
217
  recognized_text = ""
218
+ for idx, (x_min, x_max, y_min, y_max, easyocr_char, easyocr_conf) in enumerate(digits_info):
219
+ x_min, y_min = max(0, x_min), max(0, y_min)
220
+ x_max, y_max = min(thresh.shape[1], x_max), min(thresh.shape[0], y_max)
221
  if x_max <= x_min or y_max <= y_min:
222
  continue
223
+ digit_img_crop = thresh[y_min:y_max, x_min:x_max]
224
+ save_debug_image(digit_img_crop, f"07_digit_crop_{idx}_{easyocr_char}")
225
+ if easyocr_conf > 0.8 or easyocr_char in '.-' or digit_img_crop.shape[0] < 8 or digit_img_crop.shape[1] < 4:
226
+ recognized_text += easyocr_char
227
+ else:
228
+ digit_from_segments = detect_segments(digit_img_crop)
229
+ if digit_from_segments:
230
+ recognized_text += digit_from_segments
231
+ else:
232
+ recognized_text += easyocr_char
233
+
234
+ logging.info(f"Custom OCR before validation, recognized_text: {recognized_text}")
235
+ # Relaxed validation for debugging
236
+ if recognized_text:
237
+ return recognized_text
238
+ logging.info(f"Custom OCR text '{recognized_text}' failed validation.")
239
  return None
240
  except Exception as e:
241
  logging.error(f"Custom seven-segment OCR failed: {str(e)}")
242
  return None
243
 
244
  def extract_weight_from_image(pil_img):
245
+ """Extract weight from a PIL image of a digital scale display"""
246
  try:
247
  img = np.array(pil_img)
248
  img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
249
+ save_debug_image(img, "00_input_image")
250
 
251
  brightness = estimate_brightness(img)
252
+ conf_threshold = 0.3 if brightness > 150 else (0.2 if brightness > 80 else 0.1)
253
 
 
254
  roi_img, roi_bbox = detect_roi(img)
 
 
255
  custom_result = custom_seven_segment_ocr(roi_img, roi_bbox)
256
  if custom_result:
257
+ # Basic cleaning
258
+ text = re.sub(r"[^\d\.\-]", "", custom_result) # Allow negative signs
259
+ if text.count('.') > 1:
260
+ text = text.replace('.', '', text.count('.') - 1)
261
+ if text:
262
+ if text.startswith('.'):
263
+ text = "0" + text
264
+ if text.endswith('.'):
265
+ text = text.rstrip('.')
266
+ if text == '.' or text == '':
267
+ logging.warning(f"Custom OCR result '{text}' is invalid after cleaning.")
268
+ else:
269
+ try:
270
+ float(text)
271
+ logging.info(f"Custom OCR result: {text}, Confidence: 100.0%")
272
+ return text, 100.0
273
+ except ValueError:
274
+ logging.warning(f"Custom OCR result '{text}' is not a valid number, falling back.")
275
+ logging.warning(f"Custom OCR result '{custom_result}' failed validation, falling back.")
276
 
277
+ logging.info("Custom OCR failed or invalid, falling back to general EasyOCR.")
278
+ processed_roi_img = preprocess_image(roi_img)
279
+
280
+ # Try multiple thresholding approaches
281
+ if brightness > 150:
282
+ thresh = cv2.adaptiveThreshold(processed_roi_img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
283
+ cv2.THRESH_BINARY, 41, 7)
284
+ save_debug_image(thresh, "09_fallback_adaptive_thresh")
285
+ else:
286
+ _, thresh = cv2.threshold(processed_roi_img, 30, 255, cv2.THRESH_BINARY)
287
+ save_debug_image(thresh, "09_fallback_simple_thresh")
288
+
289
+ # Morphological cleaning
290
+ kernel = np.ones((3, 3), np.uint8)
291
+ thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1)
292
+ save_debug_image(thresh, "09_fallback_morph_cleaned")
293
+
294
+ results = easyocr_reader.readtext(thresh, detail=1, paragraph=False,
295
+ contrast_ths=0.1, adjust_contrast=1.0,
296
+ text_threshold=0.2, mag_ratio=5.0,
297
+ allowlist='0123456789.-', batch_size=4, y_ths=0.6)
298
 
299
  best_weight = None
300
  best_conf = 0.0
301
  best_score = 0.0
302
+ for (bbox, text, conf) in results:
303
+ logging.info(f"Fallback EasyOCR raw text: {text}, Confidence: {conf}")
304
+ text = text.lower().strip()
305
+ text = text.replace(",", ".").replace(";", ".").replace(":", ".").replace(" ", "")
306
+ text = text.replace("o", "0").replace("O", "0").replace("q", "0").replace("Q", "0")
307
+ text = text.replace("s", "5").replace("S", "5")
308
+ text = text.replace("g", "9").replace("G", "6")
309
+ text = text.replace("l", "1").replace("I", "1").replace("|", "1")
310
+ text = text.replace("b", "8").replace("B", "8")
311
+ text = text.replace("z", "2").replace("Z", "2")
312
+ text = text.replace("a", "4").replace("A", "4")
313
+ text = text.replace("e", "3")
314
+ text = text.replace("t", "7")
315
+ text = text.replace("~", "").replace("`", "")
316
+ text = re.sub(r"(kgs|kg|k|lb|g|gr|pounds|lbs)\b", "", text)
317
+ text = re.sub(r"[^\d\.\-]", "", text)
318
+ if text.count('.') > 1:
319
+ parts = text.split('.')
320
+ text = parts[0] + '.' + ''.join(parts[1:])
321
+ text = text.strip('.')
322
+ if len(text.replace('.', '').replace('-', '')) > 0: # Allow negative weights
323
+ try:
324
+ weight = float(text)
325
+ range_score = 1.0
326
+ if 0.0 <= weight <= 250:
327
+ range_score = 1.5
328
+ elif weight > 250 and weight <= 500:
329
+ range_score = 1.2
330
+ elif weight > 500 and weight <= 1000:
331
+ range_score = 1.0
332
+ else:
333
+ range_score = 0.5
334
+ digit_count = len(text.replace('.', '').replace('-', ''))
335
+ digit_score = 1.0
336
+ if digit_count >= 2 and digit_count <= 5:
337
+ digit_score = 1.3
338
+ elif digit_count == 1:
339
+ digit_score = 0.8
340
+ score = conf * range_score * digit_score
341
+ if roi_bbox:
342
+ (x_roi, y_roi, w_roi, h_roi) = roi_bbox
343
+ roi_area = w_roi * h_roi
344
+ x_min, y_min = int(min(b[0] for b in bbox)), int(min(b[1] for b in bbox))
345
+ x_max, y_max = int(max(b[0] for b in bbox)), int(max(b[1] for b in bbox))
346
+ bbox_area = (x_max - x_min) * (y_max - y_min)
347
+ if roi_area > 0 and bbox_area / roi_area < 0.02:
348
+ score *= 0.5
349
+ bbox_aspect_ratio = (x_max - x_min) / (y_max - y_min) if (y_max - y_min) > 0 else 0
350
+ if bbox_aspect_ratio < 0.1:
351
+ score *= 0.7
352
+ if score > best_score and conf > conf_threshold:
353
+ best_weight = text
354
+ best_conf = conf
355
+ best_score = score
356
+ logging.info(f"Candidate EasyOCR weight: '{text}', Conf: {conf}, Score: {score}")
357
+ except ValueError:
358
+ logging.warning(f"Could not convert '{text}' to float during EasyOCR fallback.")
359
+ continue
360
 
361
  if not best_weight:
362
+ logging.info("No valid weight detected after all attempts.")
363
  return "Not detected", 0.0
364
 
365
  if "." in best_weight:
366
  int_part, dec_part = best_weight.split(".")
367
  int_part = int_part.lstrip("0") or "0"
368
+ dec_part = dec_part.rstrip('0')
369
+ if not dec_part and int_part != "0":
370
+ best_weight = int_part
371
+ elif not dec_part and int_part == "0":
372
+ best_weight = "0"
373
+ else:
374
+ best_weight = f"{int_part}.{dec_part}"
375
  else:
376
  best_weight = best_weight.lstrip('0') or "0"
377
 
378
+ try:
379
+ final_float_weight = float(best_weight)
380
+ if final_float_weight < 0.0 or final_float_weight > 1000:
381
+ logging.warning(f"Detected weight {final_float_weight} is outside typical range, reducing confidence.")
382
+ best_conf *= 0.5
383
+ except ValueError:
384
+ logging.warning(f"Final weight '{best_weight}' is not a valid number.")
385
+ best_conf *= 0.5
386
+
387
+ logging.info(f"Final detected weight: {best_weight}, Confidence: {round(best_conf * 100, 2)}%")
388
  return best_weight, round(best_conf * 100, 2)
389
 
390
  except Exception as e:
391
+ logging.error(f"Weight extraction failed unexpectedly: {str(e)}")
392
  return "Not detected", 0.0