Sunday01's picture
up
9dce458
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