Selaa lähdekoodia

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

Andrew Klopper 6 vuotta sitten
vanhempi
commit
a62558add7

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

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

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

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
+import cv2
1
 from django.core.management.base import BaseCommand
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
 class Command(BaseCommand):
7
 class Command(BaseCommand):
7
 
9
 
8
     def add_arguments(self, parser):
10
     def add_arguments(self, parser):
9
         parser.add_argument('input_file_name')
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
         parser.add_argument('--gaussian-blur-size', type=int, default=11)
18
         parser.add_argument('--gaussian-blur-size', type=int, default=11)
16
         parser.add_argument('--adaptive-threshold-block-size', type=int, default=11)
19
         parser.add_argument('--adaptive-threshold-block-size', type=int, default=11)
39
     def handle(self, *args, **options):
42
     def handle(self, *args, **options):
40
         warnings, grid, num_rows, num_cols, block_img = extract_crossword_grid(
43
         warnings, grid, num_rows, num_cols, block_img = extract_crossword_grid(
41
             options['input_file_name'],
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
             gaussian_blur_size=options['gaussian_blur_size'],
48
             gaussian_blur_size=options['gaussian_blur_size'],
43
             adaptive_threshold_block_size=options['adaptive_threshold_block_size'],
49
             adaptive_threshold_block_size=options['adaptive_threshold_block_size'],
44
             adaptive_threshold_mean_adjustment=options['adaptive_threshold_mean_adjustment'],
50
             adaptive_threshold_mean_adjustment=options['adaptive_threshold_mean_adjustment'],
51
             sampling_threshold_quantile=options['sampling_threshold_quantile'],
57
             sampling_threshold_quantile=options['sampling_threshold_quantile'],
52
             sampling_threshold=options['sampling_threshold']
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
         for warning in warnings:
64
         for warning in warnings:
63
             print('WARNING: ' + warning)
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
-<!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
-<!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
 from base64 import b64encode
1
 from base64 import b64encode
2
+import cv2
2
 from datetime import datetime
3
 from datetime import datetime
3
 from django.http import HttpResponse, HttpResponseBadRequest
4
 from django.http import HttpResponse, HttpResponseBadRequest
4
 from django.shortcuts import render
5
 from django.shortcuts import render
5
 from django.views import View
6
 from django.views import View
6
 from .forms import CrosswordForm
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
 class HomeView(View):
11
 class HomeView(View):
17
             return HttpResponseBadRequest('Invalid form data')
18
             return HttpResponseBadRequest('Invalid form data')
18
         warnings, grid, num_rows, num_cols, block_img = extract_crossword_grid(
19
         warnings, grid, num_rows, num_cols, block_img = extract_crossword_grid(
19
             form.cleaned_data['file'].temporary_file_path(),
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
         return render(request, 'home/output.html', {
25
         return render(request, 'home/output.html', {
24
             'warnings': warnings,
26
             'warnings': warnings,
25
             'image_file_name': 'xword_{}.png'.format(datetime.now().strftime('%Y%m%d_%H%M%S')),
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
 def grid_colours_to_blocks(grid_colours, num_rows, num_cols, sampling_threshold):
155
 def grid_colours_to_blocks(grid_colours, num_rows, num_cols, sampling_threshold):
156
     grid = copy.deepcopy(grid_colours)
156
     grid = copy.deepcopy(grid_colours)
157
     warning = False
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
         for col in range(num_cols):
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
             row2 = num_rows - row - 1
165
             row2 = num_rows - row - 1
161
             col2 = num_cols - col - 1
166
             col2 = num_cols - col - 1
162
             delta1 = grid_colours[row][col] - sampling_threshold
167
             delta1 = grid_colours[row][col] - sampling_threshold
163
             delta2 = grid_colours[row2][col2] - sampling_threshold
168
             delta2 = grid_colours[row2][col2] - sampling_threshold
164
 
169
 
165
             if (delta1 > 0) and (delta2 > 0):
170
             if (delta1 > 0) and (delta2 > 0):
166
-                block = 0
171
+                filled = False
167
             elif (delta1 < 0) and (delta2 < 0):
172
             elif (delta1 < 0) and (delta2 < 0):
168
-                block = 1
173
+                filled = True
169
             else:
174
             else:
170
                 warning = True
175
                 warning = True
171
                 if abs(delta1) > abs(delta2):
176
                 if abs(delta1) > abs(delta2):
172
-                    block = 1 if delta1 < 0 else 0
177
+                    filled = delta1 < 0
173
                 else:
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
     return warning, grid
194
     return warning, grid
179
 
195
 
188
                 image[y, x] = colour
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
 def extract_crossword_grid(
207
 def extract_crossword_grid(
200
     file_name,
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
     gaussian_blur_size=11,
212
     gaussian_blur_size=11,
204
     adaptive_threshold_block_size=11,
213
     adaptive_threshold_block_size=11,
205
     adaptive_threshold_mean_adjustment=2,
214
     adaptive_threshold_mean_adjustment=2,
206
     square=True,
215
     square=True,
207
     num_dilations=1,
216
     num_dilations=1,
208
     contour_erosion_kernel_size=5,
217
     contour_erosion_kernel_size=5,
209
-    contour_erosion_iterations=6,
218
+    contour_erosion_iterations=5,
210
     line_detector_element_size=51,
219
     line_detector_element_size=51,
211
     sampling_block_size_ratio=0.25,
220
     sampling_block_size_ratio=0.25,
212
     sampling_threshold_quantile=0.3,
221
     sampling_threshold_quantile=0.3,
214
 ):
223
 ):
215
     warnings = []
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
     img = preprocess_image(original, gaussian_blur_size, adaptive_threshold_block_size, adaptive_threshold_mean_adjustment, num_dilations)
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
     biggest = find_biggest_contour(img)
234
     biggest = find_biggest_contour(img)
222
     biggest = erode_contour(img.shape, biggest, contour_erosion_kernel_size, contour_erosion_iterations)
235
     biggest = erode_contour(img.shape, biggest, contour_erosion_kernel_size, contour_erosion_iterations)
224
     top_left, top_right, bottom_right, bottom_left = get_contour_corners(img, biggest)
237
     top_left, top_right, bottom_right, bottom_left = get_contour_corners(img, biggest)
225
 
238
 
226
     img = extract_square(img, top_left, top_right, bottom_right, bottom_left)
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
     num_rows = get_line_frequency(img, line_detector_element_size, 1)
243
     num_rows = get_line_frequency(img, line_detector_element_size, 1)
229
     num_cols = get_line_frequency(img, line_detector_element_size, 0)
244
     num_cols = get_line_frequency(img, line_detector_element_size, 0)
245
     return warnings, grid, num_rows, num_cols, block_img
260
     return warnings, grid, num_rows, num_cols, block_img
246
 
261
 
247
 
262
 
248
-def grid_to_png(
263
+def draw_grid(
249
     grid,
264
     grid,
250
     num_rows,
265
     num_rows,
251
     num_cols,
266
     num_cols,
261
     for row in range(num_rows):
276
     for row in range(num_rows):
262
         y = row * step + grid_line_thickness + grid_border_size
277
         y = row * step + grid_line_thickness + grid_border_size
263
         for col in range(num_cols):
278
         for col in range(num_cols):
264
-            if grid[row][col] == 0:
279
+            if not grid[row][col]['filled']:
265
                 x = col * step + grid_line_thickness + grid_border_size
280
                 x = col * step + grid_line_thickness + grid_border_size
266
                 cv2.rectangle(output, (x, y), (x + grid_square_size - 1, y + grid_square_size - 1), 255, -1)
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
 chaussette
1
 chaussette
2
 django
2
 django
3
 django-extensions
3
 django-extensions
4
+jinja2
4
 opencv-python
5
 opencv-python
5
 pip-tools
6
 pip-tools

+ 2 - 0
requirements.txt

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

+ 12 - 0
xword/jinja2.py

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
 
25
 
26
 TEMPLATES = [
26
 TEMPLATES = [
27
     {
27
     {
28
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
28
+        'BACKEND': 'django.template.backends.jinja2.Jinja2',
29
         'DIRS': [],
29
         'DIRS': [],
30
         'APP_DIRS': True,
30
         'APP_DIRS': True,
31
         'OPTIONS': {
31
         'OPTIONS': {
32
+            'environment': 'xword.jinja2.environment',
32
             'context_processors': [
33
             'context_processors': [
33
                 'django.template.context_processors.debug',
34
                 'django.template.context_processors.debug',
34
                 'django.template.context_processors.request',
35
                 'django.template.context_processors.request',