diff --git a/CHANGELOG.md b/CHANGELOG.md
index e99fe8b6..8e08028f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,13 @@
# Changelogs
### 2022-12-30
-[v1.3.18](https://github.com/dmMaze/BallonsTranslator/releases/tag/v1.3.18)发布
+[v1.3.20](https://github.com/dmMaze/BallonsTranslator/releases/tag/v1.3.20)发布
1. 适应具有极端宽高比的图片比如条漫
2. 支持粘贴到多个选中的文本编辑框
3. 修bug
+4. OCR/翻译/修复选中文字区域, 填字样式会继承选中的文字框自己的
+ 单行文本建议选用ctc_48px, 多行日文选mangocr, 目前对多行其它语言不太行, 需要重新训练检测模型
+
### 2022-11-29
[v1.3.15](https://github.com/dmMaze/BallonsTranslator/releases/tag/v1.3.15)发布
diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md
index ea1d5fcb..1bbf14c5 100644
--- a/CHANGELOG_EN.md
+++ b/CHANGELOG_EN.md
@@ -1,10 +1,14 @@
# Changelogs
-### 2022-12-30
-[v1.3.18](https://github.com/dmMaze/BallonsTranslator/releases/tag/v1.3.18) released
+### 2022-12-31
+[v1.3.20](https://github.com/dmMaze/BallonsTranslator/releases/tag/v1.3.20) released
1. Adapted to images with extreme aspect ratio such as webtoons
2. Support paste text to multiple selected Text blocks.
3. Bugfixes
+4. OCR/Translate/Inpaint selected text blocks
+ lettering style will inherit from corresponding selected block.
+ ctc_48px is more recommended for single line text, mangocr for multi-line Japanese, need to retrain detection model to perform better
+
### 2022-11-29
[v1.3.15](https://github.com/dmMaze/BallonsTranslator/releases/tag/v1.3.15) released
diff --git a/ballontranslator/__init__.py b/ballontranslator/__init__.py
index 6b7d1663..f7ceb75a 100644
--- a/ballontranslator/__init__.py
+++ b/ballontranslator/__init__.py
@@ -4,4 +4,4 @@
# 1. MAJOR version when you make incompatible API changes;
# 2. MINOR version when you add functionality in a backwards-compatible manner;
# 3. PATCH version when you make backwards-compatible bug fixes.
-__version__ = "1.3.19"
\ No newline at end of file
+__version__ = "1.3.20"
\ No newline at end of file
diff --git a/ballontranslator/data/translate/zh_CN.qm b/ballontranslator/data/translate/zh_CN.qm
index 1411d3b0..d846f485 100644
Binary files a/ballontranslator/data/translate/zh_CN.qm and b/ballontranslator/data/translate/zh_CN.qm differ
diff --git a/ballontranslator/data/translate/zh_CN.ts b/ballontranslator/data/translate/zh_CN.ts
index 2a006fdc..3a05ca08 100644
--- a/ballontranslator/data/translate/zh_CN.ts
+++ b/ballontranslator/data/translate/zh_CN.ts
@@ -52,20 +52,50 @@
Canvas
-
+
删除
-
+
应用字体格式
-
+
自动排版
+
+
+
+ 复制
+
+
+
+
+ 粘贴
+
+
+
+
+ 翻译
+
+
+
+
+ OCR
+
+
+
+
+ OCR并翻译
+
+
+
+
+ OCR,翻译并抹字
+
ConfigPanel
@@ -178,7 +208,7 @@
DLManager
-
+
不可用
@@ -186,7 +216,7 @@
DrawingPanel
-
+
掩膜透明度
@@ -366,22 +396,22 @@
ImgtransProgressMessageBox
-
+
检测:
-
+
OCR:
-
+
修复:
-
+
翻译:
@@ -389,7 +419,7 @@
ImgtransThread
-
+
翻译失败.
@@ -427,11 +457,26 @@
画笔大小
+
+
+
+ 形状
+
+
+
+
+ 圆形
+
+
+
+
+ 方形
+
InpaintThread
-
+
修复失败.
@@ -495,27 +540,27 @@
MainWindow
-
+
项目加载失败
-
+
未保存
-
+
已保存
-
+
保存中...
-
+
导出至
@@ -523,7 +568,7 @@
ModuleThread
-
+
无法设置
@@ -604,30 +649,45 @@
PenConfigPanel
-
+
alpha值
-
+
颜色
-
+
Alpha
-
+
大小
-
+
画笔大小
+
+
+
+ 形状
+
+
+
+
+ 圆形
+
+
+
+
+ 方形
+
PresetListWidget
@@ -716,52 +776,52 @@
RectPanel
-
+
方法1
-
+
方法2
-
+
自动
-
+
自动运行修复函数.
-
+
图像修复
-
+
删除
-
+
膨胀
-
+
核大小:
-
+
空格
-
+
@@ -769,57 +829,57 @@
TextEffectPanel
-
+
特效
-
+
不透明度
-
+
阴影
-
+
修改阴影颜色
-
+
应用
-
+
取消
-
+
不透明度:
-
+
半径:
-
+
强度:
-
+
x偏移:
-
+
y偏移:
@@ -923,17 +983,17 @@
支持语言列表:
-
+
翻译器设置失败
-
+
是翻译器必填项
-
+
翻译失败.
diff --git a/ballontranslator/dl/inpaint/__init__.py b/ballontranslator/dl/inpaint/__init__.py
index 14870dcd..071c4626 100644
--- a/ballontranslator/dl/inpaint/__init__.py
+++ b/ballontranslator/dl/inpaint/__init__.py
@@ -29,8 +29,21 @@ def __init__(self, **setup_params) -> None:
def setup_inpainter(self):
raise NotImplementedError
- def inpaint(self, img: np.ndarray, mask: np.ndarray, textblock_list: List[TextBlock] = None) -> np.ndarray:
+ def inpaint(self, img: np.ndarray, mask: np.ndarray, textblock_list: List[TextBlock] = None, check_need_inpaint: bool = False) -> np.ndarray:
if not self.inpaint_by_block or textblock_list is None:
+ if check_need_inpaint:
+ ballon_msk, non_text_msk = extract_ballon_mask(img, mask)
+ if ballon_msk is not None:
+ non_text_region = np.where(non_text_msk > 0)
+ non_text_px = img[non_text_region]
+ average_bg_color = np.mean(non_text_px, axis=0)
+ std_bgr = np.std(non_text_px - average_bg_color, axis=0)
+ std_max = np.max(std_bgr)
+ inpaint_thresh = 7 if np.std(std_bgr) > 1 else 10
+ if std_max < inpaint_thresh:
+ img = img.copy()
+ img[np.where(ballon_msk > 0)] = average_bg_color
+ return img
return self._inpaint(img, mask)
else:
im_h, im_w = img.shape[:2]
@@ -41,7 +54,7 @@ def inpaint(self, img: np.ndarray, mask: np.ndarray, textblock_list: List[TextBl
im = inpainted[xyxy_e[1]:xyxy_e[3], xyxy_e[0]:xyxy_e[2]]
msk = mask[xyxy_e[1]:xyxy_e[3], xyxy_e[0]:xyxy_e[2]]
need_inpaint = True
- if self.check_need_inpaint:
+ if self.check_need_inpaint or check_need_inpaint:
ballon_msk, non_text_msk = extract_ballon_mask(im, msk)
if ballon_msk is not None:
non_text_region = np.where(non_text_msk > 0)
diff --git a/ballontranslator/dl/ocr/__init__.py b/ballontranslator/dl/ocr/__init__.py
index 2a37b91c..9fa1b6c7 100644
--- a/ballontranslator/dl/ocr/__init__.py
+++ b/ballontranslator/dl/ocr/__init__.py
@@ -1,6 +1,7 @@
from typing import Tuple, List, Dict, Union
import numpy as np
import cv2
+import logging
from ..textdetector.textblock import TextBlock
@@ -139,9 +140,15 @@ def ocr_img(self, img: np.ndarray) -> str:
return self.model(img)
def ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]):
+ im_h, im_w = img.shape[:2]
for blk in blk_list:
x1, y1, x2, y2 = blk.xyxy
- blk.text = self.model(img[y1:y2, x1:x2])
+ if y2 < im_h and x2 < im_w and \
+ x1 > 0 and y1 > 0 and x1 < x2 and y1 < y2:
+ blk.text = self.model(img[y1:y2, x1:x2])
+ else:
+ logging.warning('invalid textbbox to target img')
+ blk.text = ['']
def updateParam(self, param_key: str, param_content):
super().updateParam(param_key, param_content)
diff --git a/ballontranslator/dl/textdetector/textblock.py b/ballontranslator/dl/textdetector/textblock.py
index 91f95d96..02e44c55 100644
--- a/ballontranslator/dl/textdetector/textblock.py
+++ b/ballontranslator/dl/textdetector/textblock.py
@@ -97,7 +97,10 @@ def __init__(self, xyxy: List,
self.shadow_color = shadow_color
self.shadow_offset = shadow_offset
- def adjust_bbox(self, with_bbox=False):
+ self.region_mask: np.ndarray = None
+ self.region_inpaint_dict: dict = None
+
+ def adjust_bbox(self, with_bbox=False, x_range=None, y_range=None):
lines = self.lines_array().astype(np.int32)
if with_bbox:
self.xyxy[0] = min(lines[..., 0].min(), self.xyxy[0])
@@ -110,6 +113,13 @@ def adjust_bbox(self, with_bbox=False):
self.xyxy[2] = lines[..., 0].max()
self.xyxy[3] = lines[..., 1].max()
+ if x_range is not None:
+ self.xyxy[0] = np.clip(self.xyxy[0], x_range[0], x_range[1])
+ self.xyxy[2] = np.clip(self.xyxy[2], x_range[0], x_range[1])
+ if y_range is not None:
+ self.xyxy[1] = np.clip(self.xyxy[1], y_range[0], y_range[1])
+ self.xyxy[3] = np.clip(self.xyxy[3], y_range[0], y_range[1])
+
def sort_lines(self):
if self.distance is not None:
idx = np.argsort(self.distance)
@@ -120,6 +130,26 @@ def sort_lines(self):
def lines_array(self, dtype=np.float64):
return np.array(self.lines, dtype=dtype)
+ def set_lines_by_xywh(self, xywh: np.ndarray, angle=0, x_range=None, y_range=None, adjust_bbox=False):
+ if isinstance(xywh, List):
+ xywh = np.array(xywh)
+ lines = xywh2xyxypoly(np.array([xywh]))
+ if angle != 0:
+ cx, cy = xywh[0], xywh[1]
+ cx += xywh[2] / 2.
+ cy += xywh[3] / 2.
+ lines = rotate_polygons([cx, cy], lines, angle)
+
+ lines = lines.reshape(-1, 4, 2)
+ if x_range is not None:
+ lines[..., 0] = np.clip(lines[..., 0], x_range[0], x_range[1])
+ if y_range is not None:
+ lines[..., 1] = np.clip(lines[..., 1], y_range[0], y_range[1])
+ self.lines = lines.tolist()
+
+ if adjust_bbox:
+ self.adjust_bbox()
+
def aspect_ratio(self) -> float:
min_rect = self.min_rect()
middle_pnts = (min_rect[:, [1, 2, 3, 0]] + min_rect) / 2
@@ -188,25 +218,38 @@ def to_dict(self):
def get_transformed_region(self, img: np.ndarray, idx: int, textheight: int, maxwidth: int = None) -> np.ndarray :
direction = 'v' if self.vertical else 'h'
src_pts = np.array(self.lines[idx], dtype=np.float64)
+ im_h, im_w = img.shape[:2]
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)
+ norm_v = np.linalg.norm(vec_v)
+ norm_h = np.linalg.norm(vec_h)
+ if norm_v <= 0 or norm_h <= 0:
+ print('invalid textpolygon to target img')
+ return np.zeros((textheight, textheight, 3), dtype=np.uint8)
+ ratio = norm_v / norm_h
if direction == 'h' :
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)
+ if M is None:
+ print('invalid textpolygon to target img')
+ return np.zeros((textheight, textheight, 3), dtype=np.uint8)
region = cv2.warpPerspective(img, M, (w, h))
elif direction == 'v' :
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)
+ if M is None:
+ print('invalid textpolygon to target img')
+ return np.zeros((textheight, textheight, 3), dtype=np.uint8)
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:
diff --git a/ballontranslator/scripts/build_win.bat b/ballontranslator/scripts/build_win.bat
index c4aa3346..8ebcf91d 100644
--- a/ballontranslator/scripts/build_win.bat
+++ b/ballontranslator/scripts/build_win.bat
@@ -2,6 +2,6 @@ nuitka --standalone --mingw64 --show-memory --show-progress ^
--enable-plugin=pyqt5 --include-qt-plugins=sensible,styles ^
--nofollow-import-to=fw_qt6,numpy,urllib3,jaconv,torch,torchvision,transformers,fugashi,unidic_lite,tqdm,shapely,pyclipper,einops,termcolor,bs4,deepl,qtpy,pkuseg,pandas,spacy_pkuseg,sentencepiece,ctranslate2,python-docx,docx2txt,piexif,docx,argparse,colorama,http,email,chardet,requests,pkg_resources,yaml,PIL,multiprocessing,dbm ^
--follow-import-to=dl,utils,ui --include-plugin-directory=ballontranslator/dl,ballontranslator/ui,ballontranslator/utils ^
- --windows-product-version=1.3.19 --windows-company-name=DUMMY_WINDOWS_COMPANY_NAME --windows-product-name=BallonTranslator ^
+ --windows-product-version=1.3.20 --windows-company-name=DUMMY_WINDOWS_COMPANY_NAME --windows-product-name=BallonTranslator ^
--output-dir=release BallonTranslator
\ No newline at end of file
diff --git a/ballontranslator/ui/canvas.py b/ballontranslator/ui/canvas.py
index 0a6baff0..5f3bd283 100644
--- a/ballontranslator/ui/canvas.py
+++ b/ballontranslator/ui/canvas.py
@@ -93,6 +93,8 @@ class Canvas(QGraphicsScene):
format_textblks = Signal()
layout_textblks = Signal()
+ run_blktrans = Signal(int)
+
begin_scale_tool = Signal(QPointF)
scale_tool = Signal(QPointF)
end_scale_tool = Signal()
@@ -542,7 +544,14 @@ def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent):
menu.addSeparator()
format_act = menu.addAction(self.tr("Apply font formatting"))
layout_act = menu.addAction(self.tr("Auto layout"))
+ menu.addSeparator()
+ translate_act = menu.addAction(self.tr("translate"))
+ ocr_act = menu.addAction(self.tr("OCR"))
+ ocr_translate_act = menu.addAction(self.tr("OCR and translate"))
+ ocr_translate_inpaint_act = menu.addAction(self.tr("OCR, translate and inpaint"))
+
rst = menu.exec_(event.screenPos())
+
if rst == delete_act:
self.delete_textblks.emit()
elif rst == copy_act:
@@ -553,6 +562,14 @@ def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent):
self.format_textblks.emit()
elif rst == layout_act:
self.layout_textblks.emit()
+ elif rst == translate_act:
+ self.run_blktrans.emit(-1)
+ elif rst == ocr_act:
+ self.run_blktrans.emit(0)
+ elif rst == ocr_translate_act:
+ self.run_blktrans.emit(1)
+ elif rst == ocr_translate_inpaint_act:
+ self.run_blktrans.emit(2)
def on_hide_canvas(self):
self.clear_states()
diff --git a/ballontranslator/ui/dl_manager.py b/ballontranslator/ui/dl_manager.py
index fd5588da..296a31da 100644
--- a/ballontranslator/ui/dl_manager.py
+++ b/ballontranslator/ui/dl_manager.py
@@ -8,6 +8,7 @@
from utils.logger import logger as LOGGER
from utils.registry import Registry
+from utils.imgproc_utils import enlarge_window
from dl.translators import MissingTranslatorParams
from dl import INPAINTERS, TRANSLATORS, TEXTDETECTORS, OCR, \
VALID_TRANSLATORS, VALID_TEXTDETECTORS, VALID_INPAINTERS, VALID_OCR, \
@@ -19,6 +20,8 @@
from .configpanel import ConfigPanel
from .misc import DLModuleConfig, ProgramConfig
from .imgtrans_proj import ProjImgTrans
+from dl.textdetector import TextBlock
+
class ModuleThread(QThread):
@@ -246,6 +249,8 @@ class ImgtransThread(QThread):
update_inpaint_progress = Signal(int)
exception_occurred = Signal(str, str)
+ finish_blktrans_stage = Signal(str, int)
+
def __init__(self,
dl_config: DLModuleConfig,
textdetect_thread: TextDetectThread,
@@ -284,6 +289,36 @@ def runImgtransPipeline(self, imgtrans_proj: ProjImgTrans):
self.job = self._imgtrans_pipeline
self.start()
+ def runBlktransPipeline(self, blk_list: List[TextBlock], tgt_img: np.ndarray, mode: int):
+ self.job = lambda : self._blktrans_pipeline(blk_list, tgt_img, mode)
+ self.start()
+
+ def _blktrans_pipeline(self, blk_list: List[TextBlock], tgt_img: np.ndarray, mode: int):
+ if mode >= 0:
+ self.ocr_thread.module.ocr_blk_list(tgt_img, blk_list)
+ self.finish_blktrans_stage.emit('ocr', 100)
+ if mode != 0:
+ self.translate_thread.module.translate_textblk_lst(blk_list)
+ self.finish_blktrans_stage.emit('translate', 100)
+ if mode > 1:
+ im_h, im_w = tgt_img.shape[:2]
+ progress_prod = 100. / len(blk_list) if len(blk_list) > 0 else 0
+ for ii, blk in enumerate(blk_list):
+ xyxy = enlarge_window(blk.xyxy, im_w, im_h)
+ xyxy = np.array(xyxy)
+ x1, y1, x2, y2 = xyxy.astype(np.int64)
+ blk.region_inpaint_dict = None
+ if y2 - y1 > 2 and x2 - x1 > 2:
+ im = np.copy(tgt_img[y1: y2, x1: x2])
+ maskseg_method = self.get_maskseg_method()
+ inpaint_mask_array, ballon_mask, bub_dict = maskseg_method(im)
+ mask = self.post_process_mask(inpaint_mask_array)
+ if mask.sum() > 0:
+ inpainted = self.inpaint_thread.inpainter.inpaint(im, mask)
+ blk.region_inpaint_dict = {'img': im, 'mask': mask, 'inpaint_rect': [x1, y1, x2, y2], 'inpainted': inpainted}
+ self.finish_blktrans_stage.emit('inpaint', int((ii+1) * progress_prod))
+ self.finish_blktrans_stage.emit(str(mode), 0)
+
def _imgtrans_pipeline(self):
self.detect_counter = 0
self.ocr_counter = 0
@@ -409,6 +444,7 @@ class DLManager(QObject):
canvas_inpaint_finished = Signal(dict)
imgtrans_pipeline_finished = Signal()
+ blktrans_pipeline_finished = Signal(int)
page_trans_finished = Signal(int)
run_canvas_inpaint = False
@@ -450,6 +486,7 @@ def setupThread(self, config_panel: ConfigPanel, imgtrans_progress_msgbox: Imgtr
self.imgtrans_thread.update_translate_progress.connect(self.on_update_translate_progress)
self.imgtrans_thread.update_inpaint_progress.connect(self.on_update_inpaint_progress)
self.imgtrans_thread.exception_occurred.connect(self.handleRunTimeException)
+ self.imgtrans_thread.finish_blktrans_stage.connect(self.on_finish_blktrans_stage)
self.translator_panel = translator_panel = config_panel.trans_config_panel
translator_setup_params = merge_config_module_params(dl_config.translator_setup_params, VALID_TRANSLATORS, TRANSLATORS.get)
@@ -518,12 +555,7 @@ def inpaint(self, img: np.ndarray, mask: np.ndarray, img_key: str = None, inpain
return
self.inpaint_thread.inpaint(img, mask, img_key, inpaint_rect)
- def runImgtransPipeline(self):
- if self.imgtrans_proj.is_empty:
- LOGGER.info('proj file is empty, nothing to do')
- self.progress_msgbox.hide()
- return
- self.last_finished_index = -1
+ def terminateRunningThread(self):
if self.textdetect_thread.isRunning():
self.textdetect_thread.terminate()
if self.ocr_thread.isRunning():
@@ -532,7 +564,16 @@ def runImgtransPipeline(self):
self.inpaint_thread.terminate()
if self.translate_thread.isRunning():
self.translate_thread.terminate()
+
+ def runImgtransPipeline(self):
+ if self.imgtrans_proj.is_empty:
+ LOGGER.info('proj file is empty, nothing to do')
+ self.progress_msgbox.hide()
+ return
+ self.last_finished_index = -1
+ self.terminateRunningThread()
+ self.progress_msgbox.show_all_bars()
if not self.dl_config.enable_ocr:
self.progress_msgbox.ocr_bar.hide()
self.progress_msgbox.translate_bar.hide()
@@ -546,6 +587,32 @@ def runImgtransPipeline(self):
self.progress_msgbox.show()
self.imgtrans_thread.runImgtransPipeline(self.imgtrans_proj)
+ def runBlktransPipeline(self, blk_list: List[TextBlock], tgt_img: np.ndarray, mode: int):
+ self.terminateRunningThread()
+ self.progress_msgbox.hide_all_bars()
+ if mode >= 0:
+ self.progress_msgbox.ocr_bar.show()
+ if mode == 2:
+ self.progress_msgbox.inpaint_bar.show()
+ if mode != 0:
+ self.progress_msgbox.translate_bar.show()
+ self.progress_msgbox.zero_progress()
+ self.progress_msgbox.show()
+ self.imgtrans_thread.runBlktransPipeline(blk_list, tgt_img, mode)
+
+ def on_finish_blktrans_stage(self, stage: str, progress: int):
+ if stage == 'ocr':
+ self.progress_msgbox.updateOCRProgress(progress)
+ elif stage == 'translate':
+ self.progress_msgbox.updateTranslateProgress(progress)
+ elif stage == 'inpaint':
+ self.progress_msgbox.updateInpaintProgress(progress)
+ elif stage in {'-1', '0', '1', '2'}:
+ self.blktrans_pipeline_finished.emit(int(stage))
+ self.progress_msgbox.hide()
+ else:
+ raise NotImplementedError(f'Unknown stage: {stage}')
+
def on_update_detect_progress(self, progress: int):
ri = self.imgtrans_thread.recent_finished_index(progress)
progress = int(progress / self.imgtrans_thread.num_pages * 100)
diff --git a/ballontranslator/ui/drawing_commands.py b/ballontranslator/ui/drawing_commands.py
index dc4a0ecf..fdef7d0f 100644
--- a/ballontranslator/ui/drawing_commands.py
+++ b/ballontranslator/ui/drawing_commands.py
@@ -10,7 +10,8 @@
from utils.logger import logger
from .image_edit import ImageEditMode, PixmapItem, DrawingLayer, StrokeImgItem
-from .canvas import Canvas
+from .canvas import Canvas, TextBlkItem
+from .textedit_area import TransPairWidget
class StrokeItemUndoCommand(QUndoCommand):
@@ -71,4 +72,100 @@ def undo(self) -> None:
img_view[:] = self.undo_img
mask_view[:] = self.undo_mask
self.canvas.setInpaintLayer()
- self.canvas.setMaskLayer()
\ No newline at end of file
+ self.canvas.setMaskLayer()
+
+
+
+class RunBlkTransCommand(QUndoCommand):
+ def __init__(self, canvas: Canvas, blkitems: List[TextBlkItem], transpairw_list: List[TransPairWidget], mode: int):
+ super().__init__()
+
+ self.op_counter = -1
+ self.blkitems = blkitems
+ self.transpairw_list = transpairw_list
+
+ for blkitem, transpairw in zip(self.blkitems, self.transpairw_list):
+ if mode != 0:
+ trs = blkitem.blk.translation
+ transpairw.e_trans.setPlainTextAndKeepUndoStack(trs)
+ blkitem.setPlainTextAndKeepUndoStack(trs)
+ blkitem.blk.rich_text = ''
+ if mode >= 0:
+ transpairw.e_source.setPlainTextAndKeepUndoStack(blkitem.blk.get_text())
+
+ self.canvas = canvas
+ self.mode = mode
+ if mode > 1:
+ self.undo_img_list = []
+ self.undo_mask_list = []
+ self.redo_img_list = []
+ self.redo_mask_list = []
+ self.inpaint_rect_lst = []
+ img_array = self.canvas.imgtrans_proj.inpainted_array
+ mask_array = self.canvas.imgtrans_proj.mask_array
+ self.num_inpainted = 0
+ for item in self.blkitems:
+ inpainted_dict = item.blk.region_inpaint_dict
+ item.blk.region_inpaint_dict = None
+ if inpainted_dict is None:
+ self.undo_img_list.append(None)
+ self.undo_mask_list.append(None)
+ self.redo_mask_list.append(None)
+ self.redo_img_list.append(None)
+ self.inpaint_rect_lst.append(None)
+ else:
+ inpaint_rect = inpainted_dict['inpaint_rect']
+ img_view = img_array[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]]
+ mask_view = mask_array[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]]
+ self.undo_img_list.append(np.copy(img_view))
+ self.undo_mask_list.append(np.copy(mask_view))
+ self.redo_img_list.append(inpainted_dict['inpainted'])
+ self.redo_mask_list.append(inpainted_dict['mask'])
+ self.inpaint_rect_lst.append(inpaint_rect)
+ self.num_inpainted += 1
+
+ def redo(self) -> None:
+ if self.mode > 1 and self.num_inpainted > 0:
+ img_array = self.canvas.imgtrans_proj.inpainted_array
+ mask_array = self.canvas.imgtrans_proj.mask_array
+ for inpaint_rect, redo_img, redo_mask in zip(self.inpaint_rect_lst, self.redo_img_list, self.redo_mask_list):
+ if inpaint_rect is None:
+ continue
+ img_view = img_array[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]]
+ mask_view = mask_array[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]]
+ img_view[:] = redo_img
+ mask_view[:] = redo_mask
+ self.canvas.setInpaintLayer()
+ self.canvas.setMaskLayer()
+
+ if self.op_counter < 0:
+ self.op_counter += 1
+ return
+
+ for blkitem, transpairw in zip(self.blkitems, self.transpairw_list):
+ if self.mode != 0:
+ transpairw.e_trans.redo()
+ blkitem.redo()
+ if self.mode >= 0:
+ transpairw.e_source.redo()
+
+ def undo(self) -> None:
+ if self.mode > 1 and self.num_inpainted > 0:
+ img_array = self.canvas.imgtrans_proj.inpainted_array
+ mask_array = self.canvas.imgtrans_proj.mask_array
+ for inpaint_rect, undo_img, undo_mask in zip(self.inpaint_rect_lst, self.undo_img_list, self.undo_mask_list):
+ if inpaint_rect is None:
+ continue
+ img_view = img_array[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]]
+ mask_view = mask_array[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]]
+ img_view[:] = undo_img
+ mask_view[:] = undo_mask
+ self.canvas.setInpaintLayer()
+ self.canvas.setMaskLayer()
+
+ for blkitem, transpairw in zip(self.blkitems, self.transpairw_list):
+ if self.mode != 0:
+ transpairw.e_trans.undo()
+ blkitem.undo()
+ if self.mode >= 0:
+ transpairw.e_source.undo()
\ No newline at end of file
diff --git a/ballontranslator/ui/mainwindow.py b/ballontranslator/ui/mainwindow.py
index 4851d5c3..476adf98 100644
--- a/ballontranslator/ui/mainwindow.py
+++ b/ballontranslator/ui/mainwindow.py
@@ -1,5 +1,5 @@
import os.path as osp
-import os, re
+import os, re, copy
from typing import List
from qtpy.QtWidgets import QHBoxLayout, QVBoxLayout, QApplication, QStackedWidget, QSplitter, QListWidget, QShortcut, QListWidgetItem, QMessageBox, QTextEdit, QPlainTextEdit
@@ -9,7 +9,8 @@
from utils.logger import logger as LOGGER
from utils.io_utils import json_dump_nested_obj
from utils.text_processing import is_cjk, full_len, half_len
-from dl.textdetector import TextBlock
+from dl.textdetector.textblock import TextBlock, visualize_textblocks
+from utils.imgproc_utils import xywh2xyxypoly
from .misc import pt2px, parse_stylesheet
from .imgtrans_proj import ProjImgTrans
@@ -28,6 +29,7 @@
from . import constants as C
from .textedit_commands import GlobalRepalceAllCommand
from .framelesswindow import FramelessWindow
+from .drawing_commands import RunBlkTransCommand
class PageListView(QListWidget):
def __init__(self, *args, **kwargs) -> None:
@@ -130,6 +132,7 @@ def setupUi(self):
self.canvas.gv.hide_canvas.connect(self.onHideCanvas)
self.canvas.proj_savestate_changed.connect(self.on_savestate_changed)
self.canvas.textstack_changed.connect(self.on_textstack_changed)
+ self.canvas.run_blktrans.connect(self.on_run_blktrans)
self.bottomBar.originalSlider.valueChanged.connect(self.canvas.setOriginalTransparencyBySlider)
@@ -203,9 +206,12 @@ def setupConfig(self):
dl_manager.finish_translate_page.connect(self.finishTranslatePage)
dl_manager.imgtrans_pipeline_finished.connect(self.on_imgtrans_pipeline_finished)
dl_manager.page_trans_finished.connect(self.on_pagtrans_finished)
- self.dl_manager.setupThread(self.configPanel, self.imgtrans_progress_msgbox)
+ dl_manager.setupThread(self.configPanel, self.imgtrans_progress_msgbox)
dl_manager.progress_msgbox.showed.connect(self.on_imgtrans_progressbox_showed)
dl_manager.imgtrans_thread.mask_postprocess = self.drawingPanel.rectPanel.post_process_mask
+ dl_manager.blktrans_pipeline_finished.connect(self.on_blktrans_finished)
+ dl_manager.imgtrans_thread.get_maskseg_method = self.drawingPanel.rectPanel.get_maskseg_method
+ dl_manager.imgtrans_thread.post_process_mask = self.drawingPanel.rectPanel.post_process_mask
self.leftBar.run_imgtrans.connect(self.on_run_imgtrans)
self.bottomBar.ocrcheck_statechanged.connect(dl_manager.setOCRMode)
@@ -789,6 +795,39 @@ def on_textstack_changed(self):
if not self.page_changing:
self.global_search_widget.set_document_edited()
+ def on_run_blktrans(self, mode: int):
+ tgt_img = self.imgtrans_proj.img_array
+ if tgt_img is None:
+ return
+ blkitem_list = self.canvas.selected_text_items()
+ if len(blkitem_list) < 1:
+ return
+
+ im_h, im_w = tgt_img.shape[:2]
+ blk_list = []
+ for blkitem in blkitem_list:
+ blk = blkitem.blk
+ blk._bounding_rect = blkitem.absBoundingRect()
+ blk.vertical = blkitem.is_vertical
+ blk.text = [self.st_manager.pairwidget_list[blkitem.idx].e_source.toPlainText()]
+ blk.set_lines_by_xywh(blk._bounding_rect, angle=-blk.angle, x_range=[0, im_w-1], y_range=[0, im_h-1], adjust_bbox=True)
+ blk_list.append(blk)
+
+ # c = visualize_textblocks(tgt_img.copy(), blk_list)
+ # import cv2
+ # cv2.imshow('xx', c)
+ # cv2.waitKey(0)
+ self.dl_manager.runBlktransPipeline(blk_list, tgt_img, mode)
+
+ def on_blktrans_finished(self, mode: int):
+ blkitem_list = self.canvas.selected_text_items()
+ if len(blkitem_list) < 1:
+ return
+ pairw_list = []
+ for blk in blkitem_list:
+ pairw_list.append(self.st_manager.pairwidget_list[blk.idx])
+ self.canvas.push_undo_command(RunBlkTransCommand(self.canvas, blkitem_list, pairw_list, mode))
+
def on_imgtrans_progressbox_showed(self):
msg_size = self.dl_manager.progress_msgbox.size()
size = self.size()
diff --git a/ballontranslator/ui/scenetext_manager.py b/ballontranslator/ui/scenetext_manager.py
index 42fe35ed..f47e4eb4 100644
--- a/ballontranslator/ui/scenetext_manager.py
+++ b/ballontranslator/ui/scenetext_manager.py
@@ -27,21 +27,21 @@ def __init__(self, blk_item: TextBlkItem, ctrl, parent=None):
super().__init__(parent)
self.blk_item = blk_item
self.ctrl: SceneTextManager = ctrl
-
- def redo(self):
+ self.op_count = -1
self.ctrl.addTextBlock(self.blk_item)
+ self.pairw = self.ctrl.pairwidget_list[self.blk_item.idx]
self.ctrl.txtblkShapeControl.setBlkItem(self.blk_item)
+ def redo(self):
+ if self.op_count < 0:
+ self.op_count += 1
+ self.blk_item.setSelected(True)
+ return
+ self.ctrl.recoverTextblkItem(self.blk_item, self.pairw)
+
def undo(self):
self.ctrl.deleteTextblkItem(self.blk_item)
- def mergeWith(self, command: QUndoCommand):
- blk_item = command.blk_item
- if self.blk_item != blk_item:
- return False
- self.blk_item = blk_item
- return True
-
class DeleteBlkItemsCommand(QUndoCommand):
def __init__(self, blk_list: List[TextBlkItem], ctrl, parent=None):
@@ -662,7 +662,7 @@ def onEndCreateTextBlock(self, rect: QRectF):
block = TextBlock(xyxy)
xywh = np.copy(xyxy)
xywh[[2, 3]] -= xywh[[0, 1]]
- block.lines = xywh2xyxypoly(np.array([xywh])).reshape(-1, 4, 2).tolist()
+ block.set_lines_by_xywh(xywh)
blk_item = TextBlkItem(block, len(self.textblk_item_list), set_format=False, show_rect=True)
blk_item.set_fontformat(self.formatpanel.global_format)
self.canvas.push_undo_command(CreateItemCommand(blk_item, self))
diff --git a/ballontranslator/ui/stylewidgets.py b/ballontranslator/ui/stylewidgets.py
index f4260805..84460684 100644
--- a/ballontranslator/ui/stylewidgets.py
+++ b/ballontranslator/ui/stylewidgets.py
@@ -146,6 +146,17 @@ def zero_progress(self):
self.updateInpaintProgress(0)
self.updateTranslateProgress(0)
+ def show_all_bars(self):
+ self.detect_bar.show()
+ self.ocr_bar.show()
+ self.translate_bar.show()
+ self.inpaint_bar.show()
+
+ def hide_all_bars(self):
+ self.detect_bar.hide()
+ self.ocr_bar.hide()
+ self.translate_bar.hide()
+ self.inpaint_bar.hide()
class ColorPicker(QLabel):
colorChanged = Signal(bool)
diff --git a/ballontranslator/utils/imgproc_utils.py b/ballontranslator/utils/imgproc_utils.py
index 32f7c4a3..631718e2 100644
--- a/ballontranslator/utils/imgproc_utils.py
+++ b/ballontranslator/utils/imgproc_utils.py
@@ -168,6 +168,9 @@ def enlarge_window(rect, im_w, im_h, ratio=2.5, aspect_ratio=1.0) -> List:
w = x2 - x1
h = y2 - y1
+ if w <= 0 or h <= 0:
+ return [0, 0, 0, 0]
+
# https://numpy.org/doc/stable/reference/generated/numpy.roots.html
coeff = [aspect_ratio, w+h*aspect_ratio, (1-ratio)*w*h]
roots = np.roots(coeff)
@@ -177,6 +180,8 @@ def enlarge_window(rect, im_w, im_h, ratio=2.5, aspect_ratio=1.0) -> List:
delta_w = min(x1, im_w - x2, delta_w)
delta = min(y1, im_h - y2, delta)
rect = np.array([x1-delta_w, y1-delta, x2+delta_w, y2+delta], dtype=np.int64)
+ rect[::2] = np.clip(rect[::2], 0, im_w - 1)
+ rect[1::2] = np.clip(rect[1::2], 0, im_h - 1)
return rect.tolist()
def draw_connected_labels(num_labels, labels, stats, centroids, names="draw_connected_labels", skip_background=True):
diff --git a/doc/src/ocrselected.gif b/doc/src/ocrselected.gif
new file mode 100644
index 00000000..8586ab83
Binary files /dev/null and b/doc/src/ocrselected.gif differ