12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117 |
- # -*- 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 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)
- zoomBoxLayout = QtWidgets.QVBoxLayout()
- zoomBoxLayout.addWidget(self.zoomWidget)
- zoomLabel = QtWidgets.QLabel("Zoom")
- zoomLabel.setAlignment(Qt.AlignCenter)
- zoomLabel.setFont(QtGui.QFont(None, 10))
- zoomBoxLayout.addWidget(zoomLabel)
- zoom.setDefaultWidget(QtWidgets.QWidget())
- zoom.defaultWidget().setLayout(zoomBoxLayout)
- 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,
- )
- if self._config["canvas"]["fill_drawing"]:
- 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")
- 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()
- 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.TopToolBarArea, 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 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
|