FileOperations.py 8.3 KB

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