2019-04-24 21:18:31 +02:00
|
|
|
from __future__ import annotations
|
2019-12-15 15:29:13 +01:00
|
|
|
from typing import List
|
|
|
|
import sys
|
2019-12-15 14:38:48 +01:00
|
|
|
import multiprocessing
|
2019-04-24 21:18:31 +02:00
|
|
|
import pytesseract
|
|
|
|
import cv2
|
|
|
|
|
2019-04-29 03:04:06 +02:00
|
|
|
from . import constants
|
2019-12-15 14:38:48 +01:00
|
|
|
from . import utils
|
2019-04-25 01:40:46 +02:00
|
|
|
from .models import PredictedFrame, PredictedSubtitle
|
2019-12-15 14:38:17 +01:00
|
|
|
from .opencv_adapter import Capture
|
2019-04-25 01:40:46 +02:00
|
|
|
|
|
|
|
|
2019-04-24 21:18:31 +02:00
|
|
|
class Video:
|
|
|
|
path: str
|
|
|
|
lang: str
|
2019-04-26 00:07:25 +02:00
|
|
|
use_fullframe: bool
|
2019-04-24 21:18:31 +02:00
|
|
|
num_frames: int
|
2019-04-26 00:07:25 +02:00
|
|
|
fps: float
|
2019-04-29 03:05:02 +02:00
|
|
|
height: int
|
2021-07-13 09:12:43 +02:00
|
|
|
width: int
|
|
|
|
resize_dim: List[int]
|
2019-04-25 01:40:46 +02:00
|
|
|
pred_frames: List[PredictedFrame]
|
2019-04-26 00:07:25 +02:00
|
|
|
pred_subs: List[PredictedSubtitle]
|
2019-04-24 21:18:31 +02:00
|
|
|
|
2019-04-27 21:41:19 +02:00
|
|
|
def __init__(self, path: str):
|
2019-04-24 21:18:31 +02:00
|
|
|
self.path = path
|
2019-12-15 14:38:17 +01:00
|
|
|
with Capture(path) as v:
|
|
|
|
self.num_frames = int(v.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
|
|
self.fps = v.get(cv2.CAP_PROP_FPS)
|
|
|
|
self.height = int(v.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
2021-07-13 09:12:43 +02:00
|
|
|
self.width = int(v.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
|
|
scale_percent = 47 # apparently 32 pixels is the optimal character height for tesseract.
|
|
|
|
self.resize_dim=(int(self.width * scale_percent/100), int(self.height * scale_percent/100))
|
2019-04-24 21:18:31 +02:00
|
|
|
|
2019-04-28 15:46:24 +02:00
|
|
|
def run_ocr(self, lang: str, time_start: str, time_end: str,
|
2019-12-15 14:38:48 +01:00
|
|
|
conf_threshold: int, use_fullframe: bool) -> None:
|
2019-04-27 21:41:19 +02:00
|
|
|
self.lang = lang
|
|
|
|
self.use_fullframe = use_fullframe
|
|
|
|
|
2019-12-15 14:38:48 +01:00
|
|
|
ocr_start = utils.get_frame_index(time_start, self.fps) if time_start else 0
|
|
|
|
ocr_end = utils.get_frame_index(time_end, self.fps) if time_end else self.num_frames
|
2019-04-27 00:29:31 +02:00
|
|
|
|
2019-04-28 15:46:24 +02:00
|
|
|
if ocr_end < ocr_start:
|
|
|
|
raise ValueError('time_start is later than time_end')
|
2019-04-27 21:41:19 +02:00
|
|
|
num_ocr_frames = ocr_end - ocr_start
|
|
|
|
|
|
|
|
# get frames from ocr_start to ocr_end
|
2019-12-15 14:38:17 +01:00
|
|
|
with Capture(self.path) as v, multiprocessing.Pool() as pool:
|
|
|
|
v.set(cv2.CAP_PROP_POS_FRAMES, ocr_start)
|
|
|
|
frames = (v.read()[1] for _ in range(num_ocr_frames))
|
2019-04-27 21:41:19 +02:00
|
|
|
|
2019-12-15 14:38:17 +01:00
|
|
|
# perform ocr to frames in parallel
|
|
|
|
it_ocr = pool.imap(self._image_to_data, frames, chunksize=10)
|
2019-04-29 03:05:02 +02:00
|
|
|
self.pred_frames = [
|
2019-12-15 14:38:48 +01:00
|
|
|
PredictedFrame(i + ocr_start, data, conf_threshold)
|
|
|
|
for i, data in enumerate(it_ocr)
|
|
|
|
]
|
|
|
|
|
|
|
|
def _image_to_data(self, img) -> str:
|
2019-04-27 00:29:31 +02:00
|
|
|
if not self.use_fullframe:
|
|
|
|
# only use bottom half of the frame by default
|
2019-04-29 03:04:06 +02:00
|
|
|
img = img[self.height // 2:, :]
|
2021-07-13 09:12:43 +02:00
|
|
|
# dilate and resize
|
2021-07-13 10:20:47 +02:00
|
|
|
img=cv2.resize(cv2.dilate(img, np.ones((2, 2), np.uint8)), self.resize_dim, interpolation=cv2.INTER_AREA)
|
2021-07-13 09:12:43 +02:00
|
|
|
|
|
|
|
# mask to filter out non gray-like pixels/pixels that are not bright enough
|
|
|
|
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
|
|
|
color_mask = cv2.inRange(hsv, (0, 0, 190), (179, 20, 255))
|
|
|
|
|
|
|
|
# apply mask, inverse image so it's black text on white background, add borders to top and bottom
|
2021-07-13 16:36:35 +02:00
|
|
|
img = cv2.copyMakeBorder(cv2.bitwise_not(cv2.bitwise_and(img, img, mask=color_mask)), 10, 10, 0, 0, cv2.BORDER_CONSTANT, None, (255,255,255))
|
2021-07-13 09:12:43 +02:00
|
|
|
config = '--tessdata-dir "{}" --psm 7 -c preserve_interword_spaces=1'.format(constants.TESSDATA_DIR)
|
2019-12-15 15:29:13 +01:00
|
|
|
try:
|
|
|
|
return pytesseract.image_to_data(img, lang=self.lang, config=config)
|
|
|
|
except Exception as e:
|
|
|
|
sys.exit('{}: {}'.format(e.__class__.__name__, e))
|
2019-04-27 00:29:31 +02:00
|
|
|
|
2019-04-29 03:50:06 +02:00
|
|
|
def get_subtitles(self, sim_threshold: int) -> str:
|
|
|
|
self._generate_subtitles(sim_threshold)
|
2019-04-26 00:32:47 +02:00
|
|
|
return ''.join(
|
2019-04-27 00:28:17 +02:00
|
|
|
'{}\n{} --> {}\n{}\n\n'.format(
|
2019-04-26 00:32:47 +02:00
|
|
|
i,
|
2019-12-15 14:38:48 +01:00
|
|
|
utils.get_srt_timestamp(sub.index_start, self.fps),
|
|
|
|
utils.get_srt_timestamp(sub.index_end, self.fps),
|
2019-04-26 00:32:47 +02:00
|
|
|
sub.text)
|
|
|
|
for i, sub in enumerate(self.pred_subs))
|
|
|
|
|
2019-04-29 03:50:06 +02:00
|
|
|
def _generate_subtitles(self, sim_threshold: int) -> None:
|
2019-04-26 00:32:47 +02:00
|
|
|
self.pred_subs = []
|
|
|
|
|
2019-04-25 01:40:46 +02:00
|
|
|
if self.pred_frames is None:
|
|
|
|
raise AttributeError(
|
2019-04-27 00:28:17 +02:00
|
|
|
'Please call self.run_ocr() first to perform ocr on frames')
|
2019-04-25 01:40:46 +02:00
|
|
|
|
|
|
|
# divide ocr of frames into subtitle paragraphs using sliding window
|
2019-04-27 00:28:17 +02:00
|
|
|
WIN_BOUND = int(self.fps // 2) # 1/2 sec sliding window boundary
|
2019-04-26 00:07:25 +02:00
|
|
|
bound = WIN_BOUND
|
2019-04-25 01:40:46 +02:00
|
|
|
i = 0
|
|
|
|
j = 1
|
2019-04-27 21:41:19 +02:00
|
|
|
while j < len(self.pred_frames):
|
2019-04-25 01:40:46 +02:00
|
|
|
fi, fj = self.pred_frames[i], self.pred_frames[j]
|
|
|
|
|
|
|
|
if fi.is_similar_to(fj):
|
2019-04-26 00:07:25 +02:00
|
|
|
bound = WIN_BOUND
|
2019-04-25 01:40:46 +02:00
|
|
|
elif bound > 0:
|
|
|
|
bound -= 1
|
|
|
|
else:
|
|
|
|
# divide subtitle paragraphs
|
2019-04-26 00:07:25 +02:00
|
|
|
para_new = j - WIN_BOUND
|
2019-04-29 03:50:06 +02:00
|
|
|
self._append_sub(PredictedSubtitle(
|
|
|
|
self.pred_frames[i:para_new], sim_threshold))
|
2019-04-25 01:40:46 +02:00
|
|
|
i = para_new
|
|
|
|
j = i
|
2019-04-26 00:07:25 +02:00
|
|
|
bound = WIN_BOUND
|
2019-04-25 01:40:46 +02:00
|
|
|
|
|
|
|
j += 1
|
|
|
|
|
2019-04-26 00:32:47 +02:00
|
|
|
# also handle the last remaining frames
|
2019-04-27 21:41:19 +02:00
|
|
|
if i < len(self.pred_frames) - 1:
|
2019-04-29 03:50:06 +02:00
|
|
|
self._append_sub(PredictedSubtitle(
|
|
|
|
self.pred_frames[i:], sim_threshold))
|
2019-04-26 00:07:25 +02:00
|
|
|
|
|
|
|
def _append_sub(self, sub: PredictedSubtitle) -> None:
|
|
|
|
if len(sub.text) == 0:
|
|
|
|
return
|
|
|
|
|
|
|
|
# merge new sub to the last subs if they are similar
|
|
|
|
while self.pred_subs and sub.is_similar_to(self.pred_subs[-1]):
|
2019-04-27 00:28:17 +02:00
|
|
|
ls = self.pred_subs[-1]
|
2019-04-26 00:07:25 +02:00
|
|
|
del self.pred_subs[-1]
|
2019-04-29 03:50:06 +02:00
|
|
|
sub = PredictedSubtitle(ls.frames + sub.frames, sub.sim_threshold)
|
2019-04-26 00:07:25 +02:00
|
|
|
|
|
|
|
self.pred_subs.append(sub)
|