viklofg commited on
Commit
c369c5b
·
1 Parent(s): 5a98ee7

Update result visualization

Browse files

The result viewer renders the polygons as SVG elements instead of drawing
them onto the page. The lines' polygon and text are highlighted when hovered.

app/assets/jinja-templates/image ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <svg viewBox="0 0 {{ page.width }} {{ page.height }}" xmlns="http://www.w3.org/2000/svg">
2
+ <image height="{{ page.height }}" width="{{ page.width }}" href="/gradio_api/file={{ page.path }}" />
3
+ {%- for line in lines -%}
4
+ <a class="textline line{{loop.index}}" onmouseover="document.querySelectorAll('.line{{loop.index}}').forEach(element => {element.classList.add('highlighted')});" onmouseout="document.querySelectorAll('*').forEach(element => {element.classList.remove('highlighted')});">
5
+ <polygon id="{{ loop.index }}" points="{% for point in line.polygon %}{{ point|join(',') }}{% if not loop.last %} {% endif %}{% endfor %}"/></a>
6
+ {% endfor %}
7
+ </svg>
app/assets/jinja-templates/transcription ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- for line in lines -%}
2
+ <span
3
+ class="textline line{{loop.index}}"
4
+ onmouseover="document.querySelectorAll('.line{{loop.index}}').forEach(element => {element.classList.add('highlighted')});"
5
+ onmouseout="document.querySelectorAll('*').forEach(element => {element.classList.remove('highlighted')});"
6
+ >
7
+ {{line.text}}
8
+ </span>
9
+ <br>
10
+ {% endfor %}
app/gradio_config.py CHANGED
@@ -13,5 +13,32 @@ theme = gr.themes.Default(
13
  )
14
 
15
  css = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  """
 
13
  )
14
 
15
  css = """
16
+ .svg-image {
17
+ height: auto;
18
+ width: 100%;
19
+ margin: auto;
20
+ }
21
+
22
+ .transcription {
23
+ font-size: large;
24
+ }
25
+
26
+ /* style of textline svg elements */
27
+ .textline {
28
+ fill: transparent;
29
+ stroke: blue;
30
+ stroke-width: 10;
31
+ stroke-opacity: 0.2;
32
+ }
33
+
34
+ .highlighted polygon {
35
+ fill:blue;
36
+ fill-opacity: 0.2;
37
+ }
38
+
39
+ span.highlighted {
40
+ background-color: rgba(0%, 0%, 100%, 0.2);
41
+ font-size: large;
42
+ }
43
 
44
  """
app/main.py CHANGED
@@ -7,7 +7,8 @@ from app.tabs.submit import (
7
  custom_template_yaml,
8
  collection_submit_state,
9
  )
10
- from app.tabs.visualizer import visualizer, collection_viz_state, viz_image_gallery
 
11
  from app.tabs.templating import (
12
  templating_block,
13
  TEMPLATE_IMAGE_FOLDER,
@@ -75,7 +76,7 @@ with gr.Blocks(title="HTRflow", theme=theme, css=css, head=matomo) as demo:
75
  with gr.Tab(label="Submit Job") as tab_submit:
76
  submit.render()
77
 
78
- with gr.Tab(label="Visualize Result") as tab_visualizer:
79
  visualizer.render()
80
 
81
  @demo.load()
@@ -129,7 +130,7 @@ demo.queue()
129
  if __name__ == "__main__":
130
  demo.launch(
131
  server_name="0.0.0.0",
132
- server_port=7860,
133
  enable_monitoring=True,
134
  # show_error=True,
135
  )
 
7
  custom_template_yaml,
8
  collection_submit_state,
9
  )
10
+ from app.tabs.visualizer import visualizer, collection_viz_state
11
+
12
  from app.tabs.templating import (
13
  templating_block,
14
  TEMPLATE_IMAGE_FOLDER,
 
76
  with gr.Tab(label="Submit Job") as tab_submit:
77
  submit.render()
78
 
79
+ with gr.Tab(label="Result") as tab_visualizer:
80
  visualizer.render()
81
 
82
  @demo.load()
 
130
  if __name__ == "__main__":
131
  demo.launch(
132
  server_name="0.0.0.0",
133
+ server_port=7862,
134
  enable_monitoring=True,
135
  # show_error=True,
136
  )
app/tabs/visualizer.py CHANGED
@@ -1,193 +1,37 @@
1
  import gradio as gr
2
- import pandas as pd
3
- import numpy as np
4
- import time
5
- from collections import defaultdict
6
- from typing import List, Dict, Any
7
 
8
- from htrflow.volume.volume import Collection, ImageNode, PageNode
9
- from htrflow.utils.draw import draw_polygons
10
- from htrflow.utils import imgproc
11
- from htrflow.results import Segment
12
 
 
 
 
13
 
14
- def load_visualize_state_from_submit(col: Collection, progress):
15
- results = []
16
 
17
- total_steps = len(col.pages)
 
18
 
19
- for page_idx, page_node in enumerate(col):
20
- page_node.to_original_size()
21
- page_image = page_node.image.copy()
22
 
23
- progress((page_idx + 1) / total_steps, desc="Running Visualizer")
24
-
25
- line_polygons = []
26
- line_crops = []
27
- recog_conf_values = {}
28
-
29
- for i, node in enumerate(page_node.traverse(filter=lambda n: n.is_line())):
30
- if node.polygon:
31
- line_polygons.append(node.polygon)
32
-
33
- try:
34
- cropped_line_img = imgproc.crop(page_image, node.bbox)
35
- cropped_line_img = np.clip(cropped_line_img, 0, 255).astype(np.uint8)
36
- line_crops.append(cropped_line_img)
37
- except Exception:
38
- continue
39
-
40
- if node.text_result:
41
- recog_conf_values[i] = list(
42
- zip(node.text_result.texts, node.text_result.scores)
43
- )
44
-
45
- annotated_image = draw_polygons(image=page_image, polygons=line_polygons)
46
- annotated_page_node = np.clip(annotated_image, 0, 255).astype(np.uint8)
47
-
48
- results.append(
49
- {
50
- "page_image": page_node,
51
- "annotated_page_node": annotated_page_node,
52
- "line_crops": line_crops,
53
- "recog_conf_values": _convert_conf_values_to_df(recog_conf_values),
54
- }
55
- )
56
-
57
- return results
58
-
59
-
60
- def _convert_conf_values_to_df(
61
- conf_values: Dict[int, List[tuple[str, float]]]
62
- ) -> pd.DataFrame:
63
- """Convert recognition confidence values to a pandas DataFrame."""
64
- return pd.DataFrame(
65
- [
66
- {"Transcription": text, "Confidence Score": f"{score:.4f}"}
67
- for values in conf_values.values()
68
- for text, score in values
69
- ]
70
- )
71
 
72
 
73
  with gr.Blocks() as visualizer:
74
- with gr.Column(variant="panel"):
75
- with gr.Row():
76
- collection_viz_state = gr.State()
77
- result_collection_viz_state = gr.State()
78
- with gr.Column():
79
- viz_image_gallery = gr.Gallery(
80
- file_types=["image"],
81
- label="Visualized Images from HTRflow",
82
- interactive=False,
83
- height=600,
84
- object_fit="cover",
85
- columns=5,
86
- preview=True,
87
- )
88
-
89
- visualize_button = gr.Button(
90
- "Visualize", scale=0, min_width=200, variant="primary"
91
- )
92
-
93
- progress_bar = gr.Textbox(visible=False, show_label=False)
94
-
95
- with gr.Column():
96
- cropped_image_gallery = gr.Gallery(
97
- interactive=False,
98
- preview=True,
99
- label="Cropped Polygons",
100
- height=200,
101
- )
102
- df_for_cropped_images = gr.Dataframe(
103
- label="Cropped Transcriptions",
104
- headers=["Transcription", "Confidence Score"],
105
- interactive=False,
106
- )
107
-
108
- def on_visualize_button_clicked(collection_viz, progress=gr.Progress()):
109
- """
110
- This function:
111
- - Receives the collection (collection_viz).
112
- - Processes it into 'results' (list of dicts with annotated_page_node, line_crops, dataframe).
113
- - Returns:
114
- 1) 'results' as state
115
- 2) List of annotated_page_node images (one per page) to populate viz_image_gallery
116
- """
117
- if not collection_viz:
118
- return None, []
119
-
120
- results = load_visualize_state_from_submit(collection_viz, progress)
121
- annotated_images = [r["annotated_page_node"] for r in results]
122
- return results, annotated_images, gr.skip()
123
-
124
- visualize_button.click(lambda: gr.update(visible=True), outputs=progress_bar).then(
125
- fn=on_visualize_button_clicked,
126
- inputs=collection_viz_state,
127
- outputs=[result_collection_viz_state, viz_image_gallery, progress_bar],
128
- ).then(lambda: gr.update(visible=False), outputs=progress_bar)
129
-
130
- @viz_image_gallery.change(
131
- inputs=result_collection_viz_state,
132
- outputs=[cropped_image_gallery, df_for_cropped_images],
133
- )
134
- def update_c_gallery_and_dataframe(results):
135
- selected = results[0]
136
- return selected["line_crops"], selected["recog_conf_values"]
137
-
138
- @viz_image_gallery.select(
139
- inputs=result_collection_viz_state,
140
- outputs=[cropped_image_gallery, df_for_cropped_images],
141
- )
142
- def on_dataframe_select(evt: gr.SelectData, results):
143
- """
144
- evt.index => the index of the selected image in the gallery
145
- results => the state object from result_collection_viz_state
146
-
147
- Return the line crops and the recognized text for that index.
148
- """
149
- if results is None or evt.index is None:
150
- return [], pd.DataFrame(columns=["Transcription", "Confidence Score"])
151
-
152
- idx = evt.index
153
- selected = results[idx]
154
-
155
- return selected["line_crops"], selected["recog_conf_values"]
156
-
157
- @df_for_cropped_images.select(
158
- outputs=[cropped_image_gallery],
159
- )
160
- def on_dataframe_select(evt: gr.SelectData):
161
- return gr.update(selected_index=evt.index[0])
162
-
163
- @cropped_image_gallery.select(
164
- inputs=df_for_cropped_images, outputs=df_for_cropped_images
165
- )
166
- def return_image_from_gallery(df, evt: gr.SelectData):
167
- selected_index = evt.index
168
-
169
- def highlight_row(row):
170
- return [
171
- (
172
- "border: 1px solid blue; font-weight: bold"
173
- if row.name == selected_index
174
- else ""
175
- )
176
- for _ in row
177
- ]
178
-
179
- styler = df.style.apply(highlight_row, axis=1)
180
-
181
- return styler
182
-
183
-
184
- # TODO: if state is empty from submit? dont show visualize button...
185
 
 
 
 
 
186
 
187
- # TODO: Add reading order in dataframe
188
- # TODO: Add reading order in visualization to the left
189
- # TODO: be able to click on mask on the left?
 
190
 
 
 
 
 
191
 
192
- # TODO: Show how to clone and setup pylaia? Which would probably be a seperate docker container!
193
- # TODO: fix api! and client... Notebook for api?
 
 
1
  import gradio as gr
2
+ from jinja2 import Environment, FileSystemLoader
 
 
 
 
3
 
 
 
 
 
4
 
5
+ _ENV = Environment(loader=FileSystemLoader("app/assets/jinja-templates"))
6
+ _IMAGE_TEMPLATE = _ENV.get_template("image")
7
+ _TRANSCRIPTION_TEMPLATE = _ENV.get_template("transcription")
8
 
 
 
9
 
10
+ def render_image(state):
11
+ return _IMAGE_TEMPLATE.render(page=state[0], lines=state[0].traverse(lambda node: node.is_line()))
12
 
 
 
 
13
 
14
+ def render_transcription(state):
15
+ return _TRANSCRIPTION_TEMPLATE.render(lines=state[0].traverse(lambda node: node.is_line()))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
 
18
  with gr.Blocks() as visualizer:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ with gr.Row():
21
+ # Columns are needed here to get the scale right. The documentation
22
+ # claims all components have the `scale` argument but it doesn't
23
+ # seem to work for HTML components.
24
 
25
+ # Transcription panel
26
+ with gr.Column(scale=1):
27
+ gr.Markdown("## Transcription")
28
+ transcription = gr.HTML(elem_classes="transcription", container=True, max_height="60vh")
29
 
30
+ # Annotated image panel
31
+ with gr.Column(scale=2):
32
+ gr.Markdown("## Annotated image")
33
+ image = gr.HTML(padding=False, elem_classes="svg-image", container=True)
34
 
35
+ collection_viz_state = gr.State()
36
+ collection_viz_state.change(render_image, inputs=collection_viz_state, outputs=image)
37
+ collection_viz_state.change(render_transcription, inputs=collection_viz_state, outputs=transcription)