Browse Source

Add HTML output.
Add debugging CLI option.
Bug fixes.

Andrew Klopper 6 years ago
parent
commit
a62558add7

+ 43 - 0
home/jinja2/home/grid.html

@@ -0,0 +1,43 @@
1
+<!DOCTYPE html>
2
+<html>
3
+    <head>
4
+        <meta charset="UTF-8">
5
+        <title>Crossword</title>
6
+        <style>
7
+            .crossword {
8
+                border-right: 1px solid;
9
+                border-bottom: 1px solid;
10
+            }
11
+            .crossword td {
12
+                border-left: 1px solid;
13
+                border-top: 1px solid;
14
+            }
15
+            .crossword td.filled {
16
+                background: black;
17
+            }
18
+        </style>
19
+    </head>
20
+    <body>
21
+        <table border=0 cellspacing=0 cellpadding=0 class="crossword">
22
+            {%- set tab_index = namespace(value=1) %}
23
+            {%- set blank = '&emsp;'|safe %}
24
+            {%- for row in range(num_rows) %}
25
+                <tr>
26
+                    {%- for col in range(num_cols) %}
27
+                        {%- if grid[row][col].filled %}
28
+                            <td class="filled">&emsp;</td>
29
+                        {%- else %}
30
+                            <td class="empty" tabindex="{{ tab_index.value }}">
31
+                                {{ grid[row][col].number|default(blank) }}
32
+                            </td>
33
+                            {%- set tab_index.value = tab_index.value + 1 %}
34
+                        {%- endif %}
35
+                    {%- endfor %}
36
+                </tr>
37
+            {%- endfor %}
38
+        </table>
39
+    </body>
40
+</html>
41
+<!--
42
+vim:ts=4:sw=4:expandtab
43
+-->

+ 16 - 0
home/jinja2/home/index.html

@@ -0,0 +1,16 @@
1
+<!DOCTYPE html>
2
+<html>
3
+    <head>
4
+        <meta charset="UTF-8">
5
+        <title>Crossword Extractor</title>
6
+    </head>
7
+    <body>
8
+        <form enctype="multipart/form-data" method="post" action="">
9
+            <div>{{ form }}</div>
10
+            <div><input type="submit" value="Upload"></div>
11
+        </form>
12
+    </body>
13
+</html>
14
+<!--
15
+vim:ts=4:sw=4:expandtab
16
+-->

+ 33 - 0
home/jinja2/home/output.html

@@ -0,0 +1,33 @@
1
+<!DOCTYPE html>
2
+<html>
3
+    <head>
4
+        <meta charset="UTF-8">
5
+        <title>Extracted Crossword</title>
6
+        <style>
7
+            img {
8
+                width: 600px;
9
+            }
10
+        </style>
11
+    </head>
12
+    <body>
13
+        {% if warnings %}
14
+        <div>
15
+            Warnings:
16
+            <ul>
17
+                {% for warning in warnings %}
18
+                    <li>{{ warning }}</li>
19
+                {% endfor %}
20
+            </ul>
21
+        </div>
22
+        {% endif %}
23
+        <div>
24
+            <a href="{{ image_url }}" download="{{ image_file_name }}">Download</a>
25
+        </div>
26
+        <div>
27
+            <img alt="Crossword" src="{{ image_url }}">
28
+        </div>
29
+    </body>
30
+</html>
31
+<!--
32
+vim:ts=4:sw=4:expandtab
33
+-->

+ 53 - 14
home/management/commands/extract.py

@@ -1,5 +1,7 @@
1
+import cv2
1 2
 from django.core.management.base import BaseCommand
2
-from ...xword import extract_crossword_grid, grid_to_png
3
+from django.template.loader import render_to_string
4
+from ...xword import extract_crossword_grid, draw_grid
3 5
 
4 6
 
5 7
 class Command(BaseCommand):
@@ -7,10 +9,11 @@ class Command(BaseCommand):
7 9
 
8 10
     def add_arguments(self, parser):
9 11
         parser.add_argument('input_file_name')
10
-        parser.add_argument('output_file_name')
11 12
 
12
-        parser.add_argument('--filter-colours', action='store_true')
13
-        parser.add_argument('--colour-filter-threshold', type=int, default=48)
13
+        parser.add_argument('--debug', action='store_true')
14
+
15
+        parser.add_argument('--remove-colours', action='store_true')
16
+        parser.add_argument('--colour-removal-threshold', type=int, default=48)
14 17
 
15 18
         parser.add_argument('--gaussian-blur-size', type=int, default=11)
16 19
         parser.add_argument('--adaptive-threshold-block-size', type=int, default=11)
@@ -39,6 +42,9 @@ class Command(BaseCommand):
39 42
     def handle(self, *args, **options):
40 43
         warnings, grid, num_rows, num_cols, block_img = extract_crossword_grid(
41 44
             options['input_file_name'],
45
+            callback=debug_callback if options['debug'] else None,
46
+            remove_colours=options['remove_colours'],
47
+            colour_removal_threshold=options['colour_removal_threshold'],
42 48
             gaussian_blur_size=options['gaussian_blur_size'],
43 49
             adaptive_threshold_block_size=options['adaptive_threshold_block_size'],
44 50
             adaptive_threshold_mean_adjustment=options['adaptive_threshold_mean_adjustment'],
@@ -51,15 +57,48 @@ class Command(BaseCommand):
51 57
             sampling_threshold_quantile=options['sampling_threshold_quantile'],
52 58
             sampling_threshold=options['sampling_threshold']
53 59
         )
54
-        image = grid_to_png(
55
-            grid,
56
-            num_rows,
57
-            num_cols,
58
-            grid_line_thickness=options['grid_line_thickness'],
59
-            grid_square_size=options['grid_square_size'],
60
-            grid_border_size=options['grid_border_size']
61
-        )
60
+
61
+        if options['debug']:
62
+            debug_callback('square', block_img)
63
+
62 64
         for warning in warnings:
63 65
             print('WARNING: ' + warning)
64
-        with open(options['output_file_name'], 'wb') as f:
65
-            f.write(image)
66
+
67
+        if options['html'] is not None:
68
+            html = render_to_string('home/grid.html', {
69
+                'grid': grid,
70
+                'num_rows': num_rows,
71
+                'num_cols': num_cols,
72
+            })
73
+
74
+            with open(options['html'], 'w') as f:
75
+                f.write(html)
76
+
77
+        else:
78
+            image = draw_grid(
79
+                grid,
80
+                num_rows,
81
+                num_cols,
82
+                grid_line_thickness=options['grid_line_thickness'],
83
+                grid_square_size=options['grid_square_size'],
84
+                grid_border_size=options['grid_border_size']
85
+            )
86
+
87
+            if options['debug'] or (options['out'] is None):
88
+                debug_callback('output', image)
89
+
90
+            if options['out'] is not None:
91
+                _, png = cv2.imencode('.png', image)
92
+
93
+                with open(options['out'], 'wb') as f:
94
+                    f.write(png.tobytes())
95
+
96
+        if options['debug'] or (options['out'] is None):
97
+            while cv2.waitKey() & 0xFF != ord('q'):
98
+                pass
99
+            cv2.destroyAllWindows()
100
+
101
+
102
+def debug_callback(name, image):
103
+    cv2.namedWindow(name, cv2.WINDOW_NORMAL)
104
+    cv2.imshow(name, image)

+ 0 - 16
home/templates/home/index.html

@@ -1,16 +0,0 @@
1
-<!DOCTYPE html>
2
-<html>
3
-  <head>
4
-    <meta charset="UTF-8">
5
-    <title>Crossword Extractor</title>
6
-  </head>
7
-  <body>
8
-    <form enctype="multipart/form-data" method="post" action="">
9
-      <div>{{ form }}</div>
10
-      <div><input type="submit" value="Upload"></div>
11
-    </form>
12
-  </body>
13
-</html>
14
-<!--
15
-vim:ts=2:sw=2:expandtab
16
--->

+ 0 - 33
home/templates/home/output.html

@@ -1,33 +0,0 @@
1
-<!DOCTYPE html>
2
-<html>
3
-  <head>
4
-    <meta charset="UTF-8">
5
-    <title>Extracted Crossword</title>
6
-    <style>
7
-      img {
8
-        width: 600px;
9
-      }
10
-    </style>
11
-  </head>
12
-  <body>
13
-    {% if warnings %}
14
-    <div>
15
-      Warnings:
16
-      <ul>
17
-        {% for warning in warnings %}
18
-          <li>{{ warning }}</li>
19
-        {% endfor %}
20
-      </ul>
21
-    </div>
22
-    {% endif %}
23
-    <div>
24
-      <a href="{{ image_url }}" download="{{ image_file_name }}">Download</a>
25
-    </div>
26
-    <div>
27
-      <img alt="Crossword" src="{{ image_url }}">
28
-    </div>
29
-  </body>
30
-</html>
31
-<!--
32
-vim:ts=2:sw=2:expandtab
33
--->

+ 6 - 4
home/views.py

@@ -1,10 +1,11 @@
1 1
 from base64 import b64encode
2
+import cv2
2 3
 from datetime import datetime
3 4
 from django.http import HttpResponse, HttpResponseBadRequest
4 5
 from django.shortcuts import render
5 6
 from django.views import View
6 7
 from .forms import CrosswordForm
7
-from .xword import extract_crossword_grid, grid_to_png
8
+from .xword import extract_crossword_grid, draw_grid
8 9
 
9 10
 
10 11
 class HomeView(View):
@@ -17,11 +18,12 @@ class HomeView(View):
17 18
             return HttpResponseBadRequest('Invalid form data')
18 19
         warnings, grid, num_rows, num_cols, block_img = extract_crossword_grid(
19 20
             form.cleaned_data['file'].temporary_file_path(),
20
-            filter_colours=False
21
+            remove_colours=False
21 22
         )
22
-        image = grid_to_png(grid, num_rows, num_cols)
23
+        image = draw_grid(grid, num_rows, num_cols)
24
+        _, png = cv2.imencode('.png', image)
23 25
         return render(request, 'home/output.html', {
24 26
             'warnings': warnings,
25 27
             'image_file_name': 'xword_{}.png'.format(datetime.now().strftime('%Y%m%d_%H%M%S')),
26
-            'image_url': 'data:image/png;base64,' + b64encode(image).decode()
28
+            'image_url': 'data:image/png;base64,' + b64encode(png.tobytes()).decode()
27 29
         })

+ 36 - 23
home/xword.py

@@ -155,25 +155,41 @@ def extract_grid_colours(img, num_rows, num_cols, sampling_block_size_ratio):
155 155
 def grid_colours_to_blocks(grid_colours, num_rows, num_cols, sampling_threshold):
156 156
     grid = copy.deepcopy(grid_colours)
157 157
     warning = False
158
-    for row in range(round(num_rows / 2)):
158
+
159
+    midpoint = num_rows // 2 + (0 if num_rows % 2 == 0 else 1)
160
+    for row in range(midpoint):
159 161
         for col in range(num_cols):
162
+            # If there is an odd number of rows then row and row2 will point to
163
+            # the same row when we reach the middle. Doesn't seem worth adding a
164
+            # special case.
160 165
             row2 = num_rows - row - 1
161 166
             col2 = num_cols - col - 1
162 167
             delta1 = grid_colours[row][col] - sampling_threshold
163 168
             delta2 = grid_colours[row2][col2] - sampling_threshold
164 169
 
165 170
             if (delta1 > 0) and (delta2 > 0):
166
-                block = 0
171
+                filled = False
167 172
             elif (delta1 < 0) and (delta2 < 0):
168
-                block = 1
173
+                filled = True
169 174
             else:
170 175
                 warning = True
171 176
                 if abs(delta1) > abs(delta2):
172
-                    block = 1 if delta1 < 0 else 0
177
+                    filled = delta1 < 0
173 178
                 else:
174
-                    block = 1 if delta2 < 0 else 0
179
+                    filled = delta2 < 0
180
+
181
+            grid[row][col] = {'filled': filled}
182
+            grid[row2][col2] = {'filled': filled}
175 183
 
176
-            grid[row][col] = grid[row2][col2] = block
184
+    number = 1
185
+    for row in range(num_rows):
186
+        for col in range(num_cols):
187
+            if (not grid[row][col]['filled'] and (
188
+                (((col == 0) or grid[row][col - 1]['filled']) and (col < num_cols - 1) and not grid[row][col + 1]['filled']) or
189
+                (((row == 0) or grid[row - 1][col]['filled']) and (row < num_rows - 1) and not grid[row + 1][col]['filled'])
190
+            )):
191
+                grid[row][col]['number'] = number
192
+                number += 1
177 193
 
178 194
     return warning, grid
179 195
 
@@ -188,25 +204,18 @@ def draw_point(image, point, colour):
188 204
                 image[y, x] = colour
189 205
 
190 206
 
191
-def show_image(image):
192
-    cv2.namedWindow('xword', cv2.WINDOW_NORMAL)
193
-    cv2.imshow('xword', image)
194
-    while cv2.waitKey() & 0xFF != ord('q'):
195
-        pass
196
-    cv2.destroyAllWindows()
197
-
198
-
199 207
 def extract_crossword_grid(
200 208
     file_name,
201
-    filter_colours=False,
202
-    colour_filter_threshold=48,
209
+    callback=None,
210
+    remove_colours=False,
211
+    colour_removal_threshold=48,
203 212
     gaussian_blur_size=11,
204 213
     adaptive_threshold_block_size=11,
205 214
     adaptive_threshold_mean_adjustment=2,
206 215
     square=True,
207 216
     num_dilations=1,
208 217
     contour_erosion_kernel_size=5,
209
-    contour_erosion_iterations=6,
218
+    contour_erosion_iterations=5,
210 219
     line_detector_element_size=51,
211 220
     sampling_block_size_ratio=0.25,
212 221
     sampling_threshold_quantile=0.3,
@@ -214,9 +223,13 @@ def extract_crossword_grid(
214 223
 ):
215 224
     warnings = []
216 225
 
217
-    original = load_image_as_greyscale(file_name, filter_colours, colour_filter_threshold)
226
+    original = load_image_as_greyscale(file_name, remove_colours, colour_removal_threshold)
227
+    if callback is not None:
228
+        callback('original', original)
218 229
 
219 230
     img = preprocess_image(original, gaussian_blur_size, adaptive_threshold_block_size, adaptive_threshold_mean_adjustment, num_dilations)
231
+    if callback is not None:
232
+        callback('preprocessed', img)
220 233
 
221 234
     biggest = find_biggest_contour(img)
222 235
     biggest = erode_contour(img.shape, biggest, contour_erosion_kernel_size, contour_erosion_iterations)
@@ -224,6 +237,8 @@ def extract_crossword_grid(
224 237
     top_left, top_right, bottom_right, bottom_left = get_contour_corners(img, biggest)
225 238
 
226 239
     img = extract_square(img, top_left, top_right, bottom_right, bottom_left)
240
+    if callback is not None:
241
+        callback('pre-fft', img)
227 242
 
228 243
     num_rows = get_line_frequency(img, line_detector_element_size, 1)
229 244
     num_cols = get_line_frequency(img, line_detector_element_size, 0)
@@ -245,7 +260,7 @@ def extract_crossword_grid(
245 260
     return warnings, grid, num_rows, num_cols, block_img
246 261
 
247 262
 
248
-def grid_to_png(
263
+def draw_grid(
249 264
     grid,
250 265
     num_rows,
251 266
     num_cols,
@@ -261,9 +276,7 @@ def grid_to_png(
261 276
     for row in range(num_rows):
262 277
         y = row * step + grid_line_thickness + grid_border_size
263 278
         for col in range(num_cols):
264
-            if grid[row][col] == 0:
279
+            if not grid[row][col]['filled']:
265 280
                 x = col * step + grid_line_thickness + grid_border_size
266 281
                 cv2.rectangle(output, (x, y), (x + grid_square_size - 1, y + grid_square_size - 1), 255, -1)
267
-
268
-    _, png = cv2.imencode('.png', output)
269
-    return png.tobytes()
282
+    return output

+ 1 - 0
requirements.in

@@ -1,5 +1,6 @@
1 1
 chaussette
2 2
 django
3 3
 django-extensions
4
+jinja2
4 5
 opencv-python
5 6
 pip-tools

+ 2 - 0
requirements.txt

@@ -9,6 +9,8 @@ chaussette==1.3.0
9 9
 click==7.0                # via pip-tools
10 10
 django-extensions==2.2.6
11 11
 django==3.0.2
12
+jinja2==2.11.0
13
+markupsafe==1.1.1         # via jinja2
12 14
 numpy==1.18.1             # via opencv-python
13 15
 opencv-python==4.1.2.30
14 16
 pip-tools==4.4.0

+ 12 - 0
xword/jinja2.py

@@ -0,0 +1,12 @@
1
+#from django.contrib.staticfiles.storage import staticfiles_storage
2
+#from django.urls import reverse
3
+from jinja2 import Environment
4
+
5
+
6
+def environment(**options):
7
+    env = Environment(**options)
8
+    env.globals.update({
9
+        #‘static’: staticfiles_storage.url,
10
+        #‘url’: reverse,
11
+    })
12
+    return env

+ 2 - 1
xword/settings.py

@@ -25,10 +25,11 @@ ROOT_URLCONF = 'xword.urls'
25 25
 
26 26
 TEMPLATES = [
27 27
     {
28
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
28
+        'BACKEND': 'django.template.backends.jinja2.Jinja2',
29 29
         'DIRS': [],
30 30
         'APP_DIRS': True,
31 31
         'OPTIONS': {
32
+            'environment': 'xword.jinja2.environment',
32 33
             'context_processors': [
33 34
                 'django.template.context_processors.debug',
34 35
                 'django.template.context_processors.request',