FileOperations.py 7.3 KB

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