davanstrien HF Staff commited on
Commit
898d181
·
verified ·
1 Parent(s): 8d6560c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +98 -84
app.py CHANGED
@@ -6,9 +6,6 @@ import os
6
  # --- Helper Functions ---
7
 
8
  def get_alto_namespace(xml_file_path):
9
- """
10
- Dynamically gets the ALTO namespace from the XML file.
11
- """
12
  try:
13
  tree = ET.parse(xml_file_path)
14
  root = tree.getroot()
@@ -19,24 +16,14 @@ def get_alto_namespace(xml_file_path):
19
  return ''
20
 
21
  def parse_alto_xml(xml_file_path):
22
- """
23
- Parses an ALTO XML file to extract text content and bounding box info.
24
- Returns:
25
- - full_text (str): All extracted text concatenated.
26
- - ocr_data (list): A list of dictionaries, each with
27
- {'text': str, 'x': int, 'y': int, 'w': int, 'h': int}
28
- """
29
  full_text_lines = []
30
  ocr_data = []
31
-
32
  if not xml_file_path or not os.path.exists(xml_file_path):
33
  return "Error: XML file not provided or does not exist.", []
34
-
35
  try:
36
  ns_prefix = get_alto_namespace(xml_file_path)
37
  tree = ET.parse(xml_file_path)
38
  root = tree.getroot()
39
-
40
  for text_line in root.findall(f'.//{ns_prefix}TextLine'):
41
  line_text_parts = []
42
  for string_element in text_line.findall(f'{ns_prefix}String'):
@@ -49,103 +36,149 @@ def parse_alto_xml(xml_file_path):
49
  width = int(float(string_element.get('WIDTH')))
50
  height = int(float(string_element.get('HEIGHT')))
51
  ocr_data.append({
52
- 'text': text,
53
- 'x': hpos,
54
- 'y': vpos,
55
- 'w': width,
56
- 'h': height
57
  })
58
  except (ValueError, TypeError) as e:
59
  print(f"Warning: Could not parse coordinates for '{text}': {e}")
60
  ocr_data.append({
61
- 'text': text, 'x': 0, 'y': 0, 'w': 10, 'h': 10 # Placeholder
62
  })
63
  if line_text_parts:
64
  full_text_lines.append(" ".join(line_text_parts))
65
-
66
  return "\n".join(full_text_lines), ocr_data
67
-
68
  except ET.ParseError as e:
69
  return f"Error parsing XML: {e}", []
70
  except Exception as e:
71
  return f"An unexpected error occurred during XML parsing: {e}", []
72
 
73
-
74
  def draw_ocr_on_image(image_pil, ocr_data):
75
  """
76
- Draws bounding boxes and text from ocr_data onto the image.
77
  """
78
  if not image_pil or not ocr_data:
79
  return image_pil
80
 
81
  draw = ImageDraw.Draw(image_pil)
82
-
83
- try:
84
- # Filter for items with positive height before calculating average
85
- valid_heights = [d['h'] for d in ocr_data if d['h'] > 0]
86
- if valid_heights:
87
- avg_height = sum(valid_heights) / len(valid_heights)
88
- else:
89
- avg_height = 10 # Default if no valid heights
90
- font_size = max(8, int(avg_height * 0.6))
91
- font = ImageFont.truetype("arial.ttf", font_size)
92
- except (IOError, ZeroDivisionError): # ZeroDivisionError should be caught by the check above
93
- font = ImageFont.load_default()
94
- font_size = 10
95
- print("Arial font not found or issue with height calculation, using default font.")
96
 
97
  for item in ocr_data:
98
  x, y, w, h = item['x'], item['y'], item['w'], item['h']
99
  text = item['text']
100
- draw.rectangle([(x, y), (x + w, y + h)], outline="red", width=2)
101
- # Adjust text position to be slightly above the box, or below if no space above
102
- text_y_position = y - font_size - 2
103
- if text_y_position < 0: # If text would go off the top of the image
104
- text_y_position = y + h + 2 # Place below the box
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- draw.text((x + 2, text_y_position), text, fill="green", font=font)
 
 
 
 
 
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  return image_pil
109
 
110
  # --- Gradio Interface Function ---
111
 
112
  def process_image_and_xml(image_path, xml_path, show_overlay):
113
- """
114
- Main function for the Gradio interface.
115
- image_path and xml_path are now file paths (strings).
116
- """
117
- if image_path is None: # If no image is uploaded at all
118
  return None, "Please upload an image.", None
119
-
120
- # Try to open the image first, as it's needed for both outputs if XML is missing
121
  try:
122
  img_pil = Image.open(image_path).convert("RGB")
123
  except Exception as e:
124
  return None, f"Error loading image: {e}", None
125
 
126
- if xml_path is None: # If XML is missing, but image is present
127
  return img_pil, "Please upload an OCR XML file.", None
128
 
129
- # Both image and XML are presumably present
130
  extracted_text, ocr_box_data = parse_alto_xml(xml_path)
131
-
132
  overlay_image_pil = None
133
  if show_overlay:
134
  if ocr_box_data:
135
  img_for_overlay = img_pil.copy()
136
  overlay_image_pil = draw_ocr_on_image(img_for_overlay, ocr_box_data)
137
  elif not (isinstance(extracted_text, str) and extracted_text.startswith("Error")):
138
- # Append message if overlay is checked but no boxes, and no major XML parse error
139
  if isinstance(extracted_text, str):
140
- extracted_text += "\n(No bounding box data found or parsed for overlay)"
141
- else: # Should ideally not happen based on parse_alto_xml's return
142
- extracted_text = "(No bounding box data found or parsed for overlay)"
143
-
144
  return img_pil, extracted_text, overlay_image_pil
145
 
146
-
147
  # --- Create Gradio App ---
148
-
149
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
150
  gr.Markdown("# OCR Viewer (ALTO XML)")
151
  gr.Markdown(
@@ -153,29 +186,23 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
153
  "The app will display the image, extract and show the plain text, "
154
  "and optionally overlay the OCR predictions on the image."
155
  )
156
-
157
  with gr.Row():
158
  with gr.Column(scale=1):
159
  image_input = gr.File(label="Upload Image (PNG, JPG, etc.)", type="filepath")
160
  xml_input = gr.File(label="Upload ALTO XML File (.xml)", type="filepath")
161
  show_overlay_checkbox = gr.Checkbox(label="Show OCR Overlay on Image", value=False)
162
  submit_button = gr.Button("Process Files", variant="primary")
163
-
164
  with gr.Row():
165
  with gr.Column(scale=1):
166
  output_image_orig = gr.Image(label="Uploaded Image", type="pil", interactive=False)
167
  with gr.Column(scale=1):
168
  output_text = gr.Textbox(label="Extracted Plain Text", lines=15, interactive=False)
169
-
170
  output_image_overlay = gr.Image(label="Image with OCR Overlay", type="pil", interactive=False, visible=True)
171
 
172
  def update_interface(image_filepath, xml_filepath, show_overlay_val):
173
  if image_filepath is None and xml_filepath is None:
174
  return None, "Please upload an image and an XML file.", None
175
- # `process_image_and_xml` now handles cases where one is None
176
-
177
  img, text, overlay_img = process_image_and_xml(image_filepath, xml_filepath, show_overlay_val)
178
-
179
  return img, text, overlay_img
180
 
181
  submit_button.click(
@@ -183,17 +210,14 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
183
  inputs=[image_input, xml_input, show_overlay_checkbox],
184
  outputs=[output_image_orig, output_text, output_image_overlay]
185
  )
186
-
187
  show_overlay_checkbox.change(
188
  fn=update_interface,
189
  inputs=[image_input, xml_input, show_overlay_checkbox],
190
  outputs=[output_image_orig, output_text, output_image_overlay]
191
  )
192
-
193
  gr.Markdown("---")
194
  gr.Markdown("### Example ALTO XML Snippet (for `String` element extraction):")
195
  gr.Code(
196
- # Corrected: Omitted language parameter
197
  value="""
198
  <alto xmlns="http://www.loc.gov/standards/alto/v3/alto.xsd">
199
  <Description>...</Description>
@@ -204,13 +228,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
204
  <TextLine WIDTH="684" HEIGHT="108" ID="p13_t1" HPOS="465" VPOS="196">
205
  <String ID="p13_w1" CONTENT="Introduction" HPOS="465" VPOS="196" WIDTH="684" HEIGHT="108" STYLEREFS="font0"/>
206
  </TextLine>
207
- <TextLine WIDTH="1798" HEIGHT="51" ID="p13_t2" HPOS="492" VPOS="523">
208
- <String ID="p13_w2" CONTENT="Britain" HPOS="492" VPOS="523" WIDTH="166" HEIGHT="51" STYLEREFS="font1"/>
209
- <SP WIDTH="24" VPOS="523" HPOS="658"/>
210
- <String ID="p13_w3" CONTENT="1981" HPOS="682" VPOS="523" WIDTH="117" HEIGHT="51" STYLEREFS="font1"/>
211
- <!-- ... more String and SP elements ... -->
212
- </TextLine>
213
- <!-- ... more TextLine elements ... -->
214
  </PrintSpace>
215
  </Page>
216
  </Layout>
@@ -219,20 +237,16 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
219
  interactive=False
220
  )
221
 
222
-
223
  if __name__ == "__main__":
224
  try:
225
- img = Image.new('RGB', (2394, 3612), color = 'lightgray')
226
- img.save("dummy_image.png")
227
  print("Created dummy_image.png for testing.")
228
-
229
- example_xml_filename = "189819724.34.xml" # Make sure this file exists with your XML content
230
  if not os.path.exists(example_xml_filename):
231
- print(f"WARNING: Example XML '{example_xml_filename}' not found. Please create it or upload your own.")
232
-
233
  except ImportError:
234
  print("Pillow not installed, can't create dummy image.")
235
  except Exception as e:
236
  print(f"Error during setup: {e}")
237
-
238
  demo.launch()
 
6
  # --- Helper Functions ---
7
 
8
  def get_alto_namespace(xml_file_path):
 
 
 
9
  try:
10
  tree = ET.parse(xml_file_path)
11
  root = tree.getroot()
 
16
  return ''
17
 
18
  def parse_alto_xml(xml_file_path):
 
 
 
 
 
 
 
19
  full_text_lines = []
20
  ocr_data = []
 
21
  if not xml_file_path or not os.path.exists(xml_file_path):
22
  return "Error: XML file not provided or does not exist.", []
 
23
  try:
24
  ns_prefix = get_alto_namespace(xml_file_path)
25
  tree = ET.parse(xml_file_path)
26
  root = tree.getroot()
 
27
  for text_line in root.findall(f'.//{ns_prefix}TextLine'):
28
  line_text_parts = []
29
  for string_element in text_line.findall(f'{ns_prefix}String'):
 
36
  width = int(float(string_element.get('WIDTH')))
37
  height = int(float(string_element.get('HEIGHT')))
38
  ocr_data.append({
39
+ 'text': text, 'x': hpos, 'y': vpos, 'w': width, 'h': height
 
 
 
 
40
  })
41
  except (ValueError, TypeError) as e:
42
  print(f"Warning: Could not parse coordinates for '{text}': {e}")
43
  ocr_data.append({
44
+ 'text': text, 'x': 0, 'y': 0, 'w': 10, 'h': 10
45
  })
46
  if line_text_parts:
47
  full_text_lines.append(" ".join(line_text_parts))
 
48
  return "\n".join(full_text_lines), ocr_data
 
49
  except ET.ParseError as e:
50
  return f"Error parsing XML: {e}", []
51
  except Exception as e:
52
  return f"An unexpected error occurred during XML parsing: {e}", []
53
 
 
54
  def draw_ocr_on_image(image_pil, ocr_data):
55
  """
56
+ Draws styled bounding boxes and text from ocr_data onto the image.
57
  """
58
  if not image_pil or not ocr_data:
59
  return image_pil
60
 
61
  draw = ImageDraw.Draw(image_pil)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  for item in ocr_data:
64
  x, y, w, h = item['x'], item['y'], item['w'], item['h']
65
  text = item['text']
66
+
67
+ # 1. Draw bounding box for the original OCR segment
68
+ draw.rectangle([(x, y), (x + w, y + h)], outline="rgba(255,0,0,190)", width=1) # Red, bit transparent
69
+
70
+ # 2. Determine font for this specific item
71
+ # Adaptive font size based on item height, with min/max caps
72
+ # Making it slightly smaller relative to box height for overlay text
73
+ current_font_size = max(8, min(16, int(item['h'] * 0.60)))
74
+ is_truetype = False
75
+ try:
76
+ font = ImageFont.truetype("arial.ttf", current_font_size)
77
+ is_truetype = True
78
+ except IOError:
79
+ try:
80
+ font = ImageFont.truetype("DejaVuSans.ttf", current_font_size)
81
+ is_truetype = True
82
+ except IOError:
83
+ font = ImageFont.load_default() # Small bitmap font, size not controllable
84
+ # print("Arial and DejaVuSans fonts not found, using default PIL font for some items.")
85
+
86
+ # 3. Get actual text dimensions for precise placement
87
+ text_actual_width = 0
88
+ text_actual_height = 0
89
+ text_draw_origin_is_top_left = False # Flag to know how to use coordinates with draw.text
90
+
91
+ try: # Modern Pillow: use textbbox with anchor for precise bounds
92
+ # Using 'lt' (left-top) anchor means (0,0) in textbbox is relative to top-left of text
93
+ bbox = font.getbbox(text, anchor='lt')
94
+ text_actual_width = bbox[2] - bbox[0]
95
+ text_actual_height = bbox[3] - bbox[1]
96
+ text_draw_origin_is_top_left = True # draw.text with anchor='lt' will use xy as top-left
97
+ except (AttributeError, TypeError): # Fallback for older Pillow or if anchor not supported by getbbox
98
+ try: # Try font.getsize (deprecated but widely available)
99
+ size = font.getsize(text)
100
+ text_actual_width = size[0]
101
+ if is_truetype: # For TrueType, getsize height is usually baseline to top
102
+ ascent, descent = font.getmetrics()
103
+ text_actual_height = ascent + descent # Full line height
104
+ else: # For default font, getsize height is total height
105
+ text_actual_height = size[1]
106
+ except AttributeError: # Ultimate fallback if getsize also fails (unlikely for loaded font)
107
+ text_actual_width = len(text) * current_font_size // 2 # Very rough estimate
108
+ text_actual_height = current_font_size # Very rough estimate
109
+
110
+ # 4. Calculate position for the TOP-LEFT of the overlay text
111
+ buffer = 3 # Buffer in pixels between OCR box and text overlay
112
+ text_draw_x = x # Align left edge of text with left edge of box
113
+
114
+ # Try to place text above the box
115
+ tentative_text_top_y = y - text_actual_height - buffer
116
+ if tentative_text_top_y < buffer: # If it goes off (or too close to) the top of the image
117
+ tentative_text_top_y = y + h + buffer # Place it below the box
118
+
119
+ # Ensure it doesn't go off the bottom either
120
+ if tentative_text_top_y + text_actual_height > image_pil.height - buffer:
121
+ tentative_text_top_y = image_pil.height - text_actual_height - buffer # Pin to bottom
122
+ if tentative_text_top_y < buffer: # If image is too short for text, pin to top
123
+ tentative_text_top_y = buffer
124
+
125
+ final_text_top_y = tentative_text_top_y
126
 
127
+ # 5. Draw background for the overlay text
128
+ bg_padding = 2
129
+ bg_x0 = text_draw_x - bg_padding
130
+ bg_y0 = final_text_top_y - bg_padding
131
+ bg_x1 = text_draw_x + text_actual_width + bg_padding
132
+ bg_y1 = final_text_top_y + text_actual_height + bg_padding
133
 
134
+ draw.rectangle([(bg_x0, bg_y0), (bg_x1, bg_y1)], fill="rgba(255, 255, 220, 235)") # Light yellow, fairly opaque
135
+
136
+ # 6. Draw the overlay text
137
+ draw_coords = (text_draw_x, final_text_top_y)
138
+ if text_draw_origin_is_top_left:
139
+ try:
140
+ draw.text(draw_coords, text, fill="black", font=font, anchor='lt')
141
+ except (TypeError, AttributeError): # Fallback if anchor='lt' fails at draw time
142
+ # This fallback means background might be slightly off if is_truetype and text_draw_origin_is_top_left was True due to getbbox working
143
+ # but draw.text doesn't support anchor. For simplicity, draw at top_y assuming it's baseline.
144
+ ascent = font.getmetrics()[0] if is_truetype else text_actual_height
145
+ draw.text((text_draw_x, final_text_top_y + ascent if is_truetype else final_text_top_y), text, fill="black", font=font)
146
+ else: # Older Pillow, (x,y) is baseline for TrueType, top-left for default
147
+ if is_truetype:
148
+ ascent, _ = font.getmetrics()
149
+ draw.text((text_draw_x, final_text_top_y + ascent), text, fill="black", font=font)
150
+ else: # Default font, (x,y) is top-left
151
+ draw.text((text_draw_x, final_text_top_y), text, fill="black", font=font)
152
+
153
  return image_pil
154
 
155
  # --- Gradio Interface Function ---
156
 
157
  def process_image_and_xml(image_path, xml_path, show_overlay):
158
+ if image_path is None:
 
 
 
 
159
  return None, "Please upload an image.", None
 
 
160
  try:
161
  img_pil = Image.open(image_path).convert("RGB")
162
  except Exception as e:
163
  return None, f"Error loading image: {e}", None
164
 
165
+ if xml_path is None:
166
  return img_pil, "Please upload an OCR XML file.", None
167
 
 
168
  extracted_text, ocr_box_data = parse_alto_xml(xml_path)
 
169
  overlay_image_pil = None
170
  if show_overlay:
171
  if ocr_box_data:
172
  img_for_overlay = img_pil.copy()
173
  overlay_image_pil = draw_ocr_on_image(img_for_overlay, ocr_box_data)
174
  elif not (isinstance(extracted_text, str) and extracted_text.startswith("Error")):
 
175
  if isinstance(extracted_text, str):
176
+ extracted_text += "\n(No bounding box data for overlay)"
177
+ else:
178
+ extracted_text = "(No bounding box data for overlay)"
 
179
  return img_pil, extracted_text, overlay_image_pil
180
 
 
181
  # --- Create Gradio App ---
 
182
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
183
  gr.Markdown("# OCR Viewer (ALTO XML)")
184
  gr.Markdown(
 
186
  "The app will display the image, extract and show the plain text, "
187
  "and optionally overlay the OCR predictions on the image."
188
  )
 
189
  with gr.Row():
190
  with gr.Column(scale=1):
191
  image_input = gr.File(label="Upload Image (PNG, JPG, etc.)", type="filepath")
192
  xml_input = gr.File(label="Upload ALTO XML File (.xml)", type="filepath")
193
  show_overlay_checkbox = gr.Checkbox(label="Show OCR Overlay on Image", value=False)
194
  submit_button = gr.Button("Process Files", variant="primary")
 
195
  with gr.Row():
196
  with gr.Column(scale=1):
197
  output_image_orig = gr.Image(label="Uploaded Image", type="pil", interactive=False)
198
  with gr.Column(scale=1):
199
  output_text = gr.Textbox(label="Extracted Plain Text", lines=15, interactive=False)
 
200
  output_image_overlay = gr.Image(label="Image with OCR Overlay", type="pil", interactive=False, visible=True)
201
 
202
  def update_interface(image_filepath, xml_filepath, show_overlay_val):
203
  if image_filepath is None and xml_filepath is None:
204
  return None, "Please upload an image and an XML file.", None
 
 
205
  img, text, overlay_img = process_image_and_xml(image_filepath, xml_filepath, show_overlay_val)
 
206
  return img, text, overlay_img
207
 
208
  submit_button.click(
 
210
  inputs=[image_input, xml_input, show_overlay_checkbox],
211
  outputs=[output_image_orig, output_text, output_image_overlay]
212
  )
 
213
  show_overlay_checkbox.change(
214
  fn=update_interface,
215
  inputs=[image_input, xml_input, show_overlay_checkbox],
216
  outputs=[output_image_orig, output_text, output_image_overlay]
217
  )
 
218
  gr.Markdown("---")
219
  gr.Markdown("### Example ALTO XML Snippet (for `String` element extraction):")
220
  gr.Code(
 
221
  value="""
222
  <alto xmlns="http://www.loc.gov/standards/alto/v3/alto.xsd">
223
  <Description>...</Description>
 
228
  <TextLine WIDTH="684" HEIGHT="108" ID="p13_t1" HPOS="465" VPOS="196">
229
  <String ID="p13_w1" CONTENT="Introduction" HPOS="465" VPOS="196" WIDTH="684" HEIGHT="108" STYLEREFS="font0"/>
230
  </TextLine>
231
+ <!-- ... more TextLine and String elements ... -->
 
 
 
 
 
 
232
  </PrintSpace>
233
  </Page>
234
  </Layout>
 
237
  interactive=False
238
  )
239
 
 
240
  if __name__ == "__main__":
241
  try:
242
+ img_test = Image.new('RGB', (2394, 3612), color = 'lightgray')
243
+ img_test.save("dummy_image.png")
244
  print("Created dummy_image.png for testing.")
245
+ example_xml_filename = "189819724.34.xml"
 
246
  if not os.path.exists(example_xml_filename):
247
+ print(f"WARNING: Example XML '{example_xml_filename}' not found. Please create it (using the content from the prompt) or upload your own.")
 
248
  except ImportError:
249
  print("Pillow not installed, can't create dummy image.")
250
  except Exception as e:
251
  print(f"Error during setup: {e}")
 
252
  demo.launch()