import cv2 import numpy as np from typing import List, Tuple from shapely.geometry import Polygon, MultiPoint from functools import cached_property import copy import re import py3langid as langid from .generic import color_difference, is_right_to_left_char, is_valuable_char # from ..detection.ctd_utils.utils.imgproc_utils import union_area, xywh2xyxypoly # LANG_LIST = ['eng', 'ja', 'unknown'] # LANGCLS2IDX = {'eng': 0, 'ja': 1, 'unknown': 2} # determines render direction LANGUAGE_ORIENTATION_PRESETS = { 'CHS': 'auto', 'CHT': 'auto', 'CSY': 'h', 'NLD': 'h', 'ENG': 'h', 'FRA': 'h', 'DEU': 'h', 'HUN': 'h', 'ITA': 'h', 'JPN': 'auto', 'KOR': 'auto', 'PLK': 'h', 'PTB': 'h', 'ROM': 'h', 'RUS': 'h', 'ESP': 'h', 'TRK': 'h', 'UKR': 'h', 'VIN': 'h', 'ARA': 'hr', # horizontal reversed (right to left) 'FIL': 'h' } class TextBlock(object): """ Object that stores a block of text made up of textlines. """ def __init__(self, lines: List, texts: List[str] = None, language: str = 'unknown', font_size: float = -1, angle: int = 0, translation: str = "", fg_color: Tuple[float] = (0, 0, 0), bg_color: Tuple[float] = (0, 0, 0), line_spacing = 1., letter_spacing = 1., font_family: str = "", bold: bool = False, underline: bool = False, italic: bool = False, direction: str = 'auto', alignment: str = 'auto', rich_text: str = "", _bounding_rect: List = None, default_stroke_width = 0.2, font_weight = 50, source_lang: str = "", target_lang: str = "", opacity: float = 1., shadow_radius: float = 0., shadow_strength: float = 1., shadow_color: Tuple = (0, 0, 0), shadow_offset: List = [0, 0], prob: float = 1, **kwargs) -> None: self.lines = np.array(lines, dtype=np.int32) # self.lines.sort() self.language = language self.font_size = round(font_size) self.angle = angle self._direction = direction self.texts = texts if texts is not None else [] self.text = texts[0] for txt in texts[1:] : first_cjk = '\u3000' <= self.text[-1] <= '\u9fff' second_cjk = '\u3000' <= txt[0] <= '\u9fff' if first_cjk or second_cjk : self.text += txt else : self.text += ' ' + txt self.prob = prob self.translation = translation self.fg_colors = fg_color self.bg_colors = bg_color # self.stroke_width = stroke_width self.font_family: str = font_family self.bold: bool = bold self.underline: bool = underline self.italic: bool = italic self.rich_text = rich_text self.line_spacing = line_spacing self.letter_spacing = letter_spacing self._alignment = alignment self._source_lang = source_lang self.target_lang = target_lang self._bounding_rect = _bounding_rect self.default_stroke_width = default_stroke_width self.font_weight = font_weight self.adjust_bg_color = True self.opacity = opacity self.shadow_radius = shadow_radius self.shadow_strength = shadow_strength self.shadow_color = shadow_color self.shadow_offset = shadow_offset @cached_property def xyxy(self): """Coordinates of the bounding box""" x1 = self.lines[..., 0].min() y1 = self.lines[..., 1].min() x2 = self.lines[..., 0].max() y2 = self.lines[..., 1].max() return np.array([x1, y1, x2, y2]).astype(np.int32) @cached_property def xywh(self): x1, y1, x2, y2 = self.xyxy return np.array([x1, y1, x2-x1, y2-y1]).astype(np.int32) @cached_property def center(self) -> np.ndarray: xyxy = np.array(self.xyxy) return (xyxy[:2] + xyxy[2:]) / 2 @cached_property def unrotated_polygons(self) -> np.ndarray: polygons = self.lines.reshape(-1, 8) if self.angle != 0: polygons = rotate_polygons(self.center, polygons, self.angle) return polygons @cached_property def unrotated_min_rect(self) -> np.ndarray: polygons = self.unrotated_polygons min_x = polygons[:, ::2].min() min_y = polygons[:, 1::2].min() max_x = polygons[:, ::2].max() max_y = polygons[:, 1::2].max() min_bbox = np.array([[min_x, min_y, max_x, min_y, max_x, max_y, min_x, max_y]]) return min_bbox.reshape(-1, 4, 2).astype(np.int64) @cached_property def min_rect(self) -> np.ndarray: polygons = self.unrotated_polygons min_x = polygons[:, ::2].min() min_y = polygons[:, 1::2].min() max_x = polygons[:, ::2].max() max_y = polygons[:, 1::2].max() min_bbox = np.array([[min_x, min_y, max_x, min_y, max_x, max_y, min_x, max_y]]) if self.angle != 0: min_bbox = rotate_polygons(self.center, min_bbox, -self.angle) return min_bbox.clip(0).reshape(-1, 4, 2).astype(np.int64) @cached_property def polygon_aspect_ratio(self) -> float: """width / height""" polygons = self.unrotated_polygons.reshape(-1, 4, 2) middle_pts = (polygons[:, [1, 2, 3, 0]] + polygons) / 2 norm_v = np.linalg.norm(middle_pts[:, 2] - middle_pts[:, 0], axis=1) norm_h = np.linalg.norm(middle_pts[:, 1] - middle_pts[:, 3], axis=1) return np.mean(norm_h / norm_v) @cached_property def unrotated_size(self) -> Tuple[int, int]: """Returns width and height of unrotated bbox""" middle_pts = (self.min_rect[:, [1, 2, 3, 0]] + self.min_rect) / 2 norm_h = np.linalg.norm(middle_pts[:, 1] - middle_pts[:, 3]) norm_v = np.linalg.norm(middle_pts[:, 2] - middle_pts[:, 0]) return norm_h, norm_v @cached_property def aspect_ratio(self) -> float: """width / height""" return self.unrotated_size[0] / self.unrotated_size[1] @property def polygon_object(self) -> Polygon: min_rect = self.min_rect[0] return MultiPoint([tuple(min_rect[0]), tuple(min_rect[1]), tuple(min_rect[2]), tuple(min_rect[3])]).convex_hull @property def area(self) -> float: return self.polygon_object.area @property def real_area(self) -> float: lines = self.lines.reshape((-1, 2)) return MultiPoint([tuple(l) for l in lines]).convex_hull.area def normalized_width_list(self) -> List[float]: polygons = self.unrotated_polygons width_list = [] for polygon in polygons: width_list.append((polygon[[2, 4]] - polygon[[0, 6]]).sum()) width_list = np.array(width_list) width_list = width_list / np.sum(width_list) return width_list.tolist() def __len__(self): return len(self.lines) def __getitem__(self, idx): return self.lines[idx] def to_dict(self): blk_dict = copy.deepcopy(vars(self)) return blk_dict def get_transformed_region(self, img: np.ndarray, line_idx: int, textheight: int, maxwidth: int = None) -> np.ndarray: src_pts = np.array(self.lines[line_idx], dtype=np.float64) middle_pnt = (src_pts[[1, 2, 3, 0]] + src_pts) / 2 vec_v = middle_pnt[2] - middle_pnt[0] # vertical vectors of textlines vec_h = middle_pnt[1] - middle_pnt[3] # horizontal vectors of textlines ratio = np.linalg.norm(vec_v) / np.linalg.norm(vec_h) if ratio < 1: h = int(textheight) w = int(round(textheight / ratio)) dst_pts = np.array([[0, 0], [w - 1, 0], [w - 1, h - 1], [0, h - 1]]).astype(np.float32) M, _ = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) region = cv2.warpPerspective(img, M, (w, h)) else: w = int(textheight) h = int(round(textheight * ratio)) dst_pts = np.array([[0, 0], [w - 1, 0], [w - 1, h - 1], [0, h - 1]]).astype(np.float32) M, _ = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) region = cv2.warpPerspective(img, M, (w, h)) region = cv2.rotate(region, cv2.ROTATE_90_COUNTERCLOCKWISE) if maxwidth is not None: h, w = region.shape[: 2] if w > maxwidth: region = cv2.resize(region, (maxwidth, h)) return region @property def source_lang(self): if not self._source_lang: self._source_lang = langid.classify(self.text)[0] return self._source_lang def get_translation_for_rendering(self): text = self.translation if self.direction.endswith('r'): # The render direction is right to left so left-to-right # text/number chunks need to be reversed to look normal. text_list = list(text) l2r_idx = -1 def reverse_sublist(l, i1, i2): delta = i2 - i1 for j1 in range(i1, i2 - delta // 2): j2 = i2 - (j1 - i1) - 1 l[j1], l[j2] = l[j2], l[j1] for i, c in enumerate(text): if not is_right_to_left_char(c) and is_valuable_char(c): if l2r_idx < 0: l2r_idx = i elif l2r_idx >= 0 and i - l2r_idx > 1: # Reverse left-to-right characters for correct rendering reverse_sublist(text_list, l2r_idx, i) l2r_idx = -1 if l2r_idx >= 0 and i - l2r_idx > 1: reverse_sublist(text_list, l2r_idx, len(text_list)) text = ''.join(text_list) return text @property def is_bulleted_list(self): """ A determining factor of whether we should be sticking to the strict per textline text distribution when rendering. """ if len(self.texts) <= 1: return False bullet_regexes = [ r'[^\w\s]', # ○ ... ○ ... r'[\d]+\.', # 1. ... 2. ... r'[QA]:', # Q: ... A: ... ] bullet_type_idx = -1 for line_text in self.texts: for i, breg in enumerate(bullet_regexes): if re.search(r'(?:[\n]|^)((?:' + breg + r')[\s]*)', line_text): if bullet_type_idx >= 0 and bullet_type_idx != i: return False bullet_type_idx = i return bullet_type_idx >= 0 def set_font_colors(self, fg_colors, bg_colors): self.fg_colors = np.array(fg_colors) self.bg_colors = np.array(bg_colors) def update_font_colors(self, fg_colors: np.ndarray, bg_colors: np.ndarray): nlines = len(self) if nlines > 0: self.fg_colors += fg_colors / nlines self.bg_colors += bg_colors / nlines def get_font_colors(self, bgr=False): frgb = np.array(self.fg_colors).astype(np.int32) brgb = np.array(self.bg_colors).astype(np.int32) if bgr: frgb = frgb[::-1] brgb = brgb[::-1] if self.adjust_bg_color: fg_avg = np.mean(frgb) if color_difference(frgb, brgb) < 30: brgb = (255, 255, 255) if fg_avg <= 127 else (0, 0, 0) return frgb, brgb @property def direction(self): """Render direction determined through used language or aspect ratio.""" if self._direction not in ('h', 'v', 'hr', 'vr'): d = LANGUAGE_ORIENTATION_PRESETS.get(self.target_lang) if d in ('h', 'v', 'hr', 'vr'): return d if self.aspect_ratio < 1: return 'v' else: return 'h' return self._direction @property def vertical(self): return self.direction.startswith('v') @property def horizontal(self): return self.direction.startswith('h') @property def alignment(self): """Render alignment(/gravity) determined through used language.""" if self._alignment in ('left', 'center', 'right'): return self._alignment if len(self.lines) == 1: return 'center' if self.direction == 'h': return 'center' elif self.direction == 'hr': return 'right' else: return 'left' # x1, y1, x2, y2 = self.xyxy # polygons = self.unrotated_polygons # polygons = polygons.reshape(-1, 4, 2) # print(self.polygon_aspect_ratio, self.xyxy) # print(polygons[:, :, 0] - x1) # print() # if self.polygon_aspect_ratio < 1: # left_std = abs(np.std(polygons[:, :2, 1] - y1)) # right_std = abs(np.std(polygons[:, 2:, 1] - y2)) # center_std = abs(np.std(((polygons[:, :, 1] + polygons[:, :, 1]) - (y2 - y1)) / 2)) # print(center_std) # print('a', left_std, right_std, center_std) # else: # left_std = abs(np.std(polygons[:, ::2, 0] - x1)) # right_std = abs(np.std(polygons[:, 2:, 0] - x2)) # center_std = abs(np.std(((polygons[:, :, 0] + polygons[:, :, 0]) - (x2 - x1)) / 2)) # min_std = min(left_std, right_std, center_std) # if left_std == min_std: # return 'left' # elif right_std == min_std: # return 'right' # else: # return 'center' @property def stroke_width(self): diff = color_difference(*self.get_font_colors()) if diff > 15: return self.default_stroke_width return 0 def rotate_polygons(center, polygons, rotation, new_center=None, to_int=True): if rotation == 0: return polygons if new_center is None: new_center = center rotation = np.deg2rad(rotation) s, c = np.sin(rotation), np.cos(rotation) polygons = polygons.astype(np.float32) polygons[:, 1::2] -= center[1] polygons[:, ::2] -= center[0] rotated = np.copy(polygons) rotated[:, 1::2] = polygons[:, 1::2] * c - polygons[:, ::2] * s rotated[:, ::2] = polygons[:, 1::2] * s + polygons[:, ::2] * c rotated[:, 1::2] += new_center[1] rotated[:, ::2] += new_center[0] if to_int: return rotated.astype(np.int64) return rotated def sort_regions(regions: List[TextBlock], right_to_left=True) -> List[TextBlock]: # Sort regions from right to left, top to bottom sorted_regions = [] for region in sorted(regions, key=lambda region: region.center[1]): for i, sorted_region in enumerate(sorted_regions): if region.center[1] > sorted_region.xyxy[3]: continue if region.center[1] < sorted_region.xyxy[1]: sorted_regions.insert(i + 1, region) break # y center of region inside sorted_region so sort by x instead if right_to_left and region.center[0] > sorted_region.center[0]: sorted_regions.insert(i, region) break if not right_to_left and region.center[0] < sorted_region.center[0]: sorted_regions.insert(i, region) break else: sorted_regions.append(region) return sorted_regions # def sort_textblk_list(blk_list: List[TextBlock], im_w: int, im_h: int) -> List[TextBlock]: # if len(blk_list) == 0: # return blk_list # num_ja = 0 # xyxy = [] # for blk in blk_list: # if blk.language == 'ja': # num_ja += 1 # xyxy.append(blk.xyxy) # xyxy = np.array(xyxy) # flip_lr = num_ja > len(blk_list) / 2 # im_oriw = im_w # if im_w > im_h: # im_w /= 2 # num_gridy, num_gridx = 4, 3 # img_area = im_h * im_w # center_x = (xyxy[:, 0] + xyxy[:, 2]) / 2 # if flip_lr: # if im_w != im_oriw: # center_x = im_oriw - center_x # else: # center_x = im_w - center_x # grid_x = (center_x / im_w * num_gridx).astype(np.int32) # center_y = (xyxy[:, 1] + xyxy[:, 3]) / 2 # grid_y = (center_y / im_h * num_gridy).astype(np.int32) # grid_indices = grid_y * num_gridx + grid_x # grid_weights = grid_indices * img_area + 1.2 * (center_x - grid_x * im_w / num_gridx) + (center_y - grid_y * im_h / num_gridy) # if im_w != im_oriw: # grid_weights[np.where(grid_x >= num_gridx)] += img_area * num_gridy * num_gridx # for blk, weight in zip(blk_list, grid_weights): # blk.sort_weight = weight # blk_list.sort(key=lambda blk: blk.sort_weight) # return blk_list # # TODO: Make these cached_properties # def examine_textblk(blk: TextBlock, im_w: int, im_h: int, sort: bool = False) -> None: # lines = blk.lines_array() # middle_pnts = (lines[:, [1, 2, 3, 0]] + lines) / 2 # vec_v = middle_pnts[:, 2] - middle_pnts[:, 0] # vertical vectors of textlines # vec_h = middle_pnts[:, 1] - middle_pnts[:, 3] # horizontal vectors of textlines # # if sum of vertical vectors is longer, then text orientation is vertical, and vice versa. # center_pnts = (lines[:, 0] + lines[:, 2]) / 2 # v = np.sum(vec_v, axis=0) # h = np.sum(vec_h, axis=0) # norm_v, norm_h = np.linalg.norm(v), np.linalg.norm(h) # if blk.language == 'ja': # vertical = norm_v > norm_h # else: # vertical = norm_v > norm_h * 2 # # calculate distance between textlines and origin # if vertical: # primary_vec, primary_norm = v, norm_v # distance_vectors = center_pnts - np.array([[im_w, 0]], dtype=np.float64) # vertical manga text is read from right to left, so origin is (imw, 0) # font_size = int(round(norm_h / len(lines))) # else: # primary_vec, primary_norm = h, norm_h # distance_vectors = center_pnts - np.array([[0, 0]], dtype=np.float64) # font_size = int(round(norm_v / len(lines))) # rotation_angle = int(math.atan2(primary_vec[1], primary_vec[0]) / math.pi * 180) # rotation angle of textlines # distance = np.linalg.norm(distance_vectors, axis=1) # distance between textlinecenters and origin # rad_matrix = np.arccos(np.einsum('ij, j->i', distance_vectors, primary_vec) / (distance * primary_norm)) # distance = np.abs(np.sin(rad_matrix) * distance) # blk.lines = lines.astype(np.int32).tolist() # blk.distance = distance # blk.angle = rotation_angle # if vertical: # blk.angle -= 90 # if abs(blk.angle) < 3: # blk.angle = 0 # blk.font_size = font_size # blk.vertical = vertical # blk.vec = primary_vec # blk.norm = primary_norm # if sort: # blk.sort_lines() # def try_merge_textline(blk: TextBlock, blk2: TextBlock, fntsize_tol=1.4, distance_tol=2) -> bool: # if blk2.merged: # return False # fntsize_div = blk.font_size / blk2.font_size # num_l1, num_l2 = len(blk), len(blk2) # fntsz_avg = (blk.font_size * num_l1 + blk2.font_size * num_l2) / (num_l1 + num_l2) # vec_prod = blk.vec @ blk2.vec # vec_sum = blk.vec + blk2.vec # cos_vec = vec_prod / blk.norm / blk2.norm # distance = blk2.distance[-1] - blk.distance[-1] # distance_p1 = np.linalg.norm(np.array(blk2.lines[-1][0]) - np.array(blk.lines[-1][0])) # l1, l2 = Polygon(blk.lines[-1]), Polygon(blk2.lines[-1]) # if not l1.intersects(l2): # if fntsize_div > fntsize_tol or 1 / fntsize_div > fntsize_tol: # return False # if abs(cos_vec) < 0.866: # cos30 # return False # # if distance > distance_tol * fntsz_avg or distance_p1 > fntsz_avg * 2.5: # if distance > distance_tol * fntsz_avg: # return False # if blk.vertical and blk2.vertical and distance_p1 > fntsz_avg * 2.5: # return False # # merge # blk.lines.append(blk2.lines[0]) # blk.vec = vec_sum # blk.angle = int(round(np.rad2deg(math.atan2(vec_sum[1], vec_sum[0])))) # if blk.vertical: # blk.angle -= 90 # blk.norm = np.linalg.norm(vec_sum) # blk.distance = np.append(blk.distance, blk2.distance[-1]) # blk.font_size = fntsz_avg # blk2.merged = True # return True # def merge_textlines(blk_list: List[TextBlock]) -> List[TextBlock]: # if len(blk_list) < 2: # return blk_list # blk_list.sort(key=lambda blk: blk.distance[0]) # merged_list = [] # for ii, current_blk in enumerate(blk_list): # if current_blk.merged: # continue # for jj, blk in enumerate(blk_list[ii+1:]): # try_merge_textline(current_blk, blk) # merged_list.append(current_blk) # for blk in merged_list: # blk.adjust_bbox(with_bbox=False) # return merged_list # def split_textblk(blk: TextBlock): # font_size, distance, lines = blk.font_size, blk.distance, blk.lines # l0 = np.array(blk.lines[0]) # lines.sort(key=lambda line: np.linalg.norm(np.array(line[0]) - l0[0])) # distance_tol = font_size * 2 # current_blk = copy.deepcopy(blk) # current_blk.lines = [l0] # sub_blk_list = [current_blk] # textblock_splitted = False # for jj, line in enumerate(lines[1:]): # l1, l2 = Polygon(lines[jj]), Polygon(line) # split = False # if not l1.intersects(l2): # line_disance = abs(distance[jj+1] - distance[jj]) # if line_disance > distance_tol: # split = True # elif blk.vertical and abs(blk.angle) < 15: # if len(current_blk.lines) > 1 or line_disance > font_size: # split = abs(lines[jj][0][1] - line[0][1]) > font_size # if split: # current_blk = copy.deepcopy(current_blk) # current_blk.lines = [line] # sub_blk_list.append(current_blk) # else: # current_blk.lines.append(line) # if len(sub_blk_list) > 1: # textblock_splitted = True # for current_blk in sub_blk_list: # current_blk.adjust_bbox(with_bbox=False) # return textblock_splitted, sub_blk_list # def group_output(blks, lines, im_w, im_h, mask=None, sort_blklist=True) -> List[TextBlock]: # blk_list: List[TextBlock] = [] # scattered_lines = {'ver': [], 'hor': []} # for bbox, lang_id, conf in zip(*blks): # # cls could give wrong result # blk_list.append(TextBlock(bbox, language=LANG_LIST[lang_id])) # # step1: filter & assign lines to textblocks # bbox_score_thresh = 0.4 # mask_score_thresh = 0.1 # for line in lines: # bx1, bx2 = line[:, 0].min(), line[:, 0].max() # by1, by2 = line[:, 1].min(), line[:, 1].max() # bbox_score, bbox_idx = -1, -1 # line_area = (by2-by1) * (bx2-bx1) # for i, blk in enumerate(blk_list): # score = union_area(blk.xyxy, [bx1, by1, bx2, by2]) / line_area # if bbox_score < score: # bbox_score = score # bbox_idx = i # if bbox_score > bbox_score_thresh: # blk_list[bbox_idx].lines.append(line) # else: # if no textblock was assigned, check whether there is "enough" textmask # if mask is not None: # mask_score = mask[by1: by2, bx1: bx2].mean() / 255 # if mask_score < mask_score_thresh: # continue # blk = TextBlock([bx1, by1, bx2, by2], [line]) # examine_textblk(blk, im_w, im_h, sort=False) # if blk.vertical: # scattered_lines['ver'].append(blk) # else: # scattered_lines['hor'].append(blk) # # step2: filter textblocks, sort & split textlines # final_blk_list = [] # for blk in blk_list: # # filter textblocks # if len(blk.lines) == 0: # bx1, by1, bx2, by2 = blk.xyxy # if mask is not None: # mask_score = mask[by1: by2, bx1: bx2].mean() / 255 # if mask_score < mask_score_thresh: # continue # xywh = np.array([[bx1, by1, bx2-bx1, by2-by1]]) # blk.lines = xywh2xyxypoly(xywh).reshape(-1, 4, 2).tolist() # examine_textblk(blk, im_w, im_h, sort=True) # # split manga text if there is a distance gap # textblock_splitted = False # if len(blk.lines) > 1: # if blk.language == 'ja': # textblock_splitted = True # elif blk.vertical: # textblock_splitted = True # if textblock_splitted: # textblock_splitted, sub_blk_list = split_textblk(blk) # else: # sub_blk_list = [blk] # # modify textblock to fit its textlines # if not textblock_splitted: # for blk in sub_blk_list: # blk.adjust_bbox(with_bbox=True) # final_blk_list += sub_blk_list # # step3: merge scattered lines, sort textblocks by "grid" # final_blk_list += merge_textlines(scattered_lines['hor']) # final_blk_list += merge_textlines(scattered_lines['ver']) # if sort_blklist: # final_blk_list = sort_textblk_list(final_blk_list, im_w, im_h) # for blk in final_blk_list: # if blk.language != 'ja' and not blk.vertical: # num_lines = len(blk.lines) # if num_lines == 0: # continue # # blk.line_spacing = blk.bounding_rect()[3] / num_lines / blk.font_size # expand_size = max(int(blk.font_size * 0.1), 3) # rad = np.deg2rad(blk.angle) # shifted_vec = np.array([[[-1, -1],[1, -1],[1, 1],[-1, 1]]]) # shifted_vec = shifted_vec * np.array([[[np.sin(rad), np.cos(rad)]]]) * expand_size # lines = blk.lines_array() + shifted_vec # lines[..., 0] = np.clip(lines[..., 0], 0, im_w-1) # lines[..., 1] = np.clip(lines[..., 1], 0, im_h-1) # blk.lines = lines.astype(np.int64).tolist() # blk.font_size += expand_size # return final_blk_list def visualize_textblocks(canvas, blk_list: List[TextBlock]): lw = max(round(sum(canvas.shape) / 2 * 0.003), 2) # line width for i, blk in enumerate(blk_list): bx1, by1, bx2, by2 = blk.xyxy cv2.rectangle(canvas, (bx1, by1), (bx2, by2), (127, 255, 127), lw) for j, line in enumerate(blk.lines): cv2.putText(canvas, str(j), line[0], cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,127,0), 1) cv2.polylines(canvas, [line], True, (0,127,255), 2) cv2.polylines(canvas, [blk.min_rect], True, (127,127,0), 2) cv2.putText(canvas, str(i), (bx1, by1 + lw), 0, lw / 3, (255,127,127), max(lw-1, 1), cv2.LINE_AA) center = [int((bx1 + bx2)/2), int((by1 + by2)/2)] cv2.putText(canvas, 'a: %.2f' % blk.angle, [bx1, center[1]], cv2.FONT_HERSHEY_SIMPLEX, 1, (127,127,255), 2) cv2.putText(canvas, 'x: %s' % bx1, [bx1, center[1] + 30], cv2.FONT_HERSHEY_SIMPLEX, 1, (127,127,255), 2) cv2.putText(canvas, 'y: %s' % by1, [bx1, center[1] + 60], cv2.FONT_HERSHEY_SIMPLEX, 1, (127,127,255), 2) return canvas