j-silv commited on
Commit
2cdf547
·
1 Parent(s): eb06ac1

Add extraction script from wikipedia image

Browse files

This was my first attempt at generating each individual segment image.
It kinda works, but the issue is that the PNG is not pixel perfect and
that is what the script assumes.

I'm just adding it to be archived. The better approach is to generate
the segments myself.

Files changed (3) hide show
  1. .gitignore +2 -1
  2. led-mosaic/__init__.py +0 -0
  3. led-mosaic/extract.py +329 -0
.gitignore CHANGED
@@ -2,4 +2,5 @@
2
  __pycache__
3
  .venv
4
  venv
5
- build
 
 
2
  __pycache__
3
  .venv
4
  venv
5
+ build
6
+ images/extracted
led-mosaic/__init__.py ADDED
File without changes
led-mosaic/extract.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Description:
3
+
4
+ Takes in a grid of possible combinations of a 7-segment display
5
+ and generates individual image files for each possible combination
6
+
7
+ - Some light thresholding is applied to the original image,
8
+ such that no artifacts are present in the final processed image
9
+
10
+ - Background color is added using the alpha channel as an index mask
11
+
12
+ - Intensity of non-segment pixels is reduced to have a clearer segment image
13
+
14
+ The grid image was found on Wikipedia:
15
+ https://upload.wikimedia.org/wikipedia/commons/d/d1/7-segment.svg
16
+
17
+ Author: Justin Silver
18
+ Date: 05/28/2023
19
+ """
20
+
21
+ import numpy as np # for array manipulation
22
+ import cv2 as cv # for image stuff
23
+ import matplotlib.pyplot as plt # for plotting
24
+ import os
25
+
26
+
27
+ def load_img(img_path, resize=False, resize_factor=0.5):
28
+ """Load the 7-segment grid image using OpenCV"""
29
+
30
+ # flag needed to maintain alpha data
31
+ seven_seg_grid_img = cv.imread(img_path, flags=cv.IMREAD_UNCHANGED)
32
+
33
+ if resize == True:
34
+ seven_seg_grid_img = cv.resize(seven_seg_grid_img,
35
+ None,
36
+ fx=resize_factor,
37
+ fy=resize_factor,
38
+ interpolation=cv.INTER_AREA)
39
+
40
+ # OpenCV has BGRA ordering by default - fix RGB and BGR channels reverse order
41
+ seven_seg_grid_img = cv.cvtColor(seven_seg_grid_img, cv.COLOR_BGRA2RGBA)
42
+
43
+ return seven_seg_grid_img
44
+
45
+
46
+ def apply_img_thresholding(img):
47
+ """Apply thresholding to raw image
48
+
49
+ this is needed because there is some red in the upper edge of some digits
50
+ if this is not done, an ugly red pixel artifact is seen after the
51
+ intensity lowering code
52
+ """
53
+
54
+ (b, g, r, a) = cv.split(img)
55
+
56
+ ret, r_thresh = cv.threshold(b, 250, 255, cv.THRESH_TRUNC)
57
+ ret, g_thresh = cv.threshold(g, 250, 255, cv.THRESH_TRUNC)
58
+ ret, b_thresh = cv.threshold(r, 250, 255, cv.THRESH_TRUNC)
59
+ ret, a_thresh = cv.threshold(a, 250, 255, cv.THRESH_TRUNC)
60
+
61
+ img = cv.merge((r_thresh, g_thresh, b_thresh, a_thresh))
62
+
63
+ return img
64
+
65
+
66
+ def find_digits_in_grid(img):
67
+ """Use a state machine to find location of digit pixels in grid"""
68
+
69
+ # alpha channel is used to determine the start and end index of each digit in grid
70
+ alpha = img[:, :, 3]
71
+
72
+ # indexing into the grid to find the first digit (used for subsequent digits)
73
+ digit_location = {
74
+ 'left': 0,
75
+ 'right': 0,
76
+ 'up' : 0,
77
+ 'down' : 0
78
+ }
79
+
80
+ # state machine to find each edge of the digit
81
+ digit_search_state = 'NOTHING_FOUND'
82
+
83
+ for row_idx in range(alpha.shape[0]):
84
+ for col_idx in range(alpha.shape[1]):
85
+ pixel = alpha[row_idx,col_idx]
86
+
87
+ match digit_search_state:
88
+
89
+ # because we are going by row from top to bottom, we will reach the top of the digit first
90
+ case 'NOTHING_FOUND':
91
+
92
+ if pixel > 0:
93
+ digit_search_state = 'DIG_TOP_EDGE_FOUND'
94
+ digit_location['up'] = row_idx
95
+ digit_location['left'] = col_idx # we just need to know the left top edge of the digit to compare later
96
+ break # we don't need to look at this row anymore
97
+ # (it will just repeat the digits on the right)
98
+
99
+ # now we need to get the left edge
100
+ case 'DIG_TOP_EDGE_FOUND':
101
+
102
+ if pixel > 0:
103
+ if col_idx != digit_location['left']: # because we are at the slopeing part of the segment
104
+ digit_location['left'] = col_idx
105
+ break # we don't need to look at this row anymore,
106
+ # cause we still haven't reached the left most edge of the digit
107
+ else:
108
+ # we had the same col location twice, so we are at an edge !
109
+ digit_search_state = 'DIG_LEFT_EDGE_FOUND'
110
+ digit_location['left'] = col_idx
111
+ # we don't need to break, since we can just go all
112
+ # the way to the right to find the right edge
113
+
114
+ case 'DIG_LEFT_EDGE_FOUND':
115
+ # not too robust, because we assume there is a white line across the alpha channel (ignoring black space in middle)
116
+ if pixel == 0:
117
+ digit_search_state = 'DIG_RIGHT_EDGE_FOUND'
118
+ digit_location['right'] = col_idx-1 # -1 because we found the black space, now we need to go one before to get the digit
119
+ break
120
+
121
+ case 'DIG_RIGHT_EDGE_FOUND':
122
+ if pixel > 0:
123
+ digit_location['down'] = row_idx
124
+ break
125
+ elif col_idx == digit_location['right']:
126
+ digit_location['down'] = row_idx-1
127
+ digit_search_state = 'DIG_BOTTOM_EDGE_FOUND'
128
+ break
129
+
130
+ case 'DIG_BOTTOM_EDGE_FOUND':
131
+ # we don't need to do anything here
132
+ break
133
+
134
+ case _:
135
+ raise Exception("Invalid digit_search_state: ", digit_search_state)
136
+
137
+ if (digit_search_state == 'DIG_BOTTOM_EDGE_FOUND'):
138
+ break # to break out of upper loop, we found the whole digit now
139
+
140
+ return digit_location
141
+
142
+
143
+ def calculate_pixel_locations(digit_location, verbose=False):
144
+ """Get some offset and digit size information from digit locations"""
145
+
146
+ # number of pixels each digit's height takes up (+1 because top-left corner is at (0, 0) coordinates)
147
+ digit_height = (digit_location['down'] - digit_location['up']) + 1
148
+
149
+ # number of pixels each digit's width takes up (+1 because top-left corner is at (0, 0) coordinates)
150
+ digit_width = (digit_location['right']- digit_location['left']) + 1
151
+
152
+ # number of cols (x-axis pixels) to skip to get to the left-edge of the first digit
153
+ col_offset = digit_location['left']
154
+
155
+ # number of rows (y-axis pixels) to skip to get to the top-edge of the first digit
156
+ row_offset = digit_location['up']
157
+
158
+ if (verbose):
159
+ print(f"digit_location: {digit_location}")
160
+ print(f"digit_height: {digit_height}")
161
+ print(f"digit_width: {digit_width}")
162
+ print(f"col_offset: {col_offset}")
163
+ print(f"row_offset: {row_offset}")
164
+
165
+ return digit_height, digit_width, col_offset, row_offset
166
+
167
+
168
+ def add_special_offset(digit_row):
169
+ """Add extra offset depending on specific digit row
170
+
171
+ unfortunately the rows are not spaced out equally, whenever the .svg file was created
172
+ this fixes it except for 3 special situations. There is one pixel off (on top) for digit_row:
173
+ 7, 6, 1
174
+ It seems like the digit height is actually 1 pixel smaller than it should be... but that's ok. it is such
175
+ a small error, it won't be too noticeable I bet
176
+ Note that all the digits on this row will have the small pixel offset
177
+ """
178
+
179
+ additional_offset = 0
180
+ if (digit_row == 0):
181
+ additional_offset = 0
182
+ elif (digit_row == 1):
183
+ additional_offset = 0
184
+ elif (digit_row == 2):
185
+ additional_offset = 1
186
+ elif (digit_row == 3):
187
+ additional_offset = 1
188
+ elif (digit_row == 4):
189
+ additional_offset = 2
190
+ elif (digit_row == 5):
191
+ additional_offset = 2
192
+ elif (digit_row == 6):
193
+ additional_offset = 2
194
+ elif (digit_row == 7):
195
+ additional_offset = 2
196
+ else:
197
+ raise(Exception("Invalid digit row: ", digit_row))
198
+
199
+ return additional_offset
200
+
201
+
202
+ def apply_segment_thresholding(cropped_digit, digit_row, digit_col):
203
+ """Threshold segment pixels and reduce brightness of non-segment pixels"""
204
+ # R and B were again swapped
205
+ cropped_digit_fixed_rgba = cv.cvtColor(cropped_digit[digit_row,digit_col,...], cv.COLOR_BGRA2RGBA)
206
+
207
+ # R and B are again swapped
208
+ (b, g, r, a) = cv.split(cropped_digit_fixed_rgba)
209
+
210
+ segment_mask_r_gt_g = r > g # advanced boolean indices
211
+ segment_mask_r_gt_b = r > b # advanced boolean indices
212
+
213
+ segment_mask_r = np.logical_and(segment_mask_r_gt_g, segment_mask_r_gt_b)
214
+ r[segment_mask_r] = 255 # this should already be the case
215
+ g[segment_mask_r] = 0
216
+ b[segment_mask_r] = 0
217
+
218
+ # now we can reduce the intensity a little for the pixels that aren't included in this selection
219
+ # basically this will be all the non-segment pixels
220
+ r[np.logical_not(segment_mask_r)] = r[np.logical_not(segment_mask_r)]*0.25
221
+ g[np.logical_not(segment_mask_r)] = g[np.logical_not(segment_mask_r)]*0.25
222
+ b[np.logical_not(segment_mask_r)] = b[np.logical_not(segment_mask_r)]*0.25
223
+
224
+ # remerge in the same order that we split
225
+ cropped_digit_fixed_rgba = cv.merge((b, g, r, a))
226
+
227
+ return cropped_digit_fixed_rgba
228
+
229
+
230
+ def write_img(cropped_digit_fixed_rgba, digit_row, digit_col):
231
+ """Add background and write image"""
232
+
233
+ # https://stackoverflow.com/questions/53732747/set-white-background-for-a-png-instead-of-transparency-with-opencv
234
+ background_mask = cropped_digit_fixed_rgba[...,3] == 0 # get indexes of where alpha channel is transparent
235
+ cropped_digit_fixed_rgba[background_mask] = [0, 0, 0, 0] # use advanced boolean indexing and set background color
236
+ cropped_digit_new_background = cv.cvtColor(cropped_digit_fixed_rgba, cv.COLOR_BGRA2BGR) # we don't need alpha channel anymore
237
+
238
+ os.makedirs("images/extracted", exist_ok=True)
239
+ cv.imwrite(f"images/extracted/{digit_row*16 + digit_col}.png", cropped_digit_new_background)
240
+
241
+
242
+ def extract_digit(img, digit_location, num_dig_rows=8, num_dig_cols=16, verbose=False):
243
+ """Iterate through the grid image and write each image to an output file"""
244
+
245
+ digit_height, digit_width, col_offset, row_offset = calculate_pixel_locations(digit_location, verbose)
246
+
247
+ # 128 digits, with pixels of digit_height by digit_width, for all 4 channels
248
+ cropped_digit = np.zeros((num_dig_rows, num_dig_cols, digit_height, digit_width, 4), dtype=np.uint8)
249
+
250
+ for digit_row in range(cropped_digit.shape[0]):
251
+ for digit_col in range(cropped_digit.shape[1]):
252
+
253
+ additional_offset = add_special_offset(digit_row)
254
+
255
+ row_grid_start = row_offset*(2*digit_row + 1) + digit_height*(digit_row) + additional_offset
256
+ row_grid_end = row_grid_start+digit_height
257
+
258
+ col_grid_start = col_offset*(2*digit_col + 1) + digit_width*(digit_col)
259
+ col_grid_end = col_grid_start + digit_width
260
+
261
+ cropped_digit[digit_row,digit_col,...] = img[row_grid_start:row_grid_end,col_grid_start:col_grid_end,:]
262
+
263
+ cropped_digit_fixed_rgba = apply_segment_thresholding(cropped_digit, digit_row, digit_col)
264
+
265
+ write_img(cropped_digit_fixed_rgba, digit_row, digit_col)
266
+
267
+
268
+ cropped_digit_grid = np.zeros((num_dig_rows*digit_height,num_dig_cols*digit_width,4),dtype=np.uint8)
269
+
270
+ # there's probably a way to do this with numpy functions, I'm just not sure how
271
+ # I need to concatenate in 2 directions at the same time
272
+ for row in range(num_dig_rows) :
273
+ cropped_digit_grid[row*digit_height:digit_height*(1+row),...] = np.concatenate(cropped_digit[row,...],axis=1)
274
+
275
+ return cropped_digit_grid
276
+
277
+
278
+ def plot_img(img, cropped_digit_grid):
279
+ """Use matplotlib to plot extraction results"""
280
+
281
+ plt.figure(figsize=(20,8)) # just to get full-screen
282
+
283
+ plt.subplot(221) # 2 row, 2 columns, 1st index
284
+ plt.imshow(img)
285
+ plt.title("7 segment RGBA")
286
+
287
+ plt.subplot(222)
288
+ plt.imshow(cropped_digit_grid, cmap='gray')
289
+ plt.title("cropped_digit_grid")
290
+
291
+ plt.subplot(223)
292
+ cropped_digit_processed = cv.imread("images/extracted/1.png", flags=cv.IMREAD_UNCHANGED)
293
+ cropped_digit_processed = cv.cvtColor(cropped_digit_processed, cv.COLOR_RGBA2BGRA)
294
+ plt.imshow(cropped_digit_processed)
295
+ plt.title("cropped_digit_processed[1]")
296
+
297
+ plt.subplot(224)
298
+ cropped_digit_processed = cv.imread("images/extracted/75.png", flags=cv.IMREAD_UNCHANGED)
299
+ cropped_digit_processed = cv.cvtColor(cropped_digit_processed, cv.COLOR_RGBA2BGRA)
300
+ plt.imshow(cropped_digit_processed)
301
+ plt.title("cropped_digit_processed[75]")
302
+
303
+ plt.show()
304
+
305
+ def extract():
306
+
307
+ DEBUG = False
308
+ NUM_DIG_ROWS = 8 # number of 7 segment digits in grid img per row
309
+ NUM_DIG_COLS = 16 # number of 7 segment digits in grid img per column
310
+ OUTPUT_IMAGE_ASPECT_RATIO = 70/123 # this is the default aspect ratio from the extracted image
311
+ OUTPUT_IMAGE_HEIGHT = 30 # this is how big the output tile is in the Y axis
312
+ OUTPUT_IMAGE_WIDTH = OUTPUT_IMAGE_HEIGHT*OUTPUT_IMAGE_ASPECT_RATIO
313
+ INPUT_IMAGE_RESIZE_FACTOR = 0.5
314
+ FLAG_RESIZE_ON = True
315
+ assert(NUM_DIG_COLS*NUM_DIG_ROWS == 2**7) # 7 segments in on/off combination
316
+
317
+
318
+ seven_seg_grid_img = load_img("images/seven_segment_grid.png", FLAG_RESIZE_ON, INPUT_IMAGE_RESIZE_FACTOR)
319
+ seven_seg_grid_img = apply_img_thresholding(seven_seg_grid_img)
320
+ digit_location = find_digits_in_grid(seven_seg_grid_img)
321
+ cropped_digit_grid = extract_digit(seven_seg_grid_img, digit_location, NUM_DIG_ROWS, NUM_DIG_COLS, DEBUG)
322
+ plot_img(seven_seg_grid_img, cropped_digit_grid)
323
+
324
+
325
+
326
+
327
+
328
+ if __name__ == "__main__":
329
+ extract()