Example #1
0
def create_reactive_image_grid(n_row, n_col, image_list, image_data,
                               image_path):
    """
    Get an HTML element corresponding to the responsive image grid.

    Args:
        n_rows = int, current number of rows in the grid (Input: indicates resizing)
        n_cols = int, current number of columns in the grid (Input: indicates resizing)
        image_list = list, containing a list of file paths where the valid images for the chosen directory are stored
        image_data = dict, with keys 'position' (for visible grid locations) and 'keep' (whether to keep / remove the image) (State)
                     Note: each keys contains a list, of lists of ints, a sequence of data about each completed image group
        image_path = list, of 1 str, the filepath where the images in image-container were loaded from

    Returns: html.Div element (containing the grid of images) that can update the responsive-image-grid element
    """

    image_path = image_path[0]
    # If it doesn't already exist, add an entry (dict) for this image path into the data dictionary
    if image_path not in image_data:
        image_data[image_path] = {'position': [], 'keep': [], 'filename': []}

    # Reduce the image_list by removing the masked images (so they can no longer appear in the image grid / image zoom)
    flat_mask = utils.create_flat_mask(image_data[image_path]['position'],
                                       len(image_list))
    image_list = [
        img_src for i, img_src in enumerate(image_list) if not flat_mask[i]
    ]

    return utils.create_image_grid(n_row, n_col, image_list)
Example #2
0
def activate_deactivate_cells(n_rows, n_cols, n_left, n_right, n_up, n_down,
                              n_row1, n_row2, n_row3, n_row4, n_row5, n_row6,
                              n_row7, n_row8, n_row9, n_row1000, n_keep,
                              n_delete, image_list, image_data, image_path,
                              *args):
    """
    Global callback function for toggling classes. There are three toggle modes:
        1) Pressing a grid cell will toggle its state
        2) Pressing a directional button will force the "last-clicked" focus (only) to shift in the direction stated
        3) Resizing the grid will cause the top-left only to be in last-click focus
        4) Mark a cell as keep or delete (must already have "grouped-on" class)

    Note: some of these operations respond to key presses (e.g. directional buttons), which click hidden buttons.

    Args:
        n_rows = int, current number of rows in the grid (indicates resizing)
        n_cols = int, current number of columns in the grid (indicates resizing)
        n_left = int, number of clicks on the 'move-left' button (indicates shifting)
        n_right = int, number of clicks on the 'move-right' button (indicates shifting)
        n_up = int, number of clicks on the 'move-up' button (indicates shifting)
        n_down = int, number of clicks on the 'move-down' button (indicates shifting)
        n_row1,..9,1000 = int, number of clicks on the 'select row *' button (indicates shortcut to select many rows)
        n_keep = int, number of clicks on the 'keep-button' button
        n_delete = int, number of clicks on the 'delete-button' button
        image_list = list, of str, specifying where the image files are stored
        image_data = dict, of dict of lists of ints, a sequence of metadata about completed image groups
        image_path = str, the filepath where the images in image-container were loaded from

        *args = positional arguments split into about two equal halves (actually length 2 x N_GRID + 1):
            0) args[:N_GRID] are Inputs (activated by the grid-Buttons)
            1) args[N_GRID:-1] are States (indicating state of the grid-Tds)
            Both are in row-major order (for i in rows: for j in cols: ... )
            2) args[-1] is a tuple representing the last clicked cell

    Returns: a list of new classNames for all the grid cells plus
              - one extra element for the Image that was last clicked (zoomed image)
              - one extra element representing the cell that was last clicked (in focus)
    """

    # Unpack the single-element list
    image_path = image_path[0]
    if image_path not in image_data:
        image_data[image_path] = {'position': [], 'keep': [], 'filename': []}

    # Reduce the image_list by removing the masked images (so they can no longer appear in the image grid / image zoom)
    flat_mask = utils.create_flat_mask(image_data[image_path]['position'],
                                       len(image_list))
    image_list = [img for i, img in enumerate(image_list) if not flat_mask[i]]

    # Find the button that triggered this callback (if any)
    context = dash.callback_context
    if not context.triggered:
        return utils.resize_grid_pressed(image_list=image_list,
                                         rows_max=ROWS_MAX,
                                         cols_max=COLS_MAX,
                                         empty_image=EMPTY_IMAGE,
                                         zoom_img_style=config.IMG_STYLE_ZOOM)
    else:
        button_id = context.triggered[0]['prop_id'].split('.')[0]

    # Reset the grid
    # Note: image-container is not really a button, but fired when confirm-load-directory is pressed (we need the list
    #       inside image-container in order to populate the grid)
    if button_id in [
            'choose-grid-size', 'image-container', 'image-meta-data',
            'loaded-image-path'
    ]:
        return utils.resize_grid_pressed(image_list=image_list,
                                         rows_max=ROWS_MAX,
                                         cols_max=COLS_MAX,
                                         empty_image=EMPTY_IMAGE,
                                         zoom_img_style=config.IMG_STYLE_ZOOM)

    # Toggle the state of this button (as it was pressed)
    elif 'grid-button-' in button_id:
        current_classes, zoomed_img, cell_last_clicked = utils.image_cell_pressed(
            button_id, n_cols, COLS_MAX, ROWS_MAX * COLS_MAX, image_list,
            EMPTY_IMAGE, config.IMG_STYLE_ZOOM, *args)
        return current_classes + [zoomed_img, cell_last_clicked]

    # Toggle the grouping state of all cells in the first rows of the grid
    elif 'select-row-upto-' in button_id:
        n_rows = int(
            re.findall('select-row-upto-([0-9]+)-button', button_id)[0])
        current_classes, zoomed_img, cell_last_clicked = utils.toggle_group_in_first_n_rows(
            n_rows, n_cols, ROWS_MAX, COLS_MAX, image_list, EMPTY_IMAGE,
            config.IMG_STYLE_ZOOM, *args)
        return current_classes + [zoomed_img, cell_last_clicked]

    # Harder case: move focus in a particular direction
    elif 'move-' in button_id:
        current_classes, zoomed_img, cell_last_clicked = utils.direction_key_pressed(
            button_id, n_rows, n_cols, COLS_MAX, ROWS_MAX * COLS_MAX,
            image_list, EMPTY_IMAGE, config.IMG_STYLE_ZOOM, *args)
        return current_classes + [zoomed_img, cell_last_clicked]

    elif button_id in ['keep-button', 'delete-button']:
        current_classes, zoomed_img, cell_last_clicked = utils.keep_delete_pressed(
            button_id, n_cols, COLS_MAX, ROWS_MAX * COLS_MAX, image_list,
            EMPTY_IMAGE, config.IMG_STYLE_ZOOM, *args)
        return current_classes + [zoomed_img, cell_last_clicked]

    else:
        raise ValueError('Unrecognized button ID: %s' % str(button_id))
Example #3
0
def complete_or_undo_image_group(n_group, n_undo, n_rows, n_cols, image_list,
                                 image_data, image_path, n_images, *args):
    """
    Updates the image_mask by appending / deleting relevant info to / from it. This happens when either 'Complete group'
    or Undo' button is clicked. We also delete (resp. recreate) the unwanted files when a valid completion (resp. undo)
    is made (although those files are always backed up in the IMAGE_BACKUP_PATH) and send (resp. delete)the meta data to
    the specified database: see DATABASE_NAME and DATABASE_TABLE (in config.py).

    Args:
        n_group = int, number of times the complete-group button is clicked (Input)
        n_undo = int, number of times the undo-button is clicked (Input)
        n_rows = int, current number of rows in the grid (Input: indicates resizing)
        n_cols = int, current number of columns in the grid (Input: indicates resizing)
        image_list = list, containing a list of file paths where the valid images for the chosen directory are stored
        image_data = dict, with keys 'position' (for visible grid locations) and 'keep' (whether to keep / remove the image) (State)
                     Note: each keys contains a list, of lists of ints, a sequence of data about each completed image group
        image_path = str, the filepath where the images in image-container were loaded from
        n_images = int, number of images originally loaded in the given directory
        *args = positional arguments are States (given by the grid-Tds for knowing the class names)

    Returns:
        0) updated version of the image mask (if any new group was legitimately completed)
        1) Percentage of images completed so far
    """

    # Find the button that triggered this callback (if any)
    # Note: also prevent this button from firing when the app first loads (causing the first image to be classified)
    context = dash.callback_context
    button_id = context.triggered[0]['prop_id'].split('.')[0]
    if button_id == 'complete-group':
        mode = 'complete'
    elif button_id == 'undo-button':
        mode = 'undo'
    else:
        PreventUpdate
        return image_data, [0]

    # Unpack the single-element list
    image_path = image_path[0]
    if image_path not in image_data:
        image_data[image_path] = {'position': [], 'keep': [], 'filename': []}

    # The image_list (from image-container) contains ALL images in this directory, whereas as the list positions below
    # will refer to the reduced masked list. In order to obtain consistent filenames, we need to apply the previous version
    # of the mask to the image_list (version prior to this completion).
    all_img_filenames = [src.split('/')[-1] for src in image_list]
    prev_mask = utils.create_flat_mask(image_data[image_path]['position'],
                                       len(all_img_filenames))
    assert len(all_img_filenames) == len(
        prev_mask
    ), "Mask should correspond 1-to-1 with filenames in image-container"
    unmasked_img_filenames = [
        fname for i, fname in enumerate(all_img_filenames) if not prev_mask[i]
    ]

    if mode == 'complete':

        # Extract the image group and their meta data (filename and keep / delete)
        # Note: Need to adjust for the disconnect between the visible grid size (n_rows * n_cols) and the virtual grid size
        #       (ROWS_MAX * COLS_MAX)
        focus_position = None
        focus_filename = None
        focus_date_taken = None
        grouped_cell_positions = []
        grouped_cell_keeps = []
        grouped_filenames = []
        grouped_date_taken = []
        delete_filenames = []
        for i in range(n_rows):
            for j in range(n_cols):
                # Get the class list (str) for this cell
                my_class = args[j + i * COLS_MAX]
                # Position on the visible grid (mapped to list index)
                list_pos = j + i * n_rows
                # As the number of unmasked images shrinks (when the user completes a group, those images disappear), the
                # list position will eventually run out of the valid indices. As there's no valid metadata in this region
                # we skip over it
                if list_pos >= len(unmasked_img_filenames):
                    continue
                image_filename = unmasked_img_filenames[list_pos]

                # Check if selected to be in the group, add position if on
                if 'grouped-on' in my_class:
                    grouped_cell_positions.append(list_pos)
                    grouped_filenames.append(image_filename)
                    grouped_date_taken.append(
                        utils.get_image_taken_date(image_path,
                                                   image_filename,
                                                   default_date=None))

                if 'focus' in my_class:
                    focus_position = list_pos
                    focus_filename = image_filename
                    focus_date_taken = utils.get_image_taken_date(
                        image_path, image_filename, default_date=None)

                # Check for keep / delete status
                # Note: important not to append if keep/delete status not yet specified
                if 'keep' in my_class:
                    grouped_cell_keeps.append(True)
                elif 'delete' in my_class:
                    grouped_cell_keeps.append(False)
                    delete_filenames.append(image_filename)
                else:
                    pass

        # Check 1: some data has been collected since last click (no point appending empty lists)
        # Check 2: list lengths match, i.e. for each cell in the group, the keep / delete status has been declared
        # If either check fails, do nothing
        # TODO: flag something (warning?) to user if list lengths do not match
        # TODO: if check 2 fails, it currently junks the data - possible to hold onto it?
        if len(grouped_cell_positions) > 0 and len(
                grouped_cell_positions) == len(grouped_cell_keeps):

            image_data[image_path]['position'].append(grouped_cell_positions)
            image_data[image_path]['keep'].append(grouped_cell_keeps)
            image_data[image_path]['filename'].append(grouped_filenames)

            if not program_args.demo:

                utils.record_grouped_data(
                    image_data=image_data,
                    image_path=image_path,
                    filename_list=grouped_filenames,
                    keep_list=grouped_cell_keeps,
                    date_taken_list=grouped_date_taken,
                    image_backup_path=IMAGE_BACKUP_PATH,
                    meta_data_fpath=config.META_DATA_FPATH,
                    database_uri=config.DATABASE_URI,
                    database_table=config.DATABASE_TABLE)

        # This is a small trick for quickly saving (keeping) the focussed image (provided none have been grouped)
        elif len(
                grouped_cell_positions
        ) == 0 and focus_position is not None and focus_filename is not None:

            image_data[image_path]['position'].append([focus_position])
            image_data[image_path]['keep'].append([True])
            image_data[image_path]['filename'].append([focus_filename])

            if not program_args.demo:

                utils.record_grouped_data(
                    image_data=image_data,
                    image_path=image_path,
                    filename_list=[focus_filename],
                    keep_list=[True],
                    date_taken_list=[focus_date_taken],
                    image_backup_path=IMAGE_BACKUP_PATH,
                    meta_data_fpath=config.META_DATA_FPATH,
                    database_uri=config.DATABASE_URI,
                    database_table=config.DATABASE_TABLE)

        else:
            raise PreventUpdate

        # Note: n_images is a single-entry list
        pct_complete = utils.calc_percentage_complete(
            image_data[image_path]['position'], n_images[0])
        return image_data, pct_complete

    elif mode == 'undo':

        # Remove the last entry from each list in the metadata (corresponding to the last group)
        try:
            _ = image_data[image_path]['position'].pop()
            _ = image_data[image_path]['keep'].pop()
            filenames_undo = image_data[image_path]['filename'].pop()

            if not program_args.demo:
                utils.undo_last_group(
                    image_data=image_data,
                    image_path=image_path,
                    filename_list=filenames_undo,
                    image_backup_path=IMAGE_BACKUP_PATH,
                    meta_data_fpath=config.META_DATA_FPATH,
                    database_uri=config.DATABASE_URI,
                    database_table=config.DATABASE_TABLE,
                )

        # In case the lists are already empty
        except IndexError:
            pass

        # Note: n_images is a single-entry list
        pct_complete = utils.calc_percentage_complete(
            image_data[image_path]['position'], n_images[0])
        return image_data, pct_complete

    else:
        raise ValueError(f'Unknown mode: {mode}')
Example #4
0
def activate_deactivate_cells(n_rows, n_cols, n_left, n_right, n_up, n_down,
                              n_keep, n_delete, image_list, image_data,
                              image_path, *args):
    """
    Global callback function for toggling classes. There are three toggle modes:
        1) Pressing a grid cell will toggle its state
        2) Pressing a directional button will force the "last-clicked" focus (only) to shift in the direction stated
        3) Resizing the grid will cause the top-left only to be in last-click focus
        4) Mark a cell as keep or delete (must already have "grouped-on" class)

    Note: some of these operations respond to key presses (e.g. directional buttons), which click hidden buttons.

    Args:
        n_rows = int, current number of rows in the grid (indicates resizing)
        n_cols = int, current number of columns in the grid (indicates resizing)
        n_left = int, number of clicks on the 'move-left' button (indicates shifting)
        n_right = int, number of clicks on the 'move-right' button (indicates shifting)
        n_up = int, number of clicks on the 'move-up' button (indicates shifting)
        n_down = int, number of clicks on the 'move-down' button (indicates shifting)
        n_keep = int, number of clicks on the 'keep-button' button
        n_delete = int, number of clicks on the 'delete-button' button
        image_list = list, of str, specifying where the image files are stored
        image_data = dict, of dict of lists of ints, a sequence of metadata about completed image groups
        image_path = str, the filepath where the images in image-container were loaded from

        *args = positional arguments split into two equal halves (i.e. of length 2 x N_GRID):
            0) args[:N_GRID] are Inputs (activated by the grid-Buttons)
            1) args[N_GRID:] are States (indicating state of the grid-Tds)
            Both are in row-major order (for i in rows: for j in cols: ... )

    Returns: a list of new classNames for all the grid cells (plus one extra element for the Image that was last clicked)

    Note: args split into two halves:
        args[:N_GRID] are Inputs (Buttons)
        args[N_GRID:] are States (Tds)
    """

    # Unpack the single-element list
    image_path = image_path[0]
    if image_path not in image_data:
        image_data[image_path] = {'position': [], 'keep': [], 'filename': []}

    # Reduce the image_list by removing the masked images (so they can no longer appear in the image grid / image zoom)
    flat_mask = utils.create_flat_mask(image_data[image_path]['position'],
                                       len(image_list))
    image_list = [img for i, img in enumerate(image_list) if not flat_mask[i]]

    # Find the button that triggered this callback (if any)
    context = dash.callback_context
    if not context.triggered:
        class_names = [
            'grouped-off focus' if i + j == 0 else 'grouped-off'
            for i in range(ROWS_MAX) for j in range(COLS_MAX)
        ]
        zoomed_img = html.Img(src=image_list[0], style=config.IMG_STYLE_ZOOM)
        return class_names + [zoomed_img]
    else:
        button_id = context.triggered[0]['prop_id'].split('.')[0]

    # Reset the grid
    # Note: image-container is not really a button, but fired when confirm-load-directory is pressed (we need the list
    #       inside image-container in order to populate the grid)
    if button_id in [
            'choose-grid-size', 'image-container', 'image-meta-data',
            'loaded-image-path'
    ]:
        return utils.resize_grid_pressed(image_list)

    # Toggle the state of this button (as it was pressed)
    elif 'grid-button-' in button_id:
        return utils.image_cell_pressed(button_id, n_cols, image_list, *args)

    # Harder case: move focus in a particular direction
    elif 'move-' in button_id:
        return utils.direction_key_pressed(button_id, n_rows, n_cols,
                                           image_list, *args)

    elif button_id in ['keep-button', 'delete-button']:
        return utils.keep_delete_pressed(button_id, n_rows, n_cols, image_list,
                                         *args)

    else:
        raise ValueError('Unrecognized button ID: %s' % str(button_id))