This commit is contained in:
Jason Goodsell 2019-03-01 12:45:26 +11:00
Родитель 7abf3decfe
Коммит 7d1de0f213
15 изменённых файлов: 617 добавлений и 0 удалений

21
LICENSE Normal file
Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

81
README.md Normal file
Просмотреть файл

@ -0,0 +1,81 @@
# Introduction
Annotation-Factory Python SDK. This package works specifically with Microsoft Cognitive Services detection results. `AnnotationWriter` takes a JSON object received from Cognitive Services and produces annotation files in both VOC and YOLO formats for use in training machine learning models.
[![Build Status](https://dev.azure.com/aussiedevcrew/Annotation-Factory/_apis/build/status/Microsoft.Annotation-Factory?branchName=master)](https://dev.azure.com/aussiedevcrew/Annotation-Factory/_build/latest?definitionId=9&branchName=master)
# Getting Started
1. Install `annotationfactory` package via pip:
```
pip install annotationfactory
```
# Sample to use
```python
from annotationfactory.annotationwriter import AnnotationWriter
import annotationfactory.annotationconverter as converter
example = {
'tagId': 0,
'tagName': 'Apples',
'region': {
'left': 0.288039029,
'top': 0.411838,
'width': 0.291451037,
'height': 0.4237842
}
}
# Initialise AnnotationWriter.
writer = AnnotationWriter()
# Initialise annotation handlers.
writer.initVoc("test.jpg", 608, 608)
writer.initYolo()
# Add VOC object to writer.
writer.addVocObject(example)
writer.addVocObject(example)
# Add YOLO object to writer.
writer.addYoloObject(example)
writer.addYoloObject(example)
# Output VOC annotations to file.
writer.saveVoc("myannotation.xml")
# Output YOLO annotations to file.
writer.saveYolo("myannotation.txt")
# Converts VOC annotations back to CustomVision annotation format.
voc2cv = converter.convertVocFromPath("myannotation.xml")
# Converts YOLO annotations back to CustomVision annotation format.
# Requires a txt file with list of label names as an input.
yolo2cv = converter.convertYoloFromPath("myannotation.txt", "class.names")
```
# Run locally
```
pip install -r requirements.txt
python example/test.py
```
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

Просмотреть файл

Просмотреть файл

@ -0,0 +1,150 @@
import xmltodict
from annotationfactory.models.annotation import Annotation
from annotationfactory.models.region import Region
class AnnotationConverter:
'''
AnnotationConvert helps converts Pascal VOC and YOLO to
CustomVision annotations.
Optional:
classes ([str]): Object class list for use when
converting YOLO annotations.
'''
def __init__(self, classes=None):
self.classes = classes
def convertVoc(self, area: dict, dim: [2]) -> Annotation:
'''
Convert VOC object to CustomVision annotations.
Args:
area (dict): VOC annotation of a single region.
dim ([float, float]): Width and height of target
image.
Returns:
~annotationfactory.models.annotation
'''
(w, h) = dim
region = area["bndbox"]
tag = area["name"]
left = float(region['xmin']) / w
top = float(region['ymin']) / h
width = float(region["xmax"]) / w
height = float(region["ymax"]) / h
region = Region(left, top, width, height)
return Annotation(tagName=tag, region=region)
def convertYolo(self, line: str) -> Annotation:
'''
Convert YOLO object to CustomVision annotations.
Args:
line (str): A single line from YOLO annotation.txt
file.
Returns:
~annotationfactory.models.annotation
'''
region = line.split()
tag = None
(x_centre, y_centre, w, h) = region[1:]
left = float(x_centre) - (float(w) / 2)
top = float(y_centre) - (float(h) / 2)
width = float(w)
height = float(h)
if self.classes is not None:
tag = self.classes[int(region[0])]
region = Region(left, top, width, height)
return Annotation(tagName=tag, region=region)
def convertVocFromPath(path: str) -> [Annotation]:
'''
Convert VOC annotation.xml file to CustomVision annotations.
Args:
path (str): Path directory to annotation.xml file on local machine.
Returns:
list[~annotationfactory.models.annotation]
'''
converter = AnnotationConverter()
with open(path) as f:
voc = xmltodict.parse(f.read())["annotation"]
w = float(voc["size"]["width"])
h = float(voc["size"]["height"])
annotations = []
for i in voc["object"]:
annotations.append(converter.convertVoc(i, (w, h)))
return annotations
def convertYoloFromPath(path: str, classPath: str = None) -> [Annotation]:
'''
Convert YOLO annotation.txt file to CustomVision annotations.
Args:
path (str): Path directory to annotation.xml file on local machine.
classPath (str): Path directory to class.names file on local machine.
Returns:
list[~annotationfactory.models.annotation]
'''
converter = AnnotationConverter(parseFile(classPath))
yolo = parseFile(path)
annotations = []
for i in yolo:
annotations.append(converter.convertYolo(i))
return annotations
def parseFile(path: str) -> [str]:
'''
Reads file from path line-by-line into list.
Args:
path (str): Path directory to any.txt file on local machine.
Returns:
list[str]
'''
with open(path) as f:
result = f.read().split("\n")
if result[-1] == "":
result = result[:-1]
return result

Просмотреть файл

@ -0,0 +1,186 @@
from jinja2 import Environment, PackageLoader
from jsonschema import validate
import json
class AnnotationWriter:
'''
AnnotationWriter is a lightweight VOC/YOLO annotation generator for use in
machine learning.
'''
def __init__(self):
environment = Environment(
loader=PackageLoader('annotationfactory', 'templates'),
keep_trailing_newline=True)
self.annotation_voc_template = environment.get_template(
'annotation.xml')
self.annotation_yolo_template = environment.get_template(
'annotation.txt')
self.schema = json.loads(
environment.get_template("schema.json").render())
self.template_voc_parameters = None
self.template_yolo_parameters = None
def initVoc(self, file, width, height,
depth=3, database='Unknown', segmented=0):
'''
Initialised AnnotationWriter to generate VOC annotations.
Args:
file (str): Name of image associated to annotation.
width (int): Width of image.
height (int): Height of image.
Optional:
depth (int): Depth of image.
database (str): Name of database image belong to.
segmented (int): Number of segmentations.
'''
self.width = width
self.height = height
self.file = file
self.template_voc_parameters = {
'filename': file,
'folder': "images",
'width': self.width,
'height': self.height,
'depth': depth,
'database': database,
'segmented': segmented,
'objects': []
}
def initYolo(self):
'''
Initialised AnnotationWriter to generate YOLO annotations.
'''
self.template_yolo_parameters = []
def addVocObject(
self, annotation, pose='Unspecified', truncated=0, difficult=0):
'''
Add object annotation to VOC writer.
Args:
annotation (dict): Annotation object used to
generate VOC annotation.
Optional:
pose (str): Specify the pose of the annotation.
truncated (int): Set the truncated vaule of annotation.
difficult (int): Set difficulty index of annotation.
Raises:
NotImplementedError
jsonschema.exceptions.ValidationError
'''
if self.template_voc_parameters is None:
raise NotImplementedError("VOC has not been initialised.")
validate(annotation, self.schema)
box = annotation["region"]
name = annotation["tagName"]
xmin = float(box["left"]) * self.width
ymin = float(box["top"]) * self.height
xmax = float(box["width"]) * self.width
ymax = float(box["height"]) * self.height
self.template_voc_parameters['objects'].append({
'name': name,
'xmin': xmin,
'ymin': ymin,
'xmax': xmax,
'ymax': ymax,
'pose': pose,
'truncated': truncated,
'difficult': difficult,
})
def addYoloObject(self, annotation):
'''
Add object annotation to YOLO writer.
Args:
annotation (dict): Annotation object used to
generate YOLO annotation.
Raises:
NotImplementedError
jsonschema.exceptions.ValidationError
'''
if self.template_yolo_parameters is None:
raise NotImplementedError("YOLO has not been initialised.")
validate(annotation, self.schema)
box = annotation["region"]
tag = annotation["tagId"]
x_centre = float(box["left"] + (box["width"] / 2))
y_centre = float(box["top"] + (box["height"] / 2))
x_width = float(box["width"])
y_height = float(box["height"])
self.template_yolo_parameters.append({
'id': tag,
'x_centre': x_centre,
'y_centre': y_centre,
'x_width': x_width,
'y_height': y_height
})
def saveVoc(self, annotation_path):
'''
Output VOC annotations to file.
Args:
annotation_path (str): Path of file to write annotation to.
Raises:
NotImplementedError
'''
if self.template_voc_parameters is None:
raise NotImplementedError("VOC has not been initialised.")
with open(annotation_path, 'w') as file:
content = self.annotation_voc_template.render(
**self.template_voc_parameters)
file.write(content)
def saveYolo(self, annotation_path):
'''
Output YOLO annotations to file.
Args:
annotation_path (str): Path of file to write annotation to.
Raises:
NotImplementedError
'''
if self.template_yolo_parameters is None:
raise NotImplementedError("YOLO has not been initialised.")
with open(annotation_path, 'w') as file:
content = self.annotation_yolo_template.render(
annotation=self.template_yolo_parameters)
file.write(content)

Просмотреть файл

@ -0,0 +1,11 @@
import annotationfactory.models.region as Region
class Annotation:
tagName = None
region = None
def __init__(self, tagName: str = None, region: Region = None):
self.tagName = tagName
self.region = region

Просмотреть файл

@ -0,0 +1,12 @@
class Region:
left = 0
top = 0
width = 0
height = 0
def __init__(self, left: float, top: float, width: float, height: float):
self.left = left
self.top = top
self.width = width
self.height = height

Просмотреть файл

@ -0,0 +1,2 @@
{% for item in annotation %}{{ item.id }} {{ item.x_centre }} {{ item.y_centre }} {{ item.x_width }} {{ item.y_height }}
{% endfor %}

Просмотреть файл

@ -0,0 +1,25 @@
<annotation>
<folder>{{ folder }}</folder>
<filename>{{ filename }}</filename>
<source>
<database>{{ database }}</database>
</source>
<size>
<width>{{ width }}</width>
<height>{{ height }}</height>
<depth>{{ depth }}</depth>
</size>
<segmented>{{ segmented }}</segmented>{% for object in objects %}
<object>
<name>{{ object.name }}</name>
<pose>{{ object.pose }}</pose>
<truncated>{{ object.truncated }}</truncated>
<difficult>{{ object.difficult }}</difficult>
<bndbox>
<xmin>{{ object.xmin }}</xmin>
<ymin>{{ object.ymin }}</ymin>
<xmax>{{ object.xmax }}</xmax>
<ymax>{{ object.ymax }}</ymax>
</bndbox>
</object>{% endfor %}
</annotation>

Просмотреть файл

@ -0,0 +1,18 @@
{
"type": "object",
"properties": {
"tagId": {"type": "number"},
"tagName": {"type": "string"},
"region": {
"type": "object",
"properties": {
"left": {"type": "number"},
"top": {"type": "number"},
"width": {"type": "number"},
"height": {"type": "number"}
},
"required": ["left", "top", "width", "height"]
}
},
"required": ["tagId", "tagName", "region"]
}

42
azure-pipelines.yml Normal file
Просмотреть файл

@ -0,0 +1,42 @@
pool:
vmImage: ubuntu-16.04
trigger:
batch: true
branches:
include:
- master
paths:
include:
- annotationfactory/*
- azure-pipelines.yml
steps:
- task: UsePythonVersion@0
displayName: 'Use Python >= 3.5'
inputs:
versionSpec: '>= 3.5'
addToPath: true
architecture: 'x64' # only applies to windows agents
- script: |
python -m pip install --upgrade pip
pip install setuptools
pip install wheel
pip install twine
pip install --user --upgrade setuptools wheel
pip install --user --upgrade twine
displayName: 'Install dependencies'
- script: 'python setup.py bdist_wheel -v $(build.buildId)'
displayName: 'Build package'
- task: TwineAuthenticate@0
displayName: 'Twine Authenticate'
inputs:
externalFeeds: PyPi
- script: 'twine upload -r annotationfactory --config-file $(PYPIRC_PATH) dist/*'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
displayName: Upload

1
example/class.names Normal file
Просмотреть файл

@ -0,0 +1 @@
apple

30
example/test.py Normal file
Просмотреть файл

@ -0,0 +1,30 @@
from annotationfactory.annotationwriter import AnnotationWriter
import annotationfactory.annotationconverter as converter
example = {
'tagId': 0,
'tagName': 'apples',
'region': {
'left': 0.288039029,
'top': 0.411838,
'width': 0.291451037,
'height': 0.4237842
}
}
writer = AnnotationWriter()
writer.initVoc("test.jpg", 608, 608)
writer.initYolo()
writer.addVocObject(example)
writer.addVocObject(example)
writer.addYoloObject(example)
writer.addYoloObject(example)
writer.saveVoc("myannotation.xml")
writer.saveYolo("myannotation.txt")
converter.convertVocFromPath("myannotation.xml")
converter.convertYoloFromPath("myannotation.txt", "class.names")

3
requirements.txt Normal file
Просмотреть файл

@ -0,0 +1,3 @@
jinja2
jsonschema
xmltodict

35
setup.py Normal file
Просмотреть файл

@ -0,0 +1,35 @@
import setuptools
import sys
with open("README.md", "r", encoding="utf8") as fh:
long_description = fh.read()
version = "0.0.1"
if '-v' in sys.argv:
index = sys.argv.index('-v')
sys.argv.pop(index)
version += '.dev' + sys.argv.pop(index)
setuptools.setup(
name='annotationfactory',
version=version,
description="annotation factory python sdk",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/Microsoft/Annotation-Factory",
license='MIT',
packages=setuptools.find_packages(),
install_requires=[
'jinja2',
'jsonschema',
'xmltodict'
],
package_data={
'annotationfactory': ['templates/*', 'models/*']
},
classifiers=[
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
],
)