...
 
Commits (6)
......@@ -10,6 +10,7 @@ import subprocess
from functools import partial
from collections import defaultdict
from PIL import Image, ImageEnhance
try:
from PyQt5.QtGui import *
......@@ -195,6 +196,12 @@ class MainWindow(QMainWindow, WindowMixin):
self.scrollArea = scroll
self.canvas.scrollRequest.connect(self.scrollRequest)
QScroller.grabGesture(scroll.viewport(), QScroller.MiddleMouseButtonGesture)
properties = QScroller.scroller(scroll.viewport()).scrollerProperties()
properties.setScrollMetric(QScrollerProperties.VerticalOvershootPolicy, QScrollerProperties.OvershootAlwaysOff)
properties.setScrollMetric(QScrollerProperties.OvershootScrollTime, 0.2)
QScroller.scroller(scroll.viewport()).setScrollerProperties(properties)
self.canvas.newShape.connect(self.newShape)
self.canvas.shapeMoved.connect(self.setDirty)
self.canvas.selectionChanged.connect(self.shapeSelectionChanged)
......@@ -297,7 +304,7 @@ class MainWindow(QMainWindow, WindowMixin):
checkable=True, enabled=False)
fitWidth = action('Fit &Width', self.setFitWidth,
'Ctrl+Shift+F', 'fit-width', u'Zoom follows window width',
checkable=True, enabled=False)
checkable=True, enabled=False)
# Group zoom controls into a list for easier toggling.
zoomActions = (self.zoomWidget, zoomIn, zoomOut,
zoomOrg, fitWindow, fitWidth)
......@@ -309,6 +316,8 @@ class MainWindow(QMainWindow, WindowMixin):
self.MANUAL_ZOOM: lambda: 1,
}
enhanceImage = action('Enhance Image', self.enhanceImage, tip='Enhance image for better view')
edit = action('&Edit Label', self.editLabel,
'Ctrl+E', 'edit', u'Modify the label of the selected Box',
enabled=False)
......@@ -400,7 +409,7 @@ class MainWindow(QMainWindow, WindowMixin):
self.tools = self.toolbar('Tools')
self.actions.beginner = (
open, opendir, changeSavedir, openNextImg, openPrevImg, verify, save, save_format, None, create, copy, delete, None,
zoomIn, zoom, zoomOut, fitWindow, fitWidth)
zoomIn, zoom, zoomOut, fitWindow, fitWidth, enhanceImage)
self.actions.advanced = (
open, opendir, changeSavedir, openNextImg, openPrevImg, save, save_format, None,
......@@ -476,9 +485,9 @@ class MainWindow(QMainWindow, WindowMixin):
self.labelCoordinates = QLabel('')
self.statusBar().addPermanentWidget(self.labelCoordinates)
# Open Dir if deafult file
# open dir if defaultFilename is a directory
if self.filePath and os.path.isdir(self.filePath):
self.openDirDialog(dirpath=self.filePath)
self.importDirImages(self.filePath)
## Support Functions ##
def set_format(self, save_format):
......@@ -738,24 +747,22 @@ class MainWindow(QMainWindow, WindowMixin):
def loadLabels(self, shapes):
s = []
for label, points, line_color, fill_color, difficult in shapes:
for label, points, box, line_color, fill_color, difficult in shapes:
shape = Shape(label=label)
for x, y in points:
shape.addPoint(QPointF(x, y))
center = QPointF(*box[0])
width, height = box[1]
angle = box[2]
shape.addPoint(QPointF(center.x() - width / 2, center.y() - height / 2))
shape.addPoint(QPointF(center.x() + width / 2, center.y() - height / 2))
shape.addPoint(QPointF(center.x() + width / 2, center.y() + height / 2))
shape.addPoint(QPointF(center.x() - width / 2, center.y() + height / 2))
shape.applyRotationAngle(angle, center)
shape.updateState()
shape.difficult = difficult
shape.close()
s.append(shape)
if line_color:
shape.line_color = QColor(*line_color)
else:
shape.line_color = generateColorByText(label)
if fill_color:
shape.fill_color = QColor(*fill_color)
else:
shape.fill_color = generateColorByText(label)
shape.line_color = QColor(*line_color) if line_color else generateColorByText(label)
shape.fill_color = QColor(*fill_color) if fill_color else generateColorByText(label)
self.addLabel(shape)
self.canvas.loadShapes(s)
......@@ -767,12 +774,17 @@ class MainWindow(QMainWindow, WindowMixin):
self.labelFile.verified = self.canvas.verified
def format_shape(s):
s.updateState()
return dict(label=s.label,
line_color=s.line_color.getRgb(),
fill_color=s.fill_color.getRgb(),
points=[(p.x(), p.y()) for p in s.points],
# add chris
difficult = s.difficult)
difficult=s.difficult,
center=[s.center.x(), s.center.y()],
width=s.width,
height=s.height,
angle=s.currentAngle)
shapes = [format_shape(shape) for shape in self.canvas.shapes]
# Can add differrent annotation formats here
......@@ -943,6 +955,15 @@ class MainWindow(QMainWindow, WindowMixin):
for item, shape in self.itemsToShapes.items():
item.setCheckState(Qt.Checked if value else Qt.Unchecked)
def enhanceImage(self, value=True):
if not self.image.isNull():
pil_image = Image.fromqimage(self.image)
enhancer = ImageEnhance.Brightness(pil_image)
self.image = enhancer.enhance(2).toqimage()
shapes = self.canvas.shapes
self.canvas.loadPixmap(QPixmap.fromImage(self.image))
self.canvas.loadShapes(shapes)
def loadFile(self, filePath=None):
"""Load the specified file, or the last opened file if None."""
self.resetState()
......
......@@ -638,7 +638,7 @@ class Canvas(QWidget):
for shift in shifts:
if shift is not None:
shape.points[index] += shift
shape.updateState()
# apply the new coordinates to the latent (unrotated) array
shape.applyRotationAngle(shape.currentAngle, shape_center, False)
......@@ -959,12 +959,22 @@ class Canvas(QWidget):
self.moveOnePixel(QPointF(0,-1))
elif key == Qt.Key_Down and self.selectedShape:
self.moveOnePixel(QPointF(0,1))
elif key == Qt.Key_PageDown and self.selectedShape:
self.rotateOnDegree(-1)
elif key == Qt.Key_PageUp and self.selectedShape:
self.rotateOnDegree(1)
def moveOnePixel(self, direction):
self.boundedMoveShapeBy(self.selectedShape, direction)
self.shapeMoved.emit()
self.repaint()
def rotateOnDegree(self, direction):
angle = (self.selectedShape.currentAngle + 1. / 180. * direction * pi) % (2 * pi)
center = self.selectedShape.center
self.selectedShape.applyRotationAngle(angle, center)
self.repaint()
def setLastLabel(self, text, line_color = None, fill_color = None):
assert text
self.shapes[-1].label = text
......
......@@ -50,8 +50,13 @@ class LabelFile(object):
label = shape['label']
# Add Chris
difficult = int(shape['difficult'])
cx = shape['center'][0]
cy = shape['center'][1]
width = float(shape['width'])
height = float(shape['height'])
angle = float(shape['angle'])
bndbox = LabelFile.convertPoints2BndBox(points)
writer.addBndBox(bndbox[0], bndbox[1], bndbox[2], bndbox[3], label, difficult)
writer.addBndBox(bndbox[0], bndbox[1], bndbox[2], bndbox[3], label, difficult, cx, cy, width, height, angle)
writer.save(targetFile=filename)
return
......
......@@ -5,6 +5,7 @@ from xml.etree import ElementTree
from xml.etree.ElementTree import Element, SubElement
from lxml import etree
import codecs
import math
XML_EXT = '.xml'
ENCODE_METHOD = 'utf-8'
......@@ -74,10 +75,15 @@ class PascalVocWriter:
segmented.text = '0'
return top
def addBndBox(self, xmin, ymin, xmax, ymax, name, difficult):
def addBndBox(self, xmin, ymin, xmax, ymax, name, difficult, cx, cy, width, height, angle):
bndbox = {'xmin': xmin, 'ymin': ymin, 'xmax': xmax, 'ymax': ymax}
bndbox['name'] = name
bndbox['difficult'] = difficult
bndbox['cx'] = cx
bndbox['cy'] = cy
bndbox['width'] = width
bndbox['height'] = height
bndbox['angle'] = angle
self.boxlist.append(bndbox)
def appendObjects(self, top):
......@@ -109,6 +115,21 @@ class PascalVocWriter:
xmax.text = str(each_object['xmax'])
ymax = SubElement(bndbox, 'ymax')
ymax.text = str(each_object['ymax'])
realbox = SubElement(object_item, 'box')
cx = SubElement(realbox, 'cx')
cx.text = f"{each_object['cx']:.1f}"
cy = SubElement(realbox, 'cy')
cy.text = f"{each_object['cy']:.1f}"
width = SubElement(realbox, 'width')
width.text = f"{each_object['width']:.1f}"
height = SubElement(realbox, 'height')
height.text = f"{each_object['height']:.1f}"
angle = SubElement(realbox, 'angle')
angle.text = f"{each_object['angle']:.4f}"
angle_deg = SubElement(realbox, 'angle_deg')
angle_deg.text = f"{(each_object['angle'] / math.pi * 180):.1f}"
def save(self, targetFile=None):
root = self.genXML()
......@@ -141,13 +162,19 @@ class PascalVocReader:
def getShapes(self):
return self.shapes
def addShape(self, label, bndbox, difficult):
def addShape(self, label, bndbox, box, difficult):
xmin = int(bndbox.find('xmin').text)
ymin = int(bndbox.find('ymin').text)
xmax = int(bndbox.find('xmax').text)
ymax = int(bndbox.find('ymax').text)
points = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)]
self.shapes.append((label, points, None, None, difficult))
cx = float(box.find('cx').text)
cy = float(box.find('cy').text)
width = float(box.find('width').text)
height = float(box.find('height').text)
angle = float(box.find('angle').text)
box = ((cx, cy), (width, height), angle)
bbox_points = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)]
self.shapes.append((label, bbox_points, box, None, None, difficult))
def parseXML(self):
assert self.filepath.endswith(XML_EXT), "Unsupport file format"
......@@ -163,10 +190,11 @@ class PascalVocReader:
for object_iter in xmltree.findall('object'):
bndbox = object_iter.find("bndbox")
box = object_iter.find("box")
label = object_iter.find('name').text
# Add chris
difficult = False
if object_iter.find('difficult') is not None:
difficult = bool(int(object_iter.find('difficult').text))
self.addShape(label, bndbox, difficult)
self.addShape(label, bndbox, box, difficult)
return True
......@@ -9,7 +9,7 @@ except ImportError:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from math import cos, sin, pi
from math import cos, sin, pi, sqrt
from libs.lib import distance
from functools import reduce
import sys
......@@ -49,6 +49,9 @@ class Shape(object):
self.points = []
self.pointsWithoutRotation= []
self.currentAngle = 0 #< basically meta information
self.center = QPointF(0, 0)
self.width = 0
self.height = 0
self.fill = False
self.selected = False
self.difficult = difficult
......@@ -221,6 +224,10 @@ class Shape(object):
else:
assert False, "unsupported vertex shape"
def updateState(self):
self.center = self.getCenter()
self.width, self.height = self.getDimensions()
def getCenter(self, rotated=True):
"""
......@@ -233,6 +240,13 @@ class Shape(object):
return reduce((lambda x, y: x + y), p) / len(p)
def getDimensions(self):
dw = self.points[1] - self.points[0]
dh = self.points[3] - self.points[0]
w = sqrt(QPointF.dotProduct(dw, dw))
h = sqrt(QPointF.dotProduct(dh, dh))
return w, h
def getClosestVertex(self, point, epsilon):
"""
Returns the index and distance of shape's nearest vertex to a specified :point:
......@@ -283,6 +297,7 @@ class Shape(object):
def shift(self, offset):
self.pointsWithoutRotation = list(map(lambda a : a + offset, self.pointsWithoutRotation))
self.points = list(map(lambda a : a + offset, self.points))
self.center = self.getCenter()
def applyRotationAngle(self, angle, shape_center, fromUnrotated=True):
"""
......@@ -319,7 +334,6 @@ class Shape(object):
shape_center.y() + sin(angle) * dc.x() + cos(angle) * dc.y())
@staticmethod
def rotatePoint(toRotate, center, angle):
"""Utility function"""
......@@ -338,6 +352,8 @@ class Shape(object):
def copy(self):
shape = Shape("%s" % self.label)
self.updateState()
shape.currentAngle = self.currentAngle
shape.points = [p for p in self.points]
shape.pointsWithoutRotation = [p for p in self.pointsWithoutRotation]
shape.fill = self.fill
......@@ -348,6 +364,7 @@ class Shape(object):
if self.fill_color != Shape.fill_color:
shape.fill_color = self.fill_color
shape.difficult = self.difficult
shape.updateState()
return shape
def __len__(self):
......