123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124 |
- # -*- coding: utf-8 -*-
- import functools
- import html
- import math
- import os
- import os.path as osp
- import re
- import webbrowser
- import imgviz
- import natsort
- from qtpy import QtCore
- from qtpy.QtCore import Qt
- from qtpy import QtGui
- from qtpy import QtWidgets
- from labelme import __appname__
- from labelme import PY2
- from . import ai
- from . import utils
- from labelme.config import get_config
- from labelme.label_file import LabelFile
- from labelme.label_file import LabelFileError
- from labelme.logger import logger
- from labelme.shape import Shape
- from labelme.widgets import BrightnessContrastDialog
- from labelme.widgets import Canvas
- from labelme.widgets import FileDialogPreview
- from labelme.widgets import LabelDialog
- from labelme.widgets import LabelListWidget
- from labelme.widgets import LabelListWidgetItem
- from labelme.widgets import ToolBar
- from labelme.widgets import UniqueLabelQListWidget
- from labelme.widgets import ZoomWidget
- # FIXME
- # - [medium] Set max zoom value to something big enough for FitWidth/Window
- # TODO(unknown):
- # - Zoom is too "steppy".
- LABEL_COLORMAP = imgviz.label_colormap()
- class MainWindow(QtWidgets.QMainWindow):
- FIT_WINDOW, FIT_WIDTH, MANUAL_ZOOM = 0, 1, 2
- def __init__(
- self,
- config=None,
- filename=None,
- output=None,
- output_file=None,
- output_dir=None,
- ):
- if output is not None:
- logger.warning(
- "argument output is deprecated, use output_file instead"
- )
- if output_file is None:
- output_file = output
- # see labelme/config/default_config.yaml for valid configuration
- if config is None:
- config = get_config()
- self._config = config
- # set default shape colors
- Shape.line_color = QtGui.QColor(*self._config["shape"]["line_color"])
- Shape.fill_color = QtGui.QColor(*self._config["shape"]["fill_color"])
- Shape.select_line_color = QtGui.QColor(
- *self._config["shape"]["select_line_color"]
- )
- Shape.select_fill_color = QtGui.QColor(
- *self._config["shape"]["select_fill_color"]
- )
- Shape.vertex_fill_color = QtGui.QColor(
- *self._config["shape"]["vertex_fill_color"]
- )
- Shape.hvertex_fill_color = QtGui.QColor(
- *self._config["shape"]["hvertex_fill_color"]
- )
- # Set point size from config file
- Shape.point_size = self._config["shape"]["point_size"]
- super(MainWindow, self).__init__()
- self.setWindowTitle(__appname__)
- # Whether we need to save or not.
- self.dirty = False
- self._noSelectionSlot = False
- self._copied_shapes = None
- # Main widgets and related state.
- self.labelDialog = LabelDialog(
- parent=self,
- labels=self._config["labels"],
- sort_labels=self._config["sort_labels"],
- show_text_field=self._config["show_label_text_field"],
- completion=self._config["label_completion"],
- fit_to_content=self._config["fit_to_content"],
- flags=self._config["label_flags"],
- )
- self.labelList = LabelListWidget()
- self.lastOpenDir = None
- self.flag_dock = self.flag_widget = None
- self.flag_dock = QtWidgets.QDockWidget(self.tr("Flags"), self)
- self.flag_dock.setObjectName("Flags")
- self.flag_widget = QtWidgets.QListWidget()
- if config["flags"]:
- self.loadFlags({k: False for k in config["flags"]})
- self.flag_dock.setWidget(self.flag_widget)
- self.flag_widget.itemChanged.connect(self.setDirty)
- self.labelList.itemSelectionChanged.connect(self.labelSelectionChanged)
- self.labelList.itemDoubleClicked.connect(self.editLabel)
- self.labelList.itemChanged.connect(self.labelItemChanged)
- self.labelList.itemDropped.connect(self.labelOrderChanged)
- self.shape_dock = QtWidgets.QDockWidget(
- self.tr("Polygon Labels"), self
- )
- self.shape_dock.setObjectName("Labels")
- self.shape_dock.setWidget(self.labelList)
- self.uniqLabelList = UniqueLabelQListWidget()
- self.uniqLabelList.setToolTip(
- self.tr(
- "Select label to start annotating for it. "
- "Press 'Esc' to deselect."
- )
- )
- if self._config["labels"]:
- for label in self._config["labels"]:
- item = self.uniqLabelList.createItemFromLabel(label)
- self.uniqLabelList.addItem(item)
- rgb = self._get_rgb_by_label(label)
- self.uniqLabelList.setItemLabel(item, label, rgb)
- self.label_dock = QtWidgets.QDockWidget(self.tr("Label List"), self)
- self.label_dock.setObjectName("Label List")
- self.label_dock.setWidget(self.uniqLabelList)
- self.fileSearch = QtWidgets.QLineEdit()
- self.fileSearch.setPlaceholderText(self.tr("Search Filename"))
- self.fileSearch.textChanged.connect(self.fileSearchChanged)
- self.fileListWidget = QtWidgets.QListWidget()
- self.fileListWidget.itemSelectionChanged.connect(
- self.fileSelectionChanged
- )
- fileListLayout = QtWidgets.QVBoxLayout()
- fileListLayout.setContentsMargins(0, 0, 0, 0)
- fileListLayout.setSpacing(0)
- fileListLayout.addWidget(self.fileSearch)
- fileListLayout.addWidget(self.fileListWidget)
- self.file_dock = QtWidgets.QDockWidget(self.tr("File List"), self)
- self.file_dock.setObjectName("Files")
- fileListWidget = QtWidgets.QWidget()
- fileListWidget.setLayout(fileListLayout)
- self.file_dock.setWidget(fileListWidget)
- self.zoomWidget = ZoomWidget()
- self.setAcceptDrops(True)
- self.canvas = self.labelList.canvas = Canvas(
- epsilon=self._config["epsilon"],
- double_click=self._config["canvas"]["double_click"],
- num_backups=self._config["canvas"]["num_backups"],
- crosshair=self._config["canvas"]["crosshair"],
- )
- self.canvas.zoomRequest.connect(self.zoomRequest)
- scrollArea = QtWidgets.QScrollArea()
- scrollArea.setWidget(self.canvas)
- scrollArea.setWidgetResizable(True)
- self.scrollBars = {
- Qt.Vertical: scrollArea.verticalScrollBar(),
- Qt.Horizontal: scrollArea.horizontalScrollBar(),
- }
- self.canvas.scrollRequest.connect(self.scrollRequest)
- self.canvas.newShape.connect(self.newShape)
- self.canvas.shapeMoved.connect(self.setDirty)
- self.canvas.selectionChanged.connect(self.shapeSelectionChanged)
- self.canvas.drawingPolygon.connect(self.toggleDrawingSensitive)
- self.setCentralWidget(scrollArea)
- features = QtWidgets.QDockWidget.DockWidgetFeatures()
- for dock in ["flag_dock", "label_dock", "shape_dock", "file_dock"]:
- if self._config[dock]["closable"]:
- features = features | QtWidgets.QDockWidget.DockWidgetClosable
- if self._config[dock]["floatable"]:
- features = features | QtWidgets.QDockWidget.DockWidgetFloatable
- if self._config[dock]["movable"]:
- features = features | QtWidgets.QDockWidget.DockWidgetMovable
- getattr(self, dock).setFeatures(features)
- if self._config[dock]["show"] is False:
- getattr(self, dock).setVisible(False)
- self.addDockWidget(Qt.RightDockWidgetArea, self.flag_dock)
- self.addDockWidget(Qt.RightDockWidgetArea, self.label_dock)
- self.addDockWidget(Qt.RightDockWidgetArea, self.shape_dock)
- self.addDockWidget(Qt.RightDockWidgetArea, self.file_dock)
- # Actions
- action = functools.partial(utils.newAction, self)
- shortcuts = self._config["shortcuts"]
- quit = action(
- self.tr("&Quit"),
- self.close,
- shortcuts["quit"],
- "quit",
- self.tr("Quit application"),
- )
- open_ = action(
- self.tr("&Open"),
- self.openFile,
- shortcuts["open"],
- "open",
- self.tr("Open image or label file"),
- )
- opendir = action(
- self.tr("&Open Dir"),
- self.openDirDialog,
- shortcuts["open_dir"],
- "open",
- self.tr("Open Dir"),
- )
- openNextImg = action(
- self.tr("&Next Image"),
- self.openNextImg,
- shortcuts["open_next"],
- "next",
- self.tr("Open next (hold Ctl+Shift to copy labels)"),
- enabled=False,
- )
- openPrevImg = action(
- self.tr("&Prev Image"),
- self.openPrevImg,
- shortcuts["open_prev"],
- "prev",
- self.tr("Open prev (hold Ctl+Shift to copy labels)"),
- enabled=False,
- )
- save = action(
- self.tr("&Save"),
- self.saveFile,
- shortcuts["save"],
- "save",
- self.tr("Save labels to file"),
- enabled=False,
- )
- saveAs = action(
- self.tr("&Save As"),
- self.saveFileAs,
- shortcuts["save_as"],
- "save-as",
- self.tr("Save labels to a different file"),
- enabled=False,
- )
- deleteFile = action(
- self.tr("&Delete File"),
- self.deleteFile,
- shortcuts["delete_file"],
- "delete",
- self.tr("Delete current label file"),
- enabled=False,
- )
- changeOutputDir = action(
- self.tr("&Change Output Dir"),
- slot=self.changeOutputDirDialog,
- shortcut=shortcuts["save_to"],
- icon="open",
- tip=self.tr("Change where annotations are loaded/saved"),
- )
- saveAuto = action(
- text=self.tr("Save &Automatically"),
- slot=lambda x: self.actions.saveAuto.setChecked(x),
- icon="save",
- tip=self.tr("Save automatically"),
- checkable=True,
- enabled=True,
- )
- saveAuto.setChecked(self._config["auto_save"])
- saveWithImageData = action(
- text="Save With Image Data",
- slot=self.enableSaveImageWithData,
- tip="Save image data in label file",
- checkable=True,
- checked=self._config["store_data"],
- )
- close = action(
- "&Close",
- self.closeFile,
- shortcuts["close"],
- "close",
- "Close current file",
- )
- toggle_keep_prev_mode = action(
- self.tr("Keep Previous Annotation"),
- self.toggleKeepPrevMode,
- shortcuts["toggle_keep_prev_mode"],
- None,
- self.tr('Toggle "keep pevious annotation" mode'),
- checkable=True,
- )
- toggle_keep_prev_mode.setChecked(self._config["keep_prev"])
- createMode = action(
- self.tr("Create Polygons"),
- lambda: self.toggleDrawMode(False, createMode="polygon"),
- shortcuts["create_polygon"],
- "objects",
- self.tr("Start drawing polygons"),
- enabled=False,
- )
- createRectangleMode = action(
- self.tr("Create Rectangle"),
- lambda: self.toggleDrawMode(False, createMode="rectangle"),
- shortcuts["create_rectangle"],
- "objects",
- self.tr("Start drawing rectangles"),
- enabled=False,
- )
- createCircleMode = action(
- self.tr("Create Circle"),
- lambda: self.toggleDrawMode(False, createMode="circle"),
- shortcuts["create_circle"],
- "objects",
- self.tr("Start drawing circles"),
- enabled=False,
- )
- createLineMode = action(
- self.tr("Create Line"),
- lambda: self.toggleDrawMode(False, createMode="line"),
- shortcuts["create_line"],
- "objects",
- self.tr("Start drawing lines"),
- enabled=False,
- )
- createPointMode = action(
- self.tr("Create Point"),
- lambda: self.toggleDrawMode(False, createMode="point"),
- shortcuts["create_point"],
- "objects",
- self.tr("Start drawing points"),
- enabled=False,
- )
- createLineStripMode = action(
- self.tr("Create LineStrip"),
- lambda: self.toggleDrawMode(False, createMode="linestrip"),
- shortcuts["create_linestrip"],
- "objects",
- self.tr("Start drawing linestrip. Ctrl+LeftClick ends creation."),
- enabled=False,
- )
- createAiPolygonMode = action(
- self.tr("Create AI-Polygon"),
- lambda: self.toggleDrawMode(False, createMode="ai_polygon"),
- None,
- "objects",
- self.tr("Start drawing ai_polygon. Ctrl+LeftClick ends creation."),
- enabled=False,
- )
- editMode = action(
- self.tr("Edit Polygons"),
- self.setEditMode,
- shortcuts["edit_polygon"],
- "edit",
- self.tr("Move and edit the selected polygons"),
- enabled=False,
- )
- delete = action(
- self.tr("Delete Polygons"),
- self.deleteSelectedShape,
- shortcuts["delete_polygon"],
- "cancel",
- self.tr("Delete the selected polygons"),
- enabled=False,
- )
- duplicate = action(
- self.tr("Duplicate Polygons"),
- self.duplicateSelectedShape,
- shortcuts["duplicate_polygon"],
- "copy",
- self.tr("Create a duplicate of the selected polygons"),
- enabled=False,
- )
- copy = action(
- self.tr("Copy Polygons"),
- self.copySelectedShape,
- shortcuts["copy_polygon"],
- "copy_clipboard",
- self.tr("Copy selected polygons to clipboard"),
- enabled=False,
- )
- paste = action(
- self.tr("Paste Polygons"),
- self.pasteSelectedShape,
- shortcuts["paste_polygon"],
- "paste",
- self.tr("Paste copied polygons"),
- enabled=False,
- )
- undoLastPoint = action(
- self.tr("Undo last point"),
- self.canvas.undoLastPoint,
- shortcuts["undo_last_point"],
- "undo",
- self.tr("Undo last drawn point"),
- enabled=False,
- )
- removePoint = action(
- text="Remove Selected Point",
- slot=self.removeSelectedPoint,
- shortcut=shortcuts["remove_selected_point"],
- icon="edit",
- tip="Remove selected point from polygon",
- enabled=False,
- )
- undo = action(
- self.tr("Undo"),
- self.undoShapeEdit,
- shortcuts["undo"],
- "undo",
- self.tr("Undo last add and edit of shape"),
- enabled=False,
- )
- hideAll = action(
- self.tr("&Hide\nPolygons"),
- functools.partial(self.togglePolygons, False),
- icon="eye",
- tip=self.tr("Hide all polygons"),
- enabled=False,
- )
- showAll = action(
- self.tr("&Show\nPolygons"),
- functools.partial(self.togglePolygons, True),
- icon="eye",
- tip=self.tr("Show all polygons"),
- enabled=False,
- )
- help = action(
- self.tr("&Tutorial"),
- self.tutorial,
- icon="help",
- tip=self.tr("Show tutorial page"),
- )
- zoom = QtWidgets.QWidgetAction(self)
- zoom.setDefaultWidget(self.zoomWidget)
- self.zoomWidget.setWhatsThis(
- str(
- self.tr(
- "Zoom in or out of the image. Also accessible with "
- "{} and {} from the canvas."
- )
- ).format(
- utils.fmtShortcut(
- "{},{}".format(shortcuts["zoom_in"], shortcuts["zoom_out"])
- ),
- utils.fmtShortcut(self.tr("Ctrl+Wheel")),
- )
- )
- self.zoomWidget.setEnabled(False)
- zoomIn = action(
- self.tr("Zoom &In"),
- functools.partial(self.addZoom, 1.1),
- shortcuts["zoom_in"],
- "zoom-in",
- self.tr("Increase zoom level"),
- enabled=False,
- )
- zoomOut = action(
- self.tr("&Zoom Out"),
- functools.partial(self.addZoom, 0.9),
- shortcuts["zoom_out"],
- "zoom-out",
- self.tr("Decrease zoom level"),
- enabled=False,
- )
- zoomOrg = action(
- self.tr("&Original size"),
- functools.partial(self.setZoom, 100),
- shortcuts["zoom_to_original"],
- "zoom",
- self.tr("Zoom to original size"),
- enabled=False,
- )
- keepPrevScale = action(
- self.tr("&Keep Previous Scale"),
- self.enableKeepPrevScale,
- tip=self.tr("Keep previous zoom scale"),
- checkable=True,
- checked=self._config["keep_prev_scale"],
- enabled=True,
- )
- fitWindow = action(
- self.tr("&Fit Window"),
- self.setFitWindow,
- shortcuts["fit_window"],
- "fit-window",
- self.tr("Zoom follows window size"),
- checkable=True,
- enabled=False,
- )
- fitWidth = action(
- self.tr("Fit &Width"),
- self.setFitWidth,
- shortcuts["fit_width"],
- "fit-width",
- self.tr("Zoom follows window width"),
- checkable=True,
- enabled=False,
- )
- brightnessContrast = action(
- "&Brightness Contrast",
- self.brightnessContrast,
- None,
- "color",
- "Adjust brightness and contrast",
- enabled=False,
- )
- # Group zoom controls into a list for easier toggling.
- zoomActions = (
- self.zoomWidget,
- zoomIn,
- zoomOut,
- zoomOrg,
- fitWindow,
- fitWidth,
- )
- self.zoomMode = self.FIT_WINDOW
- fitWindow.setChecked(Qt.Checked)
- self.scalers = {
- self.FIT_WINDOW: self.scaleFitWindow,
- self.FIT_WIDTH: self.scaleFitWidth,
- # Set to one to scale to 100% when loading files.
- self.MANUAL_ZOOM: lambda: 1,
- }
- edit = action(
- self.tr("&Edit Label"),
- self.editLabel,
- shortcuts["edit_label"],
- "edit",
- self.tr("Modify the label of the selected polygon"),
- enabled=False,
- )
- fill_drawing = action(
- self.tr("Fill Drawing Polygon"),
- self.canvas.setFillDrawing,
- None,
- "color",
- self.tr("Fill polygon while drawing"),
- checkable=True,
- enabled=True,
- )
- fill_drawing.trigger()
- # Lavel list context menu.
- labelMenu = QtWidgets.QMenu()
- utils.addActions(labelMenu, (edit, delete))
- self.labelList.setContextMenuPolicy(Qt.CustomContextMenu)
- self.labelList.customContextMenuRequested.connect(
- self.popLabelListMenu
- )
- # Store actions for further handling.
- self.actions = utils.struct(
- saveAuto=saveAuto,
- saveWithImageData=saveWithImageData,
- changeOutputDir=changeOutputDir,
- save=save,
- saveAs=saveAs,
- open=open_,
- close=close,
- deleteFile=deleteFile,
- toggleKeepPrevMode=toggle_keep_prev_mode,
- delete=delete,
- edit=edit,
- duplicate=duplicate,
- copy=copy,
- paste=paste,
- undoLastPoint=undoLastPoint,
- undo=undo,
- removePoint=removePoint,
- createMode=createMode,
- editMode=editMode,
- createRectangleMode=createRectangleMode,
- createCircleMode=createCircleMode,
- createLineMode=createLineMode,
- createPointMode=createPointMode,
- createLineStripMode=createLineStripMode,
- createAiPolygonMode=createAiPolygonMode,
- zoom=zoom,
- zoomIn=zoomIn,
- zoomOut=zoomOut,
- zoomOrg=zoomOrg,
- keepPrevScale=keepPrevScale,
- fitWindow=fitWindow,
- fitWidth=fitWidth,
- brightnessContrast=brightnessContrast,
- zoomActions=zoomActions,
- openNextImg=openNextImg,
- openPrevImg=openPrevImg,
- fileMenuActions=(open_, opendir, save, saveAs, close, quit),
- tool=(),
- # XXX: need to add some actions here to activate the shortcut
- editMenu=(
- edit,
- duplicate,
- delete,
- None,
- undo,
- undoLastPoint,
- None,
- removePoint,
- None,
- toggle_keep_prev_mode,
- ),
- # menu shown at right click
- menu=(
- createMode,
- createRectangleMode,
- createCircleMode,
- createLineMode,
- createPointMode,
- createLineStripMode,
- createAiPolygonMode,
- editMode,
- edit,
- duplicate,
- copy,
- paste,
- delete,
- undo,
- undoLastPoint,
- removePoint,
- ),
- onLoadActive=(
- close,
- createMode,
- createRectangleMode,
- createCircleMode,
- createLineMode,
- createPointMode,
- createLineStripMode,
- createAiPolygonMode,
- editMode,
- brightnessContrast,
- ),
- onShapesPresent=(saveAs, hideAll, showAll),
- )
- self.canvas.vertexSelected.connect(self.actions.removePoint.setEnabled)
- self.menus = utils.struct(
- file=self.menu(self.tr("&File")),
- edit=self.menu(self.tr("&Edit")),
- view=self.menu(self.tr("&View")),
- help=self.menu(self.tr("&Help")),
- recentFiles=QtWidgets.QMenu(self.tr("Open &Recent")),
- labelList=labelMenu,
- )
- utils.addActions(
- self.menus.file,
- (
- open_,
- openNextImg,
- openPrevImg,
- opendir,
- self.menus.recentFiles,
- save,
- saveAs,
- saveAuto,
- changeOutputDir,
- saveWithImageData,
- close,
- deleteFile,
- None,
- quit,
- ),
- )
- utils.addActions(self.menus.help, (help,))
- utils.addActions(
- self.menus.view,
- (
- self.flag_dock.toggleViewAction(),
- self.label_dock.toggleViewAction(),
- self.shape_dock.toggleViewAction(),
- self.file_dock.toggleViewAction(),
- None,
- fill_drawing,
- None,
- hideAll,
- showAll,
- None,
- zoomIn,
- zoomOut,
- zoomOrg,
- keepPrevScale,
- None,
- fitWindow,
- fitWidth,
- None,
- brightnessContrast,
- ),
- )
- self.menus.file.aboutToShow.connect(self.updateFileMenu)
- # Custom context menu for the canvas widget:
- utils.addActions(self.canvas.menus[0], self.actions.menu)
- utils.addActions(
- self.canvas.menus[1],
- (
- action("&Copy here", self.copyShape),
- action("&Move here", self.moveShape),
- ),
- )
- self.tools = self.toolbar("Tools")
- # Menu buttons on Left
- self.actions.tool = (
- open_,
- opendir,
- openNextImg,
- openPrevImg,
- save,
- deleteFile,
- None,
- createMode,
- editMode,
- duplicate,
- copy,
- paste,
- delete,
- undo,
- brightnessContrast,
- None,
- zoom,
- fitWidth,
- )
- self.statusBar().showMessage(str(self.tr("%s started.")) % __appname__)
- self.statusBar().show()
- if output_file is not None and self._config["auto_save"]:
- logger.warn(
- "If `auto_save` argument is True, `output_file` argument "
- "is ignored and output filename is automatically "
- "set as IMAGE_BASENAME.json."
- )
- self.output_file = output_file
- self.output_dir = output_dir
- # Application state.
- self.image = QtGui.QImage()
- self.imagePath = None
- self.recentFiles = []
- self.maxRecent = 7
- self.otherData = None
- self.zoom_level = 100
- self.fit_window = False
- self.zoom_values = {} # key=filename, value=(zoom_mode, zoom_value)
- self.brightnessContrast_values = {}
- self.scroll_values = {
- Qt.Horizontal: {},
- Qt.Vertical: {},
- } # key=filename, value=scroll_value
- if filename is not None and osp.isdir(filename):
- self.importDirImages(filename, load=False)
- else:
- self.filename = filename
- if config["file_search"]:
- self.fileSearch.setText(config["file_search"])
- self.fileSearchChanged()
- # XXX: Could be completely declarative.
- # Restore application settings.
- self.settings = QtCore.QSettings("labelme", "labelme")
- self.recentFiles = self.settings.value("recentFiles", []) or []
- size = self.settings.value("window/size", QtCore.QSize(600, 500))
- position = self.settings.value("window/position", QtCore.QPoint(0, 0))
- state = self.settings.value("window/state", QtCore.QByteArray())
- self.resize(size)
- self.move(position)
- # or simply:
- # self.restoreGeometry(settings['window/geometry']
- self.restoreState(state)
- # Populate the File menu dynamically.
- self.updateFileMenu()
- # Since loading the file may take some time,
- # make sure it runs in the background.
- if self.filename is not None:
- self.queueEvent(functools.partial(self.loadFile, self.filename))
- # Callbacks:
- self.zoomWidget.valueChanged.connect(self.paintCanvas)
- self.populateModeActions()
- # self.firstStart = True
- # if self.firstStart:
- # QWhatsThis.enterWhatsThisMode()
- @property
- def _ai_model(self):
- if not hasattr(self, "_ai_model_initialized"):
- self._ai_model_initialized = ai.SegmentAnythingModel()
- return self._ai_model_initialized
- def menu(self, title, actions=None):
- menu = self.menuBar().addMenu(title)
- if actions:
- utils.addActions(menu, actions)
- return menu
- def toolbar(self, title, actions=None):
- toolbar = ToolBar(title)
- toolbar.setObjectName("%sToolBar" % title)
- # toolbar.setOrientation(Qt.Vertical)
- toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
- if actions:
- utils.addActions(toolbar, actions)
- self.addToolBar(Qt.LeftToolBarArea, toolbar)
- return toolbar
- # Support Functions
- def noShapes(self):
- return not len(self.labelList)
- def populateModeActions(self):
- tool, menu = self.actions.tool, self.actions.menu
- self.tools.clear()
- utils.addActions(self.tools, tool)
- self.canvas.menus[0].clear()
- utils.addActions(self.canvas.menus[0], menu)
- self.menus.edit.clear()
- actions = (
- self.actions.createMode,
- self.actions.createRectangleMode,
- self.actions.createCircleMode,
- self.actions.createLineMode,
- self.actions.createPointMode,
- self.actions.createLineStripMode,
- self.actions.createAiPolygonMode,
- self.actions.editMode,
- )
- utils.addActions(self.menus.edit, actions + self.actions.editMenu)
- def setDirty(self):
- # Even if we autosave the file, we keep the ability to undo
- self.actions.undo.setEnabled(self.canvas.isShapeRestorable)
- if self._config["auto_save"] or self.actions.saveAuto.isChecked():
- label_file = osp.splitext(self.imagePath)[0] + ".json"
- if self.output_dir:
- label_file_without_path = osp.basename(label_file)
- label_file = osp.join(self.output_dir, label_file_without_path)
- self.saveLabels(label_file)
- return
- self.dirty = True
- self.actions.save.setEnabled(True)
- title = __appname__
- if self.filename is not None:
- title = "{} - {}*".format(title, self.filename)
- self.setWindowTitle(title)
- def setClean(self):
- self.dirty = False
- self.actions.save.setEnabled(False)
- self.actions.createMode.setEnabled(True)
- self.actions.createRectangleMode.setEnabled(True)
- self.actions.createCircleMode.setEnabled(True)
- self.actions.createLineMode.setEnabled(True)
- self.actions.createPointMode.setEnabled(True)
- self.actions.createLineStripMode.setEnabled(True)
- self.actions.createAiPolygonMode.setEnabled(True)
- title = __appname__
- if self.filename is not None:
- title = "{} - {}".format(title, self.filename)
- self.setWindowTitle(title)
- if self.hasLabelFile():
- self.actions.deleteFile.setEnabled(True)
- else:
- self.actions.deleteFile.setEnabled(False)
- def toggleActions(self, value=True):
- """Enable/Disable widgets which depend on an opened image."""
- for z in self.actions.zoomActions:
- z.setEnabled(value)
- for action in self.actions.onLoadActive:
- action.setEnabled(value)
- def queueEvent(self, function):
- QtCore.QTimer.singleShot(0, function)
- def status(self, message, delay=5000):
- self.statusBar().showMessage(message, delay)
- def resetState(self):
- self.labelList.clear()
- self.filename = None
- self.imagePath = None
- self.imageData = None
- self.labelFile = None
- self.otherData = None
- self.canvas.resetState()
- def currentItem(self):
- items = self.labelList.selectedItems()
- if items:
- return items[0]
- return None
- def addRecentFile(self, filename):
- if filename in self.recentFiles:
- self.recentFiles.remove(filename)
- elif len(self.recentFiles) >= self.maxRecent:
- self.recentFiles.pop()
- self.recentFiles.insert(0, filename)
- # Callbacks
- def undoShapeEdit(self):
- self.canvas.restoreShape()
- self.labelList.clear()
- self.loadShapes(self.canvas.shapes)
- self.actions.undo.setEnabled(self.canvas.isShapeRestorable)
- def tutorial(self):
- url = "https://github.com/wkentaro/labelme/tree/main/examples/tutorial" # NOQA
- webbrowser.open(url)
- def toggleDrawingSensitive(self, drawing=True):
- """Toggle drawing sensitive.
- In the middle of drawing, toggling between modes should be disabled.
- """
- self.actions.editMode.setEnabled(not drawing)
- self.actions.undoLastPoint.setEnabled(drawing)
- self.actions.undo.setEnabled(not drawing)
- self.actions.delete.setEnabled(not drawing)
- def toggleDrawMode(self, edit=True, createMode="polygon"):
- self.canvas.setEditing(edit)
- self.canvas.createMode = createMode
- if createMode == "ai_polygon":
- self._ai_model.set_image(utils.img_data_to_arr(self.imageData))
- self.canvas.setAiCallback(
- self._ai_model.points_to_polygon_callback
- )
- else:
- self.canvas.setAiCallback(None)
- if edit:
- self.actions.createMode.setEnabled(True)
- self.actions.createRectangleMode.setEnabled(True)
- self.actions.createCircleMode.setEnabled(True)
- self.actions.createLineMode.setEnabled(True)
- self.actions.createPointMode.setEnabled(True)
- self.actions.createLineStripMode.setEnabled(True)
- self.actions.createAiPolygonMode.setEnabled(True)
- else:
- if createMode == "polygon":
- self.actions.createMode.setEnabled(False)
- self.actions.createRectangleMode.setEnabled(True)
- self.actions.createCircleMode.setEnabled(True)
- self.actions.createLineMode.setEnabled(True)
- self.actions.createPointMode.setEnabled(True)
- self.actions.createLineStripMode.setEnabled(True)
- self.actions.createAiPolygonMode.setEnabled(True)
- elif createMode == "rectangle":
- self.actions.createMode.setEnabled(True)
- self.actions.createRectangleMode.setEnabled(False)
- self.actions.createCircleMode.setEnabled(True)
- self.actions.createLineMode.setEnabled(True)
- self.actions.createPointMode.setEnabled(True)
- self.actions.createLineStripMode.setEnabled(True)
- self.actions.createAiPolygonMode.setEnabled(True)
- elif createMode == "line":
- self.actions.createMode.setEnabled(True)
- self.actions.createRectangleMode.setEnabled(True)
- self.actions.createCircleMode.setEnabled(True)
- self.actions.createLineMode.setEnabled(False)
- self.actions.createPointMode.setEnabled(True)
- self.actions.createLineStripMode.setEnabled(True)
- self.actions.createAiPolygonMode.setEnabled(True)
- elif createMode == "point":
- self.actions.createMode.setEnabled(True)
- self.actions.createRectangleMode.setEnabled(True)
- self.actions.createCircleMode.setEnabled(True)
- self.actions.createLineMode.setEnabled(True)
- self.actions.createPointMode.setEnabled(False)
- self.actions.createLineStripMode.setEnabled(True)
- self.actions.createAiPolygonMode.setEnabled(True)
- elif createMode == "circle":
- self.actions.createMode.setEnabled(True)
- self.actions.createRectangleMode.setEnabled(True)
- self.actions.createCircleMode.setEnabled(False)
- self.actions.createLineMode.setEnabled(True)
- self.actions.createPointMode.setEnabled(True)
- self.actions.createLineStripMode.setEnabled(True)
- self.actions.createAiPolygonMode.setEnabled(True)
- elif createMode == "linestrip":
- self.actions.createMode.setEnabled(True)
- self.actions.createRectangleMode.setEnabled(True)
- self.actions.createCircleMode.setEnabled(True)
- self.actions.createLineMode.setEnabled(True)
- self.actions.createPointMode.setEnabled(True)
- self.actions.createLineStripMode.setEnabled(False)
- self.actions.createAiPolygonMode.setEnabled(True)
- elif createMode == "ai_polygon":
- self.actions.createMode.setEnabled(True)
- self.actions.createRectangleMode.setEnabled(True)
- self.actions.createCircleMode.setEnabled(True)
- self.actions.createLineMode.setEnabled(True)
- self.actions.createPointMode.setEnabled(True)
- self.actions.createLineStripMode.setEnabled(True)
- self.actions.createAiPolygonMode.setEnabled(False)
- else:
- raise ValueError("Unsupported createMode: %s" % createMode)
- self.actions.editMode.setEnabled(not edit)
- def setEditMode(self):
- self.toggleDrawMode(True)
- def updateFileMenu(self):
- current = self.filename
- def exists(filename):
- return osp.exists(str(filename))
- menu = self.menus.recentFiles
- menu.clear()
- files = [f for f in self.recentFiles if f != current and exists(f)]
- for i, f in enumerate(files):
- icon = utils.newIcon("labels")
- action = QtWidgets.QAction(
- icon, "&%d %s" % (i + 1, QtCore.QFileInfo(f).fileName()), self
- )
- action.triggered.connect(functools.partial(self.loadRecent, f))
- menu.addAction(action)
- def popLabelListMenu(self, point):
- self.menus.labelList.exec_(self.labelList.mapToGlobal(point))
- def validateLabel(self, label):
- # no validation
- if self._config["validate_label"] is None:
- return True
- for i in range(self.uniqLabelList.count()):
- label_i = self.uniqLabelList.item(i).data(Qt.UserRole)
- if self._config["validate_label"] in ["exact"]:
- if label_i == label:
- return True
- return False
- def editLabel(self, item=None):
- if item and not isinstance(item, LabelListWidgetItem):
- raise TypeError("item must be LabelListWidgetItem type")
- if not self.canvas.editing():
- return
- if not item:
- item = self.currentItem()
- if item is None:
- return
- shape = item.shape()
- if shape is None:
- return
- text, flags, group_id, description = self.labelDialog.popUp(
- text=shape.label,
- flags=shape.flags,
- group_id=shape.group_id,
- description=shape.description,
- )
- if text is None:
- return
- if not self.validateLabel(text):
- self.errorMessage(
- self.tr("Invalid label"),
- self.tr("Invalid label '{}' with validation type '{}'").format(
- text, self._config["validate_label"]
- ),
- )
- return
- shape.label = text
- shape.flags = flags
- shape.group_id = group_id
- shape.description = description
- self._update_shape_color(shape)
- if shape.group_id is None:
- item.setText(
- '{} <font color="#{:02x}{:02x}{:02x}">●</font>'.format(
- html.escape(shape.label), *shape.fill_color.getRgb()[:3]
- )
- )
- else:
- item.setText("{} ({})".format(shape.label, shape.group_id))
- self.setDirty()
- if self.uniqLabelList.findItemByLabel(shape.label) is None:
- item = self.uniqLabelList.createItemFromLabel(shape.label)
- self.uniqLabelList.addItem(item)
- rgb = self._get_rgb_by_label(shape.label)
- self.uniqLabelList.setItemLabel(item, shape.label, rgb)
- def fileSearchChanged(self):
- self.importDirImages(
- self.lastOpenDir,
- pattern=self.fileSearch.text(),
- load=False,
- )
- def fileSelectionChanged(self):
- items = self.fileListWidget.selectedItems()
- if not items:
- return
- item = items[0]
- if not self.mayContinue():
- return
- currIndex = self.imageList.index(str(item.text()))
- if currIndex < len(self.imageList):
- filename = self.imageList[currIndex]
- if filename:
- self.loadFile(filename)
- # React to canvas signals.
- def shapeSelectionChanged(self, selected_shapes):
- self._noSelectionSlot = True
- for shape in self.canvas.selectedShapes:
- shape.selected = False
- self.labelList.clearSelection()
- self.canvas.selectedShapes = selected_shapes
- for shape in self.canvas.selectedShapes:
- shape.selected = True
- item = self.labelList.findItemByShape(shape)
- self.labelList.selectItem(item)
- self.labelList.scrollToItem(item)
- self._noSelectionSlot = False
- n_selected = len(selected_shapes)
- self.actions.delete.setEnabled(n_selected)
- self.actions.duplicate.setEnabled(n_selected)
- self.actions.copy.setEnabled(n_selected)
- self.actions.edit.setEnabled(n_selected == 1)
- def addLabel(self, shape):
- if shape.group_id is None:
- text = shape.label
- else:
- text = "{} ({})".format(shape.label, shape.group_id)
- label_list_item = LabelListWidgetItem(text, shape)
- self.labelList.addItem(label_list_item)
- if self.uniqLabelList.findItemByLabel(shape.label) is None:
- item = self.uniqLabelList.createItemFromLabel(shape.label)
- self.uniqLabelList.addItem(item)
- rgb = self._get_rgb_by_label(shape.label)
- self.uniqLabelList.setItemLabel(item, shape.label, rgb)
- self.labelDialog.addLabelHistory(shape.label)
- for action in self.actions.onShapesPresent:
- action.setEnabled(True)
- self._update_shape_color(shape)
- label_list_item.setText(
- '{} <font color="#{:02x}{:02x}{:02x}">●</font>'.format(
- html.escape(text), *shape.fill_color.getRgb()[:3]
- )
- )
- def _update_shape_color(self, shape):
- r, g, b = self._get_rgb_by_label(shape.label)
- shape.line_color = QtGui.QColor(r, g, b)
- shape.vertex_fill_color = QtGui.QColor(r, g, b)
- shape.hvertex_fill_color = QtGui.QColor(255, 255, 255)
- shape.fill_color = QtGui.QColor(r, g, b, 128)
- shape.select_line_color = QtGui.QColor(255, 255, 255)
- shape.select_fill_color = QtGui.QColor(r, g, b, 155)
- def _get_rgb_by_label(self, label):
- if self._config["shape_color"] == "auto":
- item = self.uniqLabelList.findItemByLabel(label)
- if item is None:
- item = self.uniqLabelList.createItemFromLabel(label)
- self.uniqLabelList.addItem(item)
- rgb = self._get_rgb_by_label(label)
- self.uniqLabelList.setItemLabel(item, label, rgb)
- label_id = self.uniqLabelList.indexFromItem(item).row() + 1
- label_id += self._config["shift_auto_shape_color"]
- return LABEL_COLORMAP[label_id % len(LABEL_COLORMAP)]
- elif (
- self._config["shape_color"] == "manual"
- and self._config["label_colors"]
- and label in self._config["label_colors"]
- ):
- return self._config["label_colors"][label]
- elif self._config["default_shape_color"]:
- return self._config["default_shape_color"]
- return (0, 255, 0)
- def remLabels(self, shapes):
- for shape in shapes:
- item = self.labelList.findItemByShape(shape)
- self.labelList.removeItem(item)
- def loadShapes(self, shapes, replace=True):
- self._noSelectionSlot = True
- for shape in shapes:
- self.addLabel(shape)
- self.labelList.clearSelection()
- self._noSelectionSlot = False
- self.canvas.loadShapes(shapes, replace=replace)
- def loadLabels(self, shapes):
- s = []
- for shape in shapes:
- label = shape["label"]
- points = shape["points"]
- shape_type = shape["shape_type"]
- flags = shape["flags"]
- description = shape.get("description", "")
- group_id = shape["group_id"]
- other_data = shape["other_data"]
- if not points:
- # skip point-empty shape
- continue
- shape = Shape(
- label=label,
- shape_type=shape_type,
- group_id=group_id,
- description=description,
- )
- for x, y in points:
- shape.addPoint(QtCore.QPointF(x, y))
- shape.close()
- default_flags = {}
- if self._config["label_flags"]:
- for pattern, keys in self._config["label_flags"].items():
- if re.match(pattern, label):
- for key in keys:
- default_flags[key] = False
- shape.flags = default_flags
- shape.flags.update(flags)
- shape.other_data = other_data
- s.append(shape)
- self.loadShapes(s)
- def loadFlags(self, flags):
- self.flag_widget.clear()
- for key, flag in flags.items():
- item = QtWidgets.QListWidgetItem(key)
- item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
- item.setCheckState(Qt.Checked if flag else Qt.Unchecked)
- self.flag_widget.addItem(item)
- def saveLabels(self, filename):
- lf = LabelFile()
- def format_shape(s):
- data = s.other_data.copy()
- data.update(
- dict(
- label=s.label.encode("utf-8") if PY2 else s.label,
- points=[(p.x(), p.y()) for p in s.points],
- group_id=s.group_id,
- description=s.description,
- shape_type=s.shape_type,
- flags=s.flags,
- )
- )
- return data
- shapes = [format_shape(item.shape()) for item in self.labelList]
- flags = {}
- for i in range(self.flag_widget.count()):
- item = self.flag_widget.item(i)
- key = item.text()
- flag = item.checkState() == Qt.Checked
- flags[key] = flag
- try:
- imagePath = osp.relpath(self.imagePath, osp.dirname(filename))
- imageData = self.imageData if self._config["store_data"] else None
- if osp.dirname(filename) and not osp.exists(osp.dirname(filename)):
- os.makedirs(osp.dirname(filename))
- lf.save(
- filename=filename,
- shapes=shapes,
- imagePath=imagePath,
- imageData=imageData,
- imageHeight=self.image.height(),
- imageWidth=self.image.width(),
- otherData=self.otherData,
- flags=flags,
- )
- self.labelFile = lf
- items = self.fileListWidget.findItems(
- self.imagePath, Qt.MatchExactly
- )
- if len(items) > 0:
- if len(items) != 1:
- raise RuntimeError("There are duplicate files.")
- items[0].setCheckState(Qt.Checked)
- # disable allows next and previous image to proceed
- # self.filename = filename
- return True
- except LabelFileError as e:
- self.errorMessage(
- self.tr("Error saving label data"), self.tr("<b>%s</b>") % e
- )
- return False
- def duplicateSelectedShape(self):
- added_shapes = self.canvas.duplicateSelectedShapes()
- self.labelList.clearSelection()
- for shape in added_shapes:
- self.addLabel(shape)
- self.setDirty()
- def pasteSelectedShape(self):
- self.loadShapes(self._copied_shapes, replace=False)
- self.setDirty()
- def copySelectedShape(self):
- self._copied_shapes = [s.copy() for s in self.canvas.selectedShapes]
- self.actions.paste.setEnabled(len(self._copied_shapes) > 0)
- def labelSelectionChanged(self):
- if self._noSelectionSlot:
- return
- if self.canvas.editing():
- selected_shapes = []
- for item in self.labelList.selectedItems():
- selected_shapes.append(item.shape())
- if selected_shapes:
- self.canvas.selectShapes(selected_shapes)
- else:
- self.canvas.deSelectShape()
- def labelItemChanged(self, item):
- shape = item.shape()
- self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked)
- def labelOrderChanged(self):
- self.setDirty()
- self.canvas.loadShapes([item.shape() for item in self.labelList])
- # Callback functions:
- def newShape(self):
- """Pop-up and give focus to the label editor.
- position MUST be in global coordinates.
- """
- items = self.uniqLabelList.selectedItems()
- text = None
- if items:
- text = items[0].data(Qt.UserRole)
- flags = {}
- group_id = None
- description = ""
- if self._config["display_label_popup"] or not text:
- previous_text = self.labelDialog.edit.text()
- text, flags, group_id, description = self.labelDialog.popUp(text)
- if not text:
- self.labelDialog.edit.setText(previous_text)
- if text and not self.validateLabel(text):
- self.errorMessage(
- self.tr("Invalid label"),
- self.tr("Invalid label '{}' with validation type '{}'").format(
- text, self._config["validate_label"]
- ),
- )
- text = ""
- if text:
- self.labelList.clearSelection()
- shape = self.canvas.setLastLabel(text, flags)
- shape.group_id = group_id
- shape.description = description
- self.addLabel(shape)
- self.actions.editMode.setEnabled(True)
- self.actions.undoLastPoint.setEnabled(False)
- self.actions.undo.setEnabled(True)
- self.setDirty()
- else:
- self.canvas.undoLastLine()
- self.canvas.shapesBackups.pop()
- def scrollRequest(self, delta, orientation):
- units = -delta * 0.1 # natural scroll
- bar = self.scrollBars[orientation]
- value = bar.value() + bar.singleStep() * units
- self.setScroll(orientation, value)
- def setScroll(self, orientation, value):
- self.scrollBars[orientation].setValue(int(value))
- self.scroll_values[orientation][self.filename] = value
- def setZoom(self, value):
- self.actions.fitWidth.setChecked(False)
- self.actions.fitWindow.setChecked(False)
- self.zoomMode = self.MANUAL_ZOOM
- self.zoomWidget.setValue(value)
- self.zoom_values[self.filename] = (self.zoomMode, value)
- def addZoom(self, increment=1.1):
- zoom_value = self.zoomWidget.value() * increment
- if increment > 1:
- zoom_value = math.ceil(zoom_value)
- else:
- zoom_value = math.floor(zoom_value)
- self.setZoom(zoom_value)
- def zoomRequest(self, delta, pos):
- canvas_width_old = self.canvas.width()
- units = 1.1
- if delta < 0:
- units = 0.9
- self.addZoom(units)
- canvas_width_new = self.canvas.width()
- if canvas_width_old != canvas_width_new:
- canvas_scale_factor = canvas_width_new / canvas_width_old
- x_shift = round(pos.x() * canvas_scale_factor) - pos.x()
- y_shift = round(pos.y() * canvas_scale_factor) - pos.y()
- self.setScroll(
- Qt.Horizontal,
- self.scrollBars[Qt.Horizontal].value() + x_shift,
- )
- self.setScroll(
- Qt.Vertical,
- self.scrollBars[Qt.Vertical].value() + y_shift,
- )
- def setFitWindow(self, value=True):
- if value:
- self.actions.fitWidth.setChecked(False)
- self.zoomMode = self.FIT_WINDOW if value else self.MANUAL_ZOOM
- self.adjustScale()
- def setFitWidth(self, value=True):
- if value:
- self.actions.fitWindow.setChecked(False)
- self.zoomMode = self.FIT_WIDTH if value else self.MANUAL_ZOOM
- self.adjustScale()
- def enableKeepPrevScale(self, enabled):
- self._config["keep_prev_scale"] = enabled
- self.actions.keepPrevScale.setChecked(enabled)
- def onNewBrightnessContrast(self, qimage):
- self.canvas.loadPixmap(
- QtGui.QPixmap.fromImage(qimage), clear_shapes=False
- )
- def brightnessContrast(self, value):
- dialog = BrightnessContrastDialog(
- utils.img_data_to_pil(self.imageData),
- self.onNewBrightnessContrast,
- parent=self,
- )
- brightness, contrast = self.brightnessContrast_values.get(
- self.filename, (None, None)
- )
- if brightness is not None:
- dialog.slider_brightness.setValue(brightness)
- if contrast is not None:
- dialog.slider_contrast.setValue(contrast)
- dialog.exec_()
- brightness = dialog.slider_brightness.value()
- contrast = dialog.slider_contrast.value()
- self.brightnessContrast_values[self.filename] = (brightness, contrast)
- def togglePolygons(self, value):
- for item in self.labelList:
- item.setCheckState(Qt.Checked if value else Qt.Unchecked)
- def loadFile(self, filename=None):
- """Load the specified file, or the last opened file if None."""
- # changing fileListWidget loads file
- if filename in self.imageList and (
- self.fileListWidget.currentRow() != self.imageList.index(filename)
- ):
- self.fileListWidget.setCurrentRow(self.imageList.index(filename))
- self.fileListWidget.repaint()
- return
- self.resetState()
- self.canvas.setEnabled(False)
- if filename is None:
- filename = self.settings.value("filename", "")
- filename = str(filename)
- if not QtCore.QFile.exists(filename):
- self.errorMessage(
- self.tr("Error opening file"),
- self.tr("No such file: <b>%s</b>") % filename,
- )
- return False
- # assumes same name, but json extension
- self.status(
- str(self.tr("Loading %s...")) % osp.basename(str(filename))
- )
- label_file = osp.splitext(filename)[0] + ".json"
- if self.output_dir:
- label_file_without_path = osp.basename(label_file)
- label_file = osp.join(self.output_dir, label_file_without_path)
- if QtCore.QFile.exists(label_file) and LabelFile.is_label_file(
- label_file
- ):
- try:
- self.labelFile = LabelFile(label_file)
- except LabelFileError as e:
- self.errorMessage(
- self.tr("Error opening file"),
- self.tr(
- "<p><b>%s</b></p>"
- "<p>Make sure <i>%s</i> is a valid label file."
- )
- % (e, label_file),
- )
- self.status(self.tr("Error reading %s") % label_file)
- return False
- self.imageData = self.labelFile.imageData
- self.imagePath = osp.join(
- osp.dirname(label_file),
- self.labelFile.imagePath,
- )
- self.otherData = self.labelFile.otherData
- else:
- self.imageData = LabelFile.load_image_file(filename)
- if self.imageData:
- self.imagePath = filename
- self.labelFile = None
- image = QtGui.QImage.fromData(self.imageData)
- if image.isNull():
- formats = [
- "*.{}".format(fmt.data().decode())
- for fmt in QtGui.QImageReader.supportedImageFormats()
- ]
- self.errorMessage(
- self.tr("Error opening file"),
- self.tr(
- "<p>Make sure <i>{0}</i> is a valid image file.<br/>"
- "Supported image formats: {1}</p>"
- ).format(filename, ",".join(formats)),
- )
- self.status(self.tr("Error reading %s") % filename)
- return False
- self.image = image
- self.filename = filename
- if self._config["keep_prev"]:
- prev_shapes = self.canvas.shapes
- self.canvas.loadPixmap(QtGui.QPixmap.fromImage(image))
- flags = {k: False for k in self._config["flags"] or []}
- if self.labelFile:
- self.loadLabels(self.labelFile.shapes)
- if self.labelFile.flags is not None:
- flags.update(self.labelFile.flags)
- self.loadFlags(flags)
- if self._config["keep_prev"] and self.noShapes():
- self.loadShapes(prev_shapes, replace=False)
- self.setDirty()
- else:
- self.setClean()
- self.canvas.setEnabled(True)
- # set zoom values
- is_initial_load = not self.zoom_values
- if self.filename in self.zoom_values:
- self.zoomMode = self.zoom_values[self.filename][0]
- self.setZoom(self.zoom_values[self.filename][1])
- elif is_initial_load or not self._config["keep_prev_scale"]:
- self.adjustScale(initial=True)
- # set scroll values
- for orientation in self.scroll_values:
- if self.filename in self.scroll_values[orientation]:
- self.setScroll(
- orientation, self.scroll_values[orientation][self.filename]
- )
- # set brightness contrast values
- dialog = BrightnessContrastDialog(
- utils.img_data_to_pil(self.imageData),
- self.onNewBrightnessContrast,
- parent=self,
- )
- brightness, contrast = self.brightnessContrast_values.get(
- self.filename, (None, None)
- )
- if self._config["keep_prev_brightness"] and self.recentFiles:
- brightness, _ = self.brightnessContrast_values.get(
- self.recentFiles[0], (None, None)
- )
- if self._config["keep_prev_contrast"] and self.recentFiles:
- _, contrast = self.brightnessContrast_values.get(
- self.recentFiles[0], (None, None)
- )
- if brightness is not None:
- dialog.slider_brightness.setValue(brightness)
- if contrast is not None:
- dialog.slider_contrast.setValue(contrast)
- self.brightnessContrast_values[self.filename] = (brightness, contrast)
- if brightness is not None or contrast is not None:
- dialog.onNewValue(None)
- self.paintCanvas()
- self.addRecentFile(self.filename)
- self.toggleActions(True)
- self.canvas.setFocus()
- self.status(str(self.tr("Loaded %s")) % osp.basename(str(filename)))
- return True
- def resizeEvent(self, event):
- if (
- self.canvas
- and not self.image.isNull()
- and self.zoomMode != self.MANUAL_ZOOM
- ):
- self.adjustScale()
- super(MainWindow, self).resizeEvent(event)
- def paintCanvas(self):
- assert not self.image.isNull(), "cannot paint null image"
- self.canvas.scale = 0.01 * self.zoomWidget.value()
- self.canvas.adjustSize()
- self.canvas.update()
- def adjustScale(self, initial=False):
- value = self.scalers[self.FIT_WINDOW if initial else self.zoomMode]()
- value = int(100 * value)
- self.zoomWidget.setValue(value)
- self.zoom_values[self.filename] = (self.zoomMode, value)
- def scaleFitWindow(self):
- """Figure out the size of the pixmap to fit the main widget."""
- e = 2.0 # So that no scrollbars are generated.
- w1 = self.centralWidget().width() - e
- h1 = self.centralWidget().height() - e
- a1 = w1 / h1
- # Calculate a new scale value based on the pixmap's aspect ratio.
- w2 = self.canvas.pixmap.width() - 0.0
- h2 = self.canvas.pixmap.height() - 0.0
- a2 = w2 / h2
- return w1 / w2 if a2 >= a1 else h1 / h2
- def scaleFitWidth(self):
- # The epsilon does not seem to work too well here.
- w = self.centralWidget().width() - 2.0
- return w / self.canvas.pixmap.width()
- def enableSaveImageWithData(self, enabled):
- self._config["store_data"] = enabled
- self.actions.saveWithImageData.setChecked(enabled)
- def closeEvent(self, event):
- if not self.mayContinue():
- event.ignore()
- self.settings.setValue(
- "filename", self.filename if self.filename else ""
- )
- self.settings.setValue("window/size", self.size())
- self.settings.setValue("window/position", self.pos())
- self.settings.setValue("window/state", self.saveState())
- self.settings.setValue("recentFiles", self.recentFiles)
- # ask the use for where to save the labels
- # self.settings.setValue('window/geometry', self.saveGeometry())
- def dragEnterEvent(self, event):
- extensions = [
- ".%s" % fmt.data().decode().lower()
- for fmt in QtGui.QImageReader.supportedImageFormats()
- ]
- if event.mimeData().hasUrls():
- items = [i.toLocalFile() for i in event.mimeData().urls()]
- if any([i.lower().endswith(tuple(extensions)) for i in items]):
- event.accept()
- else:
- event.ignore()
- def dropEvent(self, event):
- if not self.mayContinue():
- event.ignore()
- return
- items = [i.toLocalFile() for i in event.mimeData().urls()]
- self.importDroppedImageFiles(items)
- # User Dialogs #
- def loadRecent(self, filename):
- if self.mayContinue():
- self.loadFile(filename)
- def openPrevImg(self, _value=False):
- keep_prev = self._config["keep_prev"]
- if QtWidgets.QApplication.keyboardModifiers() == (
- Qt.ControlModifier | Qt.ShiftModifier
- ):
- self._config["keep_prev"] = True
- if not self.mayContinue():
- return
- if len(self.imageList) <= 0:
- return
- if self.filename is None:
- return
- currIndex = self.imageList.index(self.filename)
- if currIndex - 1 >= 0:
- filename = self.imageList[currIndex - 1]
- if filename:
- self.loadFile(filename)
- self._config["keep_prev"] = keep_prev
- def openNextImg(self, _value=False, load=True):
- keep_prev = self._config["keep_prev"]
- if QtWidgets.QApplication.keyboardModifiers() == (
- Qt.ControlModifier | Qt.ShiftModifier
- ):
- self._config["keep_prev"] = True
- if not self.mayContinue():
- return
- if len(self.imageList) <= 0:
- return
- filename = None
- if self.filename is None:
- filename = self.imageList[0]
- else:
- currIndex = self.imageList.index(self.filename)
- if currIndex + 1 < len(self.imageList):
- filename = self.imageList[currIndex + 1]
- else:
- filename = self.imageList[-1]
- self.filename = filename
- if self.filename and load:
- self.loadFile(self.filename)
- self._config["keep_prev"] = keep_prev
- def openFile(self, _value=False):
- if not self.mayContinue():
- return
- path = osp.dirname(str(self.filename)) if self.filename else "."
- formats = [
- "*.{}".format(fmt.data().decode())
- for fmt in QtGui.QImageReader.supportedImageFormats()
- ]
- filters = self.tr("Image & Label files (%s)") % " ".join(
- formats + ["*%s" % LabelFile.suffix]
- )
- fileDialog = FileDialogPreview(self)
- fileDialog.setFileMode(FileDialogPreview.ExistingFile)
- fileDialog.setNameFilter(filters)
- fileDialog.setWindowTitle(
- self.tr("%s - Choose Image or Label file") % __appname__,
- )
- fileDialog.setWindowFilePath(path)
- fileDialog.setViewMode(FileDialogPreview.Detail)
- if fileDialog.exec_():
- fileName = fileDialog.selectedFiles()[0]
- if fileName:
- self.loadFile(fileName)
- def changeOutputDirDialog(self, _value=False):
- default_output_dir = self.output_dir
- if default_output_dir is None and self.filename:
- default_output_dir = osp.dirname(self.filename)
- if default_output_dir is None:
- default_output_dir = self.currentPath()
- output_dir = QtWidgets.QFileDialog.getExistingDirectory(
- self,
- self.tr("%s - Save/Load Annotations in Directory") % __appname__,
- default_output_dir,
- QtWidgets.QFileDialog.ShowDirsOnly
- | QtWidgets.QFileDialog.DontResolveSymlinks,
- )
- output_dir = str(output_dir)
- if not output_dir:
- return
- self.output_dir = output_dir
- self.statusBar().showMessage(
- self.tr("%s . Annotations will be saved/loaded in %s")
- % ("Change Annotations Dir", self.output_dir)
- )
- self.statusBar().show()
- current_filename = self.filename
- self.importDirImages(self.lastOpenDir, load=False)
- if current_filename in self.imageList:
- # retain currently selected file
- self.fileListWidget.setCurrentRow(
- self.imageList.index(current_filename)
- )
- self.fileListWidget.repaint()
- def saveFile(self, _value=False):
- assert not self.image.isNull(), "cannot save empty image"
- if self.labelFile:
- # DL20180323 - overwrite when in directory
- self._saveFile(self.labelFile.filename)
- elif self.output_file:
- self._saveFile(self.output_file)
- self.close()
- else:
- self._saveFile(self.saveFileDialog())
- def saveFileAs(self, _value=False):
- assert not self.image.isNull(), "cannot save empty image"
- self._saveFile(self.saveFileDialog())
- def saveFileDialog(self):
- caption = self.tr("%s - Choose File") % __appname__
- filters = self.tr("Label files (*%s)") % LabelFile.suffix
- if self.output_dir:
- dlg = QtWidgets.QFileDialog(
- self, caption, self.output_dir, filters
- )
- else:
- dlg = QtWidgets.QFileDialog(
- self, caption, self.currentPath(), filters
- )
- dlg.setDefaultSuffix(LabelFile.suffix[1:])
- dlg.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
- dlg.setOption(QtWidgets.QFileDialog.DontConfirmOverwrite, False)
- dlg.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, False)
- basename = osp.basename(osp.splitext(self.filename)[0])
- if self.output_dir:
- default_labelfile_name = osp.join(
- self.output_dir, basename + LabelFile.suffix
- )
- else:
- default_labelfile_name = osp.join(
- self.currentPath(), basename + LabelFile.suffix
- )
- filename = dlg.getSaveFileName(
- self,
- self.tr("Choose File"),
- default_labelfile_name,
- self.tr("Label files (*%s)") % LabelFile.suffix,
- )
- if isinstance(filename, tuple):
- filename, _ = filename
- return filename
- def _saveFile(self, filename):
- if filename and self.saveLabels(filename):
- self.addRecentFile(filename)
- self.setClean()
- def closeFile(self, _value=False):
- if not self.mayContinue():
- return
- self.resetState()
- self.setClean()
- self.toggleActions(False)
- self.canvas.setEnabled(False)
- self.actions.saveAs.setEnabled(False)
- def getLabelFile(self):
- if self.filename.lower().endswith(".json"):
- label_file = self.filename
- else:
- label_file = osp.splitext(self.filename)[0] + ".json"
- return label_file
- def deleteFile(self):
- mb = QtWidgets.QMessageBox
- msg = self.tr(
- "You are about to permanently delete this label file, "
- "proceed anyway?"
- )
- answer = mb.warning(self, self.tr("Attention"), msg, mb.Yes | mb.No)
- if answer != mb.Yes:
- return
- label_file = self.getLabelFile()
- if osp.exists(label_file):
- os.remove(label_file)
- logger.info("Label file is removed: {}".format(label_file))
- item = self.fileListWidget.currentItem()
- item.setCheckState(Qt.Unchecked)
- self.resetState()
- # Message Dialogs. #
- def hasLabels(self):
- if self.noShapes():
- self.errorMessage(
- "No objects labeled",
- "You must label at least one object to save the file.",
- )
- return False
- return True
- def hasLabelFile(self):
- if self.filename is None:
- return False
- label_file = self.getLabelFile()
- return osp.exists(label_file)
- def mayContinue(self):
- if not self.dirty:
- return True
- mb = QtWidgets.QMessageBox
- msg = self.tr('Save annotations to "{}" before closing?').format(
- self.filename
- )
- answer = mb.question(
- self,
- self.tr("Save annotations?"),
- msg,
- mb.Save | mb.Discard | mb.Cancel,
- mb.Save,
- )
- if answer == mb.Discard:
- return True
- elif answer == mb.Save:
- self.saveFile()
- return True
- else: # answer == mb.Cancel
- return False
- def errorMessage(self, title, message):
- return QtWidgets.QMessageBox.critical(
- self, title, "<p><b>%s</b></p>%s" % (title, message)
- )
- def currentPath(self):
- return osp.dirname(str(self.filename)) if self.filename else "."
- def toggleKeepPrevMode(self):
- self._config["keep_prev"] = not self._config["keep_prev"]
- def removeSelectedPoint(self):
- self.canvas.removeSelectedPoint()
- self.canvas.update()
- if not self.canvas.hShape.points:
- self.canvas.deleteShape(self.canvas.hShape)
- self.remLabels([self.canvas.hShape])
- if self.noShapes():
- for action in self.actions.onShapesPresent:
- action.setEnabled(False)
- self.setDirty()
- def deleteSelectedShape(self):
- yes, no = QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No
- msg = self.tr(
- "You are about to permanently delete {} polygons, "
- "proceed anyway?"
- ).format(len(self.canvas.selectedShapes))
- if yes == QtWidgets.QMessageBox.warning(
- self, self.tr("Attention"), msg, yes | no, yes
- ):
- self.remLabels(self.canvas.deleteSelected())
- self.setDirty()
- if self.noShapes():
- for action in self.actions.onShapesPresent:
- action.setEnabled(False)
- def copyShape(self):
- self.canvas.endMove(copy=True)
- for shape in self.canvas.selectedShapes:
- self.addLabel(shape)
- self.labelList.clearSelection()
- self.setDirty()
- def moveShape(self):
- self.canvas.endMove(copy=False)
- self.setDirty()
- def openDirDialog(self, _value=False, dirpath=None):
- if not self.mayContinue():
- return
- defaultOpenDirPath = dirpath if dirpath else "."
- if self.lastOpenDir and osp.exists(self.lastOpenDir):
- defaultOpenDirPath = self.lastOpenDir
- else:
- defaultOpenDirPath = (
- osp.dirname(self.filename) if self.filename else "."
- )
- targetDirPath = str(
- QtWidgets.QFileDialog.getExistingDirectory(
- self,
- self.tr("%s - Open Directory") % __appname__,
- defaultOpenDirPath,
- QtWidgets.QFileDialog.ShowDirsOnly
- | QtWidgets.QFileDialog.DontResolveSymlinks,
- )
- )
- self.importDirImages(targetDirPath)
- @property
- def imageList(self):
- lst = []
- for i in range(self.fileListWidget.count()):
- item = self.fileListWidget.item(i)
- lst.append(item.text())
- return lst
- def importDroppedImageFiles(self, imageFiles):
- extensions = [
- ".%s" % fmt.data().decode().lower()
- for fmt in QtGui.QImageReader.supportedImageFormats()
- ]
- self.filename = None
- for file in imageFiles:
- if file in self.imageList or not file.lower().endswith(
- tuple(extensions)
- ):
- continue
- label_file = osp.splitext(file)[0] + ".json"
- if self.output_dir:
- label_file_without_path = osp.basename(label_file)
- label_file = osp.join(self.output_dir, label_file_without_path)
- item = QtWidgets.QListWidgetItem(file)
- item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
- if QtCore.QFile.exists(label_file) and LabelFile.is_label_file(
- label_file
- ):
- item.setCheckState(Qt.Checked)
- else:
- item.setCheckState(Qt.Unchecked)
- self.fileListWidget.addItem(item)
- if len(self.imageList) > 1:
- self.actions.openNextImg.setEnabled(True)
- self.actions.openPrevImg.setEnabled(True)
- self.openNextImg()
- def importDirImages(self, dirpath, pattern=None, load=True):
- self.actions.openNextImg.setEnabled(True)
- self.actions.openPrevImg.setEnabled(True)
- if not self.mayContinue() or not dirpath:
- return
- self.lastOpenDir = dirpath
- self.filename = None
- self.fileListWidget.clear()
- for filename in self.scanAllImages(dirpath):
- if pattern and pattern not in filename:
- continue
- label_file = osp.splitext(filename)[0] + ".json"
- if self.output_dir:
- label_file_without_path = osp.basename(label_file)
- label_file = osp.join(self.output_dir, label_file_without_path)
- item = QtWidgets.QListWidgetItem(filename)
- item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
- if QtCore.QFile.exists(label_file) and LabelFile.is_label_file(
- label_file
- ):
- item.setCheckState(Qt.Checked)
- else:
- item.setCheckState(Qt.Unchecked)
- self.fileListWidget.addItem(item)
- self.openNextImg(load=load)
- def scanAllImages(self, folderPath):
- extensions = [
- ".%s" % fmt.data().decode().lower()
- for fmt in QtGui.QImageReader.supportedImageFormats()
- ]
- images = []
- for root, dirs, files in os.walk(folderPath):
- for file in files:
- if file.lower().endswith(tuple(extensions)):
- relativePath = osp.join(root, file)
- images.append(relativePath)
- images = natsort.os_sorted(images)
- return images
|