def predict(self, image_file, min_score=0.66): input_height = 224 input_width = 224 input_mean = 128 input_std = 128 input_layer = "input" output_layer = "final_result" t = self.read_tensor_from_image_file( image_file, input_height=input_height, input_width=input_width, input_mean=input_mean, input_std=input_std) if t is None: logger.info(f'Skipping {image_file}, file format not supported by Tensorflow') return None input_name = "import/" + input_layer output_name = "import/" + output_layer input_operation = self.graph.get_operation_by_name(input_name) output_operation = self.graph.get_operation_by_name(output_name) with tf.compat.v1.Session(graph=self.graph) as sess: results = sess.run(output_operation.outputs[0], {input_operation.outputs[0]: t}) results = np.squeeze(results) response = [] top_k = results.argsort()[-5:][::-1] for i in top_k: if results[i] >= min_score: response.append((self.labels[i], results[i])) return response
def rescan_photos(self, paths): missing = missing_system_dependencies(['exiftool', ]) if missing: logger.critical(f'Missing dependencies: {missing}') exit(1) rescan_photo_libraries(paths) logger.info('Rescan complete')
def run(self, loop=True): logger.info('Starting {} {} workers'.format(self.num_workers, self.task_type)) if self.num_workers > 1: for i in range(self.num_workers): t = threading.Thread(target=self.__worker) t.start() self.threads.append(t) try: while True: requeue_stuck_tasks(self.task_type) if self.task_type == 'classify.color': task_queryset = Task.objects.filter( library__classification_color_enabled=True, type=self.task_type, status='P') elif self.task_type == 'classify.location': task_queryset = Task.objects.filter( library__classification_location_enabled=True, type=self.task_type, status='P') elif self.task_type == 'classify.face': task_queryset = Task.objects.filter( library__classification_face_enabled=True, type=self.task_type, status='P') elif self.task_type == 'classify.style': task_queryset = Task.objects.filter( library__classification_style_enabled=True, type=self.task_type, status='P') elif self.task_type == 'classify.object': task_queryset = Task.objects.filter( library__classification_object_enabled=True, type=self.task_type, status='P') else: task_queryset = Task.objects.filter(type=self.task_type, status='P') for task in task_queryset[:8]: if self.num_workers > 1: logger.debug('putting task') self.queue.put(task) else: self.__process_task(task) if self.num_workers > 1: self.queue.join() if not loop: self.__clean_up() return sleep(1) except KeyboardInterrupt: self.__clean_up()
def run_scheduler(self): while True: num_remaining = Task.objects.filter(type='classify_images', status='P').count() if num_remaining: logger.info('{} photos remaining for classification'.format( num_remaining)) process_classify_images_tasks() sleep(1)
def __process_task(self, task): try: logger.info(f'Running task: {task.type} - {task.subject_id}') task.start() self.runner(task.subject_id) task.complete() except Exception: logger.error( f'Error processing task: {task.type} - {task.subject_id}') traceback.print_exc() task.failed()
def import_photos(self): # Create demo User account try: user = User.objects.create_user(username='******', email='*****@*****.**', password='******') user.has_config_persional_info = True user.has_created_library = True user.has_configured_importing = True user.has_configured_image_analysis = True user.save() except IntegrityError: user = User.objects.get(username='******') # Create Library try: library = Library.objects.get(name='Demo Library', ) except Library.DoesNotExist: library = Library(name='Demo Library', classification_color_enabled=True, classification_location_enabled=True, classification_style_enabled=True, classification_object_enabled=True, classification_face_enabled=True, setup_stage_completed='Th') library.save() # LibraryPath as locally mounted volume LibraryPath.objects.get_or_create( library=library, type='St', backend_type='Lo', path='/data/photos/', url='/photos/', ) # Link User to Library # In dev environment user needs to be owner to access all functionality # but demo.photonix.org this could lead to the system being messed up owner = os.environ.get('ENV') == 'dev' LibraryUser.objects.get_or_create(library=library, user=user, owner=owner) # Add photos for url in URLS: dest_dir = determine_destination(url) fn = url.split('/')[-1] dest_path = str(Path(dest_dir) / fn) if not os.path.exists(dest_path): logger.info('Fetching {} -> {}'.format(url, dest_path)) download_file(url, dest_path) record_photo(dest_path, library)
def retrain_face_similarity_index(self): for library in Library.objects.all(): version_file = Path( settings.MODEL_DIR ) / 'face' / f'{library.id}_retrained_version.txt' version_date = None if os.path.exists(version_file): with open(version_file) as f: contents = f.read().strip() version_date = datetime.strptime( contents, '%Y%m%d%H%M%S').replace(tzinfo=timezone.utc) start = time() logger.info(f'Updating ANN index for Library {library.id}') if PhotoTag.objects.filter(tag__type='F').count() == 0: logger.info( ' No Face PhotoTags in Library so no point in creating face ANN index yet' ) return if version_date and PhotoTag.objects.filter( updated_at__gt=version_date, tag__type='F').count() == 0: logger.info( ' No new Face PhotoTags in Library so no point in updating face ANN index' ) return FaceModel(library_id=library.id).retrain_face_similarity_index() logger.info(f' Completed in {(time() - start):.3f}s')
async def check_libraries(): while True: await asyncio.sleep(1) current_libraries = await get_libraries() for path, id in current_libraries.items(): if path not in watching_libraries: for directory in get_directories_recursive(Path(path)): logger.info(f'Watching new path: {directory}') watch = inotify.add_watch(directory, Mask.MODIFY | Mask.CREATE | Mask.DELETE | Mask.CLOSE | Mask.MOVE) watching_libraries[path] = (id, watch) for path, (id, watch) in watching_libraries.items(): if path not in current_libraries: logger.info(f'Removing old path: {path}') inotify.rm_watch(watch) await asyncio.sleep(4)
def housekeeping(self): # Remove old cache directories try: for directory in os.listdir(settings.THUMBNAIL_ROOT): if directory not in ['photofile']: path = Path(settings.THUMBNAIL_ROOT) / directory logger.info(f'Removing old cache directory {path}') rmtree(path) except FileNotFoundError: # In case thumbnail dir hasn't been created yet pass # Regenerate any outdated thumbnails photos = Photo.objects.filter( thumbnailed_version__lt=THUMBNAILER_VERSION) if photos.count(): logger.info( f'Rescheduling {photos.count()} photos to have their thumbnails regenerated' ) for photo in photos: Task(type='generate_thumbnails', subject_id=photo.id, library=photo.library).save()
def run_processors(self): num_workers = max(int(cpu_count() / 4), 1) threads = [] logger.info( 'Starting {} thumbnail processor workers'.format(num_workers)) for i in range(num_workers): t = threading.Thread(target=worker) t.start() threads.append(t) try: while True: requeue_stuck_tasks('generate_thumbnails') num_remaining = Task.objects.filter(type='generate_thumbnails', status='P').count() if num_remaining: logger.info( '{} tasks remaining for thumbnail processing'.format( num_remaining)) # Load 'Pending' tasks onto worker threads for task in Task.objects.filter(type='generate_thumbnails', status='P')[:64]: q.put(task) logger.info('Finished thumbnail processing batch') # Wait until all threads have finished q.join() sleep(1) except KeyboardInterrupt: # Shut down threads cleanly for i in range(num_workers): q.put(None) for t in threads: t.join()
def record_photo(path, library, inotify_event_type=None): logger.info(f'Recording photo {path}') mimetype = get_mimetype(path) if not imghdr.what( path) and not mimetype in MIMETYPE_WHITELIST and subprocess.run( ['dcraw', '-i', path]).returncode: logger.error(f'File is not a supported type: {path} ({mimetype})') return None if type(library) == Library: library_id = library.id else: library_id = str(library) try: photo_file = PhotoFile.objects.get(path=path) except PhotoFile.DoesNotExist: photo_file = PhotoFile() if inotify_event_type in ['DELETE', 'MOVED_FROM']: if PhotoFile.objects.filter(path=path).exists(): return delete_photo_record(photo_file) else: return True file_modified_at = datetime.fromtimestamp(os.stat(path).st_mtime, tz=utc) if photo_file and photo_file.file_modified_at == file_modified_at: return True metadata = PhotoMetadata(path) date_taken = None possible_date_keys = [ 'Create Date', 'Date/Time Original', 'Date Time Original', 'Date/Time', 'Date Time', 'GPS Date/Time', 'File Modification Date/Time' ] for date_key in possible_date_keys: date_taken = parse_datetime(metadata.get(date_key)) if date_taken: break # If EXIF data not found. date_taken = date_taken or datetime.strptime( time.ctime(os.path.getctime(path)), "%a %b %d %H:%M:%S %Y") camera = None camera_make = metadata.get('Make', '')[:Camera.make.field.max_length] camera_model = metadata.get('Camera Model Name', '') if camera_model: camera_model = camera_model.replace(camera_make, '').strip() camera_model = camera_model[:Camera.model.field.max_length] if camera_make and camera_model: try: camera = Camera.objects.get(library_id=library_id, make=camera_make, model=camera_model) if date_taken < camera.earliest_photo: camera.earliest_photo = date_taken camera.save() if date_taken > camera.latest_photo: camera.latest_photo = date_taken camera.save() except Camera.DoesNotExist: camera = Camera(library_id=library_id, make=camera_make, model=camera_model, earliest_photo=date_taken, latest_photo=date_taken) camera.save() lens = None lens_name = metadata.get('Lens ID') if lens_name: try: lens = Lens.objects.get(name=lens_name) if date_taken < lens.earliest_photo: lens.earliest_photo = date_taken lens.save() if date_taken > lens.latest_photo: lens.latest_photo = date_taken lens.save() except Lens.DoesNotExist: lens = Lens(library_id=library_id, name=lens_name, earliest_photo=date_taken, latest_photo=date_taken) lens.save() photo = None if date_taken: try: # TODO: Match on file number/file name as well photo = Photo.objects.get(taken_at=date_taken) except Photo.DoesNotExist: pass latitude = None longitude = None if metadata.get('GPS Position'): latitude, longitude = parse_gps_location(metadata.get('GPS Position')) iso_speed = None if metadata.get('ISO'): try: iso_speed = int(re.search(r'[0-9]+', metadata.get('ISO')).group(0)) except AttributeError: pass if not photo: # Save Photo aperture = None aperturestr = metadata.get('Aperture') if aperturestr: try: aperture = Decimal(aperturestr) if aperture.is_infinite(): aperture = None except: pass photo = Photo( library_id=library_id, taken_at=date_taken, taken_by=metadata.get( 'Artist', '')[:Photo.taken_by.field.max_length] or None, aperture=aperture, exposure=metadata.get( 'Exposure Time', '')[:Photo.exposure.field.max_length] or None, iso_speed=iso_speed, focal_length=metadata.get('Focal Length') and metadata.get('Focal Length').split(' ', 1)[0] or None, flash=metadata.get('Flash') and 'on' in metadata.get('Flash').lower() or False, metering_mode=metadata.get( 'Metering Mode', '')[:Photo.metering_mode.field.max_length] or None, drive_mode=metadata.get( 'Drive Mode', '')[:Photo.drive_mode.field.max_length] or None, shooting_mode=metadata.get( 'Shooting Mode', '')[:Photo.shooting_mode.field.max_length] or None, camera=camera, lens=lens, latitude=latitude, longitude=longitude, altitude=metadata.get('GPS Altitude') and metadata.get('GPS Altitude').split(' ')[0], star_rating=metadata.get('Rating')) photo.save() for subject in metadata.get('Subject', '').split(','): subject = subject.strip() if subject: tag, _ = Tag.objects.get_or_create(library_id=library_id, name=subject, type="G") PhotoTag.objects.create(photo=photo, tag=tag, confidence=1.0) else: for photo_file in photo.files.all(): if not os.path.exists(photo_file.path): photo_file.delete() width = metadata.get('Image Width') height = metadata.get('Image Height') if metadata.get('Orientation') in [ 'Rotate 90 CW', 'Rotate 270 CCW', 'Rotate 90 CCW', 'Rotate 270 CW' ]: old_width = width width = height height = old_width # Save PhotoFile photo_file.photo = photo photo_file.path = path photo_file.width = width photo_file.height = height photo_file.mimetype = mimetype photo_file.file_modified_at = file_modified_at photo_file.bytes = os.stat(path).st_size photo_file.preferred = False # TODO photo_file.save() # Create task to ensure JPEG version of file exists (used for thumbnailing, analysing etc.) Task(type='ensure_raw_processed', subject_id=photo.id, complete_with_children=True, library=photo.library).save() return photo
async def handle_inotify_events(): async for event in inotify: if 'moved_from_attr_dict' in locals() and moved_from_attr_dict: for potential_library_path, (potential_library_id, _) in watching_libraries.items(): if str(event.path).startswith(potential_library_path): library_id = potential_library_id photo_moved_from_path = moved_from_attr_dict.get('moved_from_path') photo_moved_from_cookie = moved_from_attr_dict.get('moved_from_cookie') moved_from_attr_dict = {} if event.mask.name == 'MOVED_TO' and photo_moved_from_cookie == event.cookie: logger.info(f'Moving or renaming the photo "{str(event.path)}" from library "{library_id}"') await move_or_rename_photo_async(photo_moved_from_path, event.path, library_id) else: logger.info(f'Removing photo "{str(photo_moved_from_path)}" from library "{library_id}"') await record_photo_async(photo_moved_from_path, library_id, 'MOVED_FROM') elif Mask.CREATE in event.mask and event.path is not None and event.path.is_dir(): current_libraries = await get_libraries() for path, id in current_libraries.items(): for directory in get_directories_recursive(event.path): logger.info(f'Watching newly created child directory: {directory}') watch = inotify.add_watch(directory, Mask.MODIFY | Mask.CREATE | Mask.DELETE | Mask.CLOSE | Mask.MOVE) watching_libraries[path] = (id, watch) elif event.mask in [Mask.CLOSE_WRITE, Mask.MOVED_TO, Mask.DELETE, Mask.MOVED_FROM] or event.mask.value == 1073741888: photo_path = event.path library_id = None for potential_library_path, (potential_library_id, _) in watching_libraries.items(): if str(photo_path).startswith(potential_library_path): library_id = potential_library_id if event.mask in [Mask.DELETE, Mask.MOVED_FROM]: if event.mask.name == 'MOVED_FROM': moved_from_attr_dict = { 'moved_from_path': event.path, 'moved_from_cookie': event.cookie} else: logger.info(f'Removing photo "{photo_path}" from library "{library_id}"') await record_photo_async(photo_path, library_id, event.mask.name) elif event.mask.value == 1073741888: logger.info(f'Delete child directory with its all photos "{photo_path}" to library "{library_id}"') await delete_child_dir_all_photos_async(photo_path, library_id) else: logger.info(f'Adding photo "{photo_path}" to library "{library_id}"') await record_photo_async(photo_path, library_id, event.mask.name)
def watch_photos(self): """Management command to watch photo directory and create photo records in database.""" watching_libraries = {} with Inotify() as inotify: @sync_to_async def get_libraries(): return {l.path: l.library_id for l in LibraryPath.objects.filter(type='St', backend_type='Lo')} @sync_to_async def record_photo_async(photo_path, library_id, event_mask): record_photo(photo_path, library_id, event_mask) @sync_to_async def move_or_rename_photo_async(photo_old_path, photo_new_path, library_id): move_or_rename_photo(photo_old_path, photo_new_path, library_id) @sync_to_async def delete_child_dir_all_photos_async(photo_path, library_id): delete_child_dir_all_photos(photo_path, library_id) def get_directories_recursive(path: Path) -> Generator[Path, None, None]: """ Recursively list all directories under path, including path itself, if it's a directory. The path itself is always yielded before its children are iterated, so you can pre-process a path (by watching it with inotify) before you get the directory listing. Passing a non-directory won't raise an error or anything, it'll just yield nothing. """ if path.is_dir(): yield path for child in path.iterdir(): yield from get_directories_recursive(child) async def check_libraries(): while True: await asyncio.sleep(1) current_libraries = await get_libraries() for path, id in current_libraries.items(): if path not in watching_libraries: for directory in get_directories_recursive(Path(path)): logger.info(f'Watching new path: {directory}') watch = inotify.add_watch(directory, Mask.MODIFY | Mask.CREATE | Mask.DELETE | Mask.CLOSE | Mask.MOVE) watching_libraries[path] = (id, watch) for path, (id, watch) in watching_libraries.items(): if path not in current_libraries: logger.info(f'Removing old path: {path}') inotify.rm_watch(watch) await asyncio.sleep(4) async def handle_inotify_events(): async for event in inotify: if 'moved_from_attr_dict' in locals() and moved_from_attr_dict: for potential_library_path, (potential_library_id, _) in watching_libraries.items(): if str(event.path).startswith(potential_library_path): library_id = potential_library_id photo_moved_from_path = moved_from_attr_dict.get('moved_from_path') photo_moved_from_cookie = moved_from_attr_dict.get('moved_from_cookie') moved_from_attr_dict = {} if event.mask.name == 'MOVED_TO' and photo_moved_from_cookie == event.cookie: logger.info(f'Moving or renaming the photo "{str(event.path)}" from library "{library_id}"') await move_or_rename_photo_async(photo_moved_from_path, event.path, library_id) else: logger.info(f'Removing photo "{str(photo_moved_from_path)}" from library "{library_id}"') await record_photo_async(photo_moved_from_path, library_id, 'MOVED_FROM') elif Mask.CREATE in event.mask and event.path is not None and event.path.is_dir(): current_libraries = await get_libraries() for path, id in current_libraries.items(): for directory in get_directories_recursive(event.path): logger.info(f'Watching newly created child directory: {directory}') watch = inotify.add_watch(directory, Mask.MODIFY | Mask.CREATE | Mask.DELETE | Mask.CLOSE | Mask.MOVE) watching_libraries[path] = (id, watch) elif event.mask in [Mask.CLOSE_WRITE, Mask.MOVED_TO, Mask.DELETE, Mask.MOVED_FROM] or event.mask.value == 1073741888: photo_path = event.path library_id = None for potential_library_path, (potential_library_id, _) in watching_libraries.items(): if str(photo_path).startswith(potential_library_path): library_id = potential_library_id if event.mask in [Mask.DELETE, Mask.MOVED_FROM]: if event.mask.name == 'MOVED_FROM': moved_from_attr_dict = { 'moved_from_path': event.path, 'moved_from_cookie': event.cookie} else: logger.info(f'Removing photo "{photo_path}" from library "{library_id}"') await record_photo_async(photo_path, library_id, event.mask.name) elif event.mask.value == 1073741888: logger.info(f'Delete child directory with its all photos "{photo_path}" to library "{library_id}"') await delete_child_dir_all_photos_async(photo_path, library_id) else: logger.info(f'Adding photo "{photo_path}" to library "{library_id}"') await record_photo_async(photo_path, library_id, event.mask.name) loop = asyncio.get_event_loop() loop.create_task(check_libraries()) loop.create_task(handle_inotify_events()) try: loop.run_forever() except KeyboardInterrupt: logger.info('Shutting down') finally: loop.run_until_complete(loop.shutdown_asyncgens()) loop.close()