Ver Fonte

Add support for saving labels

The current implementation uses json to store for each label its value
and list of points. It also stores the path to the original image as
well as a textual (b64 encoded) representation of the image data itself.
The reasoning is to enable self-contained files.
Michael Pitidis há 13 anos atrás
pai
commit
bfd18b5033
1 ficheiros alterados com 53 adições e 4 exclusões
  1. 53 4
      labelme.py

+ 53 - 4
labelme.py

@@ -7,6 +7,9 @@ import sys
 
 
 from functools import partial
 from functools import partial
 from collections import defaultdict
 from collections import defaultdict
+from base64 import b64encode, b64decode
+
+import json
 
 
 from PyQt4.QtGui import *
 from PyQt4.QtGui import *
 from PyQt4.QtCore import *
 from PyQt4.QtCore import *
@@ -25,6 +28,8 @@ __appname__ = 'labelme'
 # TODO:
 # TODO:
 # - Zoom is too "steppy".
 # - Zoom is too "steppy".
 # - Add a new column in list widget with checkbox to show/hide shape.
 # - Add a new column in list widget with checkbox to show/hide shape.
+# - Make sure the `save' action is disabled when no labels are
+#   present in the image, e.g. when all of them are deleted.
 
 
 ### Utility functions and classes.
 ### Utility functions and classes.
 
 
@@ -92,6 +97,8 @@ class MainWindow(QMainWindow, WindowMixin):
                 'Ctrl+Q', 'quit', u'Exit application')
                 'Ctrl+Q', 'quit', u'Exit application')
         open = action('&Open', self.openFile,
         open = action('&Open', self.openFile,
                 'Ctrl+O', 'open', u'Open file')
                 'Ctrl+O', 'open', u'Open file')
+        save = action('&Save', self.saveFile,
+                'Ctrl+S', 'save', u'Save file')
         color = action('&Color', self.chooseColor,
         color = action('&Color', self.chooseColor,
                 'Ctrl+C', 'color', u'Choose line color')
                 'Ctrl+C', 'color', u'Choose line color')
         label = action('&New Item', self.newLabel,
         label = action('&New Item', self.newLabel,
@@ -105,6 +112,11 @@ class MainWindow(QMainWindow, WindowMixin):
         zoom = QWidgetAction(self)
         zoom = QWidgetAction(self)
         zoom.setDefaultWidget(self.zoom_widget)
         zoom.setDefaultWidget(self.zoom_widget)
 
 
+        # Store actions for further handling.
+        self.actions = struct(save=save, open=open, color=color,
+                label=label, delete=delete, zoom=zoom)
+        save.setEnabled(False)
+
         fit_window = action('&Fit Window', self.setFitWindow,
         fit_window = action('&Fit Window', self.setFitWindow,
                 'Ctrl+F', 'fit',  u'Fit image to window', checkable=True)
                 'Ctrl+F', 'fit',  u'Fit image to window', checkable=True)
 
 
@@ -112,13 +124,13 @@ class MainWindow(QMainWindow, WindowMixin):
                 file=self.menu('&File'),
                 file=self.menu('&File'),
                 edit=self.menu('&Image'),
                 edit=self.menu('&Image'),
                 view=self.menu('&View'))
                 view=self.menu('&View'))
-        addActions(self.menus.file, (open, quit))
+        addActions(self.menus.file, (open, save, quit))
         addActions(self.menus.edit, (label, color, fit_window))
         addActions(self.menus.edit, (label, color, fit_window))
 
 
         addActions(self.menus.view, (labels,))
         addActions(self.menus.view, (labels,))
 
 
         self.tools = self.toolbar('Tools')
         self.tools = self.toolbar('Tools')
-        addActions(self.tools, (open, color, None, label, delete, None,
+        addActions(self.tools, (open, save, color, None, label, delete, None,
             zoom, fit_window, None, quit))
             zoom, fit_window, None, quit))
 
 
 
 
@@ -164,6 +176,20 @@ class MainWindow(QMainWindow, WindowMixin):
         self.zoom_widget.editingFinished.connect(self.paintCanvas)
         self.zoom_widget.editingFinished.connect(self.paintCanvas)
 
 
 
 
+    def saveLabels(self, filename):
+        shapes = []
+        for shape in self.canvas.shapes:
+            data = {}
+            data['points'] = [(p.x(), p.y()) for p in shape.points]
+            data['label'] = unicode(shape.label)
+            shapes.append(data)
+        with open(filename, 'wb') as f:
+            json.dump(dict(
+                shapes=shapes,
+                image_path=unicode(self.filename),
+                image_data=b64encode(self.image_data)),
+                f, ensure_ascii=True, indent=2)
+
     def addLabel(self, label, shape):
     def addLabel(self, label, shape):
         item = QListWidgetItem(label)
         item = QListWidgetItem(label)
         self.labels[item] = shape
         self.labels[item] = shape
@@ -188,6 +214,8 @@ class MainWindow(QMainWindow, WindowMixin):
             label = self.label.text()
             label = self.label.text()
             shape = self.canvas.setLastLabel(label)
             shape = self.canvas.setLastLabel(label)
             self.addLabel(label, shape)
             self.addLabel(label, shape)
+            # Enable the save action.
+            self.actions.save.setEnabled(True)
             # TODO: Add to list of labels.
             # TODO: Add to list of labels.
         elif action == self.label.UNDO:
         elif action == self.label.UNDO:
             self.canvas.undoLastLine()
             self.canvas.undoLastLine()
@@ -222,8 +250,10 @@ class MainWindow(QMainWindow, WindowMixin):
             filename = self.settings['filename']
             filename = self.settings['filename']
         # FIXME: Load the actual file here.
         # FIXME: Load the actual file here.
         if QFile.exists(filename):
         if QFile.exists(filename):
-            # Load image
-            image = QImage(filename)
+            # Load image: read data first and store for saving into label file.
+            #image = QImage(filename)
+            self.image_data = read(filename, None)
+            image = QImage.fromData(self.image_data)
             if image.isNull():
             if image.isNull():
                 message = "Failed to read %s" % filename
                 message = "Failed to read %s" % filename
             else:
             else:
@@ -288,6 +318,18 @@ class MainWindow(QMainWindow, WindowMixin):
         if filename:
         if filename:
             self.loadFile(filename)
             self.loadFile(filename)
 
 
+    def saveFile(self):
+        assert not self.image.isNull(), "cannot save empty image"
+        # XXX: What if user wants to remove label file?
+        assert self.labels, "cannot save empty labels"
+        path = os.path.dirname(unicode(self.filename))\
+                if self.filename else '.'
+        formats = ['*.lif']
+        filename = unicode(QFileDialog.getSaveFileName(self,
+            '%s - Choose File', path, 'Label files (%s)' % ''.join(formats)))
+        if filename:
+            self.saveLabels(filename)
+
     def check(self):
     def check(self):
         # TODO: Prompt user to save labels etc.
         # TODO: Prompt user to save labels etc.
         return True
         return True
@@ -337,6 +379,13 @@ def inverted(color):
     return QColor(*[255 - v for v in color.getRgb()])
     return QColor(*[255 - v for v in color.getRgb()])
 
 
 
 
+def read(filename, default=None):
+    try:
+        with open(filename, 'rb') as f:
+            return f.read()
+    except:
+        return default
+
 class struct(object):
 class struct(object):
     def __init__(self, **kwargs):
     def __init__(self, **kwargs):
         self.__dict__.update(kwargs)
         self.__dict__.update(kwargs)