Beispiel #1
0
    def __init__(self):
        super().__init__()

        self.grid = None

        self.data = None
        self.data_subset = None
        self.subset_indices = []
        self.nonempty = []

        self.allAttrs = []
        self.stringAttrs = []
        self.domainAttrs = []
        self.label_model = DomainModel(placeholder="(No labels)")

        self.selection = None

        #: List of _ImageItems
        self.items = []

        self._errcount = 0
        self._successcount = 0

        self.imageAttrCB = gui.comboBox(
            self.controlArea,
            self,
            "imageAttr",
            box="Image Filename Attribute",
            tooltip="Attribute with image filenames",
            callback=self.change_image_attr,
            contentsLength=12,
            addSpace=True,
        )

        # cell fit (resize or crop)
        self.cellFitRB = gui.radioButtons(self.controlArea,
                                          self,
                                          "cell_fit", ["Resize", "Crop"],
                                          box="Image cell fit",
                                          callback=self.set_crop)

        self.gridSizeBox = gui.vBox(self.controlArea, "Grid size")

        form = QFormLayout(labelAlignment=Qt.AlignLeft,
                           formAlignment=Qt.AlignLeft,
                           fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
                           verticalSpacing=10)

        self.colSpinner = gui.spin(self.gridSizeBox,
                                   self,
                                   "columns",
                                   minv=2,
                                   maxv=40,
                                   callback=self.update_size)
        self.rowSpinner = gui.spin(self.gridSizeBox,
                                   self,
                                   "rows",
                                   minv=2,
                                   maxv=40,
                                   callback=self.update_size)

        form.addRow("Columns:", self.colSpinner)
        form.addRow("Rows:", self.rowSpinner)

        gui.separator(self.gridSizeBox, 10)
        self.gridSizeBox.layout().addLayout(form)

        gui.button(self.gridSizeBox,
                   self,
                   "Set size automatically",
                   callback=self.auto_set_size)

        self.label_box = gui.vBox(self.controlArea, "Labels")

        # labels control
        self.label_attr_cb = gui.comboBox(self.label_box,
                                          self,
                                          "label_attr",
                                          tooltip="Show labels",
                                          callback=self.update_size,
                                          addSpace=True,
                                          model=self.label_model)

        gui.rubber(self.controlArea)

        # auto commit
        self.autoCommitBox = gui.auto_commit(
            self.controlArea,
            self,
            "auto_commit",
            "Apply",
            checkbox_label="Apply automatically")

        self.image_grid = None
        self.cell_fit = 0

        self.thumbnailView = ThumbnailView(
            alignment=Qt.AlignTop | Qt.AlignLeft,
            focusPolicy=Qt.StrongFocus,
            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff)
        self.mainArea.layout().addWidget(self.thumbnailView)
        self.scene = self.thumbnailView.scene()
        self.scene.selectionChanged.connect(self.on_selection_changed)
        self.loader = ImageLoader(self)
    def __init__(self):
        super().__init__()

        self.grid = None

        self.data = None
        self.data_subset = None
        self.subset_indices = []
        self.nonempty = []

        self.allAttrs = []
        self.stringAttrs = []
        self.domainAttrs = []

        self.selection = None

        #: List of _ImageItems
        self.items = []

        self._errcount = 0
        self._successcount = 0

        self.imageAttrCB = gui.comboBox(
            self.controlArea, self, "imageAttr",
            box="Image Filename Attribute",
            tooltip="Attribute with image filenames",
            callback=self.change_image_attr,
            contentsLength=12,
            addSpace=True,
        )

        # cell fit (resize or crop)
        self.cellFitRB = gui.radioButtons(self.controlArea, self, "cell_fit", ["Resize", "Crop"],
                                          box="Image cell fit", callback=self.set_crop)

        self.gridSizeBox = gui.vBox(self.controlArea, "Grid size")

        form = QFormLayout(
            labelAlignment=Qt.AlignLeft,
            formAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
            verticalSpacing=10
        )

        self.colSpinner = gui.spin(self.gridSizeBox, self, "columns", minv=2, maxv=40, callback=self.update_size)
        self.rowSpinner = gui.spin(self.gridSizeBox, self, "rows", minv=2, maxv=40, callback=self.update_size)

        form.addRow("Columns:", self.colSpinner)
        form.addRow("Rows:", self.rowSpinner)

        gui.separator(self.gridSizeBox, 10)
        self.gridSizeBox.layout().addLayout(form)

        gui.button(self.gridSizeBox, self, "Set size automatically", callback=self.auto_set_size)

        gui.rubber(self.controlArea)

        # auto commit
        self.autoCommitBox = gui.auto_commit(self.controlArea, self, "auto_commit", "Apply",
                                             checkbox_label="Apply automatically")

        self.info = gui.widgetLabel(gui.vBox(self.controlArea, "Info"), "Waiting for input.\n")

        self.image_grid = None
        self.cell_fit = 0

        self.thumbnailView = ThumbnailView(
            alignment=Qt.AlignTop | Qt.AlignLeft,
            focusPolicy=Qt.StrongFocus,
            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff
        )
        self.mainArea.layout().addWidget(self.thumbnailView)
        self.scene = self.thumbnailView.scene()
        self.scene.selectionChanged.connect(self.on_selection_changed)
        self.loader = ImageLoader(self)
Beispiel #3
0
class OWImageGrid(widget.OWWidget):
    name = "Image Grid"
    description = "Visualize images in a similarity grid"
    icon = "icons/ImageGrid.svg"
    priority = 160
    keywords = ["image", "grid", "similarity"]
    graph_name = "scene"

    class Inputs:
        data = Input("Embeddings", Orange.data.Table, default=True)
        data_subset = Input("Data Subset", Orange.data.Table)

    class Outputs:
        selected_data = Output("Selected Images",
                               Orange.data.Table,
                               default=True)
        data = Output("Images", Orange.data.Table)

    settingsHandler = settings.DomainContextHandler()

    cell_fit = settings.Setting("Resize")
    columns = settings.Setting(10)
    rows = settings.Setting(10)

    imageAttr = settings.ContextSetting(0)
    imageSize = settings.Setting(100)
    label_attr = settings.ContextSetting(None,
                                         required=ContextSetting.OPTIONAL)
    label_selected = settings.Setting(True)

    auto_update = settings.Setting(True)
    auto_commit = settings.Setting(True)

    class Warning(OWWidget.Warning):
        incompatible_subset = Msg("Data subset is incompatible with Data")
        no_valid_data = Msg("No valid data")

    def __init__(self):
        super().__init__()

        self.grid = None

        self.data = None
        self.data_subset = None
        self.subset_indices = []
        self.nonempty = []

        self.allAttrs = []
        self.stringAttrs = []
        self.domainAttrs = []
        self.label_model = DomainModel(placeholder="(No labels)")

        self.selection = None

        #: List of _ImageItems
        self.items = []

        self._errcount = 0
        self._successcount = 0

        self.imageAttrCB = gui.comboBox(
            self.controlArea,
            self,
            "imageAttr",
            box="Image Filename Attribute",
            tooltip="Attribute with image filenames",
            callback=self.change_image_attr,
            contentsLength=12,
            addSpace=True,
        )

        # cell fit (resize or crop)
        self.cellFitRB = gui.radioButtons(self.controlArea,
                                          self,
                                          "cell_fit", ["Resize", "Crop"],
                                          box="Image cell fit",
                                          callback=self.set_crop)

        self.gridSizeBox = gui.vBox(self.controlArea, "Grid size")

        form = QFormLayout(labelAlignment=Qt.AlignLeft,
                           formAlignment=Qt.AlignLeft,
                           fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
                           verticalSpacing=10)

        self.colSpinner = gui.spin(self.gridSizeBox,
                                   self,
                                   "columns",
                                   minv=2,
                                   maxv=40,
                                   callback=self.update_size)
        self.rowSpinner = gui.spin(self.gridSizeBox,
                                   self,
                                   "rows",
                                   minv=2,
                                   maxv=40,
                                   callback=self.update_size)

        form.addRow("Columns:", self.colSpinner)
        form.addRow("Rows:", self.rowSpinner)

        gui.separator(self.gridSizeBox, 10)
        self.gridSizeBox.layout().addLayout(form)

        gui.button(self.gridSizeBox,
                   self,
                   "Set size automatically",
                   callback=self.auto_set_size)

        self.label_box = gui.vBox(self.controlArea, "Labels")

        # labels control
        self.label_attr_cb = gui.comboBox(self.label_box,
                                          self,
                                          "label_attr",
                                          tooltip="Show labels",
                                          callback=self.update_size,
                                          addSpace=True,
                                          model=self.label_model)

        gui.rubber(self.controlArea)

        # auto commit
        self.autoCommitBox = gui.auto_commit(
            self.controlArea,
            self,
            "auto_commit",
            "Apply",
            checkbox_label="Apply automatically")

        self.image_grid = None
        self.cell_fit = 0

        self.thumbnailView = ThumbnailView(
            alignment=Qt.AlignTop | Qt.AlignLeft,
            focusPolicy=Qt.StrongFocus,
            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff)
        self.mainArea.layout().addWidget(self.thumbnailView)
        self.scene = self.thumbnailView.scene()
        self.scene.selectionChanged.connect(self.on_selection_changed)
        self.loader = ImageLoader(self)

    def process(self, size_x=0, size_y=0):
        if self.image_grid:
            self.image_grid.process(size_x, size_y)

    def sizeHint(self):
        return QSize(600, 600)

    # checks the input data for the right meta-attributes and finds images
    @Inputs.data
    def set_data(self, data):
        self.closeContext()
        self.clear()
        self.Warning.no_valid_data.clear()
        self.data = data

        if data is not None:
            domain = data.domain
            self.allAttrs = (domain.class_vars + domain.metas +
                             domain.attributes)

            self.stringAttrs = [a for a in domain.metas if a.is_string]
            self.domainAttrs = len(domain.attributes)

            self.stringAttrs = sorted(self.stringAttrs,
                                      key=lambda attr: 0
                                      if "type" in attr.attributes else 1)

            indices = [
                i for i, var in enumerate(self.stringAttrs)
                if var.attributes.get("type") == "image"
            ]
            if indices:
                self.imageAttr = indices[0]

            self.imageAttrCB.setModel(VariableListModel(self.stringAttrs))

            # set label combo labels
            self.label_model.set_domain(domain)
            self.openContext(data)
            self.label_attr = self.label_model[0]
            self.imageAttr = max(
                min(self.imageAttr,
                    len(self.stringAttrs) - 1), 0)

            if self.is_valid_data():
                self.image_grid = ImageGrid(data)
                self.setup_scene()
            else:
                self.Warning.no_valid_data()

    @Inputs.data_subset
    def set_data_subset(self, data_subset):
        self.data_subset = data_subset

    def clear(self):
        self.data = None
        self.image_grid = None
        self.error()
        self.imageAttrCB.clear()
        self.label_attr_cb.clear()
        self.clear_scene()

    def is_valid_data(self):
        return self.data and self.stringAttrs and self.domainAttrs

    # loads the images and places them into the viewing area
    def setup_scene(self, process_grid=True):
        self.clear_scene()
        self.error()
        if self.data:
            attr = self.stringAttrs[self.imageAttr]
            assert self.thumbnailView.count() == 0
            size = QSizeF(self.imageSize, self.imageSize)

            if process_grid and self.image_grid:
                self.process()
                self.columns = self.image_grid.size_x
                self.rows = self.image_grid.size_y

            self.thumbnailView.setFixedColumnCount(self.columns)
            self.thumbnailView.setFixedRowCount(self.rows)

            for i, inst in enumerate(self.image_grid.image_list):
                label_text = (str(inst[self.label_attr])
                              if self.label_attr is not None else "")
                if label_text == "?":
                    label_text = ""

                thumbnail = GraphicsThumbnailWidget(
                    QPixmap(),
                    crop=self.cell_fit == 1,
                    add_label=self.label_selected
                    and self.label_attr is not None,
                    text=label_text)
                thumbnail.setThumbnailSize(size)
                thumbnail.instance = inst
                self.thumbnailView.addThumbnail(thumbnail)

                if not np.isfinite(inst[attr]) or inst[attr] == "?":
                    # skip missing
                    future, url = None, None
                else:
                    url = self.url_from_value(inst[attr])
                    thumbnail.setToolTip(url.toString())
                    self.nonempty.append(i)

                    if url.isValid() and url.isLocalFile():
                        reader = QImageReader(url.toLocalFile())
                        image = reader.read()
                        if image.isNull():
                            error = reader.errorString()
                            thumbnail.setToolTip(thumbnail.toolTip() + "\n" +
                                                 error)

                            self._errcount += 1
                        else:
                            pixmap = QPixmap.fromImage(image)
                            thumbnail.setPixmap(pixmap)
                            self._successcount += 1

                        future = Future()
                        future.set_result(image)
                        future._reply = None
                    elif url.isValid():
                        future = self.loader.get(url)

                        @future.add_done_callback
                        def set_pixmap(future, thumb=thumbnail):
                            if future.cancelled():
                                return

                            assert future.done()

                            if future.exception():
                                # Should be some generic error image.
                                pixmap = QPixmap()
                                thumb.setToolTip(thumb.toolTip() + "\n" +
                                                 str(future.exception()))
                            else:
                                pixmap = QPixmap.fromImage(future.result())

                            thumb.setPixmap(pixmap)

                            self._note_completed(future)
                    else:
                        future = None

                self.items.append(_ImageItem(i, thumbnail, url, future))

            if not any(not it.future.done() if it.future else False
                       for it in self.items):
                self._update_status()
                self.apply_subset()
                self.update_selection()

    def handleNewSignals(self):
        self.Warning.incompatible_subset.clear()
        self.subset_indices = []

        if self.data and self.data_subset and len(self.stringAttrs) > 0:
            transformed = self.data_subset.transform(self.data.domain)
            attr = self.stringAttrs[self.imageAttr]

            # for the subset we need to check if it contains the image
            # attribute, if all indices from the subset are in the original
            # array, and if same image on the same index
            data_indices = [e.id for e in self.data]
            if attr in self.data_subset.domain \
                and all(row.id in data_indices and self.data[np.where(
                    data_indices == row.id)[0][0], attr] == row[attr]
                        for row in transformed):
                indices = {e.id for e in transformed}

                self.subset_indices = [ex.id in indices for ex in self.data]
            else:
                self.Warning.incompatible_subset()
        self.apply_subset()

    def url_from_value(self, value):
        base = value.variable.attributes.get("origin", "")
        if QDir(base).exists():
            base = QUrl.fromLocalFile(base)
        else:
            base = QUrl(base)

        path = base.path()
        if path.strip() and not path.endswith("/"):
            base.setPath(path + "/")

        url = base.resolved(QUrl(str(value)))
        return url

    def cancel_all_futures(self):
        for item in self.items:
            if item.future is not None:
                item.future.cancel()
                if item.future._reply is not None:
                    item.future._reply.close()
                    item.future._reply.deleteLater()
                    item.future._reply = None

    def clear_scene(self):
        self.cancel_all_futures()
        self.items = []
        self.nonempty = []
        self.selection = None
        self.thumbnailView.clear()
        self._errcount = 0
        self._successcount = 0

    def change_image_attr(self):
        self.clear_scene()
        if self.is_valid_data():
            self.setup_scene()

    def thumbnail_items(self):
        return [item.widget for item in self.items]

    def update_size(self):
        try:
            self.process(self.columns, self.rows)
            self.colSpinner.setMinimum(2)
            self.rowSpinner.setMinimum(2)

        except AssertionError:
            grid_size = self.thumbnailView.grid_size()
            self.columns = grid_size[0]
            self.rows = grid_size[1]
            self.colSpinner.setMinimum(self.columns)
            self.rowSpinner.setMinimum(self.rows)
            return

        self.clear_scene()
        if self.is_valid_data():
            self.setup_scene(process_grid=False)

    def set_crop(self):
        self.thumbnailView.setCrop(self.cell_fit == 1)

    def auto_set_size(self):
        self.clear_scene()
        if self.is_valid_data():
            self.setup_scene()

    def apply_subset(self):
        if self.image_grid:
            subset_indices = (self.subset_indices if self.subset_indices else
                              [True] * len(self.items))
            ordered_subset_indices = self.image_grid.order_to_grid(
                subset_indices)

            for item, in_subset in zip(self.items, ordered_subset_indices):
                item.widget.setSubset(in_subset)

    def on_selection_changed(self, selected_items, keys):
        if self.selection is None:
            self.selection = np.zeros(len(self.items), dtype=np.uint8)

        # newly selected
        indices = [
            item.index for item in self.items if item.widget in selected_items
        ]

        # Remove from selection
        if keys & Qt.AltModifier:
            self.selection[indices] = 0
        # Append to the last group
        elif keys & Qt.ShiftModifier and keys & Qt.ControlModifier:
            self.selection[indices] = np.max(self.selection)
        # Create a new group
        elif keys & Qt.ShiftModifier:
            self.selection[indices] = np.max(self.selection) + 1
        # No modifiers: new selection
        else:
            self.selection = np.zeros(len(self.items), dtype=np.uint8)
            self.selection[indices] = 1

        self.update_selection()
        self.commit()

    def commit(self):
        if self.data:
            # add Group column (group number)
            self.Outputs.selected_data.send(
                create_groups_table(self.image_grid.image_list, self.selection,
                                    False, "Group"))

            # filter out empty cells - keep indices of cells that contain images
            # add Selected column
            # (Yes/No if one group, else Unselected or group number)
            if self.selection is not None and np.max(self.selection) > 1:
                out_data = create_groups_table(
                    self.image_grid.image_list[self.nonempty],
                    self.selection[self.nonempty])
            else:
                out_data = create_annotated_table(
                    self.image_grid.image_list[self.nonempty],
                    np.nonzero(self.selection[self.nonempty]))
            self.Outputs.data.send(out_data)

        else:
            self.Outputs.data.send(None)
            self.Outputs.selected_data.send(None)

    def update_selection(self):
        if self.selection is not None:
            pen, brush = self.compute_colors()

            for s, item, p, b in zip(self.selection, self.items, pen, brush):
                item.widget.setSelected(s > 0)
                item.widget.setSelectionColor(p, b)

    # Adapted from Scatter Plot Graph (change brush instead of pen)
    def compute_colors(self):
        no_brush = DEFAULT_SELECTION_BRUSH
        sels = np.max(self.selection)
        if sels == 1:
            brushes = [no_brush, no_brush]
        else:
            palette = ColorPaletteGenerator(number_of_colors=sels + 1)
            brushes = [no_brush] + [QBrush(palette[i]) for i in range(sels)]
        brush = [brushes[a] for a in self.selection]

        pen = [DEFAULT_SELECTION_PEN] * len(self.items)
        return pen, brush

    def send_report(self):
        if self.is_valid_data():
            items = [("Number of images", len(self.data))]
            self.report_items(items)
            self.report_plot("Grid", self.scene)

    def _note_completed(self, future):
        # Note the completed future's state
        if future.cancelled():
            return

        if future.exception():
            self._errcount += 1
            _log.debug("Error: %r", future.exception())
        else:
            self._successcount += 1

        self._update_status()

    def _update_status(self):
        count = len([item for item in self.items if item.future is not None])

        if self._errcount + self._successcount == count:
            attr = self.stringAttrs[self.imageAttr]
            if self._errcount == count and "type" not in attr.attributes:
                self.error("No images found! Make sure the '%s' attribute "
                           "is tagged with 'type=image'" % attr.name)

    def onDeleteWidget(self):
        self.cancel_all_futures()
        self.clear()
class OWImageGrid(widget.OWWidget):
    name = "Image Grid"
    description = "Visualize images in a similarity grid"
    icon = "icons/ImageGrid.svg"
    priority = 160
    keywords = ["image", "grid", "similarity"]
    graph_name = "scene"

    class Inputs:
        data = Input("Embeddings", Orange.data.Table)
        data_subset = Input("Data Subset", Orange.data.Table)

    class Outputs:
        selected_data = Output(
            "Selected Images", Orange.data.Table, default=True)
        data = Output("Images", Orange.data.Table)

    settingsHandler = settings.DomainContextHandler()

    cell_fit = settings.Setting("Resize")
    columns = settings.Setting(10)
    rows = settings.Setting(10)

    imageAttr = settings.ContextSetting(0)
    imageSize = settings.Setting(100)

    auto_update = settings.Setting(True)
    auto_commit = settings.Setting(True)

    class Warning(OWWidget.Warning):
        incompatible_subset = Msg("Data subset is incompatible with Data")
        no_valid_data = Msg("No valid data")

    def __init__(self):
        super().__init__()

        self.grid = None

        self.data = None
        self.data_subset = None
        self.subset_indices = []
        self.nonempty = []

        self.allAttrs = []
        self.stringAttrs = []
        self.domainAttrs = []

        self.selection = None

        #: List of _ImageItems
        self.items = []

        self._errcount = 0
        self._successcount = 0

        self.imageAttrCB = gui.comboBox(
            self.controlArea, self, "imageAttr",
            box="Image Filename Attribute",
            tooltip="Attribute with image filenames",
            callback=self.change_image_attr,
            contentsLength=12,
            addSpace=True,
        )

        # cell fit (resize or crop)
        self.cellFitRB = gui.radioButtons(self.controlArea, self, "cell_fit", ["Resize", "Crop"],
                                          box="Image cell fit", callback=self.set_crop)

        self.gridSizeBox = gui.vBox(self.controlArea, "Grid size")

        form = QFormLayout(
            labelAlignment=Qt.AlignLeft,
            formAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
            verticalSpacing=10
        )

        self.colSpinner = gui.spin(self.gridSizeBox, self, "columns", minv=2, maxv=40, callback=self.update_size)
        self.rowSpinner = gui.spin(self.gridSizeBox, self, "rows", minv=2, maxv=40, callback=self.update_size)

        form.addRow("Columns:", self.colSpinner)
        form.addRow("Rows:", self.rowSpinner)

        gui.separator(self.gridSizeBox, 10)
        self.gridSizeBox.layout().addLayout(form)

        gui.button(self.gridSizeBox, self, "Set size automatically", callback=self.auto_set_size)

        gui.rubber(self.controlArea)

        # auto commit
        self.autoCommitBox = gui.auto_commit(self.controlArea, self, "auto_commit", "Apply",
                                             checkbox_label="Apply automatically")

        self.info = gui.widgetLabel(gui.vBox(self.controlArea, "Info"), "Waiting for input.\n")

        self.image_grid = None
        self.cell_fit = 0

        self.thumbnailView = ThumbnailView(
            alignment=Qt.AlignTop | Qt.AlignLeft,
            focusPolicy=Qt.StrongFocus,
            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff
        )
        self.mainArea.layout().addWidget(self.thumbnailView)
        self.scene = self.thumbnailView.scene()
        self.scene.selectionChanged.connect(self.on_selection_changed)
        self.loader = ImageLoader(self)

    def process(self, size_x=0, size_y=0):
        if self.image_grid:
            self.image_grid.process(size_x, size_y)

    def sizeHint(self):
        return QSize(600, 600)

    # checks the input data for the right meta-attributes and finds the image filename.
    @Inputs.data
    def set_data(self, data):
        self.closeContext()
        self.clear()
        self.Warning.no_valid_data.clear()
        self.data = data

        if data is not None:
            domain = data.domain
            self.allAttrs = (domain.class_vars + domain.metas +
                             domain.attributes)

            self.stringAttrs = [a for a in domain.metas if a.is_string]
            self.domainAttrs = len(domain.attributes)

            self.stringAttrs = sorted(
                self.stringAttrs,
                key=lambda attr: 0 if "type" in attr.attributes else 1
            )

            indices = [i for i, var in enumerate(self.stringAttrs)
                       if var.attributes.get("type") == "image"]
            if indices:
                self.imageAttr = indices[0]

            self.imageAttrCB.setModel(VariableListModel(self.stringAttrs))

            self.openContext(data)

            self.imageAttr = max(min(self.imageAttr, len(self.stringAttrs) - 1), 0)

            if self.is_valid_data():
                self.image_grid = ImageGrid(data)
                self.setup_scene()
            else:
                self.Warning.no_valid_data()
        else:
            self.info.setText("Waiting for input.\n")

    @Inputs.data_subset
    def set_data_subset(self, data_subset):
        self.data_subset = data_subset

    def clear(self):
        self.data = None
        self.image_grid = None
        self.error()
        self.imageAttrCB.clear()
        self.clear_scene()

    def is_valid_data(self):
        return self.data and self.stringAttrs and self.domainAttrs

    # loads the images and places them into the viewing area
    def setup_scene(self, process_grid=True):
        self.error()
        if self.data:
            attr = self.stringAttrs[self.imageAttr]
            assert self.thumbnailView.count() == 0
            size = QSizeF(self.imageSize, self.imageSize)

            if process_grid and self.image_grid:
                self.process()
                self.columns = self.image_grid.size_x
                self.rows = self.image_grid.size_y

            self.thumbnailView.setFixedColumnCount(self.columns)
            self.thumbnailView.setFixedRowCount(self.rows)

            for i, inst in enumerate(self.image_grid.image_list):
                thumbnail = GraphicsThumbnailWidget(QPixmap(), crop=self.cell_fit == 1)
                thumbnail.setThumbnailSize(size)
                thumbnail.instance = inst
                self.thumbnailView.addThumbnail(thumbnail)

                if not np.isfinite(inst[attr]) or inst[attr] == "?":  # skip missing
                    future, url = None, None
                else:
                    url = self.url_from_value(inst[attr])
                    thumbnail.setToolTip(url.toString())
                    self.nonempty.append(i)

                    if url.isValid() and url.isLocalFile():
                        reader = QImageReader(url.toLocalFile())
                        image = reader.read()
                        if image.isNull():
                            error = reader.errorString()
                            thumbnail.setToolTip(
                                thumbnail.toolTip() + "\n" + error)

                            self._errcount += 1
                        else:
                            pixmap = QPixmap.fromImage(image)
                            thumbnail.setPixmap(pixmap)
                            self._successcount += 1

                        future = Future()
                        future.set_result(image)
                        future._reply = None
                    elif url.isValid():
                        future = self.loader.get(url)

                        @future.add_done_callback
                        def set_pixmap(future, thumb=thumbnail):
                            if future.cancelled():
                                return

                            assert future.done()

                            if future.exception():
                                # Should be some generic error image.
                                pixmap = QPixmap()
                                thumb.setToolTip(thumb.toolTip() + "\n" +
                                                 str(future.exception()))
                            else:
                                pixmap = QPixmap.fromImage(future.result())

                            thumb.setPixmap(pixmap)

                            self._note_completed(future)
                    else:
                        future = None

                self.items.append(_ImageItem(i, thumbnail, url, future))

            if any(not it.future.done() if it.future else False for it in self.items):
                self.info.setText("Retrieving...\n")
            else:
                self._update_status()
                self.apply_subset()
                self.update_selection()

    def handleNewSignals(self):
        self.Warning.incompatible_subset.clear()
        self.subset_indices = []

        if self.data and self.data_subset:
            transformed = self.data_subset.transform(self.data.domain)
            if np.all(self.data.domain.metas == self.data_subset.domain.metas):
                indices = {e.id for e in transformed}
                self.subset_indices = [ex.id in indices for ex in self.data]

            else:
                self.Warning.incompatible_subset()

        self.apply_subset()

    def url_from_value(self, value):
        base = value.variable.attributes.get("origin", "")
        if QDir(base).exists():
            base = QUrl.fromLocalFile(base)
        else:
            base = QUrl(base)

        path = base.path()
        if path.strip() and not path.endswith("/"):
            base.setPath(path + "/")

        url = base.resolved(QUrl(str(value)))
        return url

    def cancel_all_futures(self):
        for item in self.items:
            if item.future is not None:
                item.future.cancel()
                if item.future._reply is not None:
                    item.future._reply.close()
                    item.future._reply.deleteLater()
                    item.future._reply = None

    def clear_scene(self):
        self.cancel_all_futures()
        self.items = []
        self.nonempty = []
        self.selection = None
        self.thumbnailView.clear()
        self._errcount = 0
        self._successcount = 0

    def change_image_attr(self):
        self.clear_scene()
        if self.is_valid_data():
            self.setup_scene()

    def thumbnail_items(self):
        return [item.widget for item in self.items]

    def update_size(self):
        try:
            self.process(self.columns, self.rows)
            self.colSpinner.setMinimum(2)
            self.rowSpinner.setMinimum(2)

        except AssertionError:
            grid_size = self.thumbnailView.grid_size()
            self.columns = grid_size[0]
            self.rows = grid_size[1]
            self.colSpinner.setMinimum(self.columns)
            self.rowSpinner.setMinimum(self.rows)
            return

        self.clear_scene()
        if self.is_valid_data():
            self.setup_scene(process_grid=False)

    def set_crop(self):
        self.thumbnailView.setCrop(self.cell_fit == 1)

    def auto_set_size(self):
        self.clear_scene()
        if self.is_valid_data():
            self.setup_scene()

    def apply_subset(self):
        if self.image_grid:
            subset_indices = self.subset_indices if self.subset_indices else [True] * len(self.items)
            ordered_subset_indices = self.image_grid.order_to_grid(subset_indices)

            for item, in_subset in zip(self.items, ordered_subset_indices):
                item.widget.setSubset(in_subset)

    def on_selection_changed(self, selected_items, keys):
        if self.selection is None:
            self.selection = np.zeros(len(self.items), dtype=np.uint8)

        # newly selected
        indices = [item.index for item in self.items if item.widget in selected_items]

        # Remove from selection
        if keys & Qt.AltModifier:
            self.selection[indices] = 0
        # Append to the last group
        elif keys & Qt.ShiftModifier and keys & Qt.ControlModifier:
            self.selection[indices] = np.max(self.selection)
        # Create a new group
        elif keys & Qt.ShiftModifier:
            self.selection[indices] = np.max(self.selection) + 1
        # No modifiers: new selection
        else:
            self.selection = np.zeros(len(self.items), dtype=np.uint8)
            self.selection[indices] = 1

        self.update_selection()
        self.commit()

    def commit(self):
        if self.data:
            # add Group column (group number)
            self.Outputs.selected_data.send(
                create_groups_table(self.image_grid.image_list, self.selection,
                                    False, "Group"))

            # filter out empty cells - keep indices of cells that contain images
            # add Selected column
            # (Yes/No if one group, else Unselected or group number)
            if self.selection is not None and np.max(self.selection) > 1:
                out_data = create_groups_table(
                    self.image_grid.image_list[self.nonempty],
                    self.selection[self.nonempty])
            else:
                out_data = create_annotated_table(
                    self.image_grid.image_list[self.nonempty],
                    np.nonzero(self.selection[self.nonempty]))
            self.Outputs.data.send(out_data)

        else:
            self.Outputs.data.send(None)
            self.Outputs.selected_data.send(None)

    def update_selection(self):
        if self.selection is not None:
            pen, brush = self.compute_colors()

            for s, item, p, b in zip(self.selection, self.items, pen, brush):
                item.widget.setSelected(s > 0)
                item.widget.setSelectionColor(p, b)

    # Adapted from Scatter Plot Graph (change brush instead of pen)
    def compute_colors(self):
        no_brush = DEFAULT_SELECTION_BRUSH
        sels = np.max(self.selection)
        if sels == 1:
            brushes = [no_brush, no_brush]
        else:
            palette = ColorPaletteGenerator(number_of_colors=sels + 1)
            brushes = [no_brush] + [QBrush(palette[i]) for i in range(sels)]
        brush = [brushes[a] for a in self.selection]

        pen = [DEFAULT_SELECTION_PEN] * len(self.items)
        return pen, brush

    def send_report(self):
        if self.is_valid_data():
            items = [("Number of images", len(self.data))]
            self.report_items(items)
            self.report_plot("Grid", self.scene)

    def _note_completed(self, future):
        # Note the completed future's state
        if future.cancelled():
            return

        if future.exception():
            self._errcount += 1
            _log.debug("Error: %r", future.exception())
        else:
            self._successcount += 1

        self._update_status()

    def _update_status(self):
        count = len([item for item in self.items if item.future is not None])
        self.info.setText(
            "Retrieving:\n" +
            "{} of {} images".format(self._successcount, count))

        if self._errcount + self._successcount == count:
            if self._errcount:
                self.info.setText(
                    "Done:\n" +
                    "{} images, {} errors".format(count, self._errcount)
                )
            else:
                self.info.setText(
                    "Done:\n" +
                    "{} images".format(count)
                )
            attr = self.stringAttrs[self.imageAttr]
            if self._errcount == count and "type" not in attr.attributes:
                self.error("No images found! Make sure the '%s' attribute "
                           "is tagged with 'type=image'" % attr.name)

    def onDeleteWidget(self):
        self.cancel_all_futures()
        self.clear()