33from __future__ import annotations
44from typing import Union , Iterable
55from pathlib import Path
6- from tqdm import tqdm
6+ from tqdm import tqdm , trange
77import numpy as np
8+ import pandas as pd
89from PIL import Image
10+ import skimage
911import 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
3043def 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-
254217def 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+ })
0 commit comments