1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023 |
- # -*- coding: utf-8 -*-
- import functools
- import math
- import os
- import os.path as osp
- import re
- import webbrowser
- import imgviz
- 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 labelme import QT5
- 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 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):
- # - [high] Add polygon movement with arrow keys
- # - [high] Deselect shape when clicking and already selected(?)
- # - [low,maybe] Preview images on file dialogs.
- # - Zoom is too "steppy".
- LABEL_COLORMAP = imgviz.label_colormap(value=200)
- 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
- # 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(u"Label List"), self)
- self.label_dock.setObjectName(u"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(u"File List"), self)
- self.file_dock.setObjectName(u"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"],
- )
- 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(u"Open Dir"),
- )
- openNextImg = action(
- self.tr("&Next Image"),
- self.openNextImg,
- shortcuts["open_next"],
- "next",
- self.tr(u"Open next (hold Ctl+Shift to copy labels)"),
- enabled=False,
- )
- openPrevImg = action(
- self.tr("&Prev Image"),
- self.openPrevImg,
- shortcuts["open_prev"],
- "prev",
- self.tr(u"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(u"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,
- )
- 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,
- )
- copy = action(
- self.tr("Duplicate Polygons"),
- self.copySelectedShape,
- shortcuts["duplicate_polygon"],
- "copy",
- self.tr("Create a duplicate of the selected 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,
- )
- addPointToEdge = action(
- text=self.tr("Add Point to Edge"),
- slot=self.canvas.addPointToEdge,
- shortcut=shortcuts["add_point_to_edge"],
- icon="edit",
- tip=self.tr("Add point to the nearest edge"),
- enabled=False,
- )
- removePoint = action(
- text="Remove Selected Point",
- slot=self.removeSelectedPoint,
- 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(
- 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,
- )
- 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,
- copy=copy,
- undoLastPoint=undoLastPoint,
- undo=undo,
- addPointToEdge=addPointToEdge,
- removePoint=removePoint,
- createMode=createMode,
- editMode=editMode,
- createRectangleMode=createRectangleMode,
- createCircleMode=createCircleMode,
- createLineMode=createLineMode,
- createPointMode=createPointMode,
- createLineStripMode=createLineStripMode,
- zoom=zoom,
- zoomIn=zoomIn,
- zoomOut=zoomOut,
- zoomOrg=zoomOrg,
- 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,
- copy,
- delete,
- None,
- undo,
- undoLastPoint,
- None,
- addPointToEdge,
- None,
- toggle_keep_prev_mode,
- ),
- # menu shown at right click
- menu=(
- createMode,
- createRectangleMode,
- createCircleMode,
- createLineMode,
- createPointMode,
- createLineStripMode,
- editMode,
- edit,
- copy,
- delete,
- undo,
- undoLastPoint,
- addPointToEdge,
- removePoint,
- ),
- onLoadActive=(
- close,
- createMode,
- createRectangleMode,
- createCircleMode,
- createLineMode,
- createPointMode,
- createLineStripMode,
- editMode,
- brightnessContrast,
- ),
- onShapesPresent=(saveAs, hideAll, showAll),
- )
- self.canvas.edgeSelected.connect(self.canvasShapeEdgeSelected)
- 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,
- 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,
- copy,
- delete,
- undo,
- brightnessContrast,
- None,
- zoom,
- fitWidth,
- )
- self.statusBar().showMessage(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")
- # FIXME: QSettings.value can return None on PyQt4
- 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))
- self.resize(size)
- self.move(position)
- # or simply:
- # self.restoreGeometry(settings['window/geometry']
- self.restoreState(
- self.settings.value("window/state", QtCore.QByteArray())
- )
- # 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.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.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)
- 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 canvasShapeEdgeSelected(self, selected, shape):
- self.actions.addPointToEdge.setEnabled(
- selected and shape and shape.canAddPoint()
- )
- 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)
- 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)
- 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)
- 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)
- 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)
- 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)
- 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)
- 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 = self.labelDialog.popUp(
- text=shape.label,
- flags=shape.flags,
- group_id=shape.group_id,
- )
- 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
- if shape.group_id is None:
- item.setText(shape.label)
- else:
- item.setText("{} ({})".format(shape.label, shape.group_id))
- self.setDirty()
- if not self.uniqLabelList.findItemsByLabel(shape.label):
- item = QtWidgets.QListWidgetItem()
- item.setData(Qt.UserRole, shape.label)
- self.uniqLabelList.addItem(item)
- 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.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 not self.uniqLabelList.findItemsByLabel(shape.label):
- 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)
- rgb = self._get_rgb_by_label(shape.label)
- r, g, b = rgb
- label_list_item.setText(
- '{} <font color="#{:02x}{:02x}{:02x}">●</font>'.format(
- text, r, g, b
- )
- )
- 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.findItemsByLabel(label)[0]
- 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"]
- 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,
- )
- 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,
- 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 copySelectedShape(self):
- added_shapes = self.canvas.copySelectedShapes()
- self.labelList.clearSelection()
- for shape in added_shapes:
- self.addLabel(shape)
- self.setDirty()
- 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
- if self._config["display_label_popup"] or not text:
- previous_text = self.labelDialog.edit.text()
- text, flags, group_id = 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
- 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(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 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(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 constrast 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(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 Qt.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 Qt.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]
- )
- filename = QtWidgets.QFileDialog.getOpenFileName(
- self,
- self.tr("%s - Choose Image or Label file") % __appname__,
- path,
- filters,
- )
- if QT5:
- filename, _ = filename
- filename = str(filename)
- 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()
- if not self.canvas.hShape.points:
- self.canvas.deleteShape(self.canvas.hShape)
- self.remLabels([self.canvas.hShape])
- self.setDirty()
- if self.noShapes():
- for action in self.actions.onShapesPresent:
- action.setEnabled(False)
- 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)
- self.labelList.clearSelection()
- for shape in self.canvas.selectedShapes:
- self.addLabel(shape)
- 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.sort(key=lambda x: x.lower())
- return images
|