Skip to content

Commit 26dae96

Browse files
authored
Merge pull request #23 from fernaper/development
v1.0.0 Multiple selection!
2 parents cd18db6 + e91a390 commit 26dae96

File tree

6 files changed

+230
-36
lines changed

6 files changed

+230
-36
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ You will need to install:
1313

1414
* opencv >= 3.6.2
1515
* numpy >= 1.13.3
16+
* python-constraint >= 1.4.0
1617

1718
You can simply execute:
1819
`pip install -r requirements.txt`

opencv_draw_tools/SelectZone.py

Lines changed: 142 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import sys
55
import cv2
66

7+
import tags_constraint
8+
79
"""
810
You can change it.
911
If IGNORE_ERRORS are True, opencv_draw_tools tried to solve the problems or
@@ -29,7 +31,50 @@ def get_lighter_color(color):
2931
add = min(add,30)
3032
return (color[0] + add, color[1] + add, color[2] + add)
3133

32-
def add_tags(frame, position, tags, tag_position=None, alpha=0.75, color=(20, 20, 20), margin=5, font_info=(cv2.FONT_HERSHEY_COMPLEX_SMALL, 0.75, (255,255,255), 1)):
34+
def get_shape_tags(position, tags, margin=5,
35+
font_info=(cv2.FONT_HERSHEY_COMPLEX_SMALL, 0.75, (255,255,255), 1)):
36+
"""Get information about how much the list of tags will occupy (width and height)
37+
with the current configuration.
38+
39+
Keyword arguments:
40+
position -- touple with 4 elements (x1, y1, x2, y2)
41+
This elements must be between 0 and frame height/width.
42+
tags -- list of strings/tags you want to check shape.
43+
margin -- extra margin in pixels to be separeted with the selected zone. (default 5)
44+
font_info -- touple with 4 elements (font, font_scale, font_color, thickness)
45+
font -- opencv font (default cv2.FONT_HARSHEY_COMPLEX_SMALL)
46+
font_scale -- scale of the fontm between 0 and 1 (default 0.75)
47+
font_color -- color of the tags text, touple with 3 elements BGR (default (255,255,255) -> white)
48+
BGR = Blue - Green - Red
49+
thickness -- thickness of the text in pixels (default 1)
50+
Return:
51+
Return shape of the given tags with the given font information
52+
53+
"""
54+
font, font_scale, font_color, thickness = font_info
55+
x1, y1, x2, y2 = position
56+
57+
aux_tags = []
58+
for tag in tags:
59+
line = [x+'\n' for x in tag.split('\n')]
60+
line[0] = line[0][:-1]
61+
for element in line:
62+
aux_tags.append(element)
63+
tags = aux_tags
64+
65+
text_width = -1
66+
text_height = -1
67+
line_height = -1
68+
for tag in tags:
69+
size = cv2.getTextSize(tag, font, font_scale, thickness)
70+
text_width = max(text_width,size[0][0])
71+
text_height = max(text_height, size[0][1])
72+
line_height = max(line_height, text_height + size[1] + margin)
73+
74+
return (text_width + margin * 3, (margin + text_height)*(len(tags) - 1) + 2*text_height + margin*(len(tags)-1))
75+
76+
def add_tags(frame, position, tags, tag_position=None, alpha=0.75, color=(20, 20, 20),
77+
margin=5, font_info=(cv2.FONT_HERSHEY_COMPLEX_SMALL, 0.75, (255,255,255), 1)):
3378
"""Add tags to selected zone.
3479
3580
It was originally intended as an auxiliary method to add details to the select_zone()
@@ -40,7 +85,7 @@ def add_tags(frame, position, tags, tag_position=None, alpha=0.75, color=(20, 20
4085
position -- touple with 4 elements (x1, y1, x2, y2)
4186
This elements must be between 0 and 1 in case it is normalized
4287
or between 0 and frame height/width.
43-
tags -- list of strings/tags you want to associate to the selected zone
88+
tags -- list of strings/tags you want to associate to the selected zone.
4489
tag_position -- position where you want to add the tags, relatively to the selected zone (default None)
4590
If None provided it will auto select the zone where it fits better:
4691
- First try to put the text on the Bottom Rigth corner
@@ -95,7 +140,8 @@ def add_tags(frame, position, tags, tag_position=None, alpha=0.75, color=(20, 20
95140
fits_right = x2 + text_width + margin*3 <= f_width
96141
fits_left = x1 - (text_width + margin*3) >= 0
97142
fits_below = (text_height + margin)*len(tags) - margin <= y2 - thickness
98-
fits_inside = x1 + text_width + margin*3 <= x2 - thickness and y1 + (margin*2 + text_height)*len(tags) + text_height - margin <= y2 - thickness
143+
fits_inside = x1 + text_width + margin*3 <= x2 - thickness and \
144+
y1 + (margin*2 + text_height)*len(tags) + text_height - margin <= y2 - thickness
99145

100146
if fits_right and fits_below:
101147
tag_position = 'bottom_right'
@@ -115,12 +161,12 @@ def add_tags(frame, position, tags, tag_position=None, alpha=0.75, color=(20, 20
115161

116162
# Add triangle to know to whom each tag belongs
117163
if tag_position == 'bottom_right':
118-
pt1 = (x2 + margin - 1, y2 - (margin*2 + text_height)*len(tags) - text_height - margin)
119-
pt2 = (pt1[0], pt1[1] + text_height + margin*2)
164+
pt1 = (x2 + margin - 1, y2 - (margin + text_height)*len(tags) - text_height - margin * (len(tags)-1))
165+
pt2 = (pt1[0], pt1[1] + text_height + margin)
120166
pt3 = (pt1[0] - margin + 1, pt1[1] + int(text_height/2)+margin)
121167
elif tag_position == 'bottom_left':
122-
pt1 = (x1 - margin + 1, y2 - (margin*2 + text_height)*len(tags) - text_height - margin)
123-
pt2 = (pt1[0], pt1[1] + text_height + margin*2)
168+
pt1 = (x1 - margin + 1, y2 - (margin + text_height)*len(tags) - text_height - margin * (len(tags)-1))
169+
pt2 = (pt1[0], pt1[1] + text_height + margin)
124170
pt3 = (pt1[0] + margin - 1, pt1[1] + int(text_height/2)+margin)
125171
elif tag_position == 'top':
126172
pt1 = (x1 + margin + int(text_width/3), y1 - margin*2 + 1)
@@ -138,35 +184,38 @@ def add_tags(frame, position, tags, tag_position=None, alpha=0.75, color=(20, 20
138184
extra_adjustment = 2 if tag[-1] == '\n' else 1
139185
if tag_position == 'top':
140186
cv2.rectangle(overlay, (x1 + margin, y1 - (margin + text_height)*reverse_i - margin * (reverse_i-1) - text_height - margin * (extra_adjustment - 1 )),
141-
(x1 + text_width + margin*3, y1 - (margin + text_height)*reverse_i - margin * (reverse_i) + text_height), color,-1)
187+
(x1 + text_width + margin*3, y1 - (margin + text_height)*reverse_i - margin * (reverse_i) + text_height), color,-1)
142188
elif tag_position == 'inside':
143-
cv2.rectangle(overlay, (x1 + margin, y1 + (margin*2 + text_height)*(i+1) - text_height - margin - extra_adjustment),
144-
(x1 + text_width + margin*3, y1 + (margin*2 + text_height)*(i+1) + text_height - margin), color,-1)
189+
cv2.rectangle(overlay, (x1 + margin, y1 + (margin*2 + text_height)*(i+1) + margin*i - text_height - margin * extra_adjustment),
190+
(x1 + text_width + margin*3, y1 + (margin*2 + text_height)*(i+1) + margin*i + text_height - margin), color,-1)
145191
elif tag_position == 'bottom_left':
146-
cv2.rectangle(overlay, (x1 - (text_width + margin*3), y2 - (margin*2 + text_height)*reverse_i - text_height - margin * extra_adjustment),
147-
(x1 - margin, y2 - (margin*2 + text_height)*reverse_i + text_height - margin), color,-1)
192+
cv2.rectangle(overlay, (x1 - (text_width + margin*3), y2 - (margin + text_height)*reverse_i - margin * (reverse_i-1) - text_height - margin * (extra_adjustment - 1)),
193+
(x1 - margin, y2 - (margin + text_height)*reverse_i - margin * (reverse_i) + text_height), color,-1)
148194
elif tag_position == 'bottom_right':
149-
cv2.rectangle(overlay, (x2 + margin, y2 - (margin*2 + text_height)*reverse_i - text_height - margin * extra_adjustment),
150-
(x2 + text_width + margin*3, y2 - (margin*2 + text_height)*reverse_i + text_height - margin), color,-1)
151-
if tag_position != 'top':
152-
y1 += margin
153-
y2 += margin
195+
cv2.rectangle(overlay, (x2 + margin, y2 - (margin + text_height)*reverse_i - margin * (reverse_i-1) - text_height - margin * (extra_adjustment - 1)),
196+
(x2 + text_width + margin*3, y2 - (margin + text_height)*reverse_i - margin * (reverse_i) + text_height), color,-1)
197+
154198
cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0, frame)
155199
x1, y1, x2, y2 = position
156200
for i, tag in enumerate(tags):
157201
reverse_i = len(tags) - i
158202
extra_adjustment = int(margin*( 0.5 if tag[-1] == '\n' else 0))
159203
if tag_position == 'top':
160-
cv2.putText(frame, tag.replace('\n',''), (x1 + margin*2, y1 - (margin + text_height)*reverse_i - margin * (reverse_i-1) + int(margin/2) - extra_adjustment), font, font_scale, font_color, thickness)
204+
cv2.putText(frame, tag.replace('\n',''),
205+
(x1 + margin*2, y1 - (margin + text_height)*reverse_i - margin * (reverse_i-1) + int(margin/2) - extra_adjustment),
206+
font, font_scale, font_color, thickness)
161207
elif tag_position == 'inside':
162-
cv2.putText(frame, tag.replace('\n',''), (x1 + margin*2, y1 + (margin*2 + text_height)*(i+1) - extra_adjustment), font, font_scale, font_color, thickness)
208+
cv2.putText(frame, tag.replace('\n',''),
209+
(x1 + margin*2, y1 + (margin*2 + text_height)*(i+1) + margin*i - extra_adjustment),
210+
font, font_scale, font_color, thickness)
163211
elif tag_position == 'bottom_left':
164-
cv2.putText(frame, tag.replace('\n',''), (x1 - (text_width + margin*2), y2 - (margin*2 + text_height)*reverse_i - extra_adjustment), font, font_scale, font_color, thickness)
212+
cv2.putText(frame, tag.replace('\n',''),
213+
(x1 - (text_width + margin*2), y2 - (margin + text_height)*reverse_i - margin * (reverse_i-1) + int(margin/2) - extra_adjustment),
214+
font, font_scale, font_color, thickness)
165215
elif tag_position == 'bottom_right':
166-
cv2.putText(frame, tag.replace('\n',''), (x2 + margin*2, y2 - (margin*2 + text_height)*reverse_i - extra_adjustment), font, font_scale, font_color, thickness)
167-
if tag_position != 'top':
168-
y1 += margin
169-
y2 += margin
216+
cv2.putText(frame, tag.replace('\n',''),
217+
(x2 + margin*2, y2 - (margin + text_height)*reverse_i - margin * (reverse_i-1) + int(margin/2) - extra_adjustment),
218+
font, font_scale, font_color, thickness)
170219

171220
return frame
172221

@@ -186,7 +235,8 @@ def add_peephole(frame, position, alpha=0.5, color=(110,70,45), thickness=2, lin
186235
1 means totally visible and 0 totally invisible
187236
color -- color of the selected zone, touple with 3 elements BGR (default (110,70,45) -> dark blue)
188237
BGR = Blue - Green - Red
189-
normalized -- boolean parameter, if True, position provided normalized (between 0 and 1) else you shold provide concrete values (default False)
238+
normalized -- boolean parameter, if True, position provided normalized (between 0 and 1)
239+
else you shold provide concrete values (default False)
190240
thickness -- thickness of the drawing in pixels (default 2)
191241
corners -- boolean parameter, if True, also draw the corners of the rectangle
192242
@@ -228,7 +278,8 @@ def adjust_position(shape, position, normalized=False, thickness=0):
228278
position -- touple with 4 elements (x1, y1, x2, y2)
229279
This elements must be between 0 and 1 in case it is normalized
230280
or between 0 and frame height/width.
231-
normalized -- boolean parameter, if True, position provided normalized (between 0 and 1) else you shold provide concrete values (default False)
281+
normalized -- boolean parameter, if True, position provided normalized (between 0 and 1)
282+
else you shold provide concrete values (default False)
232283
thickness -- thickness of the drawing in pixels (default 0)
233284
234285
Return:
@@ -270,7 +321,8 @@ def adjust_position(shape, position, normalized=False, thickness=0):
270321
y1 = int(min(max(y1, thickness), y2 - thickness))
271322
return (x1, y1, x2, y2)
272323

273-
def select_zone(frame, position, tags=[], tag_position=None, alpha=0.9, color=(110,70,45), normalized=False, thickness=2, filled=False, peephole=True):
324+
def select_zone(frame, position, tags=[], tag_position=None, alpha=0.9, color=(110,70,45),
325+
normalized=False, thickness=2, filled=False, peephole=True, margin=5):
274326
"""Draw better rectangles to select zones.
275327
276328
Keyword arguments:
@@ -289,10 +341,12 @@ def select_zone(frame, position, tags=[], tag_position=None, alpha=0.9, color=(1
289341
1 means totally visible and 0 totally invisible
290342
color -- color of the selected zone, touple with 3 elements BGR (default (110,70,45) -> dark blue)
291343
BGR = Blue - Green - Red
292-
normalized -- boolean parameter, if True, position provided normalized (between 0 and 1) else you should provide concrete values (default False)
344+
normalized -- boolean parameter, if True, position provided normalized (between 0 and 1)
345+
else you should provide concrete values (default False)
293346
thickness -- thickness of the drawing in pixels (default 2)
294347
filled -- boolean parameter, if True, will draw a filled rectangle with one-third opacity compared to the rectangle (default False)
295348
peephole -- boolean parameter, if True, also draw additional effect, so it looks like a peephole
349+
margin -- extra margin in pixels to be separeted with the selected zone (default 5)
296350
297351
Return:
298352
A new drawed Frame
@@ -311,7 +365,56 @@ def select_zone(frame, position, tags=[], tag_position=None, alpha=0.9, color=(1
311365
cv2.rectangle(overlay, (x1, y1), (x2, y2), color,thickness=thickness)
312366
cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0, frame)
313367

314-
frame = add_tags(frame, position, tags, tag_position=tag_position)
368+
frame = add_tags(frame, position, tags, tag_position=tag_position, margin=margin)
369+
return frame
370+
371+
def select_multiple_zones(frame, all_selected_zones, all_tags=None, alpha=0.9, color=(110,70,45),
372+
normalized=False, thickness=2, filled=False, peephole=True, margin=5):
373+
"""Draw better rectangles to select multiple zones at the same time.
374+
It will put tags to the rectangles as better as possible, avoiding (if it is possible) overwritten information.
375+
376+
Keyword arguments:
377+
frame -- opencv frame object where you want to draw
378+
all_selected_zones -- list of touple with 4 elements (x1, y1, x2, y2)
379+
This elements must be between 0 and 1 in case it is normalized
380+
or between 0 and frame height/width in other case.
381+
all_tags -- list of lists of strings/tags you want to associate to the selected zone.
382+
The first element of the list is associated with the first element of all_selected_zones.
383+
Therefore, both lists should have the same lenght. (default None)
384+
alpha -- transparency of the selected zone on the image (default 0.9)
385+
1 means totally visible and 0 totally invisible
386+
color -- color of the selected zones, touple with 3 elements BGR (default (110,70,45) -> dark blue)
387+
BGR = Blue - Green - Red
388+
normalized -- boolean parameter, if True, position provided normalized (between 0 and 1)
389+
else you should provide concrete values (default False)
390+
thickness -- thickness of the drawing in pixels (default 2)
391+
filled -- boolean parameter, if True, will draw a filled rectangle with one-third opacity compared to the rectangle (default False)
392+
peephole -- boolean parameter, if True, also draw additional effect, so it looks like a peephole
393+
margin -- extra margin in pixels to be separeted with the selected zone (default 5)
394+
395+
Return:
396+
A new drawed Frame
397+
398+
"""
399+
all_selected_zones = [adjust_position(frame.shape[:2], zone, normalized=normalized, thickness=thickness) for zone in all_selected_zones]
400+
if not all_tags:
401+
for zone in all_selected_zones:
402+
frame = select_zone(frame, zone, alpha=alpha, color=color, normalized=normalized,
403+
thickness=thickness, filled=filled, peephole=peephole)
404+
else:
405+
f_height, f_width = frame.shape[:2]
406+
all_tags_shapes = []
407+
for i, zone in enumerate(all_selected_zones):
408+
all_tags_shapes.append(get_shape_tags(zone, all_tags[i], margin=margin))
409+
# Here you could pass the frame if you want to see where tags_constraint thinks the tags will be. Just: frame=frame \/
410+
best_position = tags_constraint.get_possible_positions(f_width, f_height, all_selected_zones, all_tags_shapes, margin=margin, frame=[])
411+
for i, zone in enumerate(all_selected_zones):
412+
if best_position:
413+
position = best_position[i]
414+
else:
415+
position = None
416+
frame = select_zone(frame, zone, tags=all_tags[i], tag_position=position, alpha=alpha, color=color,
417+
thickness=thickness, filled=filled, peephole=peephole, margin=margin)
315418
return frame
316419

317420
def webcam_test():
@@ -326,9 +429,12 @@ def webcam_test():
326429
frame = cv2.flip(frame, 1)
327430
if ret:
328431
keystroke = cv2.waitKey(1)
329-
position = (0.33,0.2,0.66,0.8)
330-
tags = ['MIT License', '(C) Copyright\n Fernando\n Perez\n Gutierrez']
331-
frame = select_zone(frame, position, tags=tags, color=(130,58,14), thickness=2, filled=True, normalized=True)
432+
position1 = (0.33,0.2,0.66,0.8)
433+
tags1 = ['MIT License', '(C) Copyright\n Fernando\n Perez\n Gutierrez\n hola\n', 'mundo']
434+
position2 = (0.22,0.4,0.3,0.88)
435+
tags2 = ['Release', 'v1.0.0']
436+
frame = select_multiple_zones(frame, [position1,position2], all_tags=[tags1,tags2], color=(14,28,200),
437+
thickness=2, filled=True, normalized=True)
332438
cv2.imshow(window_name, frame)
333439
# True if escape 'esc' is pressed
334440
if keystroke == 27:
@@ -343,6 +449,9 @@ def get_complete_help():
343449
* select_zone:
344450
{}
345451
452+
* select_multiple_zones:
453+
{}
454+
346455
* add_peephole:
347456
{}
348457
@@ -362,7 +471,7 @@ def get_complete_help():
362471
* webcam_test:
363472
{}
364473
365-
'''.format(select_zone.__doc__, add_peephole.__doc__,
474+
'''.format(select_zone.__doc__, select_multiple_zones.__doc__, add_peephole.__doc__,
366475
add_tags.__doc__, get_lighter_color.__doc__, adjust_position.__doc__,
367476
webcam_test.__doc__)
368477

opencv_draw_tools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
Copyright (c) 2019 Fernando Perez
77
For more information visit: https://github.com/fernaper/opencv-draw-tools
88
Also you can write complete_help to view full information'''
9-
__version__ = '0.2.0'
9+
__version__ = '1.0.0'
1010

1111
complete_help = '''
1212
{} - v{}

0 commit comments

Comments
 (0)