6
0

FileOperations.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import os
  2. import typing as T
  3. from collections import namedtuple
  4. from pathlib import Path
  5. import cv2
  6. from PIL import Image
  7. from tqdm import tqdm
  8. from pycs import app
  9. from pycs.database.File import File
  10. DEFAULT_JPEG_QUALITY = 80
  11. BoundingBox = namedtuple("BoundingBox", "x y w h")
  12. Size = namedtuple("Size", "max_width max_height")
  13. def file_info(data_folder: str, file_name: str, file_ext: str):
  14. """
  15. Receive file type, frame count and frames per second.
  16. The last two are always None for images.
  17. :param data_folder: path to data folder
  18. :param file_name: file name
  19. :param file_ext: file extension
  20. :return: file type, frame count, frames per second
  21. """
  22. # determine file type
  23. if file_ext.lower() in ['.jpg', '.png']:
  24. file_type = 'image'
  25. elif file_ext.lower() in ['.mp4']:
  26. file_type = 'video'
  27. else:
  28. raise ValueError(f"Unsupported file extension: {file_ext}!")
  29. # determine frames and fps for video files
  30. if file_type == 'image':
  31. frames = None
  32. fps = None
  33. else:
  34. file_path = os.path.join(data_folder, file_name + file_ext)
  35. video = cv2.VideoCapture(file_path)
  36. frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
  37. fps = video.get(cv2.CAP_PROP_FPS)
  38. video.release()
  39. # return values
  40. return file_type, frames, fps
  41. def resize_file(file: File,
  42. project_root: str,
  43. max_width: int,
  44. max_height: int) -> T.Tuple[str, str]:
  45. """
  46. If file type equals video this function extracts a thumbnail first. It calls resize_image
  47. to resize and returns the resized files directory and name.
  48. :param file: file object
  49. :param project_root: project root folder path
  50. :param max_width: maximum image or thumbnail width
  51. :param max_height: maximum image or thumbnail height
  52. :return: resized file directory, resized file name
  53. """
  54. abs_file_path = file.absolute_path
  55. # extract video thumbnail
  56. if file.type == 'video':
  57. abs_target_path = os.path.join(os.getcwd(), project_root, 'temp', f'{file.uuid}.jpg')
  58. create_thumbnail(abs_file_path, abs_target_path)
  59. abs_file_path = abs_target_path
  60. # resize image file
  61. abs_target_path = os.path.join(os.getcwd(),
  62. project_root,
  63. 'temp',
  64. f'{file.uuid}_{max_width}_{max_height}.jpg')
  65. resized = resize_image(abs_file_path, abs_target_path, max_width, max_height)
  66. # return path
  67. if resized:
  68. return os.path.split(abs_target_path)
  69. return os.path.split(abs_file_path)
  70. def crop_file(file: File, project_root: str, box: BoundingBox,
  71. max_width: int, max_height: int) -> T.Tuple[str, str]:
  72. """
  73. gets a file for the given file_id, crops the according image to the
  74. bounding box and saves the crops in the temp folder of the project.
  75. :param file: file object
  76. :param project_root: project root folder path
  77. :param max_width: maximum image or thumbnail width
  78. :param max_height: maximum image or thumbnail height
  79. :param box: BoundingBox with relative x, y coordinates and
  80. the relative height and width of the bounding box
  81. :return: directory and file name of the cropped patch
  82. """
  83. abs_file_path = file.absolute_path
  84. # extract video thumbnail
  85. if file.type == 'video':
  86. abs_target_path = os.path.join(os.getcwd(), project_root, 'temp', f'{file.uuid}.jpg')
  87. create_thumbnail(abs_file_path, abs_target_path)
  88. abs_file_path = abs_target_path
  89. bounding_box_suffix = f"{box.x}_{box.y}_{box.w}_{box.h}"
  90. # crop image file
  91. abs_target_path = os.path.join(os.getcwd(),
  92. project_root,
  93. 'temp',
  94. f'{file.uuid}_{bounding_box_suffix}.jpg')
  95. cropped = crop_image(abs_file_path, abs_target_path, box)
  96. if cropped:
  97. abs_file_path = abs_target_path
  98. # resize image
  99. target_file_name = f'{file.uuid}_{max_width}_{max_height}_{bounding_box_suffix}.jpg'
  100. abs_target_path = os.path.join(os.getcwd(),
  101. project_root,
  102. 'temp',
  103. target_file_name)
  104. resized = resize_image(abs_file_path, abs_target_path, max_width, max_height)
  105. if resized:
  106. abs_file_path = abs_target_path
  107. # return image
  108. return os.path.split(abs_file_path)
  109. def create_thumbnail(file_path: str, target_path: str) -> None:
  110. """
  111. extract a thumbnail from a video
  112. :param file_path: path to source file
  113. :param target_path: path to target file
  114. :return:
  115. """
  116. # return if file exists
  117. if os.path.exists(target_path):
  118. return
  119. # load video
  120. video = cv2.VideoCapture(file_path)
  121. # create thumbnail
  122. _, image = video.read()
  123. cv2.imwrite(target_path, image)
  124. # close video file
  125. video.release()
  126. def resize_image(file_path: str, target_path: str, max_width: int, max_height: int) -> bool:
  127. """
  128. Resize an image so width < `max_width` and height < `max_height` applies.
  129. If the image is already smaller than the given dimensions no new file is stored.
  130. :param file_path: path to source file
  131. :param target_path: path to target file
  132. :param max_width: maximum image width
  133. :param max_height: maximum image height
  134. :return: `True` if a resize operation was performed or the target file already exists
  135. """
  136. # return if file exists
  137. if os.path.exists(target_path):
  138. return True
  139. # load full size image
  140. image = Image.open(file_path)
  141. img_width, img_height = image.size
  142. # abort if file is smaller than desired
  143. if img_width < max_width and img_height < max_height:
  144. image.close()
  145. return False
  146. # calculate target size
  147. target_width = int(max_width)
  148. target_height = int(max_width * img_height / img_width)
  149. if target_height > max_height:
  150. target_height = int(max_height)
  151. target_width = int(max_height * img_width / img_height)
  152. # resize image
  153. resized_image = image.resize((target_width, target_height))
  154. # save to file
  155. resized_image.save(target_path, quality=DEFAULT_JPEG_QUALITY)
  156. # close opened files.
  157. image.close()
  158. return True
  159. def crop_image(file_path: str, target_path: str, box: BoundingBox) -> bool:
  160. """
  161. Crop an image with the given coordinates, width and height.
  162. If however no crop is applied no new file is stored.
  163. :param file_path: path to source file
  164. :param target_path: path to target file
  165. :param box: BoundingBox with relative x, y coordinates and
  166. the relative height and width of the bounding box
  167. :return: `True` if a crop operation was performed or the target file already exists
  168. """
  169. # return if file exists
  170. if os.path.exists(target_path):
  171. return True
  172. # abort if no crop would be applied
  173. if box.x <= 0 and box.y <= 0 and box.w >= 1 and box.h >= 1:
  174. return False
  175. # load full size image
  176. image = Image.open(file_path)
  177. img_width, img_height = image.size
  178. # calculate absolute crop position
  179. crop_x1 = int(img_width * box.x)
  180. crop_y1 = int(img_height * box.y)
  181. crop_x2 = min(int(img_width * box.w) + crop_x1, img_width)
  182. crop_y2 = min(int(img_height * box.h) + crop_y1, img_height)
  183. # crop image
  184. cropped_image = image.crop((crop_x1, crop_y1, crop_x2, crop_y2))
  185. # save to file
  186. cropped_image.save(target_path, quality=DEFAULT_JPEG_QUALITY)
  187. return True
  188. def find_images(folder,
  189. suffixes: T.Optional[T.List[str]] = None) -> T.List[Path]:
  190. """ walk recursively the folder and find images """
  191. suffixes = suffixes if suffixes is not None else [".jpg", ".jpeg", ".png"]
  192. images: T.List[Path] = []
  193. for root, _, files in os.walk(folder):
  194. for file in files:
  195. fpath = Path(root, file)
  196. if fpath.suffix.lower() not in suffixes:
  197. continue
  198. images.append(fpath)
  199. return images
  200. def generate_thumbnails(project: "Project", sizes = None):
  201. """ generates thumbnails for all image files in the given """
  202. if sizes is None:
  203. sizes = [Size(200, 200), Size(2000, 1200)]
  204. app.logger.info(f"Generating thumbnails for project \"{project.name}\"")
  205. files = list(project.files)
  206. for file in tqdm(files):
  207. for size in sizes:
  208. resize_file(file,
  209. project.root_folder,
  210. size.max_width,
  211. size.max_height)