Пример #1
0
    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
Пример #2
0
    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')
Пример #3
0
    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()
Пример #4
0
 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)
Пример #5
0
 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()
Пример #6
0
    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)
Пример #7
0
    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')
Пример #8
0
            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)
Пример #9
0
    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()
Пример #10
0
    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()
Пример #11
0
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
Пример #12
0
            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)
Пример #13
0
    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()