FileOperations.py 7.8 KB

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