diff --git a/labelImg.py b/labelImg.py index efd8a2976..1b41ece30 100755 --- a/labelImg.py +++ b/labelImg.py @@ -2,12 +2,17 @@ # -*- coding: utf-8 -*- import argparse import codecs +import distutils.spawn import os.path import platform -import shutil +import re import sys +import subprocess +import shutil import webbrowser as wb + from functools import partial +from collections import defaultdict try: from PyQt5.QtGui import * @@ -25,7 +30,6 @@ from PyQt4.QtCore import * from libs.combobox import ComboBox -from libs.default_label_combobox import DefaultLabelComboBox from libs.resources import * from libs.constants import * from libs.utils import * @@ -34,7 +38,6 @@ from libs.stringBundle import StringBundle from libs.canvas import Canvas from libs.zoomWidget import ZoomWidget -from libs.lightWidget import LightWidget from libs.labelDialog import LabelDialog from libs.colorDialog import ColorDialog from libs.labelFile import LabelFile, LabelFileError, LabelFileFormat @@ -98,7 +101,7 @@ def __init__(self, default_filename=None, default_prefdef_class_file=None, defau self.label_hist = [] self.last_open_dir = None self.cur_img_idx = 0 - self.img_count = len(self.m_img_list) + self.img_count = 1 # Whether we need to save or not. self.dirty = False @@ -110,11 +113,6 @@ def __init__(self, default_filename=None, default_prefdef_class_file=None, defau # Load predefined classes to the list self.load_predefined_classes(default_prefdef_class_file) - if self.label_hist: - self.default_label = self.label_hist[0] - else: - print("Not find:/data/predefined_classes.txt (optional)") - # Main widgets and related state. self.label_dialog = LabelDialog(parent=self, list_item=self.label_hist) @@ -128,11 +126,10 @@ def __init__(self, default_filename=None, default_prefdef_class_file=None, defau # Create a widget for using default label self.use_default_label_checkbox = QCheckBox(get_str('useDefaultLabel')) self.use_default_label_checkbox.setChecked(False) - self.default_label_combo_box = DefaultLabelComboBox(self,items=self.label_hist) - + self.default_label_text_line = QLineEdit() use_default_label_qhbox_layout = QHBoxLayout() use_default_label_qhbox_layout.addWidget(self.use_default_label_checkbox) - use_default_label_qhbox_layout.addWidget(self.default_label_combo_box) + use_default_label_qhbox_layout.addWidget(self.default_label_text_line) use_default_label_container = QWidget() use_default_label_container.setLayout(use_default_label_qhbox_layout) @@ -181,12 +178,10 @@ def __init__(self, default_filename=None, default_prefdef_class_file=None, defau self.file_dock.setWidget(file_list_container) self.zoom_widget = ZoomWidget() - self.light_widget = LightWidget(get_str('lightWidgetTitle')) self.color_dialog = ColorDialog(parent=self) self.canvas = Canvas(parent=self) self.canvas.zoomRequest.connect(self.zoom_request) - self.canvas.lightRequest.connect(self.light_request) self.canvas.set_drawing_shape_to_square(settings.get(SETTING_DRAW_SQUARE, False)) scroll = QScrollArea() @@ -254,7 +249,7 @@ def get_format_meta(format): return '&CreateML', 'format_createml' save_format = action(get_format_meta(self.label_file_format)[0], - self.change_format, 'Ctrl+Y', + self.change_format, 'Ctrl+', get_format_meta(self.label_file_format)[1], get_str('changeSaveFormat'), enabled=True) @@ -329,26 +324,6 @@ def get_format_meta(format): self.MANUAL_ZOOM: lambda: 1, } - light = QWidgetAction(self) - light.setDefaultWidget(self.light_widget) - self.light_widget.setWhatsThis( - u"Brighten or darken current image. Also accessible with" - " %s and %s from the canvas." % (format_shortcut("Ctrl+Shift+[-+]"), - format_shortcut("Ctrl+Shift+Wheel"))) - self.light_widget.setEnabled(False) - - light_brighten = action(get_str('lightbrighten'), partial(self.add_light, 10), - 'Ctrl+Shift++', 'light_lighten', get_str('lightbrightenDetail'), enabled=False) - light_darken = action(get_str('lightdarken'), partial(self.add_light, -10), - 'Ctrl+Shift+-', 'light_darken', get_str('lightdarkenDetail'), enabled=False) - light_org = action(get_str('lightreset'), partial(self.set_light, 50), - 'Ctrl+Shift+=', 'light_reset', get_str('lightresetDetail'), checkable=True, enabled=False) - light_org.setChecked(True) - - # Group light controls into a list for easier toggling. - light_actions = (self.light_widget, light_brighten, - light_darken, light_org) - edit = action(get_str('editLabel'), self.edit_label, 'Ctrl+E', 'edit', get_str('editLabelDetail'), enabled=False) @@ -387,8 +362,6 @@ def get_format_meta(format): zoom=zoom, zoomIn=zoom_in, zoomOut=zoom_out, zoomOrg=zoom_org, fitWindow=fit_window, fitWidth=fit_width, zoomActions=zoom_actions, - lightBrighten=light_brighten, lightDarken=light_darken, lightOrg=light_org, - lightActions=light_actions, fileMenuActions=( open, open_dir, save, save_as, close, reset_all, quit), beginner=(), advanced=(), @@ -436,8 +409,7 @@ def get_format_meta(format): labels, advanced_mode, None, hide_all, show_all, None, zoom_in, zoom_out, zoom_org, None, - fit_window, fit_width, None, - light_brighten, light_darken, light_org)) + fit_window, fit_width)) self.menus.file.aboutToShow.connect(self.update_file_menu) @@ -450,8 +422,7 @@ def get_format_meta(format): self.tools = self.toolbar('Tools') self.actions.beginner = ( open, open_dir, change_save_dir, open_next_image, open_prev_image, verify, save, save_format, None, create, copy, delete, None, - zoom_in, zoom, zoom_out, fit_window, fit_width, None, - light_brighten, light, light_darken, light_org) + zoom_in, zoom, zoom_out, fit_window, fit_width) self.actions.advanced = ( open, open_dir, change_save_dir, open_next_image, open_prev_image, save, save_format, None, @@ -527,7 +498,6 @@ def xbool(x): # Callbacks: self.zoom_widget.valueChanged.connect(self.paint_canvas) - self.light_widget.valueChanged.connect(self.paint_canvas) self.populate_mode_actions() @@ -629,8 +599,6 @@ def toggle_actions(self, value=True): """Enable/Disable widgets which depend on an opened image.""" for z in self.actions.zoomActions: z.setEnabled(value) - for z in self.actions.lightActions: - z.setEnabled(value) for action in self.actions.onLoadActive: action.setEnabled(value) @@ -932,9 +900,6 @@ def combo_selection_changed(self, index): else: self.label_list.item(i).setCheckState(2) - def default_label_combo_selection_changed(self, index): - self.default_label=self.label_hist[index] - def label_selection_changed(self): item = self.current_item() if item and self.canvas.editing(): @@ -960,7 +925,7 @@ def new_shape(self): position MUST be in global coordinates. """ - if not self.use_default_label_checkbox.isChecked(): + if not self.use_default_label_checkbox.isChecked() or not self.default_label_text_line.text(): if len(self.label_hist) > 0: self.label_dialog = LabelDialog( parent=self, list_item=self.label_hist) @@ -972,7 +937,7 @@ def new_shape(self): text = self.label_dialog.pop_up(text=self.prev_label_text) self.lastLabel = text else: - text = self.default_label + text = self.default_label_text_line.text() # Add Chris self.diffc_button.setChecked(False) @@ -1003,9 +968,7 @@ def set_zoom(self, value): self.actions.fitWidth.setChecked(False) self.actions.fitWindow.setChecked(False) self.zoom_mode = self.MANUAL_ZOOM - # Arithmetic on scaling factor often results in float - # Convert to int to avoid type errors - self.zoom_widget.setValue(int(value)) + self.zoom_widget.setValue(value) def add_zoom(self, increment=10): self.set_zoom(self.zoom_widget.value() + increment) @@ -1046,7 +1009,7 @@ def zoom_request(self, delta): move_y = min(max(move_y, 0), 1) # zoom in - units = delta // (8 * 15) + units = delta / (8 * 15) scale = 10 self.add_zoom(scale * units) @@ -1056,15 +1019,12 @@ def zoom_request(self, delta): d_v_bar_max = v_bar.maximum() - v_bar_max # get the new scrollbar values - new_h_bar_value = int(h_bar.value() + move_x * d_h_bar_max) - new_v_bar_value = int(v_bar.value() + move_y * d_v_bar_max) + new_h_bar_value = h_bar.value() + move_x * d_h_bar_max + new_v_bar_value = v_bar.value() + move_y * d_v_bar_max h_bar.setValue(new_h_bar_value) v_bar.setValue(new_v_bar_value) - def light_request(self, delta): - self.add_light(5*delta // (8 * 15)) - def set_fit_window(self, value=True): if value: self.actions.fitWidth.setChecked(False) @@ -1077,15 +1037,6 @@ def set_fit_width(self, value=True): self.zoom_mode = self.FIT_WIDTH if value else self.MANUAL_ZOOM self.adjust_scale() - def set_light(self, value): - self.actions.lightOrg.setChecked(int(value) == 50) - # Arithmetic on scaling factor often results in float - # Convert to int to avoid type errors - self.light_widget.setValue(int(value)) - - def add_light(self, increment=10): - self.set_light(self.light_widget.value() + increment) - def toggle_polygons(self, value): for item, shape in self.items_to_shapes.items(): item.setCheckState(Qt.Checked if value else Qt.Unchecked) @@ -1096,6 +1047,7 @@ def load_file(self, file_path=None): self.canvas.setEnabled(False) if file_path is None: file_path = self.settings.get(SETTING_FILENAME) + # Make sure that filePath is a regular python string, rather than QString file_path = ustr(file_path) @@ -1123,7 +1075,6 @@ def load_file(self, file_path=None): u"

Make sure %s is a valid label file.") % (e, unicode_file_path)) self.status("Error reading %s" % unicode_file_path) - return False self.image_data = self.label_file.image_data self.line_color = QColor(*self.label_file.lineColor) @@ -1157,7 +1108,7 @@ def load_file(self, file_path=None): self.paint_canvas() self.add_recent_file(self.file_path) self.toggle_actions(True) - self.show_bounding_box_from_annotation_file(self.file_path) + self.show_bounding_box_from_annotation_file(file_path) counter = self.counter_str() self.setWindowTitle(__appname__ + ' ' + file_path + ' ' + counter) @@ -1197,15 +1148,10 @@ def show_bounding_box_from_annotation_file(self, file_path): else: xml_path = os.path.splitext(file_path)[0] + XML_EXT txt_path = os.path.splitext(file_path)[0] + TXT_EXT - json_path = os.path.splitext(file_path)[0] + JSON_EXT - if os.path.isfile(xml_path): self.load_pascal_xml_by_filename(xml_path) elif os.path.isfile(txt_path): self.load_yolo_txt_by_filename(txt_path) - elif os.path.isfile(json_path): - self.load_create_ml_json_by_filename(json_path, file_path) - def resizeEvent(self, event): if self.canvas and not self.image.isNull()\ @@ -1216,7 +1162,6 @@ def resizeEvent(self, event): def paint_canvas(self): assert not self.image.isNull(), "cannot paint null image" self.canvas.scale = 0.01 * self.zoom_widget.value() - self.canvas.overlay_color = self.light_widget.color() self.canvas.label_font_size = int(0.02 * max(self.image.width(), self.image.height())) self.canvas.adjustSize() self.canvas.update() @@ -1306,13 +1251,10 @@ def change_save_dir_dialog(self, _value=False): if dir_path is not None and len(dir_path) > 1: self.default_save_dir = dir_path - self.show_bounding_box_from_annotation_file(self.file_path) - self.statusBar().showMessage('%s . Annotation will be saved to %s' % ('Change saved folder', self.default_save_dir)) self.statusBar().show() - def open_annotation_dialog(self, _value=False): if self.file_path is None: self.statusBar().showMessage('Please select image first') @@ -1329,17 +1271,6 @@ def open_annotation_dialog(self, _value=False): filename = filename[0] self.load_pascal_xml_by_filename(filename) - elif self.label_file_format == LabelFileFormat.CREATE_ML: - - filters = "Open Annotation JSON file (%s)" % ' '.join(['*.json']) - filename = ustr(QFileDialog.getOpenFileName(self, '%s - Choose a json file' % __appname__, path, filters)) - if filename: - if isinstance(filename, (tuple, list)): - filename = filename[0] - - self.load_create_ml_json_by_filename(filename, self.file_path) - - def open_dir_dialog(self, _value=False, dir_path=None, silent=False): if not self.may_continue(): return @@ -1357,9 +1288,6 @@ def open_dir_dialog(self, _value=False, dir_path=None, silent=False): target_dir_path = ustr(default_open_dir_path) self.last_open_dir = target_dir_path self.import_dir_images(target_dir_path) - self.default_save_dir = target_dir_path - if self.file_path: - self.show_bounding_box_from_annotation_file(file_path=self.file_path) def import_dir_images(self, dir_path): if not self.may_continue() or not dir_path: @@ -1420,7 +1348,7 @@ def open_prev_image(self, _value=False): self.load_file(filename) def open_next_image(self, _value=False): - # Proceeding next image without dialog if having any label + # Proceeding prev image without dialog if having any label if self.auto_saving.isChecked(): if self.default_save_dir is not None: if self.dirty is True: @@ -1434,9 +1362,6 @@ def open_next_image(self, _value=False): if self.img_count <= 0: return - - if not self.m_img_list: - return filename = None if self.file_path is None: @@ -1456,7 +1381,7 @@ def open_file(self, _value=False): path = os.path.dirname(ustr(self.file_path)) if self.file_path else '.' formats = ['*.%s' % fmt.data().decode("ascii").lower() for fmt in QImageReader.supportedImageFormats()] filters = "Image & Label files (%s)" % ' '.join(formats + ['*%s' % LabelFile.suffix]) - filename,_ = QFileDialog.getOpenFileName(self, '%s - Choose Image or Label file' % __appname__, path, filters) + filename = QFileDialog.getOpenFileName(self, '%s - Choose Image or Label file' % __appname__, path, filters) if filename: if isinstance(filename, (tuple, list)): filename = filename[0] @@ -1519,16 +1444,12 @@ def close_file(self, _value=False): def delete_image(self): delete_path = self.file_path if delete_path is not None: - idx = self.cur_img_idx + self.open_next_image() + self.cur_img_idx -= 1 + self.img_count -= 1 if os.path.exists(delete_path): os.remove(delete_path) self.import_dir_images(self.last_open_dir) - if self.img_count > 0: - self.cur_img_idx = min(idx, self.img_count - 1) - filename = self.m_img_list[self.cur_img_idx] - self.load_file(filename) - else: - self.close_file() def reset_all(self): self.settings.reset() @@ -1595,9 +1516,6 @@ def choose_shape_fill_color(self): self.set_dirty() def copy_shape(self): - if self.canvas.selected_shape is None: - # True if one accidentally touches the left mouse button before releasing - return self.canvas.end_move(copy=True) self.add_label(self.canvas.selected_shape) self.set_dirty() @@ -1682,13 +1600,11 @@ def read(filename, default=None): return default -def get_main_app(argv=None): +def get_main_app(argv=[]): """ Standard boilerplate Qt application code. Do everything but app.exec_() -- so that we can test the application in one thread """ - if not argv: - argv = [] app = QApplication(argv) app.setApplicationName(__appname__) app.setWindowIcon(new_icon("app")) diff --git a/libs/canvas.py b/libs/canvas.py index ca7986ff3..23c1f751d 100644 --- a/libs/canvas.py +++ b/libs/canvas.py @@ -23,7 +23,6 @@ class Canvas(QWidget): zoomRequest = pyqtSignal(int) - lightRequest = pyqtSignal(int) scrollRequest = pyqtSignal(int, int) newShape = pyqtSignal() selectionChanged = pyqtSignal(bool) @@ -32,7 +31,7 @@ class Canvas(QWidget): CREATE, EDIT = list(range(2)) - epsilon = 24.0 + epsilon = 11.0 def __init__(self, *args, **kwargs): super(Canvas, self).__init__(*args, **kwargs) @@ -48,7 +47,6 @@ def __init__(self, *args, **kwargs): self.prev_point = QPointF() self.offsets = QPointF(), QPointF() self.scale = 1.0 - self.overlay_color = None self.label_font_size = 8 self.pixmap = QPixmap() self.visible = {} @@ -99,11 +97,10 @@ def set_editing(self, value=True): self.prev_point = QPointF() self.repaint() - def un_highlight(self, shape=None): - if shape == None or shape == self.h_shape: - if self.h_shape: - self.h_shape.highlight_clear() - self.h_vertex = self.h_shape = None + def un_highlight(self): + if self.h_shape: + self.h_shape.highlight_clear() + self.h_vertex = self.h_shape = None def selected_vertex(self): return self.h_vertex is not None @@ -204,9 +201,10 @@ def mouseMoveEvent(self, ev): 'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y())) else: # pan - delta = ev.pos() - self.pan_initial_pos - self.scrollRequest.emit(delta.x(), Qt.Horizontal) - self.scrollRequest.emit(delta.y(), Qt.Vertical) + delta_x = pos.x() - self.pan_initial_pos.x() + delta_y = pos.y() - self.pan_initial_pos.y() + self.scrollRequest.emit(delta_x, Qt.Horizontal) + self.scrollRequest.emit(delta_y, Qt.Vertical) self.update() return @@ -215,8 +213,7 @@ def mouseMoveEvent(self, ev): # - Highlight vertex # Update shape/vertex fill and tooltip value accordingly. self.setToolTip("Image") - priority_list = self.shapes + ([self.selected_shape] if self.selected_shape else []) - for shape in reversed([s for s in priority_list if self.isVisible(s)]): + for shape in reversed([s for s in self.shapes if self.isVisible(s)]): # Look for a nearby vertex to highlight. If that fails, # check if we happen to be inside a shape. index = shape.nearest_vertex(pos, self.epsilon) @@ -268,7 +265,7 @@ def mousePressEvent(self, ev): if selection is None: # pan QApplication.setOverrideCursor(QCursor(Qt.OpenHandCursor)) - self.pan_initial_pos = ev.pos() + self.pan_initial_pos = pos elif ev.button() == Qt.RightButton and self.editing(): self.select_shape_point(pos) @@ -466,7 +463,6 @@ def de_select_shape(self): def delete_selected(self): if self.selected_shape: shape = self.selected_shape - self.un_highlight(shape) self.shapes.remove(self.selected_shape) self.selected_shape = None self.update() @@ -505,15 +501,7 @@ def paintEvent(self, event): p.scale(self.scale, self.scale) p.translate(self.offset_to_center()) - temp = self.pixmap - if self.overlay_color: - temp = QPixmap(self.pixmap) - painter = QPainter(temp) - painter.setCompositionMode(painter.CompositionMode_Overlay) - painter.fillRect(temp.rect(), self.overlay_color) - painter.end() - - p.drawPixmap(0, 0, temp) + p.drawPixmap(0, 0, self.pixmap) Shape.scale = self.scale Shape.label_font_size = self.label_font_size for shape in self.shapes: @@ -535,12 +523,12 @@ def paintEvent(self, event): p.setPen(self.drawing_rect_color) brush = QBrush(Qt.BDiagPattern) p.setBrush(brush) - p.drawRect(int(left_top.x()), int(left_top.y()), int(rect_width), int(rect_height)) + p.drawRect(left_top.x(), left_top.y(), int(rect_width), int(rect_height)) if self.drawing() and not self.prev_point.isNull() and not self.out_of_pixmap(self.prev_point): p.setPen(QColor(0, 0, 0)) - p.drawLine(int(self.prev_point.x()), 0, int(self.prev_point.x()), int(self.pixmap.height())) - p.drawLine(0, int(self.prev_point.y()), int(self.pixmap.width()), int(self.prev_point.y())) + p.drawLine(self.prev_point.x(), 0, self.prev_point.x(), self.pixmap.height()) + p.drawLine(0, self.prev_point.y(), self.pixmap.width(), self.prev_point.y()) self.setAutoFillBackground(True) if self.verified: @@ -556,7 +544,8 @@ def paintEvent(self, event): def transform_pos(self, point): """Convert from widget-logical coordinates to painter-logical coordinates.""" - return point / self.scale - self.offset_to_center() + point = point / self.scale - self.offset_to_center() + return point.toPoint() def offset_to_center(self): s = self.scale @@ -617,9 +606,7 @@ def wheelEvent(self, ev): v_delta = delta.y() mods = ev.modifiers() - if int(Qt.ControlModifier) | int(Qt.ShiftModifier) == int(mods) and v_delta: - self.lightRequest.emit(v_delta) - elif Qt.ControlModifier == int(mods) and v_delta: + if Qt.ControlModifier == int(mods) and v_delta: self.zoomRequest.emit(v_delta) else: v_delta and self.scrollRequest.emit(v_delta, Qt.Vertical) @@ -736,10 +723,6 @@ def restore_cursor(self): QApplication.restoreOverrideCursor() def reset_state(self): - self.de_select_shape() - self.un_highlight() - self.selected_shape_copy = None - self.restore_cursor() self.pixmap = None self.update()