diff --git a/.gitignore b/.gitignore
index 63754ecf1..efee683b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ labelImg.egg-info*
build/
dist/
+venv/
tags
cscope*
diff --git a/README.rst b/README.rst
index ef061f46f..fa3a6df6c 100644
--- a/README.rst
+++ b/README.rst
@@ -32,7 +32,7 @@ LabelImg is a graphical image annotation tool.
It is written in Python and uses Qt for its graphical interface.
Annotations are saved as XML files in PASCAL VOC format, the format used
-by `ImageNet `__. Besides, it also supports YOLO and CreateML formats.
+by `ImageNet `__. Besides, it also supports YOLO, RotatedYOLO and CreateML formats.
.. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo3.jpg
:alt: Demo Image
@@ -40,6 +40,9 @@ by `ImageNet `__. Besides, it also supports YOLO and
.. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo.jpg
:alt: Demo Image
+.. image:: /demo/demo6.png
+ :alt: Demo Image
+
`Watch a demo video `__
Installation
@@ -236,33 +239,41 @@ Annotation visualization
Hotkeys
~~~~~~~
-+--------------------+--------------------------------------------+
-| Ctrl + u | Load all of the images from a directory |
-+--------------------+--------------------------------------------+
-| Ctrl + r | Change the default annotation target dir |
-+--------------------+--------------------------------------------+
-| Ctrl + s | Save |
-+--------------------+--------------------------------------------+
-| Ctrl + d | Copy the current label and rect box |
-+--------------------+--------------------------------------------+
-| Ctrl + Shift + d | Delete the current image |
-+--------------------+--------------------------------------------+
-| Space | Flag the current image as verified |
-+--------------------+--------------------------------------------+
-| w | Create a rect box |
-+--------------------+--------------------------------------------+
-| d | Next image |
-+--------------------+--------------------------------------------+
-| a | Previous image |
-+--------------------+--------------------------------------------+
-| del | Delete the selected rect box |
-+--------------------+--------------------------------------------+
-| Ctrl++ | Zoom in |
-+--------------------+--------------------------------------------+
-| Ctrl-- | Zoom out |
-+--------------------+--------------------------------------------+
-| ↑→↓← | Keyboard arrows to move selected rect box |
-+--------------------+--------------------------------------------+
++--------------------+------------------------------------------------------------------------------------------+
+| Ctrl + u | Load all of the images from a directory |
++--------------------+------------------------------------------------------------------------------------------+
+| Ctrl + r | Change the default annotation target dir |
++--------------------+------------------------------------------------------------------------------------------+
+| Ctrl + s | Save |
++--------------------+------------------------------------------------------------------------------------------+
+| Ctrl + d | Copy the current label and rect box |
++--------------------+------------------------------------------------------------------------------------------+
+| Ctrl + Shift + d | Delete the current image |
++--------------------+------------------------------------------------------------------------------------------+
+| Space | Flag the current image as verified |
++--------------------+------------------------------------------------------------------------------------------+
+| w | Create a rect box |
++--------------------+------------------------------------------------------------------------------------------+
+| d | Next image |
++--------------------+------------------------------------------------------------------------------------------+
+| a | Previous image |
++--------------------+------------------------------------------------------------------------------------------+
+| del | Delete the selected rect box |
++--------------------+------------------------------------------------------------------------------------------+
+| Ctrl++ | Zoom in |
++--------------------+------------------------------------------------------------------------------------------+
+| Ctrl-- | Zoom out |
++--------------------+------------------------------------------------------------------------------------------+
+| ↑→↓← | Keyboard arrows to move selected rect box |
++--------------------+------------------------------------------------------------------------------------------+
+| z | Counter-clockwise rotation of the selected rect box at a large angle. (RotatedYOLO Only) |
++--------------------+------------------------------------------------------------------------------------------+
+| x | Counter-clockwise rotation of the selected rect box at a small angle. (RotatedYOLO Only) |
++--------------------+------------------------------------------------------------------------------------------+
+| c | Clockwise rotation of the selected rect box at a small angle. (RotatedYOLO Only) |
++--------------------+------------------------------------------------------------------------------------------+
+| v | Clockwise rotation of the selected rect box at a large angle. (RotatedYOLO Only) |
++--------------------+------------------------------------------------------------------------------------------+
**Verify Image:**
diff --git a/demo/demo6.png b/demo/demo6.png
new file mode 100644
index 000000000..457855088
Binary files /dev/null and b/demo/demo6.png differ
diff --git a/labelImg.py b/labelImg.py
index efd8a2976..d5097abfd 100755
--- a/labelImg.py
+++ b/labelImg.py
@@ -45,6 +45,7 @@
from libs.yolo_io import TXT_EXT
from libs.create_ml_io import CreateMLReader
from libs.create_ml_io import JSON_EXT
+from libs.rotated_yolo_io import RotatedYOLOReader
from libs.ustr import ustr
from libs.hashableQListWidgetItem import HashableQListWidgetItem
@@ -252,6 +253,8 @@ def get_format_meta(format):
return '&YOLO', 'format_yolo'
elif format == LabelFileFormat.CREATE_ML:
return '&CreateML', 'format_createml'
+ elif format == LabelFileFormat.ROTATED_YOLO:
+ return '&RotatedYOLO', 'format_rotated_yolo'
save_format = action(get_format_meta(self.label_file_format)[0],
self.change_format, 'Ctrl+Y',
@@ -409,6 +412,10 @@ def get_format_meta(format):
recentFiles=QMenu(get_str('menu_openRecent')),
labelList=label_menu)
+ # Save label to image folder
+ self.save_label_to_image_folder = QAction(get_str('saveLabelToImageFolder'), self)
+ self.save_label_to_image_folder.setCheckable(True)
+ self.save_label_to_image_folder.setChecked(settings.get(SETTING_SAVE_LABEL_TO_IMAGE_FOLDER, False))
# Auto saving : Enable auto saving if pressing next
self.auto_saving = QAction(get_str('autoSaveMode'), self)
self.auto_saving.setCheckable(True)
@@ -431,6 +438,7 @@ def get_format_meta(format):
add_actions(self.menus.help, (help_default, show_info, show_shortcut))
add_actions(self.menus.view, (
self.auto_saving,
+ self.save_label_to_image_folder,
self.single_class_mode,
self.display_label_option,
labels, advanced_mode, None,
@@ -550,6 +558,7 @@ def keyPressEvent(self, event):
# Support Functions #
def set_format(self, save_format):
+ self.canvas.canDrawRotatedRect = False
if save_format == FORMAT_PASCALVOC:
self.actions.save_format.setText(FORMAT_PASCALVOC)
self.actions.save_format.setIcon(new_icon("format_voc"))
@@ -567,6 +576,13 @@ def set_format(self, save_format):
self.actions.save_format.setIcon(new_icon("format_createml"))
self.label_file_format = LabelFileFormat.CREATE_ML
LabelFile.suffix = JSON_EXT
+
+ elif save_format == FORMAT_ROTATED_YOLO:
+ self.canvas.canDrawRotatedRect = True
+ self.actions.save_format.setText(FORMAT_ROTATED_YOLO)
+ self.actions.save_format.setIcon(new_icon("format_yolo"))
+ self.label_file_format = LabelFileFormat.ROTATED_YOLO
+ LabelFile.suffix = TXT_EXT
def change_format(self):
if self.label_file_format == LabelFileFormat.PASCAL_VOC:
@@ -574,6 +590,8 @@ def change_format(self):
elif self.label_file_format == LabelFileFormat.YOLO:
self.set_format(FORMAT_CREATEML)
elif self.label_file_format == LabelFileFormat.CREATE_ML:
+ self.set_format(FORMAT_ROTATED_YOLO)
+ elif self.label_file_format == LabelFileFormat.ROTATED_YOLO:
self.set_format(FORMAT_PASCALVOC)
else:
raise ValueError('Unknown label file format.')
@@ -724,7 +742,7 @@ def toggle_draw_mode(self, edit=True):
def set_create_mode(self):
assert self.advanced()
self.toggle_draw_mode(False)
-
+
def set_edit_mode(self):
assert self.advanced()
self.toggle_draw_mode(True)
@@ -837,7 +855,7 @@ def remove_label(self, shape):
def load_labels(self, shapes):
s = []
- for label, points, line_color, fill_color, difficult in shapes:
+ for label, points, line_color, fill_color, difficult, direction in shapes:
shape = Shape(label=label)
for x, y in points:
@@ -848,6 +866,7 @@ def load_labels(self, shapes):
shape.add_point(QPointF(x, y))
shape.difficult = difficult
+ shape.direction = direction
shape.close()
s.append(shape)
@@ -888,7 +907,10 @@ def format_shape(s):
fill_color=s.fill_color.getRgb(),
points=[(p.x(), p.y()) for p in s.points],
# add chris
- difficult=s.difficult)
+ difficult=s.difficult,
+ direction=s.direction,
+ center=s.center,
+ )
shapes = [format_shape(shape) for shape in self.canvas.shapes]
# Can add different annotation formats here
@@ -908,6 +930,11 @@ def format_shape(s):
annotation_file_path += JSON_EXT
self.label_file.save_create_ml_format(annotation_file_path, shapes, self.file_path, self.image_data,
self.label_hist, self.line_color.getRgb(), self.fill_color.getRgb())
+ elif self.label_file_format == LabelFileFormat.ROTATED_YOLO:
+ if annotation_file_path[-4:].lower() != ".txt":
+ annotation_file_path += TXT_EXT
+ self.label_file.save_rotated_yolo_format(annotation_file_path, shapes, self.file_path, self.image_data, self.label_hist,
+ self.line_color.getRgb(), self.fill_color.getRgb())
else:
self.label_file.save(annotation_file_path, shapes, self.file_path, self.image_data,
self.line_color.getRgb(), self.fill_color.getRgb())
@@ -1099,6 +1126,9 @@ def load_file(self, file_path=None):
# Make sure that filePath is a regular python string, rather than QString
file_path = ustr(file_path)
+ if self.save_label_to_image_folder.isChecked():
+ self.default_save_dir = os.path.dirname(file_path)
+
# Fix bug: An index error after select a directory when open a new file.
unicode_file_path = ustr(file_path)
unicode_file_path = os.path.abspath(unicode_file_path)
@@ -1178,33 +1208,42 @@ def counter_str(self):
return '[{} / {}]'.format(self.cur_img_idx + 1, self.img_count)
def show_bounding_box_from_annotation_file(self, file_path):
- if self.default_save_dir is not None:
- basename = os.path.basename(os.path.splitext(file_path)[0])
- xml_path = os.path.join(self.default_save_dir, basename + XML_EXT)
- txt_path = os.path.join(self.default_save_dir, basename + TXT_EXT)
- json_path = os.path.join(self.default_save_dir, basename + JSON_EXT)
-
- """Annotation file priority:
- PascalXML > YOLO
- """
- if os.path.isfile(xml_path):
- self.load_pascal_xml_by_filename(xml_path)
- elif os.path.isfile(txt_path):
- self.load_yolo_txt_by_filename(txt_path)
- elif os.path.isfile(json_path):
- self.load_create_ml_json_by_filename(json_path, file_path)
+ if file_path is not None:
+ if self.default_save_dir is not None:
+ basename = os.path.basename(os.path.splitext(file_path)[0])
+ xml_path = os.path.join(self.default_save_dir, basename + XML_EXT)
+ txt_path = os.path.join(self.default_save_dir, basename + TXT_EXT)
+ json_path = os.path.join(self.default_save_dir, basename + JSON_EXT)
+
+ """Annotation file priority:
+ PascalXML > YOLO
+ """
+ if os.path.isfile(xml_path):
+ self.load_pascal_xml_by_filename(xml_path)
+ elif os.path.isfile(txt_path):
+ self.check_txt_label_type(txt_path)
+ if self.label_file_format == LabelFileFormat.ROTATED_YOLO:
+ self.load_rotated_yolo_txt_by_filename(txt_path)
+ else:
+ self.load_yolo_txt_by_filename(txt_path)
+ elif os.path.isfile(json_path):
+ self.load_create_ml_json_by_filename(json_path, file_path)
- else:
- xml_path = os.path.splitext(file_path)[0] + XML_EXT
- txt_path = os.path.splitext(file_path)[0] + TXT_EXT
- json_path = os.path.splitext(file_path)[0] + JSON_EXT
-
- if os.path.isfile(xml_path):
- self.load_pascal_xml_by_filename(xml_path)
- elif os.path.isfile(txt_path):
- self.load_yolo_txt_by_filename(txt_path)
- elif os.path.isfile(json_path):
- self.load_create_ml_json_by_filename(json_path, file_path)
+ else:
+ xml_path = os.path.splitext(file_path)[0] + XML_EXT
+ txt_path = os.path.splitext(file_path)[0] + TXT_EXT
+ json_path = os.path.splitext(file_path)[0] + JSON_EXT
+
+ if os.path.isfile(xml_path):
+ self.load_pascal_xml_by_filename(xml_path)
+ elif os.path.isfile(txt_path):
+ self.check_txt_label_type(txt_path)
+ if self.label_file_format == LabelFileFormat.ROTATED_YOLO:
+ self.load_rotated_yolo_txt_by_filename(txt_path)
+ else:
+ self.load_yolo_txt_by_filename(txt_path)
+ elif os.path.isfile(json_path):
+ self.load_create_ml_json_by_filename(json_path, file_path)
def resizeEvent(self, event):
@@ -1270,6 +1309,7 @@ def closeEvent(self, event):
settings[SETTING_LAST_OPEN_DIR] = ''
settings[SETTING_AUTO_SAVE] = self.auto_saving.isChecked()
+ settings[SETTING_SAVE_LABEL_TO_IMAGE_FOLDER] = self.save_label_to_image_folder.isChecked()
settings[SETTING_SINGLE_CLASS] = self.single_class_mode.isChecked()
settings[SETTING_PAINT_LABEL] = self.display_label_option.isChecked()
settings[SETTING_DRAW_SQUARE] = self.draw_squares_option.isChecked()
@@ -1638,10 +1678,29 @@ def load_yolo_txt_by_filename(self, txt_path):
self.set_format(FORMAT_YOLO)
t_yolo_parse_reader = YoloReader(txt_path, self.image)
shapes = t_yolo_parse_reader.get_shapes()
- print(shapes)
self.load_labels(shapes)
self.canvas.verified = t_yolo_parse_reader.verified
+ def load_rotated_yolo_txt_by_filename(self, txt_path):
+ if self.file_path is None:
+ return
+ if os.path.isfile(txt_path) is False:
+ return
+
+ self.set_format(FORMAT_ROTATED_YOLO)
+ t_yolo_parse_reader = RotatedYOLOReader(txt_path)
+ shapes = t_yolo_parse_reader.get_shapes()
+ self.load_labels(shapes)
+ self.canvas.verified = t_yolo_parse_reader.verified
+
+ def check_txt_label_type(self, txt_path):
+ with open(txt_path, 'r') as f:
+ line = f.readline()
+ if len(line.strip().split()) == 5:
+ self.set_format(FORMAT_YOLO)
+ else:
+ self.set_format(FORMAT_ROTATED_YOLO)
+
def load_create_ml_json_by_filename(self, json_path, file_path):
if self.file_path is None:
return
diff --git a/libs/canvas.py b/libs/canvas.py
index ca7986ff3..a3c112725 100644
--- a/libs/canvas.py
+++ b/libs/canvas.py
@@ -1,3 +1,4 @@
+import math
try:
from PyQt5.QtGui import *
@@ -66,6 +67,9 @@ def __init__(self, *args, **kwargs):
self.verified = False
self.draw_square = False
+ # judge can draw rotate rect
+ self.canDrawRotatedRect = True
+
# initialisation for panning
self.pan_initial_pos = QPoint()
@@ -166,7 +170,11 @@ def mouseMoveEvent(self, ev):
# Polygon copy moving.
if Qt.RightButton & ev.buttons():
- if self.selected_shape_copy and self.prev_point:
+ if self.selected_vertex() and self.canDrawRotatedRect:
+ self.boundedRotateShape(pos)
+ self.shapeMoved.emit()
+ self.repaint()
+ elif self.selected_shape_copy and self.prev_point:
self.override_cursor(CURSOR_MOVE)
self.bounded_move_shape(self.selected_shape_copy, pos)
self.repaint()
@@ -396,42 +404,123 @@ def snap_point_to_canvas(self, x, y):
return x, y, True
return x, y, False
-
+
def bounded_move_vertex(self, pos):
index, shape = self.h_vertex, self.h_shape
point = shape[index]
- if self.out_of_pixmap(pos):
- size = self.pixmap.size()
- clipped_x = min(max(0, pos.x()), size.width())
- clipped_y = min(max(0, pos.y()), size.height())
- pos = QPointF(clipped_x, clipped_y)
-
- if self.draw_square:
- opposite_point_index = (index + 2) % 4
- opposite_point = shape[opposite_point_index]
-
- min_size = min(abs(pos.x() - opposite_point.x()), abs(pos.y() - opposite_point.y()))
- direction_x = -1 if pos.x() - opposite_point.x() < 0 else 1
- direction_y = -1 if pos.y() - opposite_point.y() < 0 else 1
- shift_pos = QPointF(opposite_point.x() + direction_x * min_size - point.x(),
- opposite_point.y() + direction_y * min_size - point.y())
+ if self.canDrawRotatedRect:
+ if self.out_of_pixmap(pos):
+ return
+
+ sindex = (index + 2) % 4
+ p2,p3,p4 = self.getAdjointPoints(shape.direction, shape[sindex], pos, index)
+
+ pcenter = (pos+p3)/2
+ # if one pixal out of map , do nothing
+ if self.out_of_pixmap(pcenter) or self.out_of_pixmap(p2) or \
+ self.out_of_pixmap(p3) or self.out_of_pixmap(p4):
+ return
+
+ # move 4 pixal one by one
+ shape.move_vertex_by(index, pos - point)
+ lindex = (index + 1) % 4
+
+ rindex = (index + 3) % 4
+ shape[lindex] = p2
+ shape[rindex] = p4
+ shape.close()
else:
- shift_pos = pos - point
+ if self.out_of_pixmap(pos):
+ size = self.pixmap.size()
+ clipped_x = min(max(0, pos.x()), size.width())
+ clipped_y = min(max(0, pos.y()), size.height())
+ pos = QPointF(clipped_x, clipped_y)
+
+ if self.draw_square:
+ opposite_point_index = (index + 2) % 4
+ opposite_point = shape[opposite_point_index]
+
+ min_size = min(abs(pos.x() - opposite_point.x()), abs(pos.y() - opposite_point.y()))
+ direction_x = -1 if pos.x() - opposite_point.x() < 0 else 1
+ direction_y = -1 if pos.y() - opposite_point.y() < 0 else 1
+ shift_pos = QPointF(opposite_point.x() + direction_x * min_size - point.x(),
+ opposite_point.y() + direction_y * min_size - point.y())
+ else:
+ shift_pos = pos - point
+
+ shape.move_vertex_by(index, shift_pos)
+
+ left_index = (index + 1) % 4
+ right_index = (index + 3) % 4
+ left_shift = None
+ right_shift = None
+ if index % 2 == 0:
+ right_shift = QPointF(shift_pos.x(), 0)
+ left_shift = QPointF(0, shift_pos.y())
+ else:
+ left_shift = QPointF(shift_pos.x(), 0)
+ right_shift = QPointF(0, shift_pos.y())
+ shape.move_vertex_by(right_index, right_shift)
+ shape.move_vertex_by(left_index, left_shift)
+
+ def getAdjointPoints(self, theta, p3, p1, index):
+ a1 = math.tan(theta)
+ if (a1 == 0):
+ if index % 2 == 0:
+ p2 = QPointF(p3.x(), p1.y())
+ p4 = QPointF(p1.x(), p3.y())
+ else:
+ p4 = QPointF(p3.x(), p1.y())
+ p2 = QPointF(p1.x(), p3.y())
+ else:
+ a3 = a1
+ a2 = - 1/a1
+ a4 = - 1/a1
+ b1 = p1.y() - a1 * p1.x()
+ b2 = p1.y() - a2 * p1.x()
+ b3 = p3.y() - a1 * p3.x()
+ b4 = p3.y() - a2 * p3.x()
+
+ if index % 2 == 0:
+ p2 = self.getCrossPoint(a1,b1,a4,b4)
+ p4 = self.getCrossPoint(a2,b2,a3,b3)
+ else:
+ p4 = self.getCrossPoint(a1,b1,a4,b4)
+ p2 = self.getCrossPoint(a2,b2,a3,b3)
+
+ return p2,p3,p4
+
+ def getCrossPoint(self,a1,b1,a2,b2):
+ x = (b2-b1)/(a1-a2)
+ y = (a1*b2 - a2*b1)/(a1-a2)
+ return QPointF(x,y)
+
+ def boundedRotateShape(self, pos):
+ index, shape = self.h_vertex, self.h_shape
+ point = shape[index]
+
+ angle = self.getAngle(shape.center, pos, point)
+ if not self.rotateOutOfBound(angle):
+ shape.rotate(angle)
+ self.prev_point = pos
+
+ def getAngle(self, center, p1, p2):
+ dx1 = p1.x() - center.x()
+ dy1 = p1.y() - center.y()
- shape.move_vertex_by(index, shift_pos)
+ dx2 = p2.x() - center.x()
+ dy2 = p2.y() - center.y()
- left_index = (index + 1) % 4
- right_index = (index + 3) % 4
- left_shift = None
- right_shift = None
- if index % 2 == 0:
- right_shift = QPointF(shift_pos.x(), 0)
- left_shift = QPointF(0, shift_pos.y())
+ c = math.sqrt(dx1*dx1 + dy1*dy1) * math.sqrt(dx2*dx2 + dy2*dy2)
+ if c == 0: return 0
+ y = (dx1*dx2+dy1*dy2)/c
+ if y>1: return 0
+ angle = math.acos(y)
+
+ if (dx1*dy2-dx2*dy1)>0:
+ return angle
else:
- left_shift = QPointF(shift_pos.x(), 0)
- right_shift = QPointF(0, shift_pos.y())
- shape.move_vertex_by(right_index, right_shift)
- shape.move_vertex_by(left_index, left_shift)
+ return -angle
def bounded_move_shape(self, shape, pos):
if self.out_of_pixmap(pos):
@@ -643,6 +732,32 @@ def keyPressEvent(self, ev):
self.move_one_pixel('Up')
elif key == Qt.Key_Down and self.selected_shape:
self.move_one_pixel('Down')
+ elif key == Qt.Key_Z and self.selected_shape and\
+ self.canDrawRotatedRect and not self.rotateOutOfBound(0.1):
+ self.selected_shape.rotate(0.1)
+ self.shapeMoved.emit()
+ self.update()
+ elif key == Qt.Key_X and self.selected_shape and\
+ self.canDrawRotatedRect and not self.rotateOutOfBound(0.01):
+ self.selected_shape.rotate(0.01)
+ self.shapeMoved.emit()
+ self.update()
+ elif key == Qt.Key_C and self.selected_shape and\
+ self.canDrawRotatedRect and not self.rotateOutOfBound(-0.01):
+ self.selected_shape.rotate(-0.01)
+ self.shapeMoved.emit()
+ self.update()
+ elif key == Qt.Key_V and self.selected_shape and\
+ self.canDrawRotatedRect and not self.rotateOutOfBound(-0.1):
+ self.selected_shape.rotate(-0.1)
+ self.shapeMoved.emit()
+ self.update()
+
+ def rotateOutOfBound(self, angle):
+ for i, p in enumerate(self.selected_shape.points):
+ if self.out_of_pixmap(self.selected_shape.rotatePoint(p,angle)):
+ return True
+ return False
def move_one_pixel(self, direction):
# print(self.selectedShape.points)
@@ -652,24 +767,28 @@ def move_one_pixel(self, direction):
self.selected_shape.points[1] += QPointF(-1.0, 0)
self.selected_shape.points[2] += QPointF(-1.0, 0)
self.selected_shape.points[3] += QPointF(-1.0, 0)
+ self.selected_shape.center += QPointF(-1.0, 0)
elif direction == 'Right' and not self.move_out_of_bound(QPointF(1.0, 0)):
# print("move Right one pixel")
self.selected_shape.points[0] += QPointF(1.0, 0)
self.selected_shape.points[1] += QPointF(1.0, 0)
self.selected_shape.points[2] += QPointF(1.0, 0)
self.selected_shape.points[3] += QPointF(1.0, 0)
+ self.selected_shape.center += QPointF(1.0, 0)
elif direction == 'Up' and not self.move_out_of_bound(QPointF(0, -1.0)):
# print("move Up one pixel")
self.selected_shape.points[0] += QPointF(0, -1.0)
self.selected_shape.points[1] += QPointF(0, -1.0)
self.selected_shape.points[2] += QPointF(0, -1.0)
self.selected_shape.points[3] += QPointF(0, -1.0)
+ self.selected_shape.center += QPointF(0.0, -1.0)
elif direction == 'Down' and not self.move_out_of_bound(QPointF(0, 1.0)):
# print("move Down one pixel")
self.selected_shape.points[0] += QPointF(0, 1.0)
self.selected_shape.points[1] += QPointF(0, 1.0)
self.selected_shape.points[2] += QPointF(0, 1.0)
self.selected_shape.points[3] += QPointF(0, 1.0)
+ self.selected_shape.center += QPointF(0.0, 1.0)
self.shapeMoved.emit()
self.repaint()
diff --git a/libs/constants.py b/libs/constants.py
index 1efda037c..2c1b649e8 100644
--- a/libs/constants.py
+++ b/libs/constants.py
@@ -11,9 +11,11 @@
SETTING_PAINT_LABEL = 'paintlabel'
SETTING_LAST_OPEN_DIR = 'lastOpenDir'
SETTING_AUTO_SAVE = 'autosave'
+SETTING_SAVE_LABEL_TO_IMAGE_FOLDER = 'saveLabelToImageFolder'
SETTING_SINGLE_CLASS = 'singleclass'
FORMAT_PASCALVOC='PascalVOC'
FORMAT_YOLO='YOLO'
+FORMAT_ROTATED_YOLO='RotatedYOLO'
FORMAT_CREATEML='CreateML'
SETTING_DRAW_SQUARE = 'draw/square'
SETTING_LABEL_FILE_FORMAT= 'labelFileFormat'
diff --git a/libs/create_ml_io.py b/libs/create_ml_io.py
index 3aca8d676..69a43bf4f 100644
--- a/libs/create_ml_io.py
+++ b/libs/create_ml_io.py
@@ -129,7 +129,7 @@ def add_shape(self, label, bnd_box):
y_max = bnd_box["y"] + (bnd_box["height"] / 2)
points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
- self.shapes.append((label, points, None, None, True))
+ self.shapes.append((label, points, None, None, True, 0))
def get_shapes(self):
return self.shapes
diff --git a/libs/labelFile.py b/libs/labelFile.py
index 185570bcb..7e62a838a 100644
--- a/libs/labelFile.py
+++ b/libs/labelFile.py
@@ -1,5 +1,6 @@
# Copyright (c) 2016 Tzutalin
# Create by TzuTaLin
+import math
try:
from PyQt5.QtGui import QImage
@@ -13,12 +14,14 @@
from libs.pascal_voc_io import PascalVocWriter
from libs.pascal_voc_io import XML_EXT
from libs.yolo_io import YOLOWriter
+from libs.rotated_yolo_io import RotatedYOLOWriter
class LabelFileFormat(Enum):
PASCAL_VOC = 1
YOLO = 2
CREATE_ML = 3
+ ROTATED_YOLO = 4
class LabelFileError(Exception):
@@ -111,6 +114,32 @@ def save_yolo_format(self, filename, shapes, image_path, image_data, class_list,
writer.save(target_file=filename, class_list=class_list)
return
+ def save_rotated_yolo_format(self, filename, shapes, image_path, image_data,
+ line_color=None, fill_color=None, database_src=None):
+ img_folder_path = os.path.dirname(image_path)
+ img_folder_name = os.path.split(img_folder_path)[-1]
+ img_file_name = os.path.basename(image_path)
+ if isinstance(image_data, QImage):
+ image = image_data
+ else:
+ image = QImage()
+ image.load(image_path)
+ image_shape = [image.height(), image.width(),
+ 1 if image.isGrayscale() else 3]
+ writer = RotatedYOLOWriter(img_folder_name, img_file_name,
+ image_shape, local_img_path=image_path)
+ writer.verified = self.verified
+
+ for shape in shapes:
+ label = shape['label']
+ # Add Chris
+ difficult = int(shape['difficult'])
+ points = shape['points']
+ writer.add_bnd_box(points, label, difficult)
+
+ writer.save(target_file=filename)
+ return
+
def toggle_verify(self):
self.verified = not self.verified
@@ -172,3 +201,4 @@ def convert_points_to_bnd_box(points):
y_min = 1
return int(x_min), int(y_min), int(x_max), int(y_max)
+
\ No newline at end of file
diff --git a/libs/pascal_voc_io.py b/libs/pascal_voc_io.py
index d8f7d690b..ed7006bc8 100644
--- a/libs/pascal_voc_io.py
+++ b/libs/pascal_voc_io.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf8 -*-
-import sys
from xml.etree import ElementTree
from xml.etree.ElementTree import Element, SubElement
from lxml import etree
@@ -82,7 +81,7 @@ def add_bnd_box(self, x_min, y_min, x_max, y_max, name, difficult):
bnd_box['name'] = name
bnd_box['difficult'] = difficult
self.box_list.append(bnd_box)
-
+
def append_objects(self, top):
for each_object in self.box_list:
object_item = SubElement(top, 'object')
@@ -146,8 +145,8 @@ def add_shape(self, label, bnd_box, difficult):
x_max = int(float(bnd_box.find('xmax').text))
y_max = int(float(bnd_box.find('ymax').text))
points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
- self.shapes.append((label, points, None, None, difficult))
-
+ self.shapes.append((label, points, None, None, difficult, 0))
+
def parse_xml(self):
assert self.file_path.endswith(XML_EXT), "Unsupported file format"
parser = etree.XMLParser(encoding=ENCODE_METHOD)
diff --git a/libs/rotated_yolo_io.py b/libs/rotated_yolo_io.py
new file mode 100644
index 000000000..cf036f22d
--- /dev/null
+++ b/libs/rotated_yolo_io.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# -*- coding: utf8 -*-
+import math
+import codecs
+from libs.constants import DEFAULT_ENCODING
+
+
+TXT_EXT = '.txt'
+ENCODE_METHOD = DEFAULT_ENCODING
+
+
+class RotatedYOLOWriter:
+
+ def __init__(self, folder_name, filename, img_size, database_src='Unknown', local_img_path=None):
+ self.folder_name = folder_name
+ self.filename = filename
+ self.database_src = database_src
+ self.img_size = img_size
+ self.box_list = []
+ self.local_img_path = local_img_path
+ self.verified = False
+
+ def add_bnd_box(self, points, name, difficult):
+ flat_points = []
+ for x, y in points:
+ flat_points.extend([x, y])
+ bndbox = {'points': flat_points, 'name': name, 'difficult': difficult}
+ self.box_list.append(bndbox)
+
+ def save(self, target_file=None):
+ out_file = None
+ if target_file is None:
+ out_file = codecs.open(
+ self.filename + TXT_EXT, 'w', encoding=ENCODE_METHOD)
+ else:
+ out_file = codecs.open(target_file, 'w', encoding=ENCODE_METHOD)
+
+ for box in self.box_list:
+ x1, y1, x2, y2, x3, y3, x4, y4 = box['points']
+ out_file.write("%.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f %s %s\n" %
+ (x1, y1, x2, y2, x3, y3, x4, y4, box['name'], box['difficult']))
+ out_file.close()
+
+
+class RotatedYOLOReader:
+
+ def __init__(self, file_path):
+ self.shapes = []
+ self.file_path = file_path
+ self.verified = False
+
+ self.parse_rotated_yolo_format()
+
+ def get_shapes(self):
+ return self.shapes
+
+ def get_angle(self, x1, y1, x2, y2):
+ return math.atan2(y2 - y1, x2 - x1)
+
+ def add_shape(self, x1, y1, x2, y2, x3, y3, x4, y4, label, difficult):
+ angle = self.get_angle(x1, y1, x2, y2)
+ points = [(x1, y1), (x2, y2), (x3, y3), (x4, y4)]
+ self.shapes.append((label, points, None, None, difficult, angle))
+
+ def parse_rotated_yolo_format(self):
+ bnd_box_file = open(self.file_path, 'r')
+ for bndBox in bnd_box_file:
+ x1, y1, x2, y2, x3, y3, x4, y4, label, difficult = bndBox.strip().split(' ')
+ self.add_shape(float(x1), float(y1), float(x2), float(y2), float(x3), float(y3),
+ float(x4), float(y4), label, int(difficult))
diff --git a/libs/shape.py b/libs/shape.py
index 65b5bac12..b30dca680 100644
--- a/libs/shape.py
+++ b/libs/shape.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
-
+import math
try:
from PyQt5.QtGui import *
@@ -53,6 +53,9 @@ def __init__(self, label=None, line_color=None, difficult=False, paint_label=Fal
self.MOVE_VERTEX: (1.5, self.P_SQUARE),
}
+ self.direction = 0
+ self.center = None
+
self._closed = False
if line_color is not None:
@@ -62,6 +65,7 @@ def __init__(self, label=None, line_color=None, difficult=False, paint_label=Fal
self.line_color = line_color
def close(self):
+ self.center = QPointF((self.points[0].x()+self.points[2].x()) / 2, (self.points[0].y()+self.points[2].y()) / 2)
self._closed = True
def reach_max_points(self):
@@ -84,6 +88,21 @@ def is_closed(self):
def set_open(self):
self._closed = False
+ def rotate(self, theta):
+ for i, p in enumerate(self.points):
+ self.points[i] = self.rotatePoint(p, theta)
+ self.direction -= theta
+ self.direction = self.direction % (2 * math.pi)
+
+ def rotatePoint(self, p, theta):
+ order = p-self.center
+ cosTheta = math.cos(theta)
+ sinTheta = math.sin(theta)
+ pResx = cosTheta * order.x() + sinTheta * order.y()
+ pResy = - sinTheta * order.x() + cosTheta * order.y()
+ pRes = QPointF(self.center.x() + pResx, self.center.y() + pResy)
+ return pRes
+
def paint(self, painter):
if self.points:
color = self.select_line_color if self.selected else self.line_color
@@ -189,6 +208,8 @@ def highlight_clear(self):
def copy(self):
shape = Shape("%s" % self.label)
shape.points = [p for p in self.points]
+ shape.center = self.center
+ shape.direction = self.direction
shape.fill = self.fill
shape.selected = self.selected
shape._closed = self._closed
diff --git a/libs/yolo_io.py b/libs/yolo_io.py
index 192e2c785..c6a04a7be 100644
--- a/libs/yolo_io.py
+++ b/libs/yolo_io.py
@@ -116,7 +116,7 @@ def get_shapes(self):
def add_shape(self, label, x_min, y_min, x_max, y_max, difficult):
points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
- self.shapes.append((label, points, None, None, difficult))
+ self.shapes.append((label, points, None, None, difficult, 0))
def yolo_line_to_shape(self, class_index, x_center, y_center, w, h):
label = self.classes[int(class_index)]
diff --git a/readme/README.jp.rst b/readme/README.jp.rst
index 00a550ec7..e55ed1c2d 100644
--- a/readme/README.jp.rst
+++ b/readme/README.jp.rst
@@ -22,7 +22,7 @@ labelImg
LabelImgは、PythonとQtを使うアノテーション補助ツールです。
-このツールはPascalVOCフォーマットとYOLOとCreateMLをサポートしています。
+このツールはPascalVOC形式、YOLO、RotatedYOLO、そしてCreateMLをサポートしています。
.. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo3.jpg
:alt: Demo Image
@@ -30,6 +30,9 @@ LabelImgは、PythonとQtを使うアノテーション補助ツールです。
.. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo.jpg
:alt: Demo Image
+.. image:: /demo/demo6.png
+ :alt: Demo Image
+
`サンプル動画は にあります。`__
インストール方法
@@ -190,6 +193,14 @@ Dockerの場合
+--------------------+--------------------------------------------+
| ↑→↓← | 十字キーで矩形を移動する |
+--------------------+--------------------------------------------+
+| z | 逆時針大角度に矩形を回転する (RotatedYOLOのみ対応)|
++--------------------+--------------------------------------------+
+| x | 逆時針に矩形を少しずつ回転する (RotatedYOLOのみ対応)|
++--------------------+--------------------------------------------+
+| c | 時計回りに矩形を少しずつ回転する (RotatedYOLOのみ対応)|
++--------------------+--------------------------------------------+
+| v | 時計回りに矩形を大きく回転する (RotatedYOLOのみ対応)|
++--------------------+--------------------------------------------+
開発に参加するには?
~~~~~~~~~~~~~~~~~~~~~
diff --git a/readme/README.zh.rst b/readme/README.zh.rst
index 4ea22e0e8..0dbf2e53d 100644
--- a/readme/README.zh.rst
+++ b/readme/README.zh.rst
@@ -21,7 +21,7 @@ LabelImg
LabelImg 是影像標註工具,它是用python 和 QT 寫成的.
-支持的儲存格式包括PASCAL VOC format, YOLO, createML.
+支持的儲存格式包括PASCAL VOC format, YOLO, RotatedYOLO, createML.
.. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo3.jpg
:alt: Demo Image
@@ -29,6 +29,9 @@ LabelImg 是影像標註工具,它是用python 和 QT 寫成的.
.. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo.jpg
:alt: Demo Image
+.. image:: /demo/demo6.png
+ :alt: Demo Image
+
`展示影片 `__
安裝
@@ -188,6 +191,14 @@ Use Docker
+--------------------+--------------------------------------------+
| ↑→↓← | 移動所選的物件區塊 |
+--------------------+--------------------------------------------+
+| z | 逆時針大角度選轉所選的物件區塊 (只支援 RotatedYOLO) |
++--------------------+--------------------------------------------+
+| x | 逆時針小角度選轉所選的物件區塊 (只支援 RotatedYOLO) |
++--------------------+--------------------------------------------+
+| c | 順時針小角度選轉所選的物件區塊 (只支援 RotatedYOLO) |
++--------------------+--------------------------------------------+
+| v | 順時針大角度選轉所選的物件區塊 (只支援 RotatedYOLO) |
++--------------------+--------------------------------------------+
如何貢獻
~~~~~~~~~~~~~~~~~
diff --git a/resources/strings/strings-ja-JP.properties b/resources/strings/strings-ja-JP.properties
index 239c52b37..d06be693b 100644
--- a/resources/strings/strings-ja-JP.properties
+++ b/resources/strings/strings-ja-JP.properties
@@ -75,3 +75,4 @@ fileList=ファイル一覧
files=ファイル
boxLabelText=矩形ラベル
copyPrevBounding=前の画像の矩形ラベルをこの画像にコピー
+saveLabelToImageFolder=画像フォルダにラベルを保存する
\ No newline at end of file
diff --git a/resources/strings/strings-zh-CN.properties b/resources/strings/strings-zh-CN.properties
index e944509a0..6d28cd286 100644
--- a/resources/strings/strings-zh-CN.properties
+++ b/resources/strings/strings-zh-CN.properties
@@ -87,3 +87,4 @@ menu_openRecent=最近打开(&R)
chooseLineColor=选择线条颜色
chooseFillColor=选择填充颜色
drawSquares=绘制正方形
+saveLabelToImageFolder=保存标签到图像文件夹
\ No newline at end of file
diff --git a/resources/strings/strings-zh-TW.properties b/resources/strings/strings-zh-TW.properties
index 162ab2151..f04279f25 100644
--- a/resources/strings/strings-zh-TW.properties
+++ b/resources/strings/strings-zh-TW.properties
@@ -81,3 +81,4 @@ menu_edit=編輯(&E)
menu_view=檢視(&V)
menu_help=說明(&H)
menu_openRecent=最近開啟(&R)
+saveLabelToImageFolder=儲存標籤到圖像目錄
\ No newline at end of file
diff --git a/resources/strings/strings.properties b/resources/strings/strings.properties
index 8c5d14ce8..1be51fdd4 100644
--- a/resources/strings/strings.properties
+++ b/resources/strings/strings.properties
@@ -86,4 +86,5 @@ menu_help=&Help
menu_openRecent=Open &Recent
chooseLineColor=Choose Line Color
chooseFillColor=Choose Fill Color
-drawSquares=Draw Squares
\ No newline at end of file
+drawSquares=Draw Squares
+saveLabelToImageFolder=Save label to image folder
\ No newline at end of file