videopython 0.2.1__py3-none-any.whl → 0.4.0__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.
Potentially problematic release.
This version of videopython might be problematic. Click here for more details.
- videopython/ai/__init__.py +0 -0
- videopython/{generation → ai/generation}/audio.py +25 -13
- videopython/{generation → ai/generation}/image.py +0 -3
- videopython/ai/understanding/__init__.py +0 -0
- videopython/ai/understanding/transcribe.py +37 -0
- videopython/base/effects.py +3 -3
- videopython/base/transcription.py +13 -0
- videopython/base/transforms.py +0 -2
- videopython/base/transitions.py +2 -2
- videopython/base/video.py +269 -187
- videopython/utils/__init__.py +3 -0
- videopython/utils/image.py +0 -228
- videopython/utils/text.py +727 -0
- {videopython-0.2.1.dist-info → videopython-0.4.0.dist-info}/METADATA +13 -25
- videopython-0.4.0.dist-info/RECORD +25 -0
- {videopython-0.2.1.dist-info → videopython-0.4.0.dist-info}/WHEEL +1 -1
- videopython-0.2.1.dist-info/RECORD +0 -20
- /videopython/{generation → ai/generation}/__init__.py +0 -0
- /videopython/{generation → ai/generation}/video.py +0 -0
- {videopython-0.2.1.dist-info → videopython-0.4.0.dist-info}/licenses/LICENSE +0 -0
videopython/utils/image.py
CHANGED
|
@@ -2,238 +2,10 @@ from typing import Literal
|
|
|
2
2
|
|
|
3
3
|
import cv2
|
|
4
4
|
import numpy as np
|
|
5
|
-
from PIL import Image, ImageDraw, ImageFont
|
|
6
5
|
|
|
7
|
-
from videopython.base.exceptions import OutOfBoundsError
|
|
8
6
|
from videopython.base.video import Video
|
|
9
7
|
|
|
10
8
|
|
|
11
|
-
class ImageText:
|
|
12
|
-
def __init__(
|
|
13
|
-
self,
|
|
14
|
-
image_size: tuple[int, int] = (1080, 1920), # (width, height)
|
|
15
|
-
mode: str = "RGBA",
|
|
16
|
-
background: tuple[int, int, int, int] = (0, 0, 0, 0), # Transparent background
|
|
17
|
-
):
|
|
18
|
-
self.image_size = image_size
|
|
19
|
-
self.image = Image.new(mode, image_size, color=background)
|
|
20
|
-
self._draw = ImageDraw.Draw(self.image)
|
|
21
|
-
|
|
22
|
-
@property
|
|
23
|
-
def img_array(self) -> np.ndarray:
|
|
24
|
-
return np.array(self.image)
|
|
25
|
-
|
|
26
|
-
def save(self, filename: str) -> None:
|
|
27
|
-
self.image.save(filename)
|
|
28
|
-
|
|
29
|
-
def _fit_font_width(self, text: str, font: str, max_width: int) -> int:
|
|
30
|
-
"""Find the maximum font size where the text width is less than or equal to max_width."""
|
|
31
|
-
font_size = 1
|
|
32
|
-
text_width = self.get_text_size(font, font_size, text)[0]
|
|
33
|
-
while text_width < max_width:
|
|
34
|
-
font_size += 1
|
|
35
|
-
text_width = self.get_text_size(font, font_size, text)[0]
|
|
36
|
-
max_font_size = font_size - 1
|
|
37
|
-
if max_font_size < 1:
|
|
38
|
-
raise ValueError(f"Max height {max_width} is too small for any font size!")
|
|
39
|
-
return max_font_size
|
|
40
|
-
|
|
41
|
-
def _fit_font_height(self, text: str, font: str, max_height: int) -> int:
|
|
42
|
-
"""Find the maximum font size where the text height is less than or equal to max_height."""
|
|
43
|
-
font_size = 1
|
|
44
|
-
text_height = self.get_text_size(font, font_size, text)[1]
|
|
45
|
-
while text_height < max_height:
|
|
46
|
-
font_size += 1
|
|
47
|
-
text_height = self.get_text_size(font, font_size, text)[1]
|
|
48
|
-
max_font_size = font_size - 1
|
|
49
|
-
if max_font_size < 1:
|
|
50
|
-
raise ValueError(f"Max height {max_height} is too small for any font size!")
|
|
51
|
-
return max_font_size
|
|
52
|
-
|
|
53
|
-
def _get_font_size(
|
|
54
|
-
self,
|
|
55
|
-
text: str,
|
|
56
|
-
font: str,
|
|
57
|
-
max_width: int | None = None,
|
|
58
|
-
max_height: int | None = None,
|
|
59
|
-
) -> int:
|
|
60
|
-
"""Get maximum font size for `text` to fill in the `max_width` and `max_height`."""
|
|
61
|
-
if max_width is None and max_height is None:
|
|
62
|
-
raise ValueError("You need to pass max_width or max_height")
|
|
63
|
-
if max_width is not None:
|
|
64
|
-
width_font_size = self._fit_font_width(text, font, max_width)
|
|
65
|
-
if max_height is not None:
|
|
66
|
-
height_font_size = self._fit_font_height(text, font, max_height)
|
|
67
|
-
return min([size for size in [width_font_size, height_font_size] if size is not None])
|
|
68
|
-
|
|
69
|
-
def write_text(
|
|
70
|
-
self,
|
|
71
|
-
text: str,
|
|
72
|
-
font_filename: str,
|
|
73
|
-
xy: tuple[int, int],
|
|
74
|
-
font_size: int | None = 11,
|
|
75
|
-
color: tuple[int, int, int] = (0, 0, 0),
|
|
76
|
-
max_width: int | None = None,
|
|
77
|
-
max_height: int | None = None,
|
|
78
|
-
) -> tuple[int, int]:
|
|
79
|
-
x, y = xy
|
|
80
|
-
if font_size is None and (max_width is None or max_height is None):
|
|
81
|
-
raise ValueError(f"Must set either `font_size`, or both `max_width` and `max_height`!")
|
|
82
|
-
elif font_size is None:
|
|
83
|
-
font_size = self._get_font_size(text, font_filename, max_width, max_height)
|
|
84
|
-
text_size = self.get_text_size(font_filename, font_size, text)
|
|
85
|
-
if (text_size[0] + x > self.image_size[0]) or (text_size[1] + y > self.image_size[1]):
|
|
86
|
-
raise OutOfBoundsError(f"Font size `{font_size}` is too big, text won't fit!")
|
|
87
|
-
font = ImageFont.truetype(font_filename, font_size)
|
|
88
|
-
self._draw.text((x, y), text, font=font, fill=color)
|
|
89
|
-
return text_size
|
|
90
|
-
|
|
91
|
-
def get_text_size(self, font_filename: str, font_size: int, text: str) -> tuple[int, int]:
|
|
92
|
-
"""Return bounding box size of the rendered `text` with `font_filename` and `font_size`."""
|
|
93
|
-
font = ImageFont.truetype(font_filename, font_size)
|
|
94
|
-
return font.getbbox(text)[2:]
|
|
95
|
-
|
|
96
|
-
def _split_lines_by_width(
|
|
97
|
-
self,
|
|
98
|
-
text: str,
|
|
99
|
-
font_filename: str,
|
|
100
|
-
font_size: int,
|
|
101
|
-
box_width: int,
|
|
102
|
-
) -> list[str]:
|
|
103
|
-
"""Split the `text` into lines of maximum `box_width`."""
|
|
104
|
-
words = text.split()
|
|
105
|
-
split_lines: list[list[str]] = []
|
|
106
|
-
current_line: list[str] = []
|
|
107
|
-
for word in words:
|
|
108
|
-
new_line = " ".join(current_line + [word])
|
|
109
|
-
size = self.get_text_size(font_filename, font_size, new_line)
|
|
110
|
-
if size[0] <= box_width:
|
|
111
|
-
current_line.append(word)
|
|
112
|
-
else:
|
|
113
|
-
split_lines.append(current_line)
|
|
114
|
-
current_line = [word]
|
|
115
|
-
if current_line:
|
|
116
|
-
split_lines.append(current_line)
|
|
117
|
-
lines = [" ".join(line) for line in split_lines]
|
|
118
|
-
return lines
|
|
119
|
-
|
|
120
|
-
def write_text_box(
|
|
121
|
-
self,
|
|
122
|
-
text: str,
|
|
123
|
-
font_filename: str,
|
|
124
|
-
xy: tuple[int, int],
|
|
125
|
-
box_width: int,
|
|
126
|
-
font_size: int = 11,
|
|
127
|
-
text_color: tuple[int, int, int] = (0, 0, 0),
|
|
128
|
-
background_color: None | tuple[int, int, int, int] = None,
|
|
129
|
-
background_padding: int = 0,
|
|
130
|
-
place: Literal["left", "right", "center"] = "left",
|
|
131
|
-
) -> tuple[int, int]:
|
|
132
|
-
"""Write text in box described by upper-left corner and maxium width of the box.
|
|
133
|
-
|
|
134
|
-
Args:
|
|
135
|
-
text: Text to be written inside the box.
|
|
136
|
-
font_filename: Path to the font file.
|
|
137
|
-
xy: X and Y coordinates describing upper-left of the box containing the text.
|
|
138
|
-
box_width: Pixel width of the box containing the text.
|
|
139
|
-
font_size: Font size.
|
|
140
|
-
text_color: RGB color of the text.
|
|
141
|
-
background_color: If set, adds background color to the text box. Expects RGBA values.
|
|
142
|
-
background_padding: Number of padding pixels to add when adding text background color.
|
|
143
|
-
place: Strategy for justifying the text inside the container box. Defaults to "left".
|
|
144
|
-
|
|
145
|
-
Returns:
|
|
146
|
-
Lower-left corner of the written text box.
|
|
147
|
-
"""
|
|
148
|
-
x, y = xy
|
|
149
|
-
lines = self._split_lines_by_width(text, font_filename, font_size, box_width)
|
|
150
|
-
# Run checks to see if the text will fit
|
|
151
|
-
if x + box_width > self.image_size[0]:
|
|
152
|
-
raise OutOfBoundsError(f"Box width {box_width} is too big for the image width {self.image_size[0]}!")
|
|
153
|
-
lines_height = sum([self.get_text_size(font_filename, font_size, line)[1] for line in lines])
|
|
154
|
-
if y + lines_height > self.image_size[1]:
|
|
155
|
-
available_space = self.image_size[1] - y
|
|
156
|
-
raise OutOfBoundsError(f"Text height {lines_height} is too big for the available space {available_space}!")
|
|
157
|
-
# Write lines
|
|
158
|
-
current_text_height = y
|
|
159
|
-
for line in lines:
|
|
160
|
-
line_size = self.get_text_size(font_filename, font_size, line)
|
|
161
|
-
# Write line text into the image
|
|
162
|
-
if place == "left":
|
|
163
|
-
self.write_text(
|
|
164
|
-
text=line,
|
|
165
|
-
font_filename=font_filename,
|
|
166
|
-
xy=(x, current_text_height),
|
|
167
|
-
font_size=font_size,
|
|
168
|
-
color=text_color,
|
|
169
|
-
)
|
|
170
|
-
elif place == "right":
|
|
171
|
-
x_left = x + box_width - line_size[0]
|
|
172
|
-
self.write_text(
|
|
173
|
-
text=line,
|
|
174
|
-
font_filename=font_filename,
|
|
175
|
-
xy=(x_left, current_text_height),
|
|
176
|
-
font_size=font_size,
|
|
177
|
-
color=text_color,
|
|
178
|
-
)
|
|
179
|
-
elif place == "center":
|
|
180
|
-
x_left = int(x + ((box_width - line_size[0]) / 2))
|
|
181
|
-
self.write_text(
|
|
182
|
-
text=line,
|
|
183
|
-
font_filename=font_filename,
|
|
184
|
-
xy=(x_left, current_text_height),
|
|
185
|
-
font_size=font_size,
|
|
186
|
-
color=text_color,
|
|
187
|
-
)
|
|
188
|
-
else:
|
|
189
|
-
raise ValueError(f"Place {place} is not supported. Use one of: `left`, `right` or `center`!")
|
|
190
|
-
# Increment text height
|
|
191
|
-
current_text_height += line_size[1]
|
|
192
|
-
# Add background color for the text if set
|
|
193
|
-
if background_color is not None:
|
|
194
|
-
if len(background_color) != 4:
|
|
195
|
-
raise ValueError(f"Text background color {background_color} must be RGBA!")
|
|
196
|
-
img = self.img_array
|
|
197
|
-
# Find bounding rectangle for written text
|
|
198
|
-
box_slice = img[y:current_text_height, x : x + box_width]
|
|
199
|
-
text_mask = np.any(box_slice != 0, axis=2).astype(np.uint8)
|
|
200
|
-
xmin, xmax, ymin, ymax = self._find_smallest_bounding_rect(text_mask)
|
|
201
|
-
# Get global bounding box position
|
|
202
|
-
xmin += x - background_padding
|
|
203
|
-
xmax += x + background_padding
|
|
204
|
-
ymin += y - background_padding
|
|
205
|
-
ymax += y + background_padding
|
|
206
|
-
# Make sure we are inside image, cut to image if not
|
|
207
|
-
xmin = max(0, xmin)
|
|
208
|
-
ymin = max(0, ymin)
|
|
209
|
-
xmax = min(xmax, self.image_size[0])
|
|
210
|
-
ymax = min(ymax, self.image_size[1])
|
|
211
|
-
# Slice the bounding box and find text mask
|
|
212
|
-
bbox_slice = img[ymin:ymax, xmin:xmax]
|
|
213
|
-
bbox_text_mask = np.any(bbox_slice != 0, axis=2).astype(np.uint8)
|
|
214
|
-
# Add background color outside of text
|
|
215
|
-
bbox_slice[~bbox_text_mask.astype(bool)] = background_color
|
|
216
|
-
# Blur nicely with semi-transparent pixels from the font
|
|
217
|
-
text_slice = bbox_slice[bbox_text_mask.astype(bool)]
|
|
218
|
-
text_background = text_slice[:, :3] * (np.expand_dims(text_slice[:, -1], axis=1) / 255)
|
|
219
|
-
color_background = (1 - (np.expand_dims(text_slice[:, -1], axis=1) / 255)) * background_color
|
|
220
|
-
faded_background = text_background[:, :3] + color_background[:, :3]
|
|
221
|
-
text_slice[:, :3] = faded_background
|
|
222
|
-
text_slice[:, -1] = 255
|
|
223
|
-
bbox_slice[bbox_text_mask.astype(bool)] = text_slice
|
|
224
|
-
# Set image with the background color
|
|
225
|
-
self.image = Image.fromarray(img)
|
|
226
|
-
return (x, current_text_height)
|
|
227
|
-
|
|
228
|
-
def _find_smallest_bounding_rect(self, mask: np.ndarray) -> tuple[int, int, int, int]:
|
|
229
|
-
"""Find the smallest bounding rectangle for the mask."""
|
|
230
|
-
rows = np.any(mask, axis=1)
|
|
231
|
-
cols = np.any(mask, axis=0)
|
|
232
|
-
ymin, ymax = np.where(rows)[0][[0, -1]]
|
|
233
|
-
xmin, xmax = np.where(cols)[0][[0, -1]]
|
|
234
|
-
return xmin, xmax, ymin, ymax
|
|
235
|
-
|
|
236
|
-
|
|
237
9
|
class SlideOverImage:
|
|
238
10
|
def __init__(
|
|
239
11
|
self,
|