ScreenSubtitleReader 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ScreenSubtitleReader/__init__.py +1 -0
- ScreenSubtitleReader/__main__.py +10 -0
- ScreenSubtitleReader/core/__init__.py +0 -0
- ScreenSubtitleReader/core/__main__.py +10 -0
- ScreenSubtitleReader/core/ocr_fither.py +72 -0
- ScreenSubtitleReader/core/screen_reader.py +32 -0
- ScreenSubtitleReader/core/string_fither.py +5 -0
- ScreenSubtitleReader/ocr_module/__init__.py +1 -0
- ScreenSubtitleReader/ocr_module/angnet/__init__.py +1 -0
- ScreenSubtitleReader/ocr_module/angnet/angle.py +59 -0
- ScreenSubtitleReader/ocr_module/config.py +34 -0
- ScreenSubtitleReader/ocr_module/crnn/CRNN.py +102 -0
- ScreenSubtitleReader/ocr_module/crnn/__init__.py +1 -0
- ScreenSubtitleReader/ocr_module/crnn/keys.py +1 -0
- ScreenSubtitleReader/ocr_module/crnn/util.py +89 -0
- ScreenSubtitleReader/ocr_module/dbnet/dbnet_infer.py +97 -0
- ScreenSubtitleReader/ocr_module/dbnet/decode.py +125 -0
- ScreenSubtitleReader/ocr_module/model.py +128 -0
- ScreenSubtitleReader/ocr_module/models/__init__.py +1 -0
- ScreenSubtitleReader/ocr_module/models/angle_net.onnx +0 -0
- ScreenSubtitleReader/ocr_module/models/crnn_lite_lstm.onnx +0 -0
- ScreenSubtitleReader/ocr_module/models/dbnet.onnx +0 -0
- ScreenSubtitleReader/ocr_module/utils.py +173 -0
- ScreenSubtitleReader/setting/setting.py +98 -0
- ScreenSubtitleReader/speaker/speaker.py +60 -0
- ScreenSubtitleReader/ui/ui.py +101 -0
- screensubtitlereader-0.1.3.dist-info/METADATA +21 -0
- screensubtitlereader-0.1.3.dist-info/RECORD +31 -0
- screensubtitlereader-0.1.3.dist-info/WHEEL +5 -0
- screensubtitlereader-0.1.3.dist-info/entry_points.txt +2 -0
- screensubtitlereader-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pyclipper
|
|
4
|
+
from shapely.geometry import Polygon
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SegDetectorRepresenter:
|
|
8
|
+
def __init__(self, thresh=0.3, box_thresh=0.5, max_candidates=1000, unclip_ratio=2.0):
|
|
9
|
+
self.min_size = 3
|
|
10
|
+
self.thresh = thresh
|
|
11
|
+
self.box_thresh = box_thresh
|
|
12
|
+
self.max_candidates = max_candidates
|
|
13
|
+
self.unclip_ratio = unclip_ratio
|
|
14
|
+
|
|
15
|
+
def __call__(self, pred, height, width):
|
|
16
|
+
"""
|
|
17
|
+
batch: (image, polygons, ignore_tags
|
|
18
|
+
batch: a dict produced by dataloaders.
|
|
19
|
+
image: tensor of shape (N, C, H, W).
|
|
20
|
+
polygons: tensor of shape (N, K, 4, 2), the polygons of objective regions.
|
|
21
|
+
ignore_tags: tensor of shape (N, K), indicates whether a region is ignorable or not.
|
|
22
|
+
shape: the original shape of images.
|
|
23
|
+
filename: the original filenames of images.
|
|
24
|
+
pred:
|
|
25
|
+
binary: text region segmentation map, with shape (N, H, W)
|
|
26
|
+
thresh: [if exists] thresh hold prediction with shape (N, H, W)
|
|
27
|
+
thresh_binary: [if exists] binarized with threshhold, (N, H, W)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
pred = pred[0, :, :]
|
|
31
|
+
segmentation = self.binarize(pred)
|
|
32
|
+
|
|
33
|
+
boxes, scores = self.boxes_from_bitmap(pred, segmentation, width, height)
|
|
34
|
+
|
|
35
|
+
return boxes, scores
|
|
36
|
+
|
|
37
|
+
def binarize(self, pred):
|
|
38
|
+
return pred > self.thresh
|
|
39
|
+
|
|
40
|
+
def boxes_from_bitmap(self, pred, bitmap, dest_width, dest_height):
|
|
41
|
+
"""
|
|
42
|
+
_bitmap: single map with shape (H, W),
|
|
43
|
+
whose values are binarized as {0, 1}
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
assert len(bitmap.shape) == 2
|
|
47
|
+
# bitmap = _bitmap.cpu().numpy() # The first channel
|
|
48
|
+
# pred = pred.cpu().detach().numpy()
|
|
49
|
+
height, width = bitmap.shape
|
|
50
|
+
# print(bitmap)
|
|
51
|
+
# cv2.imwrite("./test/output/test.jpg",(bitmap * 255).astype(np.uint8))
|
|
52
|
+
contours, _ = cv2.findContours((bitmap * 255).astype(np.uint8), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
|
|
53
|
+
num_contours = min(len(contours), self.max_candidates)
|
|
54
|
+
boxes = np.zeros((num_contours, 4, 2), dtype=np.int16)
|
|
55
|
+
scores = np.zeros((num_contours,), dtype=np.float32)
|
|
56
|
+
rects = []
|
|
57
|
+
for index in range(num_contours):
|
|
58
|
+
contour = contours[index].squeeze(1)
|
|
59
|
+
points, sside = self.get_mini_boxes(contour)
|
|
60
|
+
if sside < self.min_size:
|
|
61
|
+
continue
|
|
62
|
+
points = np.array(points)
|
|
63
|
+
score = self.box_score_fast(pred, contour)
|
|
64
|
+
if self.box_thresh > score:
|
|
65
|
+
continue
|
|
66
|
+
# print(points)
|
|
67
|
+
box = self.unclip(points, unclip_ratio=self.unclip_ratio).reshape(-1, 1, 2)
|
|
68
|
+
# print(box)
|
|
69
|
+
box, sside = self.get_mini_boxes(box)
|
|
70
|
+
if sside < self.min_size + 2:
|
|
71
|
+
continue
|
|
72
|
+
box = np.array(box)
|
|
73
|
+
if not isinstance(dest_width, int):
|
|
74
|
+
dest_width = dest_width.item()
|
|
75
|
+
dest_height = dest_height.item()
|
|
76
|
+
|
|
77
|
+
box[:, 0] = np.clip(np.round(box[:, 0] / width * dest_width), 0, dest_width)
|
|
78
|
+
box[:, 1] = np.clip(np.round(box[:, 1] / height * dest_height), 0, dest_height)
|
|
79
|
+
boxes[index, :, :] = box.astype(np.int16)
|
|
80
|
+
scores[index] = score
|
|
81
|
+
return boxes, scores
|
|
82
|
+
|
|
83
|
+
def unclip(self, box, unclip_ratio=1.5):
|
|
84
|
+
poly = Polygon(box)
|
|
85
|
+
|
|
86
|
+
distance = poly.area * unclip_ratio / (poly.length )
|
|
87
|
+
offset = pyclipper.PyclipperOffset()
|
|
88
|
+
offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)
|
|
89
|
+
expanded = np.array(offset.Execute(distance))
|
|
90
|
+
return expanded
|
|
91
|
+
|
|
92
|
+
def get_mini_boxes(self, contour):
|
|
93
|
+
bounding_box = cv2.minAreaRect(contour)
|
|
94
|
+
points = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0])
|
|
95
|
+
|
|
96
|
+
index_1, index_2, index_3, index_4 = 0, 1, 2, 3
|
|
97
|
+
if points[1][1] > points[0][1]:
|
|
98
|
+
index_1 = 0
|
|
99
|
+
index_4 = 1
|
|
100
|
+
else:
|
|
101
|
+
index_1 = 1
|
|
102
|
+
index_4 = 0
|
|
103
|
+
if points[3][1] > points[2][1]:
|
|
104
|
+
index_2 = 2
|
|
105
|
+
index_3 = 3
|
|
106
|
+
else:
|
|
107
|
+
index_2 = 3
|
|
108
|
+
index_3 = 2
|
|
109
|
+
|
|
110
|
+
box = [points[index_1], points[index_2], points[index_3], points[index_4]]
|
|
111
|
+
return box, min(bounding_box[1])
|
|
112
|
+
|
|
113
|
+
def box_score_fast(self, bitmap, _box):
|
|
114
|
+
h, w = bitmap.shape[:2]
|
|
115
|
+
box = _box.copy()
|
|
116
|
+
xmin = np.clip(np.floor(box[:, 0].min()).astype(int), 0, w - 1)
|
|
117
|
+
xmax = np.clip(np.ceil(box[:, 0].max()).astype(int), 0, w - 1)
|
|
118
|
+
ymin = np.clip(np.floor(box[:, 1].min()).astype(int), 0, h - 1)
|
|
119
|
+
ymax = np.clip(np.ceil(box[:, 1].max()).astype(int), 0, h - 1)
|
|
120
|
+
|
|
121
|
+
mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8)
|
|
122
|
+
box[:, 0] = box[:, 0] - xmin
|
|
123
|
+
box[:, 1] = box[:, 1] - ymin
|
|
124
|
+
cv2.fillPoly(mask, box.reshape(1, -1, 2).astype(np.int32), 1)
|
|
125
|
+
return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from .config import *
|
|
2
|
+
from .crnn import CRNNHandle
|
|
3
|
+
from .angnet import AngleNetHandle
|
|
4
|
+
from .utils import draw_bbox, crop_rect, sorted_boxes, get_rotate_crop_image
|
|
5
|
+
from PIL import Image
|
|
6
|
+
import numpy as np
|
|
7
|
+
import cv2
|
|
8
|
+
import copy
|
|
9
|
+
from .dbnet.dbnet_infer import DBNET
|
|
10
|
+
import time
|
|
11
|
+
import traceback
|
|
12
|
+
|
|
13
|
+
class OcrHandle(object):
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.text_handle = DBNET(model_path)
|
|
16
|
+
self.crnn_handle = CRNNHandle(crnn_model_path)
|
|
17
|
+
if angle_detect:
|
|
18
|
+
self.angle_handle = AngleNetHandle(angle_net_path)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def crnnRecWithBox(self,im, boxes_list,score_list):
|
|
22
|
+
"""
|
|
23
|
+
crnn模型,ocr识别
|
|
24
|
+
@@model,
|
|
25
|
+
@@converter,
|
|
26
|
+
@@im:Array
|
|
27
|
+
@@text_recs:text box
|
|
28
|
+
@@ifIm:是否输出box对应的img
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
results = []
|
|
32
|
+
boxes_list = sorted_boxes(np.array(boxes_list))
|
|
33
|
+
|
|
34
|
+
line_imgs = []
|
|
35
|
+
for index, (box, score) in enumerate(zip(boxes_list[:angle_detect_num], score_list[:angle_detect_num])):
|
|
36
|
+
tmp_box = copy.deepcopy(box)
|
|
37
|
+
partImg_array = get_rotate_crop_image(im, tmp_box.astype(np.float32))
|
|
38
|
+
partImg = Image.fromarray(partImg_array).convert("RGB")
|
|
39
|
+
line_imgs.append(partImg)
|
|
40
|
+
|
|
41
|
+
angle_res = False
|
|
42
|
+
if angle_detect:
|
|
43
|
+
angle_res = self.angle_handle.predict_rbgs(line_imgs)
|
|
44
|
+
|
|
45
|
+
count = 1
|
|
46
|
+
for index, (box ,score) in enumerate(zip(boxes_list,score_list)):
|
|
47
|
+
|
|
48
|
+
tmp_box = copy.deepcopy(box)
|
|
49
|
+
partImg_array = get_rotate_crop_image(im, tmp_box.astype(np.float32))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
partImg = Image.fromarray(partImg_array).convert("RGB")
|
|
53
|
+
|
|
54
|
+
if angle_detect and angle_res:
|
|
55
|
+
partImg = partImg.rotate(180)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if not is_rgb:
|
|
59
|
+
partImg = partImg.convert('L')
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
if is_rgb:
|
|
63
|
+
simPred = self.crnn_handle.predict_rbg(partImg) ##识别的文本
|
|
64
|
+
else:
|
|
65
|
+
simPred = self.crnn_handle.predict(partImg) ##识别的文本
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(traceback.format_exc())
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if simPred.strip() != '':
|
|
71
|
+
results.append([tmp_box,simPred,score])
|
|
72
|
+
count += 1
|
|
73
|
+
|
|
74
|
+
return results
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def text_predict(self,img,short_size):
|
|
78
|
+
boxes_list, score_list = self.text_handle.process(np.asarray(img).astype(np.uint8),short_size=short_size)
|
|
79
|
+
result = self.crnnRecWithBox(np.array(img), boxes_list,score_list)
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
from PIL import ImageGrab
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
print("正在初始化 OCR 模型,请稍候...")
|
|
89
|
+
try:
|
|
90
|
+
ocr = OcrHandle()
|
|
91
|
+
print("✅ 模型初始化成功!")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"❌ 模型初始化失败。\n错误信息: {e}")
|
|
94
|
+
exit(1)
|
|
95
|
+
|
|
96
|
+
print("📸 正在截取屏幕...")
|
|
97
|
+
# 使用 ImageGrab 截取全屏
|
|
98
|
+
im = ImageGrab.grab()
|
|
99
|
+
|
|
100
|
+
# 【关键修正】颜色通道转换:
|
|
101
|
+
# ImageGrab 截取的是 RGB 格式的 PIL 图像,而原项目默认使用 cv2.imread (BGR 格式)。
|
|
102
|
+
# 为了保证模型识别效果不出偏差,这里将其转为 OpenCV 标准的 BGR numpy 数组
|
|
103
|
+
img = cv2.cvtColor(np.array(im), cv2.COLOR_RGB2BGR)
|
|
104
|
+
|
|
105
|
+
print("🔍 开始执行 OCR 识别...")
|
|
106
|
+
start_time = time.time()
|
|
107
|
+
|
|
108
|
+
# 调用识别接口 (如果是全屏高分辨率截图,short_size 可以保持 960,或者视情况调高)
|
|
109
|
+
results = ocr.text_predict(img, short_size=960)
|
|
110
|
+
|
|
111
|
+
cost_time = time.time() - start_time
|
|
112
|
+
print(f"✅ 识别完成!耗时: {cost_time:.3f} 秒")
|
|
113
|
+
print("=" * 50)
|
|
114
|
+
|
|
115
|
+
if not results:
|
|
116
|
+
print("未能识别到任何文字。")
|
|
117
|
+
else:
|
|
118
|
+
print("部分识别结果如下 (最多显示前20条):")
|
|
119
|
+
# 截屏识别出来的文本通常很多,测试时为了清爽只打印前 20 条
|
|
120
|
+
for i, res in enumerate(results[:20]):
|
|
121
|
+
text = res[1]
|
|
122
|
+
score = res[2]
|
|
123
|
+
print(f"文本: {text.strip()} | 置信度: {score:.4f}")
|
|
124
|
+
|
|
125
|
+
if len(results) > 20:
|
|
126
|
+
print(f"... (还有 {len(results) - 20} 条未显示)")
|
|
127
|
+
|
|
128
|
+
print("=" * 50)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Packaged ONNX model files for chineseocr_lite."""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import cv2
|
|
3
|
+
from PIL import Image
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def rotate_cut_img(im, degree, x_center, y_center, w, h, leftAdjust=False, rightAdjust=False, alph=0.2):
|
|
7
|
+
# degree_ = degree * 180.0 / np.pi
|
|
8
|
+
# print(degree_)
|
|
9
|
+
right = 0
|
|
10
|
+
left = 0
|
|
11
|
+
if rightAdjust:
|
|
12
|
+
right = 1
|
|
13
|
+
if leftAdjust:
|
|
14
|
+
left = 1
|
|
15
|
+
|
|
16
|
+
box = (max(1, x_center - w / 2 - left * alph * (w / 2))
|
|
17
|
+
, y_center - h / 2, # ymin
|
|
18
|
+
min(x_center + w / 2 + right * alph * (w / 2), im.size[0] - 1)
|
|
19
|
+
, y_center + h / 2) # ymax
|
|
20
|
+
|
|
21
|
+
newW = box[2] - box[0]
|
|
22
|
+
newH = box[3] - box[1]
|
|
23
|
+
tmpImg = im.rotate(degree, center=(x_center, y_center)).crop(box)
|
|
24
|
+
|
|
25
|
+
return tmpImg, newW, newH
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def crop_rect(img, rect, alph=0.15):
|
|
29
|
+
img = np.asarray(img)
|
|
30
|
+
# get the parameter of the small rectangle
|
|
31
|
+
# print("rect!")
|
|
32
|
+
# print(rect)
|
|
33
|
+
center, size, angle = rect[0], rect[1], rect[2]
|
|
34
|
+
min_size = min(size)
|
|
35
|
+
|
|
36
|
+
if angle > -45:
|
|
37
|
+
center, size = tuple(map(int, center)), tuple(map(int, size))
|
|
38
|
+
# angle-=270
|
|
39
|
+
size = (int(size[0] + min_size * alph), int(size[1] + min_size * alph))
|
|
40
|
+
height, width = img.shape[0], img.shape[1]
|
|
41
|
+
M = cv2.getRotationMatrix2D(center, angle, 1)
|
|
42
|
+
# size = tuple([int(rect[1][1]), int(rect[1][0])])
|
|
43
|
+
img_rot = cv2.warpAffine(img, M, (width, height))
|
|
44
|
+
# cv2.imwrite("debug_im/img_rot.jpg", img_rot)
|
|
45
|
+
img_crop = cv2.getRectSubPix(img_rot, size, center)
|
|
46
|
+
else:
|
|
47
|
+
center = tuple(map(int, center))
|
|
48
|
+
size = tuple([int(rect[1][1]), int(rect[1][0])])
|
|
49
|
+
size = (int(size[0] + min_size * alph), int(size[1] + min_size * alph))
|
|
50
|
+
angle -= 270
|
|
51
|
+
height, width = img.shape[0], img.shape[1]
|
|
52
|
+
M = cv2.getRotationMatrix2D(center, angle, 1)
|
|
53
|
+
img_rot = cv2.warpAffine(img, M, (width, height))
|
|
54
|
+
# cv2.imwrite("debug_im/img_rot.jpg", img_rot)
|
|
55
|
+
img_crop = cv2.getRectSubPix(img_rot, size, center)
|
|
56
|
+
img_crop = Image.fromarray(img_crop)
|
|
57
|
+
return img_crop
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def draw_bbox(img_path, result, color=(255, 0, 0), thickness=2):
|
|
61
|
+
if isinstance(img_path, str):
|
|
62
|
+
img_path = cv2.imread(img_path)
|
|
63
|
+
# img_path = cv2.cvtColor(img_path, cv2.COLOR_BGR2RGB)
|
|
64
|
+
img_path = img_path.copy()
|
|
65
|
+
for point in result:
|
|
66
|
+
point = point.astype(int)
|
|
67
|
+
cv2.line(img_path, tuple(point[0]), tuple(point[1]), color, thickness)
|
|
68
|
+
cv2.line(img_path, tuple(point[1]), tuple(point[2]), color, thickness)
|
|
69
|
+
cv2.line(img_path, tuple(point[2]), tuple(point[3]), color, thickness)
|
|
70
|
+
cv2.line(img_path, tuple(point[3]), tuple(point[0]), color, thickness)
|
|
71
|
+
return img_path
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def sort_box(boxs):
|
|
75
|
+
res = []
|
|
76
|
+
for box in boxs:
|
|
77
|
+
# box = [x if x>0 else 0 for x in box ]
|
|
78
|
+
x1, y1, x2, y2, x3, y3, x4, y4 = box[:8]
|
|
79
|
+
newBox = [[x1, y1], [x2, y2], [x3, y3], [x4, y4]]
|
|
80
|
+
# sort x
|
|
81
|
+
newBox = sorted(newBox, key=lambda x: x[0])
|
|
82
|
+
x1, y1 = sorted(newBox[:2], key=lambda x: x[1])[0]
|
|
83
|
+
index = newBox.index([x1, y1])
|
|
84
|
+
newBox.pop(index)
|
|
85
|
+
newBox = sorted(newBox, key=lambda x: -x[1])
|
|
86
|
+
x4, y4 = sorted(newBox[:2], key=lambda x: x[0])[0]
|
|
87
|
+
index = newBox.index([x4, y4])
|
|
88
|
+
newBox.pop(index)
|
|
89
|
+
newBox = sorted(newBox, key=lambda x: -x[0])
|
|
90
|
+
x2, y2 = sorted(newBox[:2], key=lambda x: x[1])[0]
|
|
91
|
+
index = newBox.index([x2, y2])
|
|
92
|
+
newBox.pop(index)
|
|
93
|
+
|
|
94
|
+
newBox = sorted(newBox, key=lambda x: -x[1])
|
|
95
|
+
x3, y3 = sorted(newBox[:2], key=lambda x: x[0])[0]
|
|
96
|
+
|
|
97
|
+
res.append([x1, y1, x2, y2, x3, y3, x4, y4])
|
|
98
|
+
return res
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def solve(box):
|
|
102
|
+
"""
|
|
103
|
+
绕 cx,cy点 w,h 旋转 angle 的坐标
|
|
104
|
+
x = cx-w/2
|
|
105
|
+
y = cy-h/2
|
|
106
|
+
x1-cx = -w/2*cos(angle) +h/2*sin(angle)
|
|
107
|
+
y1 -cy= -w/2*sin(angle) -h/2*cos(angle)
|
|
108
|
+
|
|
109
|
+
h(x1-cx) = -wh/2*cos(angle) +hh/2*sin(angle)
|
|
110
|
+
w(y1 -cy)= -ww/2*sin(angle) -hw/2*cos(angle)
|
|
111
|
+
(hh+ww)/2sin(angle) = h(x1-cx)-w(y1 -cy)
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
x1, y1, x2, y2, x3, y3, x4, y4 = box[:8]
|
|
115
|
+
cx = (x1 + x3 + x2 + x4) / 4.0
|
|
116
|
+
cy = (y1 + y3 + y4 + y2) / 4.0
|
|
117
|
+
w = (np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) + np.sqrt((x3 - x4) ** 2 + (y3 - y4) ** 2)) / 2
|
|
118
|
+
h = (np.sqrt((x2 - x3) ** 2 + (y2 - y3) ** 2) + np.sqrt((x1 - x4) ** 2 + (y1 - y4) ** 2)) / 2
|
|
119
|
+
|
|
120
|
+
sinA = (h * (x1 - cx) - w * (y1 - cy)) * 1.0 / (h * h + w * w) * 2
|
|
121
|
+
angle = np.arcsin(sinA)
|
|
122
|
+
return angle, w, h, cx, cy
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def sorted_boxes(dt_boxes):
|
|
126
|
+
"""
|
|
127
|
+
Sort text boxes in order from top to bottom, left to right
|
|
128
|
+
args:
|
|
129
|
+
dt_boxes(array):detected text boxes with shape [4, 2]
|
|
130
|
+
return:
|
|
131
|
+
sorted boxes(array) with shape [4, 2]
|
|
132
|
+
"""
|
|
133
|
+
num_boxes = dt_boxes.shape[0]
|
|
134
|
+
sorted_boxes = sorted(dt_boxes, key=lambda x: (x[0][1], x[0][0]))
|
|
135
|
+
_boxes = list(sorted_boxes)
|
|
136
|
+
|
|
137
|
+
for i in range(num_boxes - 1):
|
|
138
|
+
if abs(_boxes[i+1][0][1] - _boxes[i][0][1]) < 10 and \
|
|
139
|
+
(_boxes[i + 1][0][0] < _boxes[i][0][0]):
|
|
140
|
+
tmp = _boxes[i]
|
|
141
|
+
_boxes[i] = _boxes[i + 1]
|
|
142
|
+
_boxes[i + 1] = tmp
|
|
143
|
+
return _boxes
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_rotate_crop_image(img, points):
|
|
147
|
+
img_height, img_width = img.shape[0:2]
|
|
148
|
+
left = int(np.min(points[:, 0]))
|
|
149
|
+
right = int(np.max(points[:, 0]))
|
|
150
|
+
top = int(np.min(points[:, 1]))
|
|
151
|
+
bottom = int(np.max(points[:, 1]))
|
|
152
|
+
img_crop = img[top:bottom, left:right, :].copy()
|
|
153
|
+
points[:, 0] = points[:, 0] - left
|
|
154
|
+
points[:, 1] = points[:, 1] - top
|
|
155
|
+
img_crop_width = int(np.linalg.norm(points[0] - points[1]))
|
|
156
|
+
img_crop_height = int(np.linalg.norm(points[0] - points[3]))
|
|
157
|
+
pts_std = np.float32([[0, 0], [img_crop_width, 0],\
|
|
158
|
+
[img_crop_width, img_crop_height], [0, img_crop_height]])
|
|
159
|
+
|
|
160
|
+
M = cv2.getPerspectiveTransform(points, pts_std)
|
|
161
|
+
dst_img = cv2.warpPerspective(
|
|
162
|
+
img_crop,
|
|
163
|
+
M, (img_crop_width, img_crop_height),
|
|
164
|
+
borderMode=cv2.BORDER_REPLICATE)
|
|
165
|
+
dst_img_height, dst_img_width = dst_img.shape[0:2]
|
|
166
|
+
if dst_img_height * 1.0 / dst_img_width >= 1.5:
|
|
167
|
+
dst_img = np.rot90(dst_img)
|
|
168
|
+
return dst_img
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def app_url(version, name):
|
|
172
|
+
url = '/{}/{}'.format(version, name)
|
|
173
|
+
return url
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import threading
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
# 如果尚未安装,可以运行: pip install platformdirs
|
|
5
|
+
from platformdirs import user_data_dir
|
|
6
|
+
|
|
7
|
+
class AppConfig:
|
|
8
|
+
_instance = None
|
|
9
|
+
_lock = threading.Lock() # 确保线程安全的单例模式
|
|
10
|
+
|
|
11
|
+
def __new__(cls, *args, **kwargs):
|
|
12
|
+
"""单例模式:确保全局只有一个配置实例"""
|
|
13
|
+
if not cls._instance:
|
|
14
|
+
with cls._lock:
|
|
15
|
+
if not cls._instance:
|
|
16
|
+
cls._instance = super().__new__(cls)
|
|
17
|
+
# 标记是否已经初始化,防止 __init__ 重复调用
|
|
18
|
+
cls._instance._initialized = False
|
|
19
|
+
return cls._instance
|
|
20
|
+
|
|
21
|
+
def __init__(self, app_name="ScreenSubtitleReader"):
|
|
22
|
+
if self._initialized:
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
# 1. 确定跨平台的 AppData 存储路径
|
|
26
|
+
# 在 Windows 上通常是: C:\Users\用户名\AppData\Local\MyTkinterApp
|
|
27
|
+
self.config_dir = Path(user_data_dir(app_name))
|
|
28
|
+
self.config_file = self.config_dir / "config.json"
|
|
29
|
+
|
|
30
|
+
# 2. 默认属性
|
|
31
|
+
self._volume = 100
|
|
32
|
+
self._speed = 270
|
|
33
|
+
|
|
34
|
+
# 3. 启动初始化:自动读取或创建文件
|
|
35
|
+
self._load_or_create_config()
|
|
36
|
+
self._initialized = True
|
|
37
|
+
|
|
38
|
+
def _load_or_create_config(self):
|
|
39
|
+
"""读取配置文件,如果不存在则创建默认配置"""
|
|
40
|
+
try:
|
|
41
|
+
if self.config_file.exists():
|
|
42
|
+
# 文件存在,读取数据
|
|
43
|
+
with open(self.config_file, "r", encoding="utf-8") as f:
|
|
44
|
+
data = json.load(f)
|
|
45
|
+
# 使用 get 提供降级容错,防止 JSON 被人为改坏导致崩溃
|
|
46
|
+
self._volume = int(data.get("volume", 100))
|
|
47
|
+
self._speed = int(data.get("speed", 270))
|
|
48
|
+
print(f"成功加载配置: {self.config_file}")
|
|
49
|
+
else:
|
|
50
|
+
# 文件不存在,保存当前的默认值
|
|
51
|
+
self.save()
|
|
52
|
+
print(f"创建默认配置文件: {self.config_file}")
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"配置加载或初始化失败,使用默认值。错误: {e}")
|
|
55
|
+
|
|
56
|
+
def save(self):
|
|
57
|
+
"""将当前属性序列化并自动存盘"""
|
|
58
|
+
try:
|
|
59
|
+
# 确保应用数据文件夹存在(不存在则自动递归创建)
|
|
60
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
data = {
|
|
63
|
+
"volume": self._volume,
|
|
64
|
+
"speed": self._speed
|
|
65
|
+
}
|
|
66
|
+
# 写入文件
|
|
67
|
+
with open(self.config_file, "w", encoding="utf-8") as f:
|
|
68
|
+
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
69
|
+
print("配置已自动保存到磁盘。")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(f"配置保存失败: {e}")
|
|
72
|
+
|
|
73
|
+
# --- 使用 Property 属性装饰器,既能像属性一样访问,又能触发自动存盘 ---
|
|
74
|
+
@property
|
|
75
|
+
def volume(self) -> int:
|
|
76
|
+
return self._volume
|
|
77
|
+
|
|
78
|
+
@volume.setter
|
|
79
|
+
def volume(self, value: int):
|
|
80
|
+
if not isinstance(value, int):
|
|
81
|
+
raise TypeError("音量必须是整型 (int)")
|
|
82
|
+
self._volume = value
|
|
83
|
+
self.save() # 更改时自动存盘
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def speed(self) -> int:
|
|
87
|
+
return self._speed
|
|
88
|
+
|
|
89
|
+
@speed.setter
|
|
90
|
+
def speed(self, value: int):
|
|
91
|
+
if not isinstance(value, int):
|
|
92
|
+
raise TypeError("语速必须是整型 (int)")
|
|
93
|
+
self._speed = value
|
|
94
|
+
self.save() # 更改时自动存盘
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
apc = AppConfig()
|
|
98
|
+
print(apc.speed)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import pyttsx3
|
|
2
|
+
import queue
|
|
3
|
+
import time
|
|
4
|
+
import threading
|
|
5
|
+
from ..setting.setting import AppConfig
|
|
6
|
+
|
|
7
|
+
class Speaker:
|
|
8
|
+
_instance = None
|
|
9
|
+
_lock = threading.Lock() # 确保线程安全的单例模式
|
|
10
|
+
def __new__(cls, *args, **kwargs):
|
|
11
|
+
"""单例模式:确保全局只有一个配置实例"""
|
|
12
|
+
if not cls._instance:
|
|
13
|
+
with cls._lock:
|
|
14
|
+
if not cls._instance:
|
|
15
|
+
cls._instance = super().__new__(cls)
|
|
16
|
+
# 标记是否已经初始化,防止 __init__ 重复调用
|
|
17
|
+
cls._instance._initialized = False
|
|
18
|
+
return cls._instance
|
|
19
|
+
|
|
20
|
+
def __init__(self,volum = None ,speed = None) -> None:
|
|
21
|
+
if self._initialized:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
config_manager = AppConfig()
|
|
25
|
+
self.volum = volum if volum else config_manager.volume
|
|
26
|
+
self.speed = speed if speed else config_manager.speed
|
|
27
|
+
|
|
28
|
+
self.voice_tasks = queue.Queue()
|
|
29
|
+
|
|
30
|
+
# 启动工作线程
|
|
31
|
+
self.thread = threading.Thread(target=self.speak_text_block_thread, daemon=True)
|
|
32
|
+
self.thread.start()
|
|
33
|
+
|
|
34
|
+
self._initialized = True
|
|
35
|
+
|
|
36
|
+
def add_sentence(self, sentence: str):
|
|
37
|
+
self.voice_tasks.put(sentence)
|
|
38
|
+
|
|
39
|
+
def speak_text_block_thread(self):
|
|
40
|
+
# 【关键修改】在子线程内部初始化引擎
|
|
41
|
+
engine = pyttsx3.init()
|
|
42
|
+
engine.setProperty('rate',self.speed )
|
|
43
|
+
|
|
44
|
+
while True:
|
|
45
|
+
# queue.get() 默认是阻塞的,没有任务时会一直等在这里,不会消耗CPU
|
|
46
|
+
speak_sentence = self.voice_tasks.get()
|
|
47
|
+
|
|
48
|
+
if speak_sentence:
|
|
49
|
+
engine.say(speak_sentence)
|
|
50
|
+
engine.runAndWait()
|
|
51
|
+
|
|
52
|
+
# 通知队列当前任务已处理完毕(良好的队列使用习惯)
|
|
53
|
+
self.voice_tasks.task_done()
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
sp = Speaker()
|
|
57
|
+
for i in range(99):
|
|
58
|
+
sp.add_sentence(f"阿巴阿巴阿巴 {i}")
|
|
59
|
+
|
|
60
|
+
time.sleep(100)
|