├── .gitignore
├── .ipynb_checkpoints
└── Docstrum-checkpoint.ipynb
├── Docstrum.ipynb
├── README.md
├── assets
├── Docstrum_Visualized_Steps.gif
└── Text-line_Grouping_Process.gif
├── box.py
├── box.pyc
├── colors.py
├── colors.pyc
├── content.py
├── dimension.py
├── dimension.pyc
├── geometry.py
├── geometry.pyc
├── images_bank
├── AC_2c_title_clean.jpg
├── AC_dense.jpg
└── AC_dense_clean.jpg
├── main.py
├── margin.py
├── output
└── AC_2c_title_clean.jpg
├── page.py
├── page.pyc
├── stopwatch.py
├── stopwatch.pyc
├── text.py
└── text.pyc
/.gitignore:
--------------------------------------------------------------------------------
1 | # Datasets
2 | docstrums/
3 | images/
4 | output/
5 | images_bank
6 |
7 | # For Mac OS
8 | .DS_Store
9 |
--------------------------------------------------------------------------------
/Docstrum.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {
7 | "collapsed": false
8 | },
9 | "outputs": [],
10 | "source": [
11 | "#!/usr/bin/python\n",
12 | "# find . -name '.DS_Store' -type f -delete\n",
13 | "# Chulwoo Pack\n",
14 | "\n",
15 | "import sys\n",
16 | "import os\n",
17 | "from page import Page\n",
18 | "\n",
19 | "SHOW_STEPS = True # change this to false if you just want to see the final output for each page.\n",
20 | "SAVE_OUTPUT = True\n",
21 | "SAVE_DOCSTRUM = True\n",
22 | "\n",
23 | "inputFolder = os.path.join('images')\n",
24 | "outputFolder = os.path.join('output')\n",
25 | "\n",
26 | " \n",
27 | "inputPath = os.path.join(inputFolder, os.listdir(inputFolder)[0])\n",
28 | "outputPath = os.path.join(outputFolder, os.listdir(inputFolder)[0])\n",
29 | "\n",
30 | "page = Page(inputPath, SHOW_STEPS, SAVE_DOCSTRUM)\n",
31 | "#page = Page(inputPath, SHOW_STEPS)\n",
32 | " \n",
33 | "if SAVE_OUTPUT:\n",
34 | " page.save(outputPath) # save a copy of what is displayed. Used for getting images for the paper.\n",
35 | " \n",
36 | "page.show((800, 800))"
37 | ]
38 | },
39 | {
40 | "cell_type": "code",
41 | "execution_count": null,
42 | "metadata": {
43 | "collapsed": false
44 | },
45 | "outputs": [],
46 | "source": [
47 | "# LINE EXTRACTION TESTING\n",
48 | "\n",
49 | "import sys\n",
50 | "import os\n",
51 | "from page import Page\n",
52 | "\n",
53 | "import cv2\n",
54 | "import math\n",
55 | "import numpy\n",
56 | "import subprocess\n",
57 | "import os\n",
58 | "\n",
59 | "import colors\n",
60 | "import geometry as g\n",
61 | "from box import Box\n",
62 | "import text\n",
63 | "from dimension import Dimension\n",
64 | "from stopwatch import Stopwatch\n",
65 | "import numpy\n",
66 | "import matplotlib.pyplot as plt\n",
67 | "import ntpath\n",
68 | "\n",
69 | "\n",
70 | "SHOW_STEPS = True # change this to false if you just want to see the final output for each page.\n",
71 | "SAVE_OUTPUT = True\n",
72 | "SAVE_DOCSTRUM = False\n",
73 | "\n",
74 | "inputFolder = os.path.join('images')\n",
75 | "outputFolder = os.path.join('output')\n",
76 | "\n",
77 | " \n",
78 | "inputPath = os.path.join(inputFolder, os.listdir(inputFolder)[0])\n",
79 | "outputPath = os.path.join(outputFolder, os.listdir(inputFolder)[0])\n",
80 | "\n",
81 | "page = Page(inputPath, SHOW_STEPS, SAVE_DOCSTRUM)\n",
82 | "\n",
83 | "\n",
84 | "if True:\n",
85 | " page.save(outputPath) # save a copy of what is displayed. Used for getting images for the paper.\n",
86 | " \n",
87 | "page.show((800, 800))"
88 | ]
89 | },
90 | {
91 | "cell_type": "code",
92 | "execution_count": null,
93 | "metadata": {
94 | "collapsed": false
95 | },
96 | "outputs": [],
97 | "source": [
98 | "print(\"%.2f\" %1.237)"
99 | ]
100 | },
101 | {
102 | "cell_type": "code",
103 | "execution_count": null,
104 | "metadata": {
105 | "collapsed": false
106 | },
107 | "outputs": [],
108 | "source": [
109 | "for line in page.lines:\n",
110 | " line.group = None\n",
111 | "\n",
112 | "for line in page.lines:\n",
113 | " print(line.group)"
114 | ]
115 | },
116 | {
117 | "cell_type": "code",
118 | "execution_count": null,
119 | "metadata": {
120 | "collapsed": false
121 | },
122 | "outputs": [],
123 | "source": [
124 | "#NER VERSION: ADJUSTIVE SEARCHING ORDER\n",
125 | "import cv2\n",
126 | "\n",
127 | "from shapely.geometry import Point # For checking overlap\n",
128 | "from shapely.geometry.polygon import Polygon # For checking overlap\n",
129 | "from shapely.geometry import MultiPoint # For checking overlap\n",
130 | "\n",
131 | "import progressbar # For displaying progressbar\n",
132 | "from time import sleep # For displaying progressbar\n",
133 | "\n",
134 | "#from stopwatch import Stopwatch # For checking run-time\n",
135 | "\n",
136 | "#stopwatch = Stopwatch()\n",
137 | "\n",
138 | "image = page.image.copy()\n",
139 | "\n",
140 | "EPS = 1e-10\n",
141 | "group_idx = 0\n",
142 | "threshold_angle = 1.0\n",
143 | "threshold_paralldist = 1.7 * 13.0\n",
144 | "threshold_perpendist = 1.7 * 17.0 #[1.5~1.7]\n",
145 | "threshold_overlap = 1.0\n",
146 | "threshold_early_skip = 100\n",
147 | "threshold_visualize_line_width = 5\n",
148 | "\n",
149 | "SHOW_DETAIL = False\n",
150 | "SHOW_VISUAL_STEP = True\n",
151 | "EARLY_SKIP = False\n",
152 | "\n",
153 | "########\n",
154 | "# INIT #\n",
155 | "########\n",
156 | "# Get lines\n",
157 | "_my_lines = page.lines\n",
158 | "# Remove dots\n",
159 | "my_lines = []\n",
160 | "for _my_line in _my_lines:\n",
161 | " if(_my_line.start.x-_my_line.end.x==0 and _my_line.start.y-_my_line.end.y==0):\n",
162 | " continue\n",
163 | " else:\n",
164 | " my_lines.append(_my_line)\n",
165 | "# Sorting lines\n",
166 | "my_lines.sort(key=lambda line:((line.start.y+line.end.y)/2,(line.start.x+line.end.x)/2))\n",
167 | "# Lines assigned a group\n",
168 | "my_lines_in_group = []\n",
169 | "# Lines not assigned any group yet\n",
170 | "my_lines_no_group = []\n",
171 | "for i in range(0,len(my_lines)-1):\n",
172 | " my_lines_no_group.append(i)\n",
173 | "if SHOW_DETAIL: print(\"no_group:\",my_lines_in_group)\n",
174 | "if SHOW_DETAIL: print(\"in_group:\",my_lines_no_group)\n",
175 | "# First line, not dot (its index, i)\n",
176 | "\n",
177 | "bar = progressbar.ProgressBar(maxval=len(my_lines_no_group), \\\n",
178 | " widgets=[progressbar.Bar('=', '[', ']'), ' ', progressbar.Percentage()])\n",
179 | "progress_idx = 0\n",
180 | "bar.start()\n",
181 | "\n",
182 | "max_loop = len(my_lines)\n",
183 | "#act_loop = 0\n",
184 | "\n",
185 | "for act_loop in xrange(max_loop): # Make sure looked up every lines \n",
186 | " #flag_found_none = True\n",
187 | " if ((len(my_lines_in_group) == 0) and (len(my_lines_no_group) == 0)):\n",
188 | " break\n",
189 | " #test_act_loop = test_act_loop+1\n",
190 | " \n",
191 | " progress_idx = progress_idx+2 # Update progressbar\n",
192 | " bar.update(act_loop) # Update progressbar\n",
193 | " sleep(0.1) \n",
194 | " #######################\n",
195 | " # Set the ith element #\n",
196 | " #######################\n",
197 | " i = -1\n",
198 | " if EARLY_SKIP:\n",
199 | " early_skip = threshold_early_skip\n",
200 | " ## TODO: Since, currently there is no lines in my_lines_in_group queue and lines are remained, find another line as ith element in my_lines_no_group queue with excluding dots. \n",
201 | " if(len(my_lines_in_group) == 0):\n",
202 | " delta_x_i = 0\n",
203 | " delta_y_i = 0\n",
204 | " for candidate_line_idx in my_lines_no_group[:]:\n",
205 | " x_O_i = my_lines[candidate_line_idx].start.x\n",
206 | " y_O_i = page.image.shape[0] - my_lines[candidate_line_idx].start.y\n",
207 | " x_F_i = my_lines[candidate_line_idx].end.x\n",
208 | " y_F_i = page.image.shape[0] - my_lines[candidate_line_idx].end.y \n",
209 | " #delta_x_i = abs(x_F_i - x_O_i)\n",
210 | " delta_x_i = float(x_F_i - x_O_i)\n",
211 | " #delta_y_i = abs(y_F_i - y_O_i)\n",
212 | " delta_y_i = float(y_F_i - y_O_i)\n",
213 | " if (delta_x_i != 0 and delta_y_i != 0): # Found!\n",
214 | " i = candidate_line_idx\n",
215 | " my_lines_no_group.remove(candidate_line_idx)\n",
216 | " break\n",
217 | " else:\n",
218 | " i = my_lines_in_group.pop(0)\n",
219 | " \n",
220 | " \n",
221 | " # TODO: more sophisticated way to break?\n",
222 | " if (i == -1):\n",
223 | " break\n",
224 | " \n",
225 | " # Visualize ith element\n",
226 | " if SHOW_VISUAL_STEP:\n",
227 | " image = page.image.copy()\n",
228 | " cv2.line(image, ((my_lines[i].start.x,my_lines[i].start.y)),((my_lines[i].end.x,my_lines[i].end.y)), (0,0,255),threshold_visualize_line_width)\n",
229 | " page.display(image, title='Visualization of text-line groupping step')\n",
230 | " \n",
231 | " # No more lines to search\n",
232 | " if (len(my_lines_no_group) == 0):\n",
233 | " break\n",
234 | " else:\n",
235 | " my_lines[i].noise = False # This assure that dot-wise noise to be excluded from grouping process\n",
236 | " #######################\n",
237 | " # Set the jth element #\n",
238 | " #######################\n",
239 | " for j in my_lines_no_group[:]:\n",
240 | " if EARLY_SKIP:\n",
241 | " if early_skip < 0:\n",
242 | " break\n",
243 | " if SHOW_VISUAL_STEP:\n",
244 | " cv2.line(image, ((my_lines[j].start.x,my_lines[j].start.y)),((my_lines[j].end.x,my_lines[j].end.y)), (255,0,0),threshold_visualize_line_width)\n",
245 | " page.display(image, title='Visualization of text-line groupping step')\n",
246 | "\n",
247 | " sameGroup = False\n",
248 | " ################################\n",
249 | " # CALCULATE GEOMETRIC FEATURES #\n",
250 | " ################################\n",
251 | " # Point setting\n",
252 | " x_O_i = my_lines[i].start.x\n",
253 | " y_O_i = page.image.shape[0] - my_lines[i].start.y\n",
254 | " x_F_i = my_lines[i].end.x\n",
255 | " y_F_i = page.image.shape[0] - my_lines[i].end.y \n",
256 | "\n",
257 | " x_O_j = my_lines[j].start.x\n",
258 | " y_O_j = page.image.shape[0] - my_lines[j].start.y\n",
259 | " x_F_j = my_lines[j].end.x\n",
260 | " y_F_j = page.image.shape[0] - my_lines[j].end.y\n",
261 | "\n",
262 | " #delta_x_i = abs(x_F_i - x_O_i)\n",
263 | " #delta_y_i = abs(y_F_i - y_O_i)\n",
264 | " #delta_x_j = abs(x_F_j - x_O_j)\n",
265 | " #delta_y_j = abs(y_F_j - y_O_j)\n",
266 | " delta_x_i = float(x_F_i - x_O_i)\n",
267 | " delta_y_i = float(y_F_i - y_O_i)\n",
268 | " delta_x_j = float(x_F_j - x_O_j)\n",
269 | " delta_y_j = float(y_F_j - y_O_j)\n",
270 | " \n",
271 | " \n",
272 | " # ith or jth line is dot, so skip it\n",
273 | " if (delta_x_j == 0 and delta_y_j == 0):\n",
274 | " my_lines_no_group.remove(j)\n",
275 | " continue\n",
276 | "\n",
277 | " if SHOW_DETAIL:\n",
278 | " print(\"\\n****************************************************************\")\n",
279 | " print(\"# of in_group:\",len(my_lines_in_group),my_lines_in_group)\n",
280 | " print(\"# of no_group:\",len(my_lines_no_group),my_lines_no_group)\n",
281 | " print(i, my_lines[i].points)\n",
282 | " print(j, my_lines[j].points)\n",
283 | " print(\"i:\",x_O_i,y_O_i,\"-\",x_F_i,y_F_i)\n",
284 | " print(\"j:\",x_O_j,y_O_j,\"-\",x_F_j,y_F_j)\n",
285 | "\n",
286 | " # Calculate angle\n",
287 | " theta_i_j = math.atan2(delta_y_j,delta_x_j-math.atan2(delta_y_i,delta_x_i))\n",
288 | " if SHOW_DETAIL:\n",
289 | " print(\"Angle:\",theta_i_j)\n",
290 | "\n",
291 | " # Calculate overlap\n",
292 | " #if delta_x_j == 0:\n",
293 | " # delta_x_j = 0.1\n",
294 | " #if delta_y_i == 0:\n",
295 | " # delta_y_i = 0.1\n",
296 | " #if delta_y_j == 0:\n",
297 | " # delta_y_j = 0.1\n",
298 | " #if delta_x_i == 0:\n",
299 | " # delta_x_i = 0.1\n",
300 | "\n",
301 | "\n",
302 | " x_A_j = (x_O_i*delta_x_i*delta_x_j + x_O_j*delta_y_i*delta_y_j + delta_x_j*delta_y_i*(y_O_i-y_O_j))/(delta_y_i*delta_y_j + delta_x_i*delta_x_j + EPS)\n",
303 | " if (delta_x_j != 0):\n",
304 | " y_A_j = (delta_y_j/delta_x_j)*(x_A_j - x_O_j) + y_O_j\n",
305 | " else:\n",
306 | " x_A_j = y_O_j\n",
307 | "\n",
308 | " x_B_j = (x_F_i*delta_x_i*delta_x_j + x_F_j*delta_y_i*delta_y_j + delta_x_j*delta_y_i*(y_F_i-y_F_j))/(delta_y_i*delta_y_j + delta_x_i*delta_x_j + EPS)\n",
309 | " if (delta_x_j != 0):\n",
310 | " y_B_j = (delta_y_j/delta_x_j)*(x_B_j - x_F_j) + y_F_j\n",
311 | " else:\n",
312 | " x_B_j = y_F_j\n",
313 | "\n",
314 | " # Find C and D ponts\n",
315 | " #x_middle_candidates = [x_O_j, x_F_j, x_A_j, x_B_j]\n",
316 | " #x_middle_candidates.sort()\n",
317 | " #y_middle_candidates = [y_O_j, y_F_j, y_A_j, y_B_j]\n",
318 | " #y_middle_candidates.sort()\n",
319 | " C_D_candidates = [(x_O_j,y_O_j), (x_F_j,y_F_j), (x_A_j,y_A_j), (x_B_j,y_B_j)]\n",
320 | " if (delta_x_j != 0):\n",
321 | " C_D_candidates.sort(key=lambda x:x[0]) # sort by x\n",
322 | " elif (delta_y_j != 0):\n",
323 | " C_D_candidates.sort(key=lambda x:x[1]) # sort by y\n",
324 | " x_C_j,y_C_j = C_D_candidates[1]\n",
325 | " x_D_j,y_D_j = C_D_candidates[2]\n",
326 | "\n",
327 | " if SHOW_DETAIL:\n",
328 | " print(\"x_A_j,y_A_j\",x_A_j,y_A_j)\n",
329 | " print(\"x_B_j,y_B_j\",x_B_j,y_B_j)\n",
330 | " print(\"x_C_j,y_C_j\",x_C_j,y_C_j)\n",
331 | " print(\"x_D_j,y_D_j\",x_D_j,y_D_j)\n",
332 | "\n",
333 | " #x_i_j_components = [int(x_O_i), int(x_F_i), int(x_O_j), int(x_F_j)]\n",
334 | " #x_i_j_components.sort()\n",
335 | " #y_i_j_components = [int(y_O_i), int(y_F_i), int(y_O_j), int(y_F_j)]\n",
336 | " #y_i_j_components.sort()\n",
337 | " # convert to int in order to allow generous overlap\n",
338 | " #if ((int(x_O_j) <= int(x_C_j) <= int(x_F_j) and (int(y_O_j) <= int(y_C_j) <= int(y_F_j) or int(y_F_j) <= int(y_C_j) <= int(y_O_j))) or (int(x_O_i) <= int(x_C_j) <= int(x_F_i) and (int(y_O_i) <= int(x_C_j) <= int(y_F_i) or int(y_F_i) <= int(y_C_j) <= int(y_O_i)))) and ((int(x_O_j) <= int(x_D_j) <= int(x_F_j) and (int(y_O_j) <= int(y_D_j) <= int(y_F_j) or int(y_F_j) <= int(y_D_j) <= int(y_O_j))) or (int(x_O_i) <= int(x_D_j) <= int(x_F_i) and (int(y_O_i) <= int(y_D_j) <= int(y_F_i) or int(y_F_i) <= int(y_D_j) <= int(y_O_i)))):\n",
339 | " #if ((x_i_j_components[0] <= int(x_C_j[0]) <= x_i_j_components[-1]) and (y_i_j_components[0] <= int(y_C_j) <= y_i_j_components[-1]) and (x_i_j_components[0] <= int(x_D_j) <= x_i_j_components[-1]) and (y_i_j_components[0] <= int(y_D_j) <= y_i_j_components[-1])):\n",
340 | " #convex_hull = MultiPoint([(x_O_j, y_O_j), (x_O_j, y_F_j), (x_F_j, y_F_j), (x_F_j, y_O_j)])\n",
341 | " #polygon = Polygon([(x_O_i, y_O_i), (x_F_i, y_F_i),(x_F_j, y_F_j), (x_O_j, y_O_j)])\n",
342 | " #convex_hull = MultiPoint([(x_O_i, y_O_i), (x_F_i, y_F_i),(x_F_j, y_F_j), (x_O_j, y_O_j)]).convex_hull\n",
343 | " polygon = Polygon([(x_O_j, y_O_j), (x_O_j, y_F_j), (x_F_j, y_F_j), (x_F_j, y_O_j)])\n",
344 | " C_point = Point(x_C_j, y_C_j)\n",
345 | " D_point = Point(x_D_j, y_D_j)\n",
346 | " #if polygon.area != convex_hull.area:\n",
347 | " # overlap = False\n",
348 | " #elif (convex_hull.contains(C_point) and convex_hull.contains(D_point)):\n",
349 | " #if (convex_hull.contains(C_point) and convex_hull.contains(D_point)):\n",
350 | " if (polygon.contains(C_point) or polygon.touches(C_point)) and (polygon.contains(D_point) or polygon.touches(D_point)):\n",
351 | " overlap = True\n",
352 | " else:\n",
353 | " overlap = False\n",
354 | " \n",
355 | " #p_j = (math.sqrt(math.pow(y_D_j-y_C_j,2)+math.pow(x_D_j-x_C_j,2)))/2.0\n",
356 | " p_j = math.sqrt(math.pow(y_D_j-y_C_j,2)+math.pow(x_D_j-x_C_j,2))\n",
357 | " l_j = math.sqrt(math.pow(y_F_j-y_O_j,2)+math.pow(x_F_j-x_O_j,2))\n",
358 | " if (l_j == 0):\n",
359 | " l_j = 0.1\n",
360 | " if overlap:\n",
361 | " p_i_j = p_j/l_j\n",
362 | " else:\n",
363 | " p_i_j = -p_j/l_j\n",
364 | "\n",
365 | " if SHOW_DETAIL:\n",
366 | " print(\"Overlap?\",overlap)\n",
367 | " print(\"p_j:\",p_j)\n",
368 | " print(\"p_i_j:\",p_i_j)\n",
369 | "\n",
370 | " # Calculate parallel_dist\n",
371 | " if overlap:\n",
372 | " d_i_j_a = p_j\n",
373 | " else:\n",
374 | " d_i_j_a = -p_j\n",
375 | " if SHOW_DETAIL:\n",
376 | " print(\"parallel_dist: \",d_i_j_a)\n",
377 | "\n",
378 | " # Calculate perpend_dist\n",
379 | " x_M_j = (x_C_j + x_D_j)/2.0\n",
380 | " y_M_j = (y_C_j + y_D_j)/2.0\n",
381 | " if SHOW_DETAIL:\n",
382 | " print(\"x_M_j,y_M_j\",x_M_j,y_M_j)\n",
383 | " print(\"delta_x_i:\",delta_x_i)\n",
384 | " print(\"delta_y_i:\",delta_y_i)\n",
385 | " print(\"delta_x_j:\",delta_x_j)\n",
386 | " print(\"delta_y_j:\",delta_y_j)\n",
387 | "\n",
388 | " if delta_x_i != 0.0 and delta_y_i != 0.0:\n",
389 | " d_e_i_j = ((x_M_j - x_O_i) - (y_M_j - y_O_i)*delta_x_i/(delta_y_i + EPS))/((delta_x_i**2)/(delta_y_i**2 + EPS) + 1)**0.5 \n",
390 | " elif delta_y_i == 0.0:\n",
391 | " d_e_i_j = int(y_M_j) - int(y_O_i)\n",
392 | " elif delta_x_i == 0.0:\n",
393 | " d_e_i_j = int(x_M_j) - int(x_O_i)\n",
394 | " d_e_i_j = abs(d_e_i_j)\n",
395 | "\n",
396 | " if SHOW_DETAIL:\n",
397 | " print(\"perpend_dist: \",d_e_i_j)\n",
398 | "\n",
399 | " ######################\n",
400 | " # DECIDING GROUPNESS #\n",
401 | " #######################\n",
402 | " # 1. angle check\n",
403 | " if theta_i_j < threshold_angle:\n",
404 | " if SHOW_DETAIL: print(\"... Angle ok!\")\n",
405 | " # 2. perpend_dist check\n",
406 | " if 0 < d_e_i_j < threshold_perpendist:\n",
407 | " if SHOW_DETAIL: print(\"... Perpendicular ok!\")\n",
408 | " # 3.a. overlap check\n",
409 | " # 3.b. parallel_dist check\n",
410 | " if ((overlap and p_i_j <= threshold_overlap)):\n",
411 | " if SHOW_DETAIL: print(\"... Overlap & p_i_j ok!\")\n",
412 | " # Group!\n",
413 | " sameGroup = True\n",
414 | " elif (abs(d_i_j_a) < threshold_paralldist):\n",
415 | " if SHOW_DETAIL: print(\"... Parallel ok!\")\n",
416 | " # Group!\n",
417 | " sameGroup = True\n",
418 | "\n",
419 | " if SHOW_DETAIL:\n",
420 | " print(\"same group? \",sameGroup)\n",
421 | " if sameGroup:\n",
422 | "\n",
423 | " if EARLY_SKIP:\n",
424 | " early_skip = threshold_early_skip\n",
425 | " if SHOW_DETAIL:\n",
426 | " print(\"before group idx: \",group_idx)\n",
427 | " print(\"before i's group: \", my_lines[i].group)\n",
428 | " print(\"before j's group: \", my_lines[j].group)\n",
429 | " if (my_lines[i].group == None) and (my_lines[j].group == None):\n",
430 | " if SHOW_DETAIL:\n",
431 | " print(\"... case 1\")\n",
432 | " # Assign to a new block\n",
433 | " group_idx = group_idx + 1\n",
434 | " my_lines[i].group = group_idx\n",
435 | " my_lines[j].group = group_idx\n",
436 | " #my_lines_no_group.remove(i) # update queue\n",
437 | " my_lines_in_group.append(i) # update queue\n",
438 | " my_lines_no_group.remove(j) # update queue\n",
439 | " my_lines_in_group.append(j) # update queue\n",
440 | " elif (my_lines[i].group == None):\n",
441 | " if SHOW_DETAIL: print(\"... case 2\")\n",
442 | " # Unassigned text-line is assigned to the block of the other\n",
443 | " my_lines[i].group = my_lines[j].group\n",
444 | " #my_lines_no_group.remove(i) # update queue\n",
445 | " my_lines_in_group.append(i) # update queue\n",
446 | " elif (my_lines[j].group == None):\n",
447 | " if SHOW_DETAIL: print(\"... case 3\")\n",
448 | " # Unassigned text-line is assigned to the block of the other\n",
449 | " my_lines[j].group = my_lines[i].group\n",
450 | " my_lines_no_group.remove(j) # update queue\n",
451 | " my_lines_in_group.append(j) # update queue\n",
452 | " if SHOW_DETAIL: print(\"after group idx: \",group_idx)\n",
453 | " if SHOW_DETAIL: print(\"after i's group: \", my_lines[i].group)\n",
454 | " if SHOW_DETAIL: print(\"after j's group: \", my_lines[j].group)\n",
455 | " if SHOW_VISUAL_STEP:\n",
456 | " cv2.line(image, ((my_lines[j].start.x,my_lines[j].start.y)),((my_lines[j].end.x,my_lines[j].end.y)), (0,255,0),threshold_visualize_line_width)\n",
457 | " page.display(image, title='Visualization of text-line groupping step')\n",
458 | " else:\n",
459 | " if EARLY_SKIP:\n",
460 | " early_skip = early_skip - 1\n",
461 | " \n",
462 | " if (my_lines[i].noise == False and my_lines[i].group == None):\n",
463 | " group_idx = group_idx + 1\n",
464 | " my_lines[i].group = group_idx\n",
465 | "bar.finish()\n",
466 | "\n",
467 | "print(\"Total iter: [%d/%d]\" %(act_loop,max_loop))\n",
468 | "print(\"Done!\")"
469 | ]
470 | },
471 | {
472 | "cell_type": "code",
473 | "execution_count": null,
474 | "metadata": {
475 | "collapsed": false
476 | },
477 | "outputs": [],
478 | "source": [
479 | "\n",
480 | "dist = [10,20,20,50,20,20,30,25,25,25,25,25,25]\n",
481 | "n, bins, patches = plt.hist(dist, numpy.max(dist)-numpy.min(dist)+1, facecolor='orange', alpha=0.5)\n",
482 | "n_copy = n.copy()\n",
483 | "print(n)\n",
484 | "print(n_copy)\n",
485 | "n_copy[::-1].sort()\n",
486 | "print(n)\n",
487 | "print(n_copy)\n",
488 | "print(bins)\n",
489 | "print(numpy.argmax(n)+numpy.min(dist))\n",
490 | "print(bins.max())\n",
491 | "\n",
492 | "#n_copy[::-1].sort()\n",
493 | "a = numpy.where(n == n_copy[2])\n",
494 | "print(\"Test\",a)\n",
495 | "\n",
496 | "if len(a[0])>1:\n",
497 | " _max = a[0][int(len(a[0])/2)]\n",
498 | "else:\n",
499 | " _max = a[0][0]\n",
500 | "\n",
501 | "_max+numpy.min(dist)\n",
502 | "\n",
503 | "for i in range(5):\n",
504 | " print(i)"
505 | ]
506 | },
507 | {
508 | "cell_type": "code",
509 | "execution_count": null,
510 | "metadata": {
511 | "collapsed": true
512 | },
513 | "outputs": [],
514 | "source": [
515 | "peakind = signal.find_peaks_cwt([10,20,20,50,20,20,30], np.arange(1,10))"
516 | ]
517 | },
518 | {
519 | "cell_type": "code",
520 | "execution_count": null,
521 | "metadata": {
522 | "collapsed": false
523 | },
524 | "outputs": [],
525 | "source": [
526 | "peakind"
527 | ]
528 | },
529 | {
530 | "cell_type": "code",
531 | "execution_count": null,
532 | "metadata": {
533 | "collapsed": false
534 | },
535 | "outputs": [],
536 | "source": [
537 | "x_O_i = 491\n",
538 | "y_O_i = 615\n",
539 | "x_F_i = 757\n",
540 | "y_F_i = 600\n",
541 | "x_O_j = 491\n",
542 | "y_O_j = 615\n",
543 | "x_F_j = 757\n",
544 | "y_F_j = 600\n",
545 | "x_C_j = 757\n",
546 | "y_C_j = int(637.6500942238861)\n",
547 | "\n",
548 | "delta_y_j = 7.0\n",
549 | "delta_x_j = 658.0\n",
550 | "x_A_j = 4144.4503916449075\n",
551 | "x_F_j = 3890.0\n",
552 | "y_F_j = 1856.0\n",
553 | "(delta_y_j/delta_x_j)*(x_A_j - x_F_j) + y_F_j\n"
554 | ]
555 | },
556 | {
557 | "cell_type": "code",
558 | "execution_count": null,
559 | "metadata": {
560 | "collapsed": false
561 | },
562 | "outputs": [],
563 | "source": [
564 | "('i:', 2639, 4875, '-', 2676, 4872)\n",
565 | "('j:', 992, 4892, '-', 2222, 4879)\n",
566 | "('Angle:', -0.010568017106413556)\n",
567 | "('x_A_j,y_A_j', 2638.9670025686564, 4874.593031680169)\n",
568 | "('x_B_j,y_B_j', 2676.178357373372, 4874.19974093833)\n",
569 | "('x_C_j,y_C_j', 2222, 4879)\n",
570 | "('x_D_j,y_D_j', 2638.9670025686564, 4874.593031680169)\n",
571 | "\n",
572 | "x_O_i = 2639\n",
573 | "y_O_i = 4875\n",
574 | "x_F_i = 2676\n",
575 | "y_F_i = 4872\n",
576 | "\n",
577 | "x_O_j = 896\n",
578 | "y_O_j = 4802\n",
579 | "x_F_j = 1415\n",
580 | "y_F_j = 4805\n",
581 | "\n",
582 | "x_C_j = 2222\n",
583 | "y_C_j = 4879\n",
584 | "x_D_j = 2638#.9670025686564\n",
585 | "y_D_j = 4874#.593031680169\n",
586 | "\n",
587 | "\n",
588 | "#polygon = Polygon([(x_O_i-1, y_O_i+1), (x_F_i+1, y_F_i+1),(x_F_j+1, y_F_j-1), (x_O_j-1, y_O_j-1)])\n",
589 | "polygon = Polygon([(x_O_i, y_O_i), (x_F_i, y_F_i),(x_F_j, y_F_j), (x_O_j, y_O_j)])\n",
590 | "#C_point = Point(x_C_j, y_C_j)\n",
591 | "#D_point = Point(x_D_j, y_D_j)\n",
592 | "polygon.area"
593 | ]
594 | },
595 | {
596 | "cell_type": "code",
597 | "execution_count": null,
598 | "metadata": {
599 | "collapsed": false
600 | },
601 | "outputs": [],
602 | "source": [
603 | "from shapely.geometry import MultiPoint\n",
604 | "convex_hull = MultiPoint([(x_O_i, y_O_i), (x_F_i, y_F_i),(x_F_j, y_F_j), (x_O_j, y_O_j)]).convex_hull\n",
605 | "convex_hull.area"
606 | ]
607 | },
608 | {
609 | "cell_type": "code",
610 | "execution_count": null,
611 | "metadata": {
612 | "collapsed": false
613 | },
614 | "outputs": [],
615 | "source": [
616 | "x_O_j = 2476\n",
617 | "y_O_j = 1938\n",
618 | "x_F_j = 2840\n",
619 | "y_F_j = 1932\n",
620 | "x_A_j = 2363\n",
621 | "y_A_j = 1939\n",
622 | "x_B_j = 2631\n",
623 | "y_B_j = 1935\n",
624 | "\n",
625 | "C_D_candidates = [(x_O_j,y_O_j), (x_F_j,y_F_j), (x_A_j,y_A_j), (x_B_j,y_B_j)]\n",
626 | "C_D_candidates.sort(key=lambda x:x[0])\n",
627 | "print(C_D_candidates[1])\n",
628 | "print(C_D_candidates[2])\n"
629 | ]
630 | },
631 | {
632 | "cell_type": "code",
633 | "execution_count": null,
634 | "metadata": {
635 | "collapsed": false
636 | },
637 | "outputs": [],
638 | "source": [
639 | "C_D_candidates"
640 | ]
641 | },
642 | {
643 | "cell_type": "code",
644 | "execution_count": null,
645 | "metadata": {
646 | "collapsed": false
647 | },
648 | "outputs": [],
649 | "source": [
650 | "C_D_candidates"
651 | ]
652 | },
653 | {
654 | "cell_type": "code",
655 | "execution_count": null,
656 | "metadata": {
657 | "collapsed": false
658 | },
659 | "outputs": [],
660 | "source": [
661 | "C_x,C_y = C_D_candidates[1]\n",
662 | "print(C_x)\n",
663 | "print(C_y)\n"
664 | ]
665 | },
666 | {
667 | "cell_type": "code",
668 | "execution_count": null,
669 | "metadata": {
670 | "collapsed": false
671 | },
672 | "outputs": [],
673 | "source": [
674 | "Polygon([(0-1, y_O_i+1), (x_F_i+1, y_F_i+1),(x_F_j+1, y_F_j-1), (x_O_j-1, y_O_j-1)])"
675 | ]
676 | },
677 | {
678 | "cell_type": "code",
679 | "execution_count": null,
680 | "metadata": {
681 | "collapsed": true
682 | },
683 | "outputs": [],
684 | "source": []
685 | },
686 | {
687 | "cell_type": "code",
688 | "execution_count": null,
689 | "metadata": {
690 | "collapsed": false
691 | },
692 | "outputs": [],
693 | "source": [
694 | "######################\n",
695 | "# Draw Grouped Lines #\n",
696 | "######################\n",
697 | "import cv2\n",
698 | "image = page.image.copy()\n",
699 | "for my_line in my_lines:\n",
700 | " #print(my_line.start.x)\n",
701 | "# print my_line.group\n",
702 | " if my_line.group == None:\n",
703 | " continue\n",
704 | " blue = 0\n",
705 | " green = 0 \n",
706 | " red = 0\n",
707 | " else:\n",
708 | " blue = (my_line.group*100)%255\n",
709 | " green = (my_line.group*200)%255\n",
710 | " red = (my_line.group*300)%255\n",
711 | " \n",
712 | " #print(blue,green,red)\n",
713 | " cv2.line(image, (my_line.start.x,my_line.start.y), (my_line.end.x,my_line.end.y), (blue,green,red),10)\n",
714 | " \n",
715 | "#cv2.imwrite(outputPath, image) \n",
716 | "maxDimension = Dimension(800, 800)\n",
717 | "displayDimension = Dimension(image.shape[1], image.shape[0])\n",
718 | "displayDimension.fitInside(maxDimension)\n",
719 | "image = cv2.resize(image, tuple(displayDimension))\n",
720 | "cv2.namedWindow('title', cv2.CV_WINDOW_AUTOSIZE)\n",
721 | "cv2.imshow('grouped', image)\n",
722 | "cv2.waitKey()\n",
723 | "cv2.destroyAllWindows()"
724 | ]
725 | },
726 | {
727 | "cell_type": "code",
728 | "execution_count": null,
729 | "metadata": {
730 | "collapsed": false
731 | },
732 | "outputs": [],
733 | "source": [
734 | "#######################\n",
735 | "# Draw Bounding Boxes #\n",
736 | "#######################\n",
737 | "import cv2\n",
738 | "import numpy\n",
739 | "THRESHOLD_POLY_EXAGGERATE = 10 # Unit: Pixel\n",
740 | "image = page.image.copy()\n",
741 | "tot_groups = group_idx+1\n",
742 | "group_table = []\n",
743 | "for group_idx in range(tot_groups):\n",
744 | " group_table.append([])\n",
745 | "\n",
746 | "for my_line in my_lines:\n",
747 | " for group_idx in range(1,tot_groups):\n",
748 | " if my_line.group == None:\n",
749 | " continue\n",
750 | " elif my_line.group == group_idx:\n",
751 | " exaggerated_left_start_x = my_line.start.x-THRESHOLD_POLY_EXAGGERATE\n",
752 | " exaggerated_up_start_y = my_line.start.y+THRESHOLD_POLY_EXAGGERATE\n",
753 | " exaggerated_down_start_y = my_line.start.y-THRESHOLD_POLY_EXAGGERATE\n",
754 | " \n",
755 | " exaggerated_right_end_x = my_line.end.x+THRESHOLD_POLY_EXAGGERATE\n",
756 | " exaggerated_up_end_y = my_line.end.y+THRESHOLD_POLY_EXAGGERATE\n",
757 | " exaggerated_down_end_y = my_line.end.y-THRESHOLD_POLY_EXAGGERATE\n",
758 | " \n",
759 | " group_table[group_idx-1].append([exaggerated_left_start_x,exaggerated_up_start_y])\n",
760 | " group_table[group_idx-1].append([exaggerated_left_start_x,exaggerated_down_start_y])\n",
761 | " \n",
762 | " group_table[group_idx-1].append([exaggerated_right_end_x,exaggerated_up_end_y])\n",
763 | " group_table[group_idx-1].append([exaggerated_right_end_x,exaggerated_down_end_y])\n",
764 | " \n",
765 | " \n",
766 | "\n",
767 | "for group_idx in range(1,tot_groups):\n",
768 | " points = numpy.array(group_table[group_idx-1], dtype='int')\n",
769 | " rect = cv2.minAreaRect(points)\n",
770 | " box = cv2.cv.BoxPoints(rect) # cv2.boxPoints(rect) for OpenCV 3.x\n",
771 | " box = numpy.int0(box)\n",
772 | " cv2.drawContours(image,numpy.int32([box]),0,(0,0,255),7)\n",
773 | " #convex_hull = cv2.convexHull(points)\n",
774 | " #cv2.polylines(image, numpy.int32([convex_hull]), True, (0, 0, 255), thickness=2)\n",
775 | "\n",
776 | "\n",
777 | "maxDimension = Dimension(800, 800)\n",
778 | "displayDimension = Dimension(image.shape[1], image.shape[0])\n",
779 | "displayDimension.fitInside(maxDimension)\n",
780 | "image = cv2.resize(image, tuple(displayDimension))\n",
781 | "cv2.namedWindow('Polylines', cv2.CV_WINDOW_AUTOSIZE)\n",
782 | "cv2.imshow('Polylines', image)\n",
783 | "cv2.waitKey()\n",
784 | "cv2.destroyAllWindows()\n"
785 | ]
786 | },
787 | {
788 | "cell_type": "code",
789 | "execution_count": null,
790 | "metadata": {
791 | "collapsed": false
792 | },
793 | "outputs": [],
794 | "source": [
795 | "cv2.imwrite(outputPath, image)"
796 | ]
797 | },
798 | {
799 | "cell_type": "code",
800 | "execution_count": null,
801 | "metadata": {
802 | "collapsed": false
803 | },
804 | "outputs": [],
805 | "source": [
806 | "group_table"
807 | ]
808 | },
809 | {
810 | "cell_type": "code",
811 | "execution_count": null,
812 | "metadata": {
813 | "collapsed": false
814 | },
815 | "outputs": [],
816 | "source": [
817 | "cv2.imwrite(outputPath, image)"
818 | ]
819 | },
820 | {
821 | "cell_type": "code",
822 | "execution_count": null,
823 | "metadata": {
824 | "collapsed": false
825 | },
826 | "outputs": [],
827 | "source": [
828 | "################################\n",
829 | "# Draw BoundingBox (Rectangle) #\n",
830 | "################################\n",
831 | "import cv2\n",
832 | "import numpy\n",
833 | "image = page.image.copy()\n",
834 | "boundingbox_table = numpy.zeros((group_idx+1,4)) # [min_x,max_x,min_y,max_y]\n",
835 | "boundingbox_table[:,0] = image.shape[1]\n",
836 | "boundingbox_table[:,1] = 0\n",
837 | "boundingbox_table[:,2] = image.shape[0]\n",
838 | "boundingbox_table[:,3] = 0\n",
839 | "\n",
840 | "# Find BoundingBoxes for Each Group\n",
841 | "for my_line in my_lines:\n",
842 | " for i in range(1,group_idx+1):\n",
843 | " if my_line.group == None:\n",
844 | " # Update if found new min or max\n",
845 | " if my_line.start.x < boundingbox_table[-1,0]:\n",
846 | " boundingbox_table[-1,0] = my_line.start.x\n",
847 | " if my_line.end.x > boundingbox_table[-1,1]:\n",
848 | " boundingbox_table[-1,1] = my_line.end.x\n",
849 | " if my_line.start.y < boundingbox_table[-1,2]:\n",
850 | " boundingbox_table[-1,2] = my_line.start.y\n",
851 | " if my_line.end.y > boundingbox_table[-1,3]:\n",
852 | " boundingbox_table[-1,3] = my_line.end.y\n",
853 | " elif my_line.group == i:\n",
854 | " # Update if found new min or max\n",
855 | " if my_line.start.x < boundingbox_table[i-1,0]:\n",
856 | " boundingbox_table[i-1,0] = my_line.start.x\n",
857 | " if my_line.end.x > boundingbox_table[i-1,1]:\n",
858 | " boundingbox_table[i-1,1] = my_line.end.x\n",
859 | " if my_line.start.y < boundingbox_table[i-1,2]:\n",
860 | " boundingbox_table[i-1,2] = my_line.start.y\n",
861 | " if my_line.end.y > boundingbox_table[i-1,3]:\n",
862 | " boundingbox_table[i-1,3] = my_line.end.y\n",
863 | " \n",
864 | "# Draw BoundingBoxes \n",
865 | "for i in range(group_idx+1):\n",
866 | " x_min = int(boundingbox_table[i,0])\n",
867 | " x_max = int(boundingbox_table[i,1])\n",
868 | " y_min = int(boundingbox_table[i,2])\n",
869 | " y_max = int(boundingbox_table[i,3])\n",
870 | " cv2.rectangle(image,(x_min,y_max),(x_max,y_min),(0,0,255),5) # (image, Top-Left, Bottom-Right, BGR_Color, Width)\n",
871 | "\n",
872 | "\n",
873 | "maxDimension = Dimension(800, 800)\n",
874 | "displayDimension = Dimension(image.shape[1], image.shape[0])\n",
875 | "displayDimension.fitInside(maxDimension)\n",
876 | "image = cv2.resize(image, tuple(displayDimension))\n",
877 | "cv2.namedWindow('title', cv2.CV_WINDOW_AUTOSIZE)\n",
878 | "cv2.imshow('grouped', image)\n",
879 | "cv2.waitKey()\n",
880 | "cv2.destroyAllWindows()"
881 | ]
882 | },
883 | {
884 | "cell_type": "code",
885 | "execution_count": null,
886 | "metadata": {
887 | "collapsed": false
888 | },
889 | "outputs": [],
890 | "source": [
891 | "cv2.imwrite(outputPath, image)"
892 | ]
893 | },
894 | {
895 | "cell_type": "code",
896 | "execution_count": null,
897 | "metadata": {
898 | "collapsed": false,
899 | "scrolled": true
900 | },
901 | "outputs": [],
902 | "source": [
903 | "############################\n",
904 | "############################\n",
905 | "############################\n",
906 | "############################\n",
907 | "#### LEGACY CODES BELOW ####\n",
908 | "############################\n",
909 | "############################\n",
910 | "############################\n",
911 | "############################"
912 | ]
913 | },
914 | {
915 | "cell_type": "code",
916 | "execution_count": null,
917 | "metadata": {
918 | "collapsed": false
919 | },
920 | "outputs": [],
921 | "source": [
922 | "#WORKING OLD VERSION: BUT CLUSMY RESULT\n",
923 | "#from __future__ import division\n",
924 | "#i=0\n",
925 | "#j=5\n",
926 | "\n",
927 | "SHOW_DETAIL = True\n",
928 | "SHOW_DETAIL = True\n",
929 | "\n",
930 | "# Sorting lines\n",
931 | "my_lines = page.lines\n",
932 | "my_lines.sort(key=lambda line:((line.start.y+line.end.y)/2,(line.start.x+line.end.x)/2))\n",
933 | "#my_lines[0].start.x = 2\n",
934 | "#my_lines[0].start.y = 0\n",
935 | "#my_lines[0].end.x = 6\n",
936 | "#my_lines[0].end.y = 0\n",
937 | "\n",
938 | "#my_lines[5].start.x = 1\n",
939 | "#my_lines[5].start.y = 1\n",
940 | "#my_lines[5].end.x = 5\n",
941 | "#my_lines[5].end.y = 3\n",
942 | "\n",
943 | "#my_lines[0].group = None\n",
944 | "#my_lines[5].group = None\n",
945 | "\n",
946 | "EPS = 3#1e-3\n",
947 | "group_idx = 0\n",
948 | "threshold_angle = 1.0\n",
949 | "threshold_perpendist = 1.3 * 60.0\n",
950 | "threshold_overlap = 1.0\n",
951 | "threshold_paralldist = 1.5 * 40.0\n",
952 | "\n",
953 | "for idx_my_line, my_line in enumerate(my_lines):\n",
954 | " if(idx_my_line+1 == len(my_lines)-1):\n",
955 | " break\n",
956 | " i = idx_my_line\n",
957 | " for j in range(i+1,len(my_lines)-1):\n",
958 | " #for j in range(i+1,30):\n",
959 | " sameGroup = False\n",
960 | " ################################\n",
961 | " # CALCULATE GEOMETRIC FEATURES #\n",
962 | " ################################\n",
963 | " # Point setting\n",
964 | " x_O_i = my_lines[i].start.x\n",
965 | " #y_O_i = my_lines[i].start.y\n",
966 | " y_O_i = page.image.shape[0] - my_lines[i].start.y\n",
967 | " x_F_i = my_lines[i].end.x\n",
968 | " #y_F_i = my_lines[i].end.y\n",
969 | " y_F_i = page.image.shape[0] - my_lines[i].end.y \n",
970 | "\n",
971 | " x_O_j = my_lines[j].start.x\n",
972 | " #y_O_j = my_lines[j].start.y\n",
973 | " y_O_j = page.image.shape[0] - my_lines[j].start.y\n",
974 | " x_F_j = my_lines[j].end.x\n",
975 | " #y_F_j = my_lines[j].end.y\n",
976 | " y_F_j = page.image.shape[0] - my_lines[j].end.y\n",
977 | " \n",
978 | " delta_x_i = abs(x_F_i - x_O_i)\n",
979 | " delta_y_i = abs(y_F_i - y_O_i)\n",
980 | " delta_x_j = abs(x_F_j - x_O_j)\n",
981 | " delta_y_j = abs(y_F_j - y_O_j)\n",
982 | " \n",
983 | " # ith or jth line is dot, so skip it\n",
984 | " if ((delta_x_i == 0 and delta_y_i == 0) or (delta_x_j == 0 and delta_y_j == 0)):\n",
985 | " continue\n",
986 | " \n",
987 | " if SHOW_DETAIL:\n",
988 | " print(\"\\n****************************************************************\")\n",
989 | " print(i, my_lines[i].points)\n",
990 | " print(j, my_lines[j].points)\n",
991 | " print(\"i:\",x_O_i,y_O_i,\"-\",x_F_i,y_F_i)\n",
992 | " print(\"j:\",x_O_j,y_O_j,\"-\",x_F_j,y_F_j)\n",
993 | " \n",
994 | " # Calculate angle\n",
995 | " theta_i_j = math.atan2(delta_y_j,delta_x_j-math.atan2(delta_y_i,delta_x_i))\n",
996 | " if SHOW_DETAIL:\n",
997 | " print(\"Angle:\",theta_i_j)\n",
998 | "\n",
999 | " # Calculate overlap\n",
1000 | " #if delta_x_j == 0:\n",
1001 | " # delta_x_j = 0.1\n",
1002 | " #if delta_y_i == 0:\n",
1003 | " # delta_y_i = 0.1\n",
1004 | " #if delta_y_j == 0:\n",
1005 | " # delta_y_j = 0.1\n",
1006 | " #if delta_x_i == 0:\n",
1007 | " # delta_x_i = 0.1\n",
1008 | "\n",
1009 | "\n",
1010 | " x_A_j = (x_O_i*delta_x_i*delta_x_j + x_O_j*delta_y_i*delta_y_j + delta_x_j*delta_y_i*(y_O_i-y_O_j))/(delta_y_i*delta_y_j + delta_x_i*delta_x_j + EPS)\n",
1011 | " if (delta_x_j != 0):\n",
1012 | " y_A_j = (delta_y_j/delta_x_j)*(x_A_j - x_O_j) + y_O_j\n",
1013 | " else:\n",
1014 | " x_A_j = y_O_j\n",
1015 | "\n",
1016 | " x_B_j = (x_F_i*delta_x_i*delta_x_j + x_F_j*delta_y_i*delta_y_j + delta_x_j*delta_y_i*(y_F_i-y_F_j))/(delta_y_i*delta_y_j + delta_x_i*delta_x_j + EPS)\n",
1017 | " if (delta_x_j != 0):\n",
1018 | " y_B_j = (delta_y_j/delta_x_j)*(x_A_j - x_F_j) + y_F_j\n",
1019 | " else:\n",
1020 | " x_B_j = y_F_j\n",
1021 | "\n",
1022 | " x_middle_candidates = [x_O_j, x_F_j, x_A_j, x_B_j]\n",
1023 | " x_middle_candidates.sort()\n",
1024 | " y_middle_candidates = [y_O_j, y_F_j, y_A_j, y_B_j]\n",
1025 | " y_middle_candidates.sort()\n",
1026 | "\n",
1027 | " x_C_j = x_middle_candidates[-2]\n",
1028 | " y_C_j = y_middle_candidates[-2]\n",
1029 | "\n",
1030 | " x_D_j = x_middle_candidates[-3]\n",
1031 | " y_D_j = y_middle_candidates[-3]\n",
1032 | " if SHOW_DETAIL:\n",
1033 | " print(\"x_A_j,y_A_j\",x_A_j,y_A_j)\n",
1034 | " print(\"x_B_j,y_B_j\",x_B_j,y_B_j)\n",
1035 | " print(\"x_C_j,y_C_j\",x_C_j,y_C_j)\n",
1036 | " print(\"x_D_j,y_D_j\",x_D_j,y_D_j)\n",
1037 | "\n",
1038 | " if ((x_O_j <= x_C_j <= x_F_j and y_O_j <= y_C_j <= y_F_j) or (x_O_i <= x_C_j <= x_F_i and y_O_i <= x_C_j <= y_F_i)) and ((x_O_j <= x_D_j <= x_F_j and y_O_j <= y_D_j <= y_F_j) or (x_O_i <= x_D_j <= x_F_i and y_O_i <= y_D_j <= y_F_i)):\n",
1039 | " overlap = True\n",
1040 | " else:\n",
1041 | " overlap = False\n",
1042 | " # Force to be true; no overlap is required in the default mode\n",
1043 | " #overlap = True\n",
1044 | "\n",
1045 | " p_j = (math.sqrt(math.pow(y_D_j-y_C_j,2)+math.pow(x_D_j-x_C_j,2)))/2.0\n",
1046 | " l_j = math.sqrt(math.pow(y_F_j-y_O_j,2)+math.pow(x_F_j-x_O_j,2))\n",
1047 | " if (l_j == 0):\n",
1048 | " l_j = 0.1\n",
1049 | " if overlap:\n",
1050 | " p_i_j = p_j/l_j\n",
1051 | " else:\n",
1052 | " p_i_j = -p_j/l_j\n",
1053 | " \n",
1054 | " if SHOW_DETAIL:\n",
1055 | " print(\"Overlap?\",overlap)\n",
1056 | " print(\"p_j:\",p_j)\n",
1057 | " print(\"p_i_j:\",p_i_j)\n",
1058 | "\n",
1059 | " # Calculate parallel_dist\n",
1060 | " if overlap:\n",
1061 | " d_i_j_a = p_j\n",
1062 | " else:\n",
1063 | " d_i_j_a = -p_j\n",
1064 | " if SHOW_DETAIL:\n",
1065 | " print(\"parallel_dist: \",d_i_j_a)\n",
1066 | "\n",
1067 | " # Calculate perpend_dist\n",
1068 | " x_M_j = (x_C_j + x_D_j)/2.0\n",
1069 | " y_M_j = (y_C_j + y_D_j)/2.0\n",
1070 | " if SHOW_DETAIL:\n",
1071 | " print(\"x_M_j,y_M_j\",x_M_j,y_M_j)\n",
1072 | " print(\"delta_x_i:\",delta_x_i)\n",
1073 | " print(\"delta_y_i:\",delta_y_i)\n",
1074 | " print(\"delta_x_j:\",delta_x_j)\n",
1075 | " print(\"delta_y_j:\",delta_y_j)\n",
1076 | " \n",
1077 | " if delta_x_i != 0.0 and delta_x_i != 0.0:\n",
1078 | " d_e_i_j = ((x_M_j - x_O_i) - (y_M_j - y_O_i)*delta_x_i/(delta_y_i + EPS))/((delta_x_i**2)/(delta_y_i**2 + EPS) + 1)**0.5\n",
1079 | " #((x_M_j - x_O_i) - (y_M_j - y_O_i)*delta_x_i/(delta_y_i + EPS))/(math.pow(math.pow(delta_x_i,2)/math.pow(delta_y_i,2)+1,0.5) + EPS)\n",
1080 | " elif delta_y_i == 0.0:\n",
1081 | " d_e_i_j = y_M_j - y_O_i \n",
1082 | " elif delta_x_i == 0.0:\n",
1083 | " d_e_i_j = x_M_j - x_O_i\n",
1084 | " d_e_i_j = abs(d_e_i_j)\n",
1085 | " \n",
1086 | " if SHOW_DETAIL:\n",
1087 | " print(\"perpend_dist: \",d_e_i_j)\n",
1088 | "\n",
1089 | " ######################\n",
1090 | " # DECIDING GROUPNESS #\n",
1091 | " #######################\n",
1092 | " # 1. angle check\n",
1093 | " if theta_i_j < threshold_angle:\n",
1094 | " if SHOW_DETAIL: print(\"... Angle ok!\")\n",
1095 | " # 2. perpend_dist check\n",
1096 | " if 0 < d_e_i_j < threshold_perpendist:\n",
1097 | " if SHOW_DETAIL: print(\"... Perpendicular ok!\")\n",
1098 | " # 3.a. overlap check\n",
1099 | " # 3.b. parallel_dist check\n",
1100 | " if ((overlap and p_i_j < threshold_overlap)):\n",
1101 | " if SHOW_DETAIL: print(\"... Overlap & p_i_j ok!\")\n",
1102 | " # Group!\n",
1103 | " sameGroup = True\n",
1104 | " elif (abs(d_i_j_a) < threshold_paralldist):\n",
1105 | " if SHOW_DETAIL: print(\"... Parallel ok!\")\n",
1106 | " # Group!\n",
1107 | " sameGroup = True\n",
1108 | " \n",
1109 | " if SHOW_DETAIL:\n",
1110 | " print(\"same group? \",sameGroup)\n",
1111 | " if sameGroup:\n",
1112 | " if SHOW_DETAIL:\n",
1113 | " print(\"before group idx: \",group_idx)\n",
1114 | " print(\"before i's group: \", my_lines[i].group)\n",
1115 | " print(\"before j's group: \", my_lines[j].group)\n",
1116 | " if (my_lines[i].group == None) and (my_lines[j].group == None):\n",
1117 | " if SHOW_DETAIL:\n",
1118 | " print(\"... case 1\")\n",
1119 | " # Assign to a new block\n",
1120 | " group_idx = group_idx + 1\n",
1121 | " my_lines[i].group = group_idx\n",
1122 | " my_lines[j].group = group_idx\n",
1123 | " elif (my_lines[i].group == None):\n",
1124 | " if SHOW_DETAIL: print(\"... case 2\")\n",
1125 | " # Unassigned text-line is assigned to the block of the other\n",
1126 | " my_lines[i].group = my_lines[j].group\n",
1127 | " elif (my_lines[j].group == None):\n",
1128 | " if SHOW_DETAIL: print(\"... case 3\")\n",
1129 | " # Unassigned text-line is assigned to the block of the other\n",
1130 | " my_lines[j].group = my_lines[i].group\n",
1131 | " if SHOW_DETAIL: print(\"after group idx: \",group_idx)\n",
1132 | " if SHOW_DETAIL: print(\"after i's group: \", my_lines[i].group)\n",
1133 | " if SHOW_DETAIL: print(\"after j's group: \", my_lines[j].group)\n",
1134 | " #else:\n",
1135 | " # Block merge\n",
1136 | "\n",
1137 | "print(\"Done!\")"
1138 | ]
1139 | },
1140 | {
1141 | "cell_type": "code",
1142 | "execution_count": null,
1143 | "metadata": {
1144 | "collapsed": true
1145 | },
1146 | "outputs": [],
1147 | "source": [
1148 | "##############################\n",
1149 | "# Draw BoundingBox (Polygon) #\n",
1150 | "##############################\n",
1151 | "import cv2\n",
1152 | "import numpy\n",
1153 | "image = page.image.copy()\n",
1154 | "# point_0 (x_0, y_0) x_0:* y_0:Max\n",
1155 | "# point_1 (x_1, y_1) x_1:Max y_1:*\n",
1156 | "# point_2 (x_2, y_2) x_2:* y_2:Min\n",
1157 | "# point_3 (x_3, y_3) x_3:Min y_3:*\n",
1158 | "boundingbox_table = numpy.zeros((group_idx+1,8)) # [x_0,y_0,x_1,y_1,x_2,y_2,x_3,y_3]\n",
1159 | "boundingbox_table[:,0] = 0 # x_0\n",
1160 | "boundingbox_table[:,1] = 0 # y_0\n",
1161 | "boundingbox_table[:,2] = 0 # x_1\n",
1162 | "boundingbox_table[:,3] = 0 # y_1\n",
1163 | "boundingbox_table[:,4] = 0 # x_2\n",
1164 | "boundingbox_table[:,5] = image.shape[0] # y_2\n",
1165 | "boundingbox_table[:,6] = image.shape[1] # x_3\n",
1166 | "boundingbox_table[:,7] = 0 # y_3\n",
1167 | "\n",
1168 | "# Find BoundingBoxes for Each Group\n",
1169 | "for my_line in my_lines:\n",
1170 | " for i in range(1,group_idx+1):\n",
1171 | " \n",
1172 | " \n",
1173 | " if my_line.group == i:\n",
1174 | " # Update if found new min or max\n",
1175 | " if my_line.start.y > boundingbox_table[i-1,1]:\n",
1176 | " boundingbox_table[i-1,0] = my_line.start.x\n",
1177 | " boundingbox_table[i-1,1] = my_line.start.y\n",
1178 | " if my_line.end.y > boundingbox_table[i-1,1]:\n",
1179 | " boundingbox_table[i-1,0] = my_line.end.x\n",
1180 | " boundingbox_table[i-1,1] = my_line.end.y\n",
1181 | " \n",
1182 | " if my_line.start.x > boundingbox_table[i-1,2]:\n",
1183 | " boundingbox_table[i-1,2] = my_line.start.x\n",
1184 | " boundingbox_table[i-1,3] = my_line.start.y\n",
1185 | " if my_line.end.y > boundingbox_table[i-1,2]:\n",
1186 | " boundingbox_table[i-1,2] = my_line.end.x\n",
1187 | " boundingbox_table[i-1,3] = my_line.end.y\n",
1188 | " \n",
1189 | " if my_line.start.y < boundingbox_table[i-1,5]:\n",
1190 | " boundingbox_table[i-1,4] = my_line.start.x\n",
1191 | " boundingbox_table[i-1,5] = my_line.start.y\n",
1192 | " if my_line.end.y < boundingbox_table[i-1,5]:\n",
1193 | " boundingbox_table[i-1,4] = my_line.end.x\n",
1194 | " boundingbox_table[i-1,5] = my_line.end.y\n",
1195 | " \n",
1196 | " if my_line.start.x < boundingbox_table[i-1,6]:\n",
1197 | " boundingbox_table[i-1,6] = my_line.start.x\n",
1198 | " boundingbox_table[i-1,7] = my_line.start.y\n",
1199 | " if my_line.end.x < boundingbox_table[i-1,6]:\n",
1200 | " boundingbox_table[i-1,6] = my_line.end.x\n",
1201 | " boundingbox_table[i-1,7] = my_line.end.y\n",
1202 | "\n",
1203 | "# Draw BoundingBoxes \n",
1204 | "for i in range(group_idx+1):\n",
1205 | " x_0 = int(boundingbox_table[i,0])\n",
1206 | " y_0 = int(boundingbox_table[i,1])\n",
1207 | " x_1 = int(boundingbox_table[i,2])\n",
1208 | " y_1 = int(boundingbox_table[i,3])\n",
1209 | " x_2 = int(boundingbox_table[i,4])\n",
1210 | " y_2 = int(boundingbox_table[i,5])\n",
1211 | " x_3 = int(boundingbox_table[i,6])\n",
1212 | " y_3 = int(boundingbox_table[i,7])\n",
1213 | " \n",
1214 | " pts = numpy.array([[x_0,y_0],[x_1,y_1],[x_2,y_2],[x_3,y_3]], numpy.int32)\n",
1215 | " pts = pts.reshape((-1,1,2))\n",
1216 | " cv2.polylines(image,[pts],True,(0,0,255),5)\n",
1217 | "\n",
1218 | "maxDimension = Dimension(800, 800)\n",
1219 | "displayDimension = Dimension(image.shape[1], image.shape[0])\n",
1220 | "displayDimension.fitInside(maxDimension)\n",
1221 | "image = cv2.resize(image, tuple(displayDimension))\n",
1222 | "cv2.namedWindow('title', cv2.CV_WINDOW_AUTOSIZE)\n",
1223 | "cv2.imshow('grouped', image)\n",
1224 | "cv2.waitKey()\n",
1225 | "cv2.destroyAllWindows()"
1226 | ]
1227 | }
1228 | ],
1229 | "metadata": {
1230 | "kernelspec": {
1231 | "display_name": "Python 2",
1232 | "language": "python",
1233 | "name": "python2"
1234 | },
1235 | "language_info": {
1236 | "codemirror_mode": {
1237 | "name": "ipython",
1238 | "version": 2
1239 | },
1240 | "file_extension": ".py",
1241 | "mimetype": "text/x-python",
1242 | "name": "python",
1243 | "nbconvert_exporter": "python",
1244 | "pygments_lexer": "ipython2",
1245 | "version": "2.7.13"
1246 | }
1247 | },
1248 | "nbformat": 4,
1249 | "nbformat_minor": 2
1250 | }
1251 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Docstrum Algorithm
3 | ## Getting Started
4 | This repo is for developing a Docstrum algorithm presented by O’Gorman (1993).
5 |
6 | ## Disclaimer
7 | This source code is built on top of the work by Chadoliver. Please find the original code from here (https://github.com/chadoliver/cosc428-structor).
8 |
9 | ## Objective
10 | This project aims at segmenting a document image into meaningful components. The domain of image is specified on historical machine-printed/hand-written document image.
11 |
12 | ## Dependencies
13 | - python 2.7
14 | - Packages:
15 | - `numpy`
16 | - `cv2`
17 |
18 | ## Process
19 |
20 |
21 | - Pre-processing [Optional for vertical-line removal](https://docs.opencv.org/3.2.0/d1/dee/tutorial_moprh_lines_detection.html)
22 | - Blurring [Bilateral Filtering](https://en.wikipedia.org/wiki/Bilateral_filter)
23 | - [Otsu's thresholding](https://en.wikipedia.org/wiki/Otsu%27s_method)
24 | - Morphological [erosion & dilation](https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_imgproc/py_morphological_ops/py_morphological_ops.html)
25 | - Smoothing (Averaging)
26 | - Static thresholding
27 | - Nearest-Neighbor Clustering and Docstrum Plot
28 | - Spacing and Orientation Estimation
29 | - Determination of Text-lines
30 | - Structural Block Determination
31 | - Post-processing
32 | - TBD
33 |
34 | ## Evaluation
35 | - TBD
36 |
37 | ## Citing Docstrum
38 | O'Gorman, L., 1993. The document spectrum for page layout analysis. IEEE Transactions on Pattern Analysis and Machine Intelligence, 15(11), pp.1162-1173. [pdf](http://ieeexplore.ieee.org/abstract/document/244677/).
39 |
40 | @article{o1993document,
41 | title={The document spectrum for page layout analysis},
42 | author={O'Gorman, Lawrence},
43 | journal={IEEE Transactions on Pattern Analysis and Machine Intelligence},
44 | volume={15},
45 | number={11},
46 | pages={1162--1173},
47 | year={1993},
48 | publisher={IEEE}
49 | }
50 |
51 | ## Notes
52 | ### How to remove .DS_Store
53 | ```
54 | find . -name '.DS_Store' -type f -delete
55 | ```
56 |
--------------------------------------------------------------------------------
/assets/Docstrum_Visualized_Steps.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chulwoopack/docstrum/6559d78f6ec0e14f3372e1e91629b81a96465e42/assets/Docstrum_Visualized_Steps.gif
--------------------------------------------------------------------------------
/assets/Text-line_Grouping_Process.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chulwoopack/docstrum/6559d78f6ec0e14f3372e1e91629b81a96465e42/assets/Text-line_Grouping_Process.gif
--------------------------------------------------------------------------------
/box.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy
3 | import math
4 |
5 | import geometry as g
6 | import colors
7 |
8 | class EmptyObject:
9 |
10 | def __init__(self):
11 | pass
12 |
13 | def distance(start, end):
14 |
15 | rise = float(end[1]) - float(start[1])
16 | run = float(end[0]) - float(start[0])
17 |
18 | distance = math.sqrt(rise**2 + run**2)
19 |
20 | return distance
21 |
22 | def midpoint(start, end):
23 |
24 | midX = float(start[0]+end[0]) / 2
25 | midY = float(start[1]+end[1]) / 2
26 |
27 | return (midX, midY)
28 |
29 | def angle(start, end):
30 |
31 | rise = float(end[1]) - float(start[1])
32 | run = float(end[0]) - float(start[0])
33 |
34 | radians = math.atan2(rise, run)
35 | degrees = math.degrees(radians)
36 |
37 | return degrees
38 |
39 | class Box:
40 |
41 | def __init__(self, points):
42 |
43 | self.rect = cv2.minAreaRect(points) # rect = ((center_x,center_y),(width,height),angle)
44 | self.points = self.rectToPoints(self.rect)
45 |
46 | self.setImportantPoints(self.points) # sets up properties such as self.top.left, self.center.right, etc
47 |
48 | self.width = distance(self.center.left, self.center.right)
49 | self.height = distance(self.top.left, self.bottom.left)
50 | self.area = self.width * self.height
51 | self.angle = angle(self.top.left, self.top.right)
52 |
53 | self.words = [] # children words, with a center of mass inside the box.
54 | self.isLine = False # temporary flag, until I set up a proper Lines class.
55 |
56 | def rectToPoints(self, rect):
57 |
58 | points = cv2.cv.BoxPoints(rect) # Find four vertices of rectangle from above rect
59 | points = numpy.int0(numpy.around(points)) # Round the values and make them integers
60 | return points
61 |
62 | def setImportantPoints(self, points):
63 | # figures out which point is top-left, etc, and assigns each to one of: self.top.left, self.top.right,
64 | # self.bottom.left, and self.bottom.right
65 |
66 | points = sorted(points, key=lambda point: point[0]) # sort by x position.
67 | left = sorted(points[:2], key=lambda point: point[1]) # [top-left, bottom-left]
68 | right = sorted(points[2:], key=lambda point: point[1]) # [top-right, bottom-right]
69 |
70 | self.top = EmptyObject()
71 | self.top.left = left[0]
72 | self.top.right = right[0]
73 |
74 | self.bottom = EmptyObject()
75 | self.bottom.left = left[1]
76 | self.bottom.right = right[1]
77 |
78 | self.center = EmptyObject()
79 | self.center.left = midpoint(self.top.left, self.bottom.left)
80 | self.center.right = midpoint(self.top.right, self.bottom.right)
81 | self.center.center = midpoint(self.center.left, self.center.right)
82 |
83 | def isTouchingEdge(self, shape, closenessThreshold=200):
84 |
85 | isTouching = False
86 | shape = (shape[1], shape[0]) # switch width and height so that it matches the format of a Point()
87 |
88 | for point in self.points:
89 | if point[0] <= (0 + closenessThreshold):
90 | isTouching = True
91 | elif point[1] <= (0 + closenessThreshold):
92 | isTouching = True
93 | elif point[0] >= (shape[0] - closenessThreshold):
94 | isTouching = True
95 | elif point[1] >= (shape[1] - closenessThreshold):
96 | isTouching = True
97 |
98 | return isTouching
99 |
100 | def contains(self, word):
101 |
102 | boxContour = numpy.array(self.points)
103 | retval = cv2.pointPolygonTest(numpy.array([self.points]), word.center, False)
104 |
105 | if retval > 0:
106 | return True
107 | else:
108 | return False
109 |
110 | def paint(self, image, color, width=5):
111 |
112 | #image = g.Point(self.center.center).paint(image, colors.RED)
113 | cv2.polylines(image, [self.points], True, color, width, cv2.CV_AA)
114 |
115 | #image = g.Point(self.center.left).paint(image, colors.BLUE)
116 | #image = g.Point(self.center.right).paint(image, colors.GREEN)
117 |
118 | return image
119 |
--------------------------------------------------------------------------------
/box.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chulwoopack/docstrum/6559d78f6ec0e14f3372e1e91629b81a96465e42/box.pyc
--------------------------------------------------------------------------------
/colors.py:
--------------------------------------------------------------------------------
1 | class EmptyObject:
2 |
3 | def __init__(self):
4 | pass
5 |
6 | greyscale = EmptyObject()
7 | greyscale.BLACK = 0
8 | greyscale.WHITE = 255
9 | greyscale.MID_GREY = 127
10 |
11 |
12 | WHITE = (255, 255, 255)
13 | LIGHT_GREY = (191, 191, 191)
14 | MID_GREY = (127, 128, 128)
15 | DARK_GREY = (63, 63, 63)
16 | BLACK = (0, 0, 0)
17 |
18 | BLUE = (255, 0, 0)
19 | CYAN = (255, 255, 0)
20 |
21 | GREEN = (0, 255, 0)
22 | LIME_GREEN = (0, 255, 102)
23 |
24 | YELLOW = (0, 255, 255)
25 | BURNT_YELLOW = (0, 223, 255)
26 | ORANGE = (0, 127, 255)
27 |
28 | RED = (0, 0, 255)
29 | MAGENTA = (255,0,255)
30 |
31 | PURPLE = (191, 0, 191)
32 |
33 | class cycle:
34 |
35 | def __init__(self, *colors):
36 |
37 | self.colors = colors
38 | self.index = 0 # this will continually loop through self.colors
39 |
40 | def next(self):
41 | color = self.colors[self.index]
42 | self.index = (self.index+1) % len(self.colors)
43 | return color
44 |
45 | def __iter__(self, limit=None):
46 | class Iterator():
47 | def __init__(self, colors, limit):
48 | self.colors = colors
49 | self.index = 0 # this will continually loop through self.colors
50 | self.limit = limit
51 | self.iterCounter = 0 # this will continually count upwards (it won't loop around)
52 | def __iter__(self):
53 | return self
54 | def next(self):
55 | if self.limit != None and self.limit > self.iterCounter:
56 | raise StopIteration
57 | else:
58 | color = self.colors[self.index]
59 | self.index = (self.index+1) % len(self.colors)
60 | self.iterCounter += 1
61 | return color
62 |
63 | return Iterator(self.colors, limit)
64 |
--------------------------------------------------------------------------------
/colors.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chulwoopack/docstrum/6559d78f6ec0e14f3372e1e91629b81a96465e42/colors.pyc
--------------------------------------------------------------------------------
/content.py:
--------------------------------------------------------------------------------
1 | import numpy
2 | from box import Box
3 | import colors
4 |
5 | class BoilerPlate:
6 |
7 | def __init__(self):
8 | self.pageNum = None
9 | self.chapterTitle = None # only one of [.chapterTitle, .bookTitle] will be set -- they're mutually exclusive.
10 | self.bookTitle = None
11 |
12 | def paint(self, image, color):
13 |
14 | image = self.pageNum.paint(image, color, box=True)
15 | if self.chapterTitle is not None:
16 | image = self.chapterTitle.paint(image, color, box=True)
17 | if self.bookTitle is not None:
18 | image = self.bookTitle.paint(image, color, box=True)
19 |
20 | return image
21 |
22 | class SectionTitle:
23 |
24 | def __init__(self, firstLine=None):
25 |
26 | self.contentType = "SectionTitle"
27 |
28 | if firstLine is None:
29 | self.lines = []
30 | else:
31 | self.lines = [firstLine]
32 |
33 | def append(self, line):
34 |
35 | self.lines.append(line)
36 |
37 | def paint(self, image, color=colors.MAGENTA):
38 |
39 | for line in self.lines:
40 | image = line.paint(image, color, box=True)
41 | return image
42 |
43 | class Figure:
44 |
45 | def __init__(self):
46 |
47 | self.contentType = "Figure"
48 | self.image = None
49 | self.caption = []
50 |
51 | def paint(self, image, color=colors.CYAN):
52 |
53 | image = self.image.paint(image, color, box=True)
54 | for line in self.caption:
55 | image = line.paint(image, color, box=True)
56 | return image
57 |
58 | class Paragraph:
59 |
60 | def __init__(self, firstLine=None):
61 |
62 | self.contentType = "Paragraph"
63 |
64 | if firstLine is None:
65 | self.lines = []
66 | else:
67 | self.lines = [firstLine]
68 |
69 | def append(self, line):
70 |
71 | self.lines.append(line)
72 |
73 | def __add__(self, other):
74 | # designed to be used when adding Content()s together, so that paragraphs which are split over
75 | # a page can be reconstituted.
76 | pass
77 |
78 | def __getitem__(self, val):
79 | return self.lines.__getitem__(val)
80 |
81 | def __len__(self):
82 | return self.lines.__len__()
83 |
84 | def paint(self, image, color=colors.RED):
85 |
86 | points = []
87 | for line in self.lines:
88 | for word in line.words:
89 | for point in word.contour:
90 | points.append(point)
91 | points = numpy.array(points) # This needs to have the format [ [[a,b]], [[c,d]] ]
92 | box = Box(points)
93 | image = box.paint(image, color)
94 |
95 | for line in self.lines:
96 | image = line.paint(image, colors.BURNT_YELLOW, centerLine=True)
97 |
98 | return image
99 |
100 |
101 | class ChapterStart:
102 |
103 | def __init__(self, lines):
104 |
105 | self.contentType = "ChapterStart"
106 | self.chapterNum = lines.pull()
107 | self.titleLines = []
108 | self.quoteLines = []
109 |
110 | while not lines.peekStart().isHorizontalRule:
111 | self.titleLines.append(lines.pull())
112 | lines.pull() # discard the