Skip to content

Commit 41dd15f

Browse files
authored
Merge pull request #294 from dclong/dev
Merge dev into main
2 parents d313965 + c2f5436 commit 41dd15f

File tree

6 files changed

+194
-71
lines changed

6 files changed

+194
-71
lines changed

dsutil/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
from . import git
44
from . import poetry
55

6-
__version__ = "0.64.0"
6+
__version__ = "0.65.0"

dsutil/cv.py

Lines changed: 86 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,41 @@
33
from __future__ import annotations
44
from typing import Union, Iterable
55
from pathlib import Path
6-
from tqdm import tqdm
6+
from tqdm import tqdm, trange
77
import numpy as np
8+
import pandas as pd
89
from PIL import Image
10+
import skimage
911
import cv2
1012

1113

12-
def video_to_image(video, frames_per_image: int = 60, output: str = "frame_{:0>7}.png"):
13-
"""Extract images from a video.
14+
def video_to_image(
15+
file: str,
16+
step: int,
17+
bbox: Union[None, tuple[int, int, int, int]] = None,
18+
output: str = "frame_{:0>7}.png"
19+
):
20+
"""Extract images from a video file.
1421
15-
:param video: The path to a video file.
16-
:param frames_per_image: Extract 1 image every frames_per_image.
17-
:param output: The pattern of the output files for the extracted images.
22+
:param file: The path to video file.
23+
:param bbox: A bounding box.
24+
If specified, crop images using the bounding box.
25+
:param output_dir: The directory to save extracted images.
26+
:param step: Keep 1 image every step frames.
1827
"""
19-
vidcap = cv2.VideoCapture(video)
20-
count = 0
21-
while True:
22-
success, image = vidcap.read()
28+
Path(output.format(0)).parent.mkdir(parents=True, exist_ok=True)
29+
vidcap = cv2.VideoCapture(file)
30+
total = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
31+
for idx in trange(total):
32+
success, arr = vidcap.read()
2333
if not success:
2434
break
25-
if count % frames_per_image == 0:
26-
cv2.imwrite(output.format(count), image)
27-
count += 1
35+
if idx % step == 0:
36+
img = Image.fromarray(np.flip(arr, 2))
37+
if bbox:
38+
img = img.crop(bbox)
39+
img.save(output.format(idx))
40+
vidcap.release()
2841

2942

3043
def resize_image(
@@ -201,56 +214,6 @@ def deshade_3(img, threshold=0.4, cutoff=30) -> Image.Image:
201214
return Image.fromarray(arr)
202215

203216

204-
def highlight_frame(
205-
rgb: tuple[int, int, int],
206-
shape: tuple[int, int],
207-
thickness: int = 3
208-
) -> Image.Image:
209-
"""Generate a rectangle frame with the specified color and thickness.
210-
211-
:param rgb: The color in RGB (as a tuple) to use for the frame.
212-
:param shape: The shape of the frame.
213-
:param thickness: The thickness of the frame.
214-
:return: A PIL image presenting the frame.
215-
"""
216-
nrow = shape[0]
217-
ncol = shape[1]
218-
arr = np.zeros((nrow, ncol, 3), np.uint8)
219-
arr[0:thickness, :, 0] = rgb[0]
220-
arr[0:thickness, :, 1] = rgb[1]
221-
arr[0:thickness, :, 2] = rgb[2]
222-
arr[(nrow - thickness):nrow, :, 0] = rgb[0]
223-
arr[(nrow - thickness):nrow, :, 1] = rgb[1]
224-
arr[(nrow - thickness):nrow, :, 2] = rgb[2]
225-
arr[:, 0:thickness, 0] = rgb[0]
226-
arr[:, 0:thickness, 1] = rgb[1]
227-
arr[:, 0:thickness, 2] = rgb[2]
228-
arr[:, (ncol - thickness):ncol, 0] = rgb[0]
229-
arr[:, (ncol - thickness):ncol, 1] = rgb[1]
230-
arr[:, (ncol - thickness):ncol, 2] = rgb[2]
231-
return Image.fromarray(arr)
232-
233-
234-
def frame_image(
235-
img: Image.Image, rgb: tuple[int, int, int], thickness: int = 3
236-
) -> Image.Image:
237-
"""Add a highlight frame to an image.
238-
239-
:param img: A PIL image.
240-
:param rgb: The color in RGB (as a tuple) to use for the frame.
241-
:param thickness: The thickness of the frame.
242-
:return: A new image with the frame added.
243-
"""
244-
shape = img.size
245-
shape = (shape[1], shape[0])
246-
frame = highlight_frame(rgb, shape=shape, thickness=thickness)
247-
mask = highlight_frame((255, 255, 255), shape=shape,
248-
thickness=thickness).convert("1")
249-
img = img.copy()
250-
img.paste(frame, mask=mask)
251-
return img
252-
253-
254217
def add_frames(
255218
arr: Union[np.ndarray, Image.Image],
256219
bboxes: list[tuple[int, int, int, int]],
@@ -275,3 +238,63 @@ def add_frames(
275238
arr[y1:y2, x1, :] = rgb
276239
arr[y1:y2, x2, :] = rgb
277240
return arr
241+
242+
243+
def duplicate_image(
244+
path: Union[str, Path],
245+
copies: int,
246+
des_dir: Union[str, Path, None] = None,
247+
noise_amount: float = 0.05
248+
):
249+
"""Duplicate an image with some noises added.
250+
251+
:param path: The path to the image to be duplicated.
252+
:param copies: The number of copies to duplicate.
253+
:param noise_amount: Proportion of image pixels to replace with noise on range [0, 1].
254+
"""
255+
if isinstance(path, str):
256+
path = Path(path)
257+
if isinstance(des_dir, str):
258+
des_dir = Path(des_dir)
259+
if des_dir is None:
260+
des_dir = path.parent
261+
des_dir.mkdir(parents=True, exist_ok=True)
262+
for i in range(copies):
263+
file_i = des_dir / f"{path.stem}_copy{i}.png"
264+
noise = skimage.util.random_noise(
265+
np.array(Image.open(path)), mode="s&p", amount=noise_amount
266+
)
267+
Image.fromarray(np.array(noise * 255, dtype=np.uint8)).save(file_i)
268+
269+
270+
def structural_similarity(im1, im2) -> float:
271+
"""Extend skimage.metrics.structural_similarity
272+
to calculate the similarity of (any) two images.
273+
274+
:param im1: A PIL image.
275+
:param im2: Another PIL image.
276+
"""
277+
size = im1.size
278+
if im2.size != size:
279+
im2 = im2.resize(size)
280+
return skimage.metrics.structural_similarity(
281+
np.array(im1), np.array(im2), multichannel=True
282+
)
283+
284+
285+
def calc_image_similarities(img: Union[Image.Image, str, Path], dir_: Union[str, Path]):
286+
"""Calculate the similarities between an image and all images in a directory.
287+
288+
:param img: A PIL image or the path to an image file.
289+
:param dir_: A directory containing images.
290+
"""
291+
if isinstance(img, (str, Path)):
292+
img = Image.open(img)
293+
if isinstance(dir_, str):
294+
dir_ = Path(dir_)
295+
paths = list(dir_.glob("*.png"))
296+
sims = [structural_similarity(img, Image.open(p)) for p in tqdm(paths)]
297+
return pd.DataFrame({
298+
"path": paths,
299+
"similarity": sims,
300+
})

dsutil/docker/builder.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,18 @@ def _push_image_timing(repo: str, tag: str) -> DockerActionResult:
5454
# print()
5555
time_begin = time.perf_counter_ns()
5656
try:
57-
retry(lambda: sp.run(f"docker push {repo}:{tag}", shell=True, check=True), times=3)
58-
return DockerActionResult(True, "", repo, tag, "push", (time.perf_counter_ns() - time_begin) / 1E9)
57+
retry(
58+
lambda: sp.run(f"docker push {repo}:{tag}", shell=True, check=True),
59+
times=3
60+
)
61+
return DockerActionResult(
62+
True, "", repo, tag, "push", (time.perf_counter_ns() - time_begin) / 1E9
63+
)
5964
except Exception as err:
60-
return DockerActionResult(False, str(err), repo, tag, "push", (time.perf_counter_ns() - time_begin) / 1E9)
65+
return DockerActionResult(
66+
False, str(err), repo, tag, "push",
67+
(time.perf_counter_ns() - time_begin) / 1E9
68+
)
6169

6270

6371
#def _is_image_pushed(msg: dict[str, Any]):

poetry.lock

Lines changed: 92 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "dsutil"
3-
version = "0.64.0"
3+
version = "0.65.0"
44
description = "A utils Python package for data scientists."
55
authors = ["Benjamin Du <[email protected]>"]
66

@@ -42,6 +42,7 @@ nbformat = { version = ">=5.0.7", optional = true }
4242
nbconvert = { version = ">=5.6.1", optional = true }
4343
yapf = { version = ">=0.30.0", optional = true}
4444
dulwich = ">=0.20.24"
45+
scikit-image = ">=0.18.3"
4546

4647
[tool.poetry.extras]
4748
cv = ["opencv-python", "pillow"]

0 commit comments

Comments
 (0)