Session.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. from datetime import datetime, timedelta
  2. import pickle
  3. import random
  4. import subprocess
  5. from warnings import warn
  6. import os
  7. from tqdm import tqdm
  8. import matplotlib.image as mpimg
  9. from skimage import transform, io
  10. import IPython.display as display
  11. from py.FileUtils import list_folders, list_jpegs_recursive, verify_expected_subfolders
  12. from py.ImageUtils import display_images, get_image_date
  13. class Session:
  14. def __init__(self, folder: str):
  15. self.folder = folder
  16. # session name = folder name[33:], the first 33 characters are always the same
  17. self.name = os.path.basename(folder)[33:]
  18. print(f"Session '{self.name}' at folder: {self.folder}")
  19. assert self.name != ""
  20. verify_expected_subfolders(self.folder)
  21. self.scanned = False
  22. # maps lapse files to their exif dates (for statistic and prediction purposes)
  23. self.lapse_dates = {}
  24. # maps motion files to their exif dates (for statistic purposes)
  25. self.motion_dates = {}
  26. # maps exif dates to lapse files (for prediction purposes)
  27. self.lapse_map = {}
  28. # maps exif dates to motion files (for csv mapping purposes, generated on demand)
  29. self.motion_map = None
  30. self.load_scans()
  31. if not self.scanned:
  32. print("Session not scanned. Run session.scan() to create scan files")
  33. def load_scans(self):
  34. lapse_dates_file = os.path.join("session_scans", self.name, "lapse_dates.pickle")
  35. motion_dates_file = os.path.join("session_scans", self.name, "motion_dates.pickle")
  36. lapse_map_file = os.path.join("session_scans", self.name, "lapse_map.pickle")
  37. lapse_dates_exists = os.path.isfile(lapse_dates_file)
  38. motion_dates_exists = os.path.isfile(motion_dates_file)
  39. lapse_map_exists = os.path.isfile(lapse_map_file)
  40. if lapse_dates_exists and motion_dates_exists and lapse_map_exists:
  41. with open(lapse_dates_file, "rb") as handle:
  42. self.lapse_dates = pickle.load(handle)
  43. with open(motion_dates_file, "rb") as handle:
  44. self.motion_dates = pickle.load(handle)
  45. with open(lapse_map_file, "rb") as handle:
  46. self.lapse_map = pickle.load(handle)
  47. self.scanned = True
  48. print("Loaded scans.")
  49. else:
  50. if not (not lapse_dates_exists and not motion_dates_exists and not lapse_map_exists):
  51. warn(f"Warning: Only partial scan data available. Not loading.")
  52. self.scanned = False
  53. def save_scans(self):
  54. os.makedirs(os.path.join("session_scans", self.name), exist_ok=True)
  55. lapse_dates_file = os.path.join("session_scans", self.name, "lapse_dates.pickle")
  56. motion_dates_file = os.path.join("session_scans", self.name, "motion_dates.pickle")
  57. lapse_map_file = os.path.join("session_scans", self.name, "lapse_map.pickle")
  58. with open(lapse_dates_file, "wb") as handle:
  59. pickle.dump(self.lapse_dates, handle, protocol=pickle.HIGHEST_PROTOCOL)
  60. print(f"Saved {lapse_dates_file}")
  61. with open(motion_dates_file, "wb") as handle:
  62. pickle.dump(self.motion_dates, handle, protocol=pickle.HIGHEST_PROTOCOL)
  63. print(f"Saved {motion_dates_file}")
  64. with open(lapse_map_file, "wb") as handle:
  65. pickle.dump(self.lapse_map, handle, protocol=pickle.HIGHEST_PROTOCOL)
  66. print(f"Saved {lapse_map_file}")
  67. def scan(self, force=False, auto_save=True):
  68. if self.scanned and not force:
  69. raise ValueError("Session is already scanned. Use force=True to scan anyway and override scan progress.")
  70. # Scan motion dates
  71. print("Scanning motion dates...")
  72. self.motion_dates = {}
  73. motion_folder = os.path.join(self.folder, "Motion")
  74. for file in tqdm(list_jpegs_recursive(motion_folder)):
  75. self.motion_dates[os.path.relpath(file, motion_folder)] = get_image_date(file)
  76. # Scan lapse dates
  77. print("Scanning lapse dates...")
  78. self.lapse_dates = {}
  79. lapse_folder = os.path.join(self.folder, "Lapse")
  80. for file in tqdm(list_jpegs_recursive(lapse_folder)):
  81. self.lapse_dates[os.path.relpath(file, lapse_folder)] = get_image_date(file)
  82. # Create lapse map
  83. print("Creating lapse map...")
  84. self.lapse_map = {}
  85. for file, date in self.lapse_dates.items():
  86. if date in self.lapse_map:
  87. self.lapse_map[date].append(file)
  88. else:
  89. self.lapse_map[date] = [file]
  90. self.scanned = True
  91. # Auto save
  92. if auto_save:
  93. print("Saving...")
  94. self.save_scans()
  95. def check_lapse_duplicates(self) -> bool:
  96. total = 0
  97. total_duplicates = 0
  98. total_multiples = 0
  99. deviant_duplicates = []
  100. for date, files in tqdm(self.lapse_map.items()):
  101. total += 1
  102. if len(files) > 1:
  103. total_duplicates += 1
  104. file_size = -1
  105. for f in files:
  106. f_size = os.path.getsize(os.path.join(self.folder, "Lapse", f))
  107. if file_size == -1:
  108. file_size = f_size
  109. elif f_size != file_size:
  110. deviant_duplicates.append(date)
  111. break
  112. if len(files) > 2:
  113. total_multiples += 1
  114. deviant_duplicates.sort()
  115. print(f"* {total} lapse dates")
  116. print(f"* {total_duplicates} duplicates")
  117. print(f"* {total_multiples} multiples (more than two files per date)")
  118. print(f"* {len(deviant_duplicates)} deviant duplicates: {deviant_duplicates}")
  119. return total, total_duplicates, total_multiples, deviant_duplicates
  120. def open_images_for_date(self, date: datetime):
  121. img_names = self.lapse_map.get(date, [])
  122. if len(img_names) == 0:
  123. warn("No images for this date!")
  124. for i, img_name in enumerate(img_names):
  125. full_path = os.path.join(self.folder, "Lapse", img_name)
  126. print(f"#{i+1} {full_path}")
  127. subprocess.call(("xdg-open", full_path))
  128. def get_motion_image_from_filename(self, filename: str) -> "MotionImage":
  129. if filename in self.motion_dates:
  130. return MotionImage(self, filename, self.motion_dates[filename])
  131. else:
  132. raise ValueError(f"Unknown motion file name: {filename}")
  133. def __generate_motion_map(self):
  134. if self.motion_map is not None:
  135. return
  136. print("Generating motion map...")
  137. self.motion_map = {}
  138. for filename, date in self.motion_dates.items():
  139. if date in self.motion_map:
  140. self.motion_map[date].append(filename)
  141. else:
  142. self.motion_map[date] = [filename]
  143. def get_motion_images_from_date(self, date: datetime):
  144. self.__generate_motion_map()
  145. filenames = self.motion_map.get(date, [])
  146. return [MotionImage(self, filename, date) for filename in filenames]
  147. def get_random_motion_image(self, day_only=False, night_only=False) -> "MotionImage":
  148. if len(self.motion_dates) == 0:
  149. raise ValueError("No motion images in session!")
  150. img = None
  151. while img is None or (day_only and img.is_nighttime()) or (night_only and img.is_daytime()):
  152. filename = random.choice(list(self.motion_dates.keys()))
  153. img = MotionImage(self, filename, self.motion_dates[filename])
  154. return img
  155. def get_closest_lapse_images(self, motion_file: str):
  156. date: datetime = self.motion_dates[motion_file]
  157. previous_date = date.replace(minute=0, second=0)
  158. next_date = previous_date + timedelta(hours=1)
  159. while not previous_date in self.lapse_map:
  160. previous_date -= timedelta(hours=1)
  161. while not next_date in self.lapse_map:
  162. next_date += timedelta(hours=1)
  163. if len(self.lapse_map[previous_date]) > 1:
  164. warn(f"There are multiple lapse images for date {previous_date}! Choosing the first one.")
  165. if len(self.lapse_map[next_date]) > 1:
  166. warn(f"There are multiple lapse images for date {next_date}! Choosing the first one.")
  167. return LapseImage(self, self.lapse_map[previous_date][0], previous_date), LapseImage(self, self.lapse_map[next_date][0], next_date)
  168. class SessionImage:
  169. def __init__(self, session: Session, subfolder: str, filename: str, date: datetime):
  170. self.session = session
  171. self.subfolder = subfolder
  172. self.filename = filename
  173. self.date = date
  174. if not os.path.isfile(self.get_full_path()):
  175. raise ValueError(f"File {subfolder}/{filename} in session folder {session.folder} not found!")
  176. def get_full_path(self) -> str:
  177. return os.path.join(self.session.folder, self.subfolder, self.filename)
  178. def open(self):
  179. full_path = self.get_full_path()
  180. print(f"Opening {full_path}...")
  181. subprocess.call(("xdg-open", full_path))
  182. def read(self, truncate_y = (40, 40), scale=1, gray=True):
  183. full_path = self.get_full_path()
  184. img = io.imread(full_path, as_gray=gray)
  185. # truncate
  186. if truncate_y is not None:
  187. if truncate_y[0] > 0 and truncate_y[1] > 0:
  188. img = img[truncate_y[0]:(-truncate_y[1]),:]
  189. elif truncate_y[0] > 0:
  190. img = img[truncate_y[0]:,:]
  191. elif truncate_y[1] > 0:
  192. img = img[:(-truncate_y[1]),:]
  193. # scale
  194. if scale is not None and scale < 1:
  195. img = transform.rescale(img, scale, multichannel=not gray)
  196. return img
  197. def is_daytime(self):
  198. return 6 <= self.date.hour <= 18
  199. def is_nighttime(self):
  200. return not self.is_daytime()
  201. def to_ipython_image(self, width=500, height=None):
  202. return display.Image(filename=self.get_full_path(), width=width, height=height)
  203. class MotionImage(SessionImage):
  204. def __init__(self, session: Session, filename: str, date: datetime):
  205. super().__init__(session, "Motion", filename, date)
  206. if not self.filename in session.motion_dates:
  207. raise ValueError(f"File name {filename} not in session!")
  208. def get_closest_lapse_images(self):
  209. before, after = self.session.get_closest_lapse_images(self.filename)
  210. # rel = 0 if motion image was taken at before lapse image, rel = 1 if motion image was taken at after lapse image
  211. rel = (self.date - before.date).total_seconds() / (after.date - before.date).total_seconds()
  212. return before, after, rel
  213. class LapseImage(SessionImage):
  214. def __init__(self, session: Session, filename: str, date: datetime):
  215. super().__init__(session, "Lapse", filename, date)
  216. if not self.filename in session.lapse_dates:
  217. raise ValueError(f"File name {filename} not in session!")