Welcome to Moose’s documentation!

Moose是一个数据处理的自动化框架,旨在提供一套快速、灵活、便捷、幂等式的数据处理方案。它不仅提供了常用的数据处理类和函数,也提供了一套命令行式的接口以处理和维护版本内单一、重复、繁琐的任务。

快速上手教程

简介

Moose是一个数据处理的自动化框架,旨在提供一套快速、灵活、便捷、幂等式的数据处理方案。它不仅提供了常用的数据处理类和函数,也提供了一套命令行式的接口以处理和维护版本内单一、重复、繁琐的任务。

Moose 致力于解决什么?

Moose从实际工作中遇到的一系列问题产生:

  • 工作琐碎重复。工作内容在很大程度上是相似的,这既反映在同批次的工作之间可能只是作用对象不同,也反映在不同批次的工作中很多处理的细节是相似或者一致的;
  • 移植性差。脚本在不同的操作系统、开发环境或网络环境中无法正常工作,这导致交接难度变大,需要时间成本去理解、修改或重写代码;
  • 复用性差。由于输入输出的格式不统一,高层代码无法被直接使用,底层函数和类也因为总是需要适配参数而作用有限;
  • 代码膨胀过快。由于移植性差,因此需要同时维护多个版本的代码,这导致功能上的增加和修改都需要成倍的工作量,代码管理成本上升的同时也增大了出错的几率;
  • 对结果没有信心。程序或脚本缺少相关的过程验证,导致得到的结果可信度降低,同时由于数据量大,处理过程中发生的错误也容易被掩盖和忽略;
  • 缺乏日志信息。处理过程本身没有被记录,处理前后的数据关联也不明显,错误难以被追溯和定位。

Moose 有哪些特点?

基于上面的认识,我们编写了Moose框架,它包含以下特点:

  • 分解过程。行为(Action)被拆分成多个步骤或组件,并提供了默认的实现。当业务变化时,只需对特定的部分进行定制化即可,重写之后的组件也可以被发布和纳入到后续的使用中;
  • 适配数据。引入模型(Model)对输入数据进行标准化,避免由于输入的变化引起处理过程的改动;
  • 提供底层库。对于在不同平台上会产生不同效果的函数和方法,我们提供了兼容性更强的替代品,这些重新编写的函数和方法消除了平台的差异性,使开发人员关注更高层的业务逻辑;
  • 作用对象和过程分离。提出订单(Order)的概念,通过将每次处理所需的参数填写到订单文件中,既避免了不同阶段时参数的重复输入(因为上一阶段的输出文件很可能就是下一阶段的输入),又能通过分析订单文件了解数据的流转节点;
  • 命令行工具。提供了丰富的命令行工具简化创建相关文件的操作,交互方式更一致,即使对实现不了解也可以完成相关处理;

如果Moose能解决你的问题或者你对它感兴趣,赶紧了解 安装指南 来安装吧。

安装指南

Moose 运行在Python 2.7和Python 3.3及以上版本。

安装发布版

这是较为推荐的安装方法:

  1. 安装pip。最简单的方法是使用 pip 安装脚本。如果你的Python发布版已经安装了pip,你可能需要更新以避免由于版本过老导致安装失败。
  2. 了解 virtualenvvirtualenvwrapper 并安装教程。这些工具提供了一个独立的Python环境,相比在全局范围内安装包要更加有效且实用,并且不需要管理员权限。
  3. 安装并且激活虚拟环境后,在命令提示符中输入 pip install Moose

安装开发者版本

如果你希望安装最新版本的Moose,可以通过以下教程来尝试:

  1. 确认你已安装Git并且可以从命令行中访问。

  2. 获取Moose的主分支代码::

    $ git clone https://github.com/muma378/moose.git
    

    这会为你在当前目录下创建一个名为 moose 文件夹。

  3. 确认你的Python解释器可以加载Moose代码。最常见的办法是使用 virtualenvvirtualenvwrapperpip

  4. 在安装和激活virtualenv之后,运行以下命令::

    $ pip install -e moose/
    

    现在,Moose的代码就可以被导入(importable),并且可以使用 moose-admin 等命令行工具了。至此,我们就完成了安装!

当你想要更新你的Moose代码时,只需要在moose文件夹内运行命令 git pull,Git便会自动下载任何更新。

需要知道的事情

Moose完全由Python编写并且依赖于以下几个关键的Python包(Packages):

  • azure,微软 Azure云服务的SDK的Python版本;
  • pymongo,包含多个通过Python与MongoDB交互的工具;
  • pymssql,一个基于Python DB-API (PEP-249)的访问SQLServer数据库的接口;
  • MySQLdb,一个提供了访问MySQL数据库的线程兼容的Python库;
  • OpenCV,经典图像处理库OpenCV的Python版本;
  • Pillow,一个友好的Python图像库,提供了更直白的图像处理函数;

快速上手教程

在这篇教程中,我们假设你已经安装好了Moose并且了解一个数据标注项目的基本流程,如果还没有的话请参考 安装指南标注任务创建指南

我们将以一个名为 cityscape 的图片标注项目开始,该项目需要在 标注平台 上创建任务,上传图片数据并建立索引关系,待标注人员完成后,再将结果从后端提取出来,整理成相应格式并等待交付客户。

这篇教程将会着重讲述Moose相关的操作,包含以下要点:

  1. 创建一个新的Moose项目;
  2. 创建一个新的application;
  3. 编写动作(action):upload,完成数据上传和索引建立;
  4. 创建订单(order),定义动作接口;
  5. 抽象模型(model),导出标注数据;
  6. 完成单元化测试。

创建一个Moose项目

在开始为一个数据标注(采集)项目编写相关的业务逻辑之前,我们需要创建一个Moose Project。在你想要保存代码的地方打开命令行,输入:

$ moose-admin startproject tutorial

这将会在当前文件夹中创建以下内容:

tutorial/   # 项目的Python模块
    __init__.py     # 一个空文件,用来告诉Python解释器把该文件夹看做包(Package)
    settings.py     # 该Moose项目的配置信息
    template.cfg    # 整个项目的订单文件模板
    manage.py   # 一个用来和Moose进行交互的命令行小工具

创建一个新的application

现在,你的工作环境——Moose Project——已经创建起来,我们可以开始真正的工作了。 每一个你通过Moose编写的 application 都遵循一套特定的规则,他们既是Python的一个package,同时也是与一个“项目”——不同于前文提到的 Moose Project,这里指的是 工程意义 上的项目——相关的所有(代码层面上的)业务逻辑的集合。 Moose提供了一系列的工具来自动创建相应的文件和文件夹,使得开发者可以专注于编写业务逻辑的代码而不是创建文件。为了创建(一个名叫 cityscape 的) app,你需要在之前创建的Moose Project的目录下(在与 manage.py 同一级目录里)输入:

$ python manage.py startapp cityscape

这将会为你创建一个叫 cityscape 的文件夹,内部结构如下:

cityscape/
    configs/    # 用来存放订单文件,我们会在下一节解释什么是订单
    data/       # 用来存放输入输出数据
    __init__.py
    actions.py      # 在这个文件中,我们定义所有可被执行的动作(Action)
    apps.py         # 将actions.py中的Action类和命令行中的参数绑定
    models.py       # 从后端获取的结果的对象表示
    template.cfg    # 该application的订单文件的模板
    tests.py

这其中尤其需要注意的是 cityscape/apps.py 。它是整个app的控制中心,负责连接各个单元和模块,使正确的配置文件(configs)被加载、动作(actions)能通过命令行被触发、数据被存储到正确的位置(data)。我们之后会了解到它是如何控制的,现在,你唯一需要做的是提供一些项目相关的信息:

cityscape/app.py
 # -*- coding: utf-8 -*-
 from moose.apps import AppConfig

 class CityscapeConfig(AppConfig):
     name = 'cityscape'
     verbose_name = u"街景道路标注"

编写动作(action):upload,完成数据上传和索引建立

项目很快开始运作起来,我们接到的第一个任务是创建一期标注任务,并且向我们提供了以下信息:

  • 数据来源:/data/cityscape/vol1 目录下的所有图片
  • 模板名称:街景图片多边形标注 v1.1
  • 索引格式:
{
    "url": "path/to/image01.jpg", "dataTitle": "image01.jpg"
}

我们在这里不过多地解释这些信息代表的含义,如果你还不了解,请参考我们的文档 《标注任务创建指南》 。假设你已经根据这些信息在 标注平台 上创建了一期任务,并且返回了以下信息:

  • 任务ID:10000
  • 任务名称:2017第2000期图片标注任务
  • 任务批次:cityscape - vol1

接下来要做的就是上传原始图片并且建立索引关系。这些动作被抽象成一个叫 AbstractAction 的类,我们在 cityscape/actions.py 定义它的实现:

cityscape/actions.py
 # -*- coding: utf-8 -*-
 import os
 import json
 from moose.actions import base
 from moose.connection.cloud import AzureBlobService
 from tutorial import settings

 class Upload(base.BaseAction):

     def run(self, **kwargs):
         """
         Inherited classes must implement this interface,
         which will be called then to perform the operation.
         """
         task_id = '10000'

         # Phase 1. establishes the connection to azure and do uploading files
         azure = AzureBlobService(settings.AZURE)
         # Assume there was only one file in '/data/cityscape/vol1'.
         images = [('vol1/a.jpg', '/data/cityscape/vol1/a.jpg'), ]
         blobs = azure.upload(task_id, images, overwrite=True)

         # Phase 2. creates the index file to declare the relationships
         # between files uploaded and names to display
         index_file = os.path.join(self.app.data_dirname, task_id+'.json')
         with open(index_file, 'w') as f:
             for blob_file in blobs:
                 item = {
                     'url': blob_file,
                     'dataTitle': os.path.basename(blob_file)
                 }
                 f.write(json.dumps(item))
         return '{} files uploaded.' % len(blobs)

此外,你还需要在 tutorial/settings.pyAZURE 字段中填入对应的账号密码以及端点(ENDPOINT)的值才能运行,如果你不知道填什么内容,请咨询IT相关信息。

为了避免我们的教程陷入过多细节的讨论,我们跳过了部分具体实现,例如 AzureBlobService 类。目前你只需要了解:通过继承 actions.base.BaseAction 并对接口 run 添加实现,我们完成了原始文件的上传和索引文件的生成这两个功能。

为了使得这个 Action 可以在命令行里被调用,我们还需要做一件事情——在 AppConfig 中注册该动作。在 cityscape/apps.py 中添加以下内容:

cityscape/app.py
 class CityscapeConfig(AppConfig):
     name = 'cityscape'
     verbose_name = u"街景道路标注"

     def ready(self):
         self.register('Upload', 'upload')   # now we can type `-a upload` to refer the action 'cityscape.actions.Upload'

注册完成之后,我们就可以在命令行中通过指定 -a upload 选项来运行我们之前在 cityscape.actions.Upload 中编写的代码了。在’cityscape/configs/’下创建一个文件叫”placeholder.cfg”, 我们在其中输入以下内容:

cityscape/configs/placeholder.cfg
 [meta]
 keys = upload

 [upload]

然后在与之前相同的位置下输入:

$ python manage.py run cityscape -a upload -c placeholder.cfg

此时,你应该可以在命令行中看见文件上传的进度条,以及生成的 cityscape/data/10000.json 文件了。至于我们手动创建的 placeholder.cfg 是起什么作用,我们会在下一节中进行详细说明。

创建订单(order),定义动作接口

如果你是一位有经验的开发者,那么你一定已经意识到我们之前的代码中存在一点问题——包含过多的“魔法常量”(magic constant),不仅如此,在实际的工作中我们还会发现,那些业务逻辑相关的代码通常是固定的,反而是这些“魔法常量”会经常性地改变。

为了避免频繁地修改我们的代码,我们提出订单(order)这一术语(terminology)。通过将每次处理所需的参数按照.CONFIG的格式定义好——我们称之为 订单模板,后续的订单会自动按照该模板生成。通过填写相应的内容来“告诉”application诸如任务ID、数据位置等必要的信息。一个常见的订单模板格式如下:

cityscape/template.cfg
 [meta]
 keys = common,upload,export

 [common]
 name =
 root = /path/to/data
 relpath = extra/path/to/remove

 [upload]
 task_id = ?

 [export]
 title = 2017第?期图片标注任务

需要注意的是,订单模板的格式不是强制性的——只要能定义你的接口,你可以组织成任意格式!我们推荐使用如上的格式是希望即使在不同的app中也能复用同一个action(或者尽量少地去修改它),并且这种格式比较好地概括了我们日常工作中一个订单会用到的属性。

我们将上面的模板复制到 cityscape/template.cfg 文件中,然后在命令行中运行:

$ python manage.py genconf cityscape -c trial.cfg

如果你是在Linux或macOS X平台上运行,并且已经安装了 vim 的话,那么此时会用vim的打开你刚才创建的 cityscape/configs/trial.cfg 已提供一个快速编辑的界面。

你可以通过在 tutorial/tutorial/settings.py 中设置EDITOR的值来使用你喜欢的文本编辑器——只要保证它能通过在命令行里指定文件名的方式打开。 此外,你还可以通过 editconf 命令快速打开一个订单文件,以对其进行修改。

我们在里面填上需要的值,并且对刚才编写的action Upload 进行修改:

cityscape/configs/trial.cfg
 [common]
 root = /data/cityscape/vol1
 relpath = /data/cityscape

 [upload]
 task_id = 10000

 [export]
 title = 2017第2000期图片标注任务
cityscape/actions.py
 class Upload(base.BaseAction):

     def run(self, **kwargs):
         config = kwargs['config']
         task_id = config.upload['task_id']
         # ···

         images = [('vol1/a.jpg', '/data/cityscape/vol1/a.jpg'), ]
         # ···

完成以上修改后,在命令行里运行(run)时通过指定订单文件名就可以按照该订单的配置来执行——我们通过指定使用 trial.cfg 完成与上一节相同的功能:

$ python manage.py run cityscape -a upload -c trial.cfg

action 的接口独立出来之后我们发现,如之前期望的,很多动作可以被复用。我们也确实在 moose.actions 模块中定义了一些常见的动作,比如 upload.SimpleUpload, upload.ReferredUpload, upload.MultipleUpload 等等。通过查阅相应的[API文档]()发现之前编写的 action: upload 已经被 SimpleUpload 实现了,只需要继承它并做些细微的调整即可。因此,我们的最终版本是这样的:

cityscape/actions.py
 from moose.actions import upload

 class Upload(upload.SimpleUpload):
     default_pattern = "*.jpg"

抽象模型(model),导出标注数据

现在标注人员已经完成了所有的标注,是时候将数据按照一定格式导出并交付客户了。类似于upload, 我们使用已经定义的 actions.export.SimpleExport 实现导出功能,但是查阅[API文档]()发现需要同时定义成员变量 data_model

简单来说,data_model 就是一条原本是JSON字符串的标注数据的对象形式。

由于标注结果的格式通常不统一,并且项目内可能会因为需求或效率的变化而使用不同的模板,这导致产生的数据差异较大,功能难以被复用。因此,我们需要抽象出一层“适配器”的角色,通过定义并向其他对象暴露一系列统一的接口,其他对象只需要调用该接口,不用了解具体实现。同时,这些接口在子类中被继承和实现(或映射),不需要去关心这个接口将被用于做什么。

我们在 moose.models.BaseModel 中定义了一些常见的接口,诸如:filepathdatafilelink(task_id)is_effective() 等等,有些提供了默认实现,另外一些则要求子类必须实现。他们具体的作用可以参考[API文档 model]()。现在,我们输入以下内容:

cityscape/actions.py
 from moose.actions import export

 class Export(export.SimpleExport):
     data_model = 'cityscape.models.CityscapeModel'
cityscape/models.py
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from moose import models
 from moose.models import fields

 # Create your models here.
 class CityscapeModel(models.BaseModel):
     mark_result = fields.ResultMappingField(prop_name='markResult')

     @property
     def filepath(self):
         return self.source['url']

     @property
     def data(self):
         segmentations = []
         for geometry, properties in self.mark_result:
             segmentations.append({
                 'category': properties['type']['currentDataKey'],
                 'coordinates': geometry['coordinates'],
             })
         return segmentations

我们解释一下其中的设计要点:

  • SimpleUpload 相似, actions.export.SimpleExport 控制整个(导出)动作的流程,包括 从数据库中提取标注数据 => 加载数据以实例化Model => 调用实例化后的接口,对标准化后的数据进行处理
  • BaseExport.query_class 定义了查询类,通过继承 connection.query.BaseQuery 实现自定义查询——也就是说 当查询条件发生了变化 才需要进行修改;
  • BaseExport.query_context 定义了数据库使用的驱动类(*_handler)和表实际名称到查询类中使用的别名的映射(*_context)——也就是说只有 当使用的数据库发生了变化 才需要修改;
  • 每条标注数据以 {‘source’: {‘url’: ‘’}, ‘result’: {‘markResult’: ‘’}} 的格式返回,其中,顶层的字段 sourceresult 分别对应数据库中的表示该数据 原始信息和标注信息 的两张表,它们的子字段则可能随模板不同而变化;
  • 为了方便接下来对标注结果中 annotation[‘result’][‘markResult’] 的调用,CityscapeModel 通过声明 mark_result = fields.ResultMappingField(prop=’markResult’) 完成了 字典的键值到类的属性的映射
  • cityscape.models.CityscapeModel 继承 models.BaseModel 并实现了接口 data 以提供一个 可读性更好的数据格式,这个接口随后会被 SimpleExportdump() 调用以将其写至文本文件中,完成我们所谓的导出功能。

基于原始数据和处理数据分离的原则设计,即使中途更换了模板,我们也只需新建一个 NewCityscapeModel 提供同样的标准化接口(如 data ),并修改 data_model = ‘cityscape.models.NewCityscapeModel’ 即可。

之后,同 Upload 一样,当我们在 app 中对该Action进行注册:

cityscape/app.py
 class CityscapeConfig(AppConfig):
     name = 'cityscape'
     verbose_name = u"街景道路标注"

     def ready(self):
         self.register('Upload', 'upload')
         self.register('Export', 'export')   # 和上面的upload一样,现在我们可以通过键入 `-a export` 来调用Export了

此时,我们在命令行中输入:

$ python manage.py run cityscape -a export -c trial.cfg

即可完成导出标注结果。

接下来……

到这里,你就已经掌握了Moose最核心的思想和内容,其他的特性和内容万变不离其宗,基本上都是在这层设计思想上不断细化和完善的结果。这听起来似乎剩下的内容都不重要,然而,正像我们设计Moose的初衷是为了最大化的复用代码、节省时间,其他的模块和语法糖也是基于同样的目的。所以,去探索下这些功能吧,你了解的模块和特性越多,你将来所花费的时间也会越少。因为很有可能你碰到的问题,其他人已经碰到过了。

Moose示例

了解怎样使用Moose的最快的办法就是通过实际项目开始。为此,我们提供了为三个标注项目编写的app作为参考,以帮助用户快速上手使用Moose。该Moose项目 datatanghttp://git.datatang.com/xiaoyang/datatang,包含的三个项目分别涉及音频标注项目导出、图片标注项目上传及导出、视频标注项目的抽取上传和导出。可以在README.md中了解到更多项目相关的信息。

如果你对git比较熟悉,可以checkout下来相应代码并且试着运行。如果不是,你也可以通过点击 这里 下载。

Moose Cookbook

安装

  • 使用 虚拟环境
  • Moose目前并不稳定,功能和bug修复的更新非常频繁,安装开发者版本并且依靠 git pull 获取最新更新是一个更好的选择,详细查看 安装开发者版本
  • 为了避免由于依赖库的兼容性问题导致安装失败,我们尽可能减少了安装Moose时的依赖,移除了部分不常用但在某些模块会使用到的第三方库,因此如果在使用过程中抛出导入异常,手动安装这些模块即可,包括:
    • MySQL-python>=1.2.5
    • pysmb>=1.1.17
    • Pillow>=3.4.1

创建项目和App

  • Moose中,project和app名 使用单个单词 ,不要有中横线或下划线,长度控制在5~12个间,没必要是项目的完整译名,取一个唯一的关键词即可(不关键也没有关系;-));

  • 项目(project)和app的界限并没有那么严格和清晰,同一个项目完全既可以创建project也可以使用app。如果有的话,那么把一些 相似的项目 ,或者一个 大项目下不同的执行方式 (比如采集分为线上、线下、众包)作为不同的app放在一个project下,会是一个好的主意;

  • 我们默认为每个app创建了一些文件,这在 快速上手教程 — 创建一个新的application 中已有说明。但是有些逻辑放在这些模块中可能并不合适,如下的文件命名规则为你提供了一些参考:

    appname/
        configs.py      # 适合存放与app相关的环境变量,例如路径、时长等
        files.py                # 适合存放文件对象抽象及处理方面的逻辑
        check.py                # 适合存放标注结果检查的逻辑
        metadata.py     # 适合存放metadata解析及生成相关的逻辑
        info/                   # 存放与该app运行或业务相关的文本文件(excel, csv, pdf)
        scripts/                # 简单的可执行的脚本
    
  • 如果一个project下多个model或action相似,可以将其抽象、提取,放至里层的project目录下。同样的,如果创建的app需要一个共同的定制的模板,也可以在里层的project下创建app模板:

    projectname/
        projectname/
            __init__.py
            settings.py
            template.cfg
            regular/            # 定制化的app创建模板
                __init__.py
                actions.py-tpl
                apps.py-tpl
                models.py-tpl
                tests.py.tpl
            models/             # 提取通用model模块
                cityscape.py     # 街景标注通用模板
                ocr.py                  # OCR通用模板
            actions/            # 提取通用action模块
                ...
    

    并可以通过创建时指定路径以按照定制的模板创建app:

    $ python manage.py startapp appname --template projectname/regular/
    

    具体细节可以参考 datatang/regular

App实战

  • 对于一个简单的任务,我们提供了 SimpleAction ,继承并在 execute 方法中提供实现,可以最快地开始;对于一个标准化的任务,如果没有已经提供的实现,需要继承 CommonAction 并实现所有接口(否则会抛出 NotImplementedError ),这有点麻烦,但对于代码的标准化有很大好处;
  • CommonAction 中方法 parseset_environscheduleset_context 看起来功能有些重合,然而实际上,parseschedule 旨在提供主要的流程的实现,通常在一类动作(例如上传)的基类中提供实现, set_environset_context 则被用于其子类中,需要对参数进行局部添加和修改的情况里 [1]
  • 通常情况下,我们对要完成的工作提供了标准化的实现,继承对应的基类并在其预留的接口里改动以适配当前任务是最好的选择。例如:

上传

  • 我们为上传提供了 SimpleUpload,继承后指定要上传的文件的名称的模式即可,例如:

    default_pattern = "*.jpg"
    

    即可对根路径下所有满足 *.jpg 的文件进行上传处理;

  • 上传任务中,变化最多的就是索引文件的格式,我们提供了接口 index(blob_paris, context) 作为修改索引格式的入口(当然如果需要额外信息的话,需要在之前流程中修改context里的内容);

  • 当同一个文件被多次用于不同期数的标注中,应该考虑上传一次文件,后续的索引使用完整的文件url代替。 ReferredUpload 提供了默认的实现;

  • SimpleUpload 中的参数 dirs 可帮助你一次完成;当需要将一个文件夹下的数据平均上传到多期任务中时, AverageUpload 提供了默认的实现;当进行视频追踪类标注,即一系列的图片(指按照一定规则从视频里抽帧的图片)需要按照每300张一条数据的方式上传时, VideosUpload 提供了相应的实现;

导出

  • 导出总是涉及到提取数据,使用参数 use_cachecache_lifetime 控制是否缓存提取到的结果以及缓存多长时间;
  • 打开开关 download_source (设置为Ture),开始下载原始文件。这会使得程序变成多线程,如果这使得调试不方便,将 settings.DEBUG 设置为True,打开调试模式,下载将会变成单线程;
  • TaskExportSimpleExport 没有太大区别,除了 TaskExport 会根据task id自动获取到标题和批次信息,不再需要手动提供;
  • 对于所有继承Export系列类的子类,handle_model(data_model) 是控制导出流程的入口。如果需要对数据处理的逻辑进行定制,从这里开始;
  • 对于图像类导出任务,我们提供了 ImageExport ,它基本能解决80%的此类任务;

Tips

  • 不要用 capitalize(),除了首字母变大写,也会让其他字母变小写;
  • 不要使用 import sys;reload(sys);sys.setdefaultencoding('utf-8') 以及任何你不了解但好像能work的代码片段,StackOverflow 有一个关于为什么禁止使用的讨论;
  • Moose为很多常用的代码片段提供了shortcuts,使用这些函数,减少不必要的冗余,使所有代码看起来更加统一和标准:
    • 使用 moose.shortcuts 模块的 ivist 代替os.walk对文件夹进行遍历;
    • 使用 moose.utils._os 模块的 npath (native path)和 upath (unicode path) 替代对路径的encode和decode,因为不同平台上文件系统编码会有所区别;
    • 使用 moose.utils._os 模块的 ppath (posix path)和 wpath (windows path)替代对路径分隔符的replace(例如 path.replace(‘\\’, '/') );
    • 使用 moose.utils._os 模块的在路径前加上 normpath ,将路径分隔符(“”,“__”)标准化;
    • 使用 moose.utils._os 模块的 makedirsmakeparents 替代os.makedirs和os.makedirs(dirname(filepath)) ,在创建文件夹前会先判断文件夹是否已经存在;
    • 使用 moose.utils._os 模块的 safe_join 代替os.path.join,在合并路径之前会先检查拼接的路径是否在同一根路径下;
    • 使用 moose.utils.encoding 模块的 smart_strsmart_unicode 代替encode和decode,对于Python 2 和Python 3具有更好的兼容性;
    • 使用 moose.utils.encoding 模块的 iri_to_uriuri_to_iri 进行uri和iri之间的转换,使用 escape_uri_path
    • 使用 moose.utils.serialize 模块的 load_xlsxdump_xlsx 代替自己对excel文件进行load和dump操作;
    • 使用 moose.utils.serialize 模块的 load_csvdump_csv 代替自己对csv文件进行load和dump操作;
    • 当除了将只有 self 参数的方法变成属性,还需要保存值避免重复计算时,使用 moose.utils.functional 模块的 cached_property 代替装饰器property;
    • 使用 moose.utils.listutils 模块的 stripl 对列表中的每个字符串进行strip处理; sliceislice 对一个整数进行平均切片操作;使用 islicel 对列表进行平均切片;
    • 使用 moose.utils.module_loading 模块的 import_string 代替import_module来动态导入类或函数;
    • 使用 moose.utils.lru_cache 模块的 lru_cache 装饰器缓存带参数的函数结果;
    • 使用 moose.utils.xmlutils 模块的 dict2xmlxml2dict 完成无属性的的xml和json格式的转换 [2]
[1]SimpleActionCommonAction 还没有按照如上的描述实现,主要是对旧代码的兼容问题导致这一修改比较棘手;
[2]还未实现
简介
了解Moose是什么以及它用于解决什么问题。
安装指南
在你的电脑上安装Moose。
快速上手教程
开始你的第一个Moose项目。
Moose示例
通过Moose示例了解更多。
Moose Cookbook
Moose Project编写规范和建议

基本概念

命令行工具

Moose 提供了一系列的基于命令行的工具,以方便开发人员快速地创建和控制 Moose 项目。其中,有一些命令是全局的,可以在系统中任何地方被调用;另一些类似于子命令的形式,需要作为工具的参数被调用,也是我们与Moose交互的主要方式。

为了区分两者,我们称前者为Moose工具(Tools),后者为Moose命令(commands)。下面两节分别列出了两者,并对它们的作用和参数进行了解释。

Moose工具

输入:

moose-admin startproject myproject [project_dir]

会在 project_dir 创建一个Moose项目,

Moose命令

动作(Actions)

Action 是对客观世界里的一套动作的抽象,它将具体的动作按照流程化的方式分解,以使它更容易被理解和复用。

数据处理的世界也存在一系列可以被复用的行为,我们定义模块 actions ,并且对其中最常见的操作进行抽象,帮助使用者只需要实现最少的“动作”。

moose.actions.base

class base.AbstractAction

AbstractAction是所有actions的抽象类,它定义了接口 run() 作为action的子类被调用的入口。

run(**kwargs)

所有Action类的入口。

参数:kwargs (dict) – action 执行所需要的参数列表。
class base.BaseAction(app_config, stdout=None, stderr=None, style=None)

BaseAction定义了Action的初始化参数和标准流程,所有Action类都应继承该类,并按照它提供的流程去分解过程并对方法进行实现。

参数:
  • app_config (moose.apps.AppConfig) – App的对象表示,提供了该app的路径等信息。
  • stdout (moose.core.OutputWrapper) – 标准输出,子类通过调用 self.stdout.write() 输出到标准输出。
  • stderr (moose.core.OutputWrapper) – 标准错误输出对象,子类通过调用 self.stderr.write() 输出到标准错误输出。
  • style (moose.core.Style) – 定义输出的格式和显示效果。
stats_class

类变量,定义用于统计的类,默认值为 "moose.actions.stats.StatsCollector"

stats_dump

类变量,是否输出统计结果,默认为 True

run(**kwargs)

定义了具体流程,如下:

def run(self, **kwargs):
    environment = self.parse(kwargs)
    for context in self.schedule(environment):
        stats_id = self.execute(context)
        self.stats.close_action(self, stats_id)
    self.teardown(environment)
    return '\n'.join(self.output

模型(Model)

外部世界的数据总是复杂的。它们形态万千,格式不一。一方面,同一数据可能会使用JSON,XML或CSV等格式去表示;另一方面,两套数据内部即使意义相同的字段也可能会使用不同的名称去命名,或者用不同的格式来组织。就像通天塔的寓言——上帝为了阻止人类建造通往天堂的高塔而发明了不同的语言。毫无疑问,不同的数据格式给我们的工作也带了不少的麻烦。

类似于经典的 MVC模式 通过控制器(Controller)解耦模型(Model)和视图(View),我们通过抽象出Model对象来适配不同的数据格式,使得在其上的各种动作(Action)可以使用相同的接口。我们会在接下来的一节讲述该模块的接口。

moose.models.BaseModel

class moose.models.BaseModel(annotation)

BaseModel是所有model的基类,它定义了model子类应该包含的属性并且提供了默认实现。

首先,它读取一个标准格式的标注记录,该记录从数据库中获取,并被组织成一致的格式({“source”: {x: y}, “result”: {a: b}});然后,会根据我们在声明中对 Field 的定义,将字典中的各个字段映射成该对象的属性;最后,我们通过实现规定的接口,保证不同的数据格式提供了一套统一的接口。

参数:annotation (dict) – annotation 代表了一条数据的原始信息和标注结果,它们分别被存放在MongoDB中名为 sourceresult 的Document(文档)中,通过 fetch 查询并提取出来。
source

从MongoDB的Document source 里取到的所有内容。代表数据上传时的索引文件的内容,一般包含 urldataTitle 等信息。

result

从MongoDB的Document result 里取到的所有内容。代表图像/文本/音频的标注结果,一般(可能)包含 Workload (工作量统计),effective (数据有效性), markResult (标注结果集合)等信息。

effective_values

定义了标注结果中的 effective 为True的值的集合,默认为 ('1', 1)。修改该值会对方法 is_effective() 产生影响,

output_suffix

导出文件的后缀名,默认为 .json

_active()

实例化类体中定义的各个字段,将 sourceresult 中的字段映射成该对象的属性。该方法一般不需要覆盖和重载。

filepath()

@property abstract

该方法带有装饰器@property ,以使其表现出属性的特点但是可以被重载。子类必须继承并且实现该方法,并且返回被标注对象的相对路径。

filename()

@property

返回文件名,即 os.path.basename(self.filepath)

normpath()

@property

返回文件在当前系统的标准路径,即 os.path.normpath(self.filepath)

data()

@property abstract

该方法带有装饰器@property ,以使其表现出属性的特点但是可以被重载。子类必须继承并且实现该方法,并且返回标注结果的可读形式。

guid()

@property

返回该条标注结果的 guid

user_id()

@property

返回标注该条数据的用户的 personInProjectId 字段,该字段可以用来查询对应的用户名及账号。

该条数据的标注链接。

参数:task_id (str) – 该条数据对应的任务期数。

被标注对象文件的url。

参数:task_id (str) – 该条数据对应的任务期数。
clean_result()

移除掉所有以”_”开头的字段后的字典 result

to_string()

data 返回结果的JSON格式的字符串形式,使用 utf-8 编码。

我们可以展示一个常见的实现::

from moose.models import BaseModel, fields

class UigurlangModel(BaseModel):
    """
    @template_name: 图像文本单词转写V1.0
    """
    mark_result = fields.ResultMappingField(prop_name='markResult')

    @property
    def filepath(self):
        return self.source['fileName']

    @property
    def data(self):
        return self.mark_result

示例中对 BaseModelfilepath()data() 提供了实现,并且将 annotation['result']['markResult'] 里的值映射到了属性 mark_result

需要注意的是,这里 data() 返回的内容正好就是 annotation['result']['markResult'] 。另外一些时候,我们可能需要对标注结果(比如 mark_result )里的内容进行修改——移除部分无用字段、改变数据结构、重新进行计算部分结果等等,这个时候就需要对 data() 进行更多细节的实现,保证返回的结果是我们需要的人类可读(human-readable)的格式。

moose.models.GeoJSONModel

class moose.models.GeoJSONModel(annotation)

GeoJSONModel是 BaseModel 的子类,继承了所有的属性和方法。除此之外,根据 RFC 7946GeoJSON 的定义,提供了一些额外的方法去直接访问其中的属性,以跳过过多的嵌套和循环。

mark_result

result 字典中 markResult 字典的映射,默认实现为:

mark_result = fields.ResultMappingField(prop_name='markResult')

如果不是该字段(markResult)表示标注结果的集合,需要对 prop_name 进行修改。

ifeatures():

@property

生成器,被迭代调用以依次返回标注结果的多边形对象(feature['geometry'])和对应的属性(feature['properties'])。

icoordinates()

@property

生成器,被迭代调用以依次返回标注结果的多边形的坐标值(geometry['coordinates'])。

同样的,这里我们也展示一个使用 GeoJSONModel 的例子::

from moose import models

class SatelliteModel(models.GeoJSONModel):
    """
    @template_name: 卫星图片标注V2.1
    """
    @property
    def filepath(self):
        return self.source['url']

    @property
    def data(self):
        segmentations = []
        for geometry, properties in self.ifeatures:
            segmentations.append({
                'category': properties['type']['currentDataKey'],
                'coordinates': geometry['coordinates'],
            })
        return segmentations

示例中的 SatelliteModel 同样对 filepath()data() 提供了实现,但是稍有不同的是,在方法 data() 中,通过对 ifeatures() 的迭代,我们获得了所有的被标注对象的标签和坐标值,将其组装并返回。

moose.models.fields

通过上面两个例子,大家基本能了解到,所谓Model对数据的建模,是对外部数据进行计算和重组,以使其表现出更一致和更有意义的接口。就像把乐高积木的一块块组件搭建起来,构成一个有门有窗有塔楼的城堡一样。即使每套乐高积木的组件可能不尽相同,但是我们总能通过一些转换和搭配组装起一个基本功能一致的城堡。

与此同时,对于那些不需要计算和重组等复杂操作的数据,我们提供了 fields 这一模块,用以完成字典的键到类的属性的映射,避免多层嵌套的引用。

fields.AbstractMappingField

class fields.AbstractMappingField

抽象类,定义了 get_val() 这一虚函数,它的所有子类都应实现该方法。

fields.CommonMappingField

class fields.CommonMappingField(dict_name, prop_name, default=None)

AbstractMappingField 的子类,定义了对 sourceresult 表的键值的映射。

参数:
  • dict_name (str) – 只能为 sourceresult,代表所选择映射的表;
  • prop_name (str) – 代表 sourceresult 对应表中的键名;
  • defaultprop_name 不存在是默认返回的值。
get_val(anno):

从对应表(dict_name)中取出对应键值(prop_name)的过程。

def get_val(self, anno):
            return anno[self.dict_name].get(self.prop_name, self.default)
参数:anno (dict) – 同 :class: ~.BaseModel 的实例化参数 annotation 一样,通过 fetch 查询并提取出来的结果。

fields.SourceMappingField

class fields.SourceMappingField(prop_name, default=None)

CommonMappingField 的子类,定义了对 source 表的键值的映射。

dict_name

为常量”source”。

fields.ResultMappingField

class fields.ResultMappingField(prop_name, default=None)

CommonMappingField 的子类,定义了对 result 表的键值的映射。

dict_name

为常量”result”。

fields.LambdaMappingField

class fields.LambdaMappingField(lambda_fn)

AbstractMappingField 的子类,定义了匿名函数 lambda_fn 完成相应的函数映射。

参数:lambda_fn (lambda) – 定义了从 annofn(anno) 的过程映射。

下面这个例子你已经见到过了,我们稍作了修改:

from moose.models import BaseModel, fields

class UigurlangModel(BaseModel):
    """
    @template_name: 图像文本单词转写V1.0
    """
    file_name = fields.SourceMappingField(prop_name='url')
    mark_result = fields.ResultMappingField(prop_name='markResult')

    @property
    def data(self):
        return {
            "filename": self.file_name,
            "data": self.mark_result
            }

其中 data 与下面的例子是一样的作用:

class UigurlangModel(BaseModel):

    @property
    def data(self):
        return {
            "filename": self.source['url'],
            "data": self.result['markResult']
            }

看起来似乎下面这种写法更简洁,然而当实际操作中你被无尽的 []'' 淹没的时候,就不会这样认为了!

订单(Order)

Application模板

连接(Connection)

Moose 不是一个孤立的系统,文件和数据通过各种现有的服务提供——关系/非关系型数据库、文件系统、 云存储服务等等,我们要进行处理就必须提供“港口和桥梁”,将数据接入进来。 connection 模块就是被用来处理这样的任务。

对于关系型数据库,无论底层的引擎是mysql还是sqlserver, SQL连接 模块为所有的操作 提供了一个统一的接口; 除此之外, SQL操作 则基于这层抽象提供了便捷的sql操作接口, 使得数据库查询、插入、修改等操作更加方便;相应的,我们用 MongoDB连接 模块封装了 pymongo, 为对于Mongodb的操作提供了一层易用性更强的接口;

而对于微软的云存储服务 Azure blob ,微软提供了python的 azure SDKAzure Blob连接 模块则是基于该SDK的封装,进一步封装了Azure操作的常用接口。

SQL连接

moose.connection.sqlhander

class moose.connection.sqlhandler.BaseSQLHandler(settings_dict, host, conn)

BaseSQLHandler为其子类提供一些通用的方法和接口,实现最基础的连接功能,子类根据需求可在其基础上进行扩展。

参数:settings_dict (dict) – 包含数据配置的字典,包含如 HOST (要连接的数据库的主机地址),PORT (端口),USER (用户名) , PASSWORD (用户密码) 以及 CHARSET (编码方式),如果需要给数据表起别名还可能包括 TABLE_ALIAS (给数据表起别名)。
__del__()

该方法主要是用来断开数据库的连接。

__connect(settings_dict)

该方法是预留的接口函数,子类必须实现,子类可以根据要连接的数据库类型的不同进行设置并连接,该方法返回数据库连接对象。

get_connection(settings_dict)

该方法用来判断数据库配置信息是否正确及实现限制重试次数和增加重试间隔时间,如果连接成功则调用 _connect() 方法返回连接对象,否则抛错。

close()

该方法首先判断数据库是否断开,若断开则重置连接和游标,否则抛错。

_close()

该方法定义关闭数据库连接。

_get_cursor()

该方法返回该连接对象的游标。

execute(operation, operator, *args)
参数:
  • operation (str) – sql语句。
  • operator (funtion) – 真正执行sql操作的函数体,该参数根据调用该方法的主体不同而实现不同的功能。
  • args (str) – sql语句参数。

该方法对sql语句进行判断,若存在,则获取该连接的游 标及sql语句模板并传入的函数体 operator 返回执行结果。

exec_query(operation)
参数:operation (str) – sql查询语句。
def _operator(cursor, operation, *args):
    cursor.execute(operation)
    result = cursor.fetchall()
    return result

该方法用来根据输入的sql语句及内部 operator 函数作为参数调用excute()方法执行查询操作。

exec_commit(operation)
参数:operation (str) – sql增删改语句。
def _operator(cursor, operation, *args):
    cursor.execute(operation)
    self._conn.commit()
    naffected = cursor.rowcount
    stdout.info("Operation completed with '{}' rows affected.".format(naffected))
    return naffected

该方法用来根据输入的sql语句及内部 operator 函数作为参数调用excute()方法执行增删改操作。

exec_many(operation, params_seq)
参数:
  • operation (str) – sql增删改语句。
  • params_seq (str) – 参数列表。

该方法是预留接口函数,用来批量执行增删改sql语句,子类可根据具体需求自行实现。

moose.connection.mssql

操作关系型数据库 SQLServer 我们通常有两种方式即 pymssql_mssqlpymssql 是在 _mssql 上作了封装,是为了遵守python的DBAPI规范接口,查询时通常都使用 pymssql 但其在执行增删改操作时会抛错,所以使用相对原生的 _mssql

class SQLServerHandler(BaseSQLHandler)

SQLServerHandler是BaseSQLHandler的子类,它定义了基于BaseShape类的在SQLServer数据中的实现,这里一般用来进行查询操作。

db_name = 'SQLServer'

定义该数据库名称为 SQLServer

_connect(settings_dict)
参数:settings_dict (dict) – 包含数据配置的字典,包含如 HOST (要连接的数据库的主机地址),PORT (端口),USER (用户名) , PASSWORD (用户密码) 以及 CHARSET (编码方式),如果需要给数据表起别名还可能包括 TABLE_ALIAS (给数据表起别名)。

该方法根据输入的数据库配置返回SQLServer数据库的连接对象。

exec_many(operation, params_seq)
参数:
  • operation (str) – 要执行的sql语句模板
  • params_seq (str) – 要执行的sql语句参数
def _operator(cursor, operation, *args):
        if len(args) == 1:
                params_seq = args[0]
        else:
                raise SuspiciousOperation
        # see ref: http://pymssql.org/en/stable/pymssql_examples.html
        cursor.executemany(operation, params_seq)
        self._conn.commit()
        naffected = cursor.rowcount
        stdout.info("Operation completed with '{}' rows affected.".format(naffected))
        return naffected

该方法根据输入的模板、参数及内部函数 _operator 作为参数调用 excute()方法 批量操作数据库的操作,返回执行结果

class PrimitiveMssqlHandler(BaseShape)

在执行插入、更新或删除操作时,使用基本的mssql而不是 SQLServerHandler

db_name = '_mssql'

定义该数据库名称为 _mssql

_connect(settings_dict)
参数:settings_dict (dict) – 包含数据配置的字典,包含如 HOST (要连接的数据库的主机地址),PORT (端口),USER (用户名) , PASSWORD (用户密码) 以及 CHARSET (编码方式),如果需要给数据表起别名还可能包括 TABLE_ALIAS (给数据表起别名)。

该方法根据输入的数据库配置返回SQLServer数据库的连接对象。

_get_cursor()

返回位None的游标,与其他数据库驱动程序不同的是,_mssql 执行没有游标的操作。

exec_query(operation)
参数:operation (str) – 要执行的sql查询语句模板
def _operator(cursor, operation, *args):
        result = self._conn.execute_query(operation)
        return result

该方法根据输入的查询sql语句模板及差异化的内部函数体 _operator 作为参数调用 excute() 执行数据库查询操作,返回执行结果

exec_commit(operation)
参数:operation (str) – 要执行的sql增删改语句模板
def _operator(cursor, operation, *args):
        self._conn.execute_non_query(operation)
        naffected = self._conn.rows_affected
        stdout.info("Operation completed with '{}' rows affected.".format(naffected))
        return naffected

该方法根据输入的查询sql语句模板及差异化的内部函数体 _operator 作为参数调用 excute() 执行数据库增删改操作,返回执行结果

moose.connection.mysql

class MySQLHandler(BaseSQLHandler)

MySQLHandler是BaseSQLHandler的子类,它定义了基于BaseSQLHandler类的在MySQL数据中的实现。

db_name = 'MySQL'

定义该数据库名称为 MySQL

_connect(settings_dict)
参数:settings_dict (dict) – 包含数据配置的字典,包含如 HOST (要连接的数据库的主机地址),PORT (端口),USER (用户名) , PASSWORD (用户密码) 以及 CHARSET (编码方式),如果需要给数据表起别名还可能包括 TABLE_ALIAS (给数据表起别名)。

该方法根据输入的数据库配置返回MySQL数据库的连接对象。

SQL操作

用户在操作数据库时需要调用数据库接口类,而要执行某次具体的操作必须要有确定的sql语句及配置信息,Operation类将数据库接口类对象作为其参数,配合sql语句模板进行封装来适配不同的数据操作需求,使得在其上的各种动作(Action)可以使用相同的接口。

class BaseOperation(handler)

该类是所有operation类的基类,定义了类的必要配置而更多的功能则需子类根据需求自行扩展。

参数:handler (object) – 数据库接口类对象
operation_template = None

定义查询语句模板默认为None,其子类可根据不同的需求定制sql语句模板

base_handler_cls = sqlhandler.BaseSQLHandler

定义默认数据库操作的接口类为 sqlhandler.BaseSQLHandler

create_from_context(query_context)

@classmethod

参数:query_context (dict) – 数据库查询配置信息,包括数据库配置信息和数据库接口类

该方法根据输入的数据库配置信息首先判断是否合法,返回接口类对象

class BaseQuery(BaseOperation)

该类是 BaseOperation 的子类,在继承基类的同时增加了调用数据库接口类执行查询操作。

query(**context)
参数:context (dict) – 查询语句模板的参数

该方法根据输入的查询语句参数生成完整的sql语句并调用数据库接口类方法执行查询操作。

class BaseGuidQuery(BaseQuery)
该类是``BaseQuery``的子类,返回根据给定参数查询 SourceGuidResultGuid 字段的值。
参数:handler (object) – 数据库接口类对象
operation_template

定义查询语句的模板,这里定义了查询语句结果需要返回的字段为 SourceGuidResultGuid

tables = None

定义查询语句的数据表

conditions = None

定义查询语句的条件

operation_template 为:

"SELECT dr.SourceGuid, dr.DataGuid FROM $tables WHERE $conditions "
class AllGuidQuery(BaseGuidQuery)

该类是 BaseGuidQuery 的子类,指定了查询语句的数据表 $table_result dr 和查询条件字段 dr.ProjectId = {project_id}

tables = "$table_result dr"

定义查询语句的数据表为 table_result 并将该表重命名为 dr

conditions = "dr.ProjectId = {project_id}"

定义查询语句的条件数据表中的ProjectId与输入的值进行匹配。

class StatusGuidQuery(AllGuidQuery)

该类是 AllGuidQuery 的子类,它在基类条件的基础上新增了数据表的状态作为条件字段,使查询更精细化。

conditions = "dr.ProjectId = {project_id} AND dr.status = {status}"

定义查询语句模板的条件字段

STATUS

定义查询语句的条件数据表中的数据表状态

STATUS 定义如下:

STATUS = {
    'default': 0,
    'pass': 1,
    'refuse': 2,
    'revised': 3,
    }
class CreatedTimeGuidQuery(AllGuidQuery)

该类是 AllGuidQuery 的子类,它在基类条件的基础上新增了数据表中的数据创建时间字段作为条件字段,用来获取在给定日期时间之前或之后创建的记录。

conditions

定义查询语句模板的条件字段为项目ID和数据表中的数据创建时间

conditions 定义如下::

"dr.ProjectId = {project_id} AND dr.Date {less_or_more} '{datetime}'"
class AccessedTimeGuidQuery(AllGuidQuery)

该类是 AllGuidQuery 的子类,它在基类条件的基础上新增了数据表中的最后访问时间作为条件字段,用来获取在给定日期时间之前或之后访问的记录。

conditions

定义查询语句模板的条件字段为项目ID和数据表中的数据最后访问时间

conditions 定义如下::

"dr.ProjectId = {project_id} AND dr.LastEditTime {less_or_more} '{datetime}'"
class AccountGuidQuery(BaseGuidQuery)

该类是 BaseGuidQuery 的子类,它将基类的单表查询通过 dr.UserGuid = ps.ProviderUserKey 连接变成多表联合查询且查询条件字段为数据表dr中的ProjectId字段 和数据表ps中的Account字段,获取指定帐户的记录

tables = "$table_result dr, $table_person ps"

定义查询语句中数据表为 table_resulttable_person 并将它们重命名为 drps

该方法定义了查询语句模板的条件字段为 ps.Accountdr.ProjectIdps.Account 以及 dr.UserGuid = ps.ProviderUserKey

conditions 定义如下:

"dr.ProjectId = {project_id} AND dr.UserGuid = ps.ProviderUserKey AND ps.Account in {accounts}"
class TitlesGuidQuery(BaseGuidQuery)

该类是 BaseGuidQuery 的子类,它将基类的单表查询通过 ds.DataGuid = dr.SourceGuid 连接变成多表联合查询且查询条件字段为 ds.Titleds.DataGuid = dr.SourceGuid 及ProjectId字段等,获取指定标题的记录。

tables = "$table_source ds, $table_result dr"

定义查询语句中数据表为 table_sourcetable_result 并将它们重命名为 dsdr

该方法定义了查询语句模板的条件字段为 ds.Titleds.DataGuid = dr.SourceGuidds.ProjectId = {project_id} 以及 dr.ProjectId = {project_id}

conditions 定义如下:

"ds.DataGuid = dr.SourceGuid AND ds.ProjectId = {project_id} AND  dr.ProjectId = {project_id} AND ds.Title in {titles}"
class BaseUsersQuery(BaseQuery)

该类是 BaseQuery 的子类,定义了查询模板的条件为表 table_person_in_project 中的字段 ProjectId 等于表 table_person 中的字段 id 及查询字段并预留了扩展字段供子类使用。

fields = ""

定义查询语句中预留的查询字段,默认为空

tables = ""

定义查询语句中预留的数据表,默认为空

conditions = ""

定义查询语句中预留的条件,默认为空

operation_template 定义如下:

"SELECT DISTINCT pip.id, pip.PersonName $fields FROM $table_person_in_project pip, $table_person ps $tables WHERE pip.ProjectId = {project_id} AND pip.PersonId=ps.id $conditions"
class UsersInProjectQuery(BaseUsersQuery)

该类是 BaseUsersQuery 的子类,实现对查询字段的扩展,返回用户参与项目的信息

fields = ", ps.Account"

该属性定义了向模板中添加了查询字段 ps.Account

class UserGuidInProjectQuery(BaseQuery)
该类定义模板实现根据提供的 PersonNameproject_id 中获取用户guid( ProviderUserGuid)

operation_template 定义如下:

"SELECT ProviderUserGuid FROM $table_person_in_project WHERE PersonName = '{user_name}' AND ProjectId = {project_id}"
class TeamUsersInProjectQuery(BaseQuery)
该类定义模板用来获取指定用户参与项目的信息

operation_template 定义如下:

'''
SELECT pat.id, pat.PersonName, pat.Account, t.Name
        FROM
            (
                SELECT
                    person.*, pit.TeamId
                FROM
                    (
                        SELECT DISTINCT
                            pip.id, pip.PersonName, pip.ProviderUserGuid, ps.Account
                        FROM
                            $table_person_in_project pip, $table_person ps
                        WHERE
                            pip.ProjectId = {project_id}
                        AND pip.PersonId = ps.id
                    ) AS person
                LEFT JOIN $table_person_in_team pit ON pit.ProviderUserKey = Person.ProviderUserGuid
            ) AS pat
        LEFT JOIN $table_team AS t ON pat.TeamId = t.Id
'''
class DataSourceQuery(BaseQuery)
该类继承了基类 BaseQuery ,定义了根据表 table_source 中匹配字段 ProjectId 进行查询的模板。

operation_template 定义如下:

"SELECT * FROM $table_source ds WHERE ds.ProjectId={project_id}"
class DataResultQuery(BaseQuery)
该类继承了基类 BaseQuery ,定义了根据表 table_result 中匹配字段 ProjectId 进行查询的模板。

operation_template 定义如下:

"SELECT * FROM $table_result ds WHERE ds.ProjectId={project_id}"
class DataInfoQuery(BaseQuery)
该类继承了基类 BaseQuery ,定义了根据 ``table_source.DataGuid=table_result.SourceGuid``查询指定项目信息的模板。

operation_template 定义如下:

"SELECT ds.Title, ds.FileName, dr.Status, dr.IsValid, dr.UserGuid, dr.SourceGuid, dr.DataGuid "
"FROM $table_source ds, $table_result dr WHERE ds.DataGuid=dr.SourceGuid AND "
"dr.ProjectId={project_id} AND ds.ProjectId={project_id}"
class ProjectInfoQuery(BaseQuery)
该类定义了根据输入指定项目ID返回该项目所有信息的模板

operation_template 定义如下:

"SELECT * FROM $table_project WHERE id={project_id}"
class ProjectInfoByBatchQuery(BaseQuery)
该类定义了根据输入的 batch 字段返回该项目所有信息的模板

operation_template 定义如下:

"SELECT * FROM $table_project WHERE batch='{batch_name}'"
class AcqInfoByGuidQuery(BaseQuery)
该类定义了根据输入的 DataGuid 字段返回该项目所有信息的模板

operation_template 定义如下:

"SELECT * FROM $table_acquisition WHERE DataGuid= '{data_guid}'"
class AcqInfoByUserQuery(BaseQuery)
该类定义了根据输入的 ProjectIdUserGuid 字段且 isValid = 1 返回该项目所有信息的模板

operation_template 定义如下:

"SELECT * FROM $table_acquisition WHERE ProjectId = {project_id} AND UserGuid = '{user_guid}' AND isValid = 1"
class AcqToMarkByUserQuery(BaseQuery)
该类定义了根据输入的 ProjectIdUserGuid 字段且 isValid = 1 返回该项目指定输出字段的模板

operation_template 定义如下:

"SELECT {project_id},Title,DataGuid,DataVersion,UserGuid,Duration,FileName,'{create_time}' "
"FROM $table_acquisition WHERE ProjectId = {acquisition_id} AND UserGuid = '{user_guid}' "
"AND isValid = 1"
class BaseInsert(BaseOperation)

该类继承了 BaseOperation ,定义了单条数据插入的方法,其子类可通过修改sql语句模板进行不同的操作。

execute(**context)
参数:context (dict) – 查询语句模板的参数

该方法根据输入的模板参数按照指定的sql语句执行插入数据操作,返回插入后的结果。

class AcqToMarkByUser(BaseInsert)
该类定义了一个根据条件字段为 ProjectIdUserGuidisValid = 1 查询得到数据然后插入指定数据表 table_source 的模板

operation_template 定义如下:

"INSERT INTO $table_source (ProjectID,Title,DataGuid,DataVersion,UserGuid,Duration,FileName, "
"CreateTime) SELECT {project_id},Title,DataGuid,DataVersion,UserGuid,Duration,FileName, "
"'{create_time}' FROM $table_acquisition WHERE ProjectId = {acquisition_id} AND UserGuid = "
"'{user_guid}' AND isValid = 1"
class AcqToMarkByDataguid(BaseInsert)
该类定义了一个根据条件字段为 DataGuidisValid = 1 查询得到数据然后插入指定数据表 table_source 的模板

operation_template 定义如下:

"INSERT INTO $table_source (ProjectID,Title,DataGuid,DataVersion,UserGuid,Duration,FileName, "
"CreateTime) SELECT {project_id},Title,DataGuid,DataVersion,UserGuid,Duration,FileName, "
"'{create_time}' FROM $table_acquisition WHERE DataGuid = '{data_guid}' AND isValid = 1"
class BulkInsert(BaseOperation)

该类继承了 BaseOperation ,定义了 批量 插入数据的方法,其子类可通过修改sql语句模板进行不同的操作。

execute(**context)
参数:context (dict) – 查询语句模板的参数

该方法根据输入的模板参数按照指定的sql语句执行插入数据操作,返回插入后的结果。

class BulkAcqToMarkByDataguid(BulkInsert)

operation_template 定义如下:

"INSERT INTO $table_source ({project_id},%s,%s,%s,%s,%f,%s,{create_time})"

MongoDB连接

class MongoDBHandler(settings_dict, displayed_mongo_url, _client, _db_name, _db, _coll_name, _coll)
该类实现对MongoDB数据库提供一个对外接口类,其内部封装了数据库的常用操作,用户只要按需传入指定参数即可实现功能。
参数:settings_dict (dict) – 包含数据配置的字典。包含数据配置的字典,包含如 HOST (要连接的数据库的主机地址),PORT (端口),USER (用户名) , PASSWORD (用户密码)
coll_source = 'Source'

定义原始数据表名称

coll_result = 'Result'

定义结果(标注后)数据表名称

__del__()

该方法实现断开数据库连接

__connect()

该方法通过调用 __get_mongo_url 得到数据库地址来实现数据连接,返回MongoDB客户端对象

__get_mongo_url(settings_dict)
参数:settings_dict (dict) – 数据库配置信息

该方法通过输入的数据配置参数返回数据库连接url

__get_displayed_url(settings_dict)
参数:settings_dict (dict) – 数据库配置信息

该方法返回一个将用户密码注释的数据库连接地址

db()

@property

该方法返回数据库对象,如果不存在则报错

set_database(db_name)
参数:db_name (str) – 数据库名称

该方法用来新建数据库及设置数据库名称

coll()

@property

该方法返回数据表对象,如果不存在则报错

set_collection(coll_name)
参数:coll_name (str) – 数据表名称

该方法用来新建数据表及设置数据表名称

execute(coll_name, operator)
参数:
  • coll_name (str) – 数据表名称
  • operator (function) – 执行sql操作函数的函数体

该方法根据输入的数据表名称创建数据表并调用主体的内部函数体 _operator 执行数据库操作,返回执行结果,若尝试重连次数超过指定次数则抛错。

fetch(coll_name, filter=None, *args, **kwargs)
参数:
  • coll_name (str) – 数据表名称
  • filter (dict) – 条件字段
  • args (str) – 位置参数
  • kwargs (dict) – 关键字参数
def _operator():
        documents = []
        for doc in self.coll.find(filter, *args, **kwargs):
            documents.append(doc)
        return documents

该方法将其内部函数体 _operator() 及传入的数据表名称作为参数,调用方法 excute() 实现从数据库拉取数据的操作

fetch_source(filter=None, *args, **kwargs)
参数:
  • filter (dict) – 条件字段
  • args (str) – 位置参数
  • kwargs (dict) – 关键字参数

该方法调用 fetch 返回指定表 coll_source 的表中查询指定的filter字段的结果

fetch_result(filter=None, *args, **kwargs)
参数:
  • filter (dict) – 条件字段
  • args (str) – 位置参数
  • kwargs (dict) – 关键字参数

该方法调用 fetch 返回指定表 coll_result 的表中查询指定的filter字段的结果

insert(coll_name, documents, **kwargs)
参数:
  • coll_name (str) – 数据表名称
  • documents (dic) – 要写入数据表的数据
  • kwargs (dict) – 关键字参数
def _operator():
        return self.coll.insert_many(documents, **kwargs)

该方法将其内部函数体 _operator() 及传入的数据表名称作为参数,调用方法 excute() 实现数据库的批量写入操作。

update(coll_name,filter, documents, **kwargs)
参数:
  • coll_name (str) – 数据表名称
  • filter (dict) – 条件字段
  • documents (dic) – 要写入数据表的数据
  • kwargs (dict) – 关键字参数
def _operator():
        return self.coll.update_many(filter,{'$set': document},**kwargs)

该方法将其内部函数体 _operator() 及传入的数据表名称作为参数,调用方法 excute() 实现数据库指定字段的批量更新操作。

close()

该方法实现断开数据库连接的操作

Azure Blob连接

moose.connection.cloud

class moose.connection.cloud.AzureBlobService(settings_dict)
该类实现对 Azure 云数据库的封装,对输入参数进行异常处理等操作保证程序的稳定性,对外提供统一的接口,不同用户只需提供不同的配置参数即可连接服务进行创建容器及其内部blob对象的操作。
参数:settings_dict (dict) – 包含数据库配置的字典。包含数据配置的字典,包含如 ACCOUNT (用户名),KEY (连接密钥),ENDPOINT (连接站点) , TIMEOUT (超时时间)。
blob_pattern = 'http://([\w\.]+)/(\w+)/(.*)'

定义 blob 的格式

create_container(container_name,set_public=False)
参数:
  • container_name (str) – 容器名称
  • set_public (bool) – 用来设置权限是否支持公共访问

该方法根据输入的参数 set_public 设置访问权限,返回名为 container_name 的容器对象,如果已经该容器已存在则不再返回。

list_containers(prefix=None)
参数:prefix (str) – 容器名称的前缀,用来过滤只返回容器名称以指定前缀开头的容器,默认为None即返回所有容器。

该方法返回一个列表,列出指定帐户下的容器。

list_blobs(container_name,prefix=None, suffix=None)
参数:
  • container_name (str) – 容器名称
  • prefix (str) – blobname 的前缀,用来过滤只返回容器名称以指定前缀开头的 blobname ,默认为None即返回所有 blobname
  • suffix (str) – blobname 的后缀,用来过滤只返回容器名称以指定后缀结尾的 blobname ,默认为None即返回所有 blobname

该方法根据blobname的前缀或后缀进行筛选后列出容器上的所有blobname的列表,返回的blob_names是posix样式的路径,无论创建时名称是什么。

create_blob_from_path(container_name, blob_name, filepath)
参数:
  • container_name (str) – 容器名称
  • blob_name (str) – blob名称
  • filepath (str) – 要上传文件的路径

该方法根据参数 filepath 上传文件到容器中,生成带有属性和元数据的 Blob 实例

upload(container_name, blob_pairs, overwrite=False)
参数:
  • container_name (str) – 容器名称
  • blob_pairs (str) – 一个包含 blob_name 和blob对象文件的本地路径的元祖
  • overwrite (bool) – 定义是否覆盖原 blob对象

该方法首先判断容器是否存在,如果不存在则创建容器,然后根据参数 overwrite 判断上传是否覆盖容器中的原文件,返回包含bolbname的列表

get_blob_to_path(container_name, blob_name, filepath)
参数:
  • container_name (str) – 容器名称
  • blob_name (str) – blob名称
  • filepath (str) – 要上传文件的路径

该方法是从容器 container_name 中下指定blob对象 blob_name 到指定的地址 filepath

download(container_name, dest, blob_names=None)
参数:
  • container_name (str) – 容器名称
  • dest (str) – 下载目标地址
  • blob_names (list) – 包含blobname的列表

该方法返回从容器中获取的blob对象到指定的目标地址,如果参数 blobnames 为None,则下载container中的所有blob对象

get_blob_to_text(container_name, blob_name)

预留接口

get_blobs(container_name, blob_names=None)

预留接口

set_container_acl(container_name, set_public=True)
参数:
  • container_name (str) – 容器名称
  • set_public (bool) – 用来设置权限是否支持公共访问

该方法实现设置与共享访问签名一起使用的指定容器或存储访问策略的权限。权限指示容器中的blob是否可以公开访问

delete_blobs(container_name, blob_names)
参数:
  • container_name (str) – 容器名称
  • blob_names (list) – blob对象名称列表

该方法执行从指定容器删除指定的 blob 对象,返回包含被删除的blob对象名称的列表

copy_blobs(blob_names, container_name, src_container=None, pattern=None)
参数:
  • blob_names (list) – blob对象名称列表
  • container_name (str) – 要复制的目标容器名称
  • src_container (str) – 数据源容器名称
  • pattern (str) – 匹配 blob 对象名称的模式

该方法实现将blob_names中列出的blob对象复制到dest容器,如果给定 src_containerblob_names 可以作为容器的相对路径,如果没有给定 blob_names 则按照匹配模式复制到目标容器中,如果blob_names为None则复制全部

copy_container(src_container, dst_container, pattern=None)
参数:
  • src_container (str) – 源容器名称
  • dst_container (str) – 目标容器名称
  • pattern (str) – 匹配 blob 对象名称的模式

该方法按照指定匹配模式复制blob对象到目标容器,如果目标容器不存在则在复制前创建该容器

SQL连接
sql连接的处理类,用来管理sql连接,抽象底层实现,提供统一接口。
SQL操作
基于 SQL连接 的sql操作的快捷方式。
MongoDB连接
Mongodb连接的处理类,提供了基于 pymongo 封装后更加易用的接口。
Azure Blob连接
提供了Azure Blob服务的操作接口,用于完成文件的上传和下载。

图像/音频处理库

图形绘制(Drawer)

数据处理中常见的一类工作就是对标注结果的可视化处理,通过将标注的点、线、矩形、多边形等图形绘制到原始 图像上,使得对标注效果可以做快速判断。另一方面,相比于坐标点集合的JSON文件,有些人更偏爱使用 mask图进行图像识别方面的训练,换句话说,这要求将标注结果绘制到一张与原始图像同样大小的黑色的 画布上。

然而在实际情况中,同一张图片往往包含多种图形的标注,例如街景标注中,可能既包含车道线(线),人、车( 多边形)的标注,也需要框出所有的交通灯和交通标志(矩形框)。而且,在绘制的时候,根据标注的实际情况, 绘制方法可能是轮廓也可能是填充。如何既提供通用快速的绘制接口,又能根据实际情况进行扩展(改变绘制方法 、颜色、粗细等),就是 toolbox.image.drawer 的设计目的。

因此,我们将 Shape(图形) 这一对象抽象出来,提供了验证和绘制的接口,并且提供默认的绘制实现, 调用绘制接口在图上进行绘制则由 Painter(绘制器) 完成。开发人员只需要关注图形的初始化和绘制方法 的实现即可。

Shapes(图形)

Shapes 是对被绘制图形的抽象,提供了一系列的绘制图形的基础属性如形状、颜色、线宽等,也提供 了对坐标值的格式进行验证、标准化、绘制的方法。

class moose.toolbox.image.drawer.BaseShape(coordinates, label, color=None, filled=None, thickness=None, **options)

BaseShape是所有图形类的基类,它定义了Shape子类应该包含的属性、需要实现的接口并且为通用 方法提供了默认实现。

参数:
  • coordinates (list) – 表示图形的坐标点的集合。对于不同的图形可能有不同的格式要求,例如对于点, 要求 coordinates 的值是包含两个元素的列表 [x, y] ,对于多边形,要求是 [[x1, y1], [x2, y2] ... [xn, yn], [x1, y1]] 的格式,等等。对于具体格式的检查在 _is_valid_coordinates() 中有相应的实现。
  • label (str) – 该图形对应物体的标签。
  • color (tuple) – 该图形被绘制时的颜色,可以是一个整型(灰度图)也可以是包含三个整型的元组(RGB)。 默认值是 default_color = settings.DEFAULT_COLOR
  • filled (boolean) – 该图形被绘制时是采用内部填充的方式还是绘制轮廓的方式,默认值根据类变量 drawn_filled 决定,对于线、点等图形不发挥作用。
  • thickness (int) – 该图形被绘制时的线的粗细,默认值为 default_thickness = settings.DEFAULT_THICKNESS
  • options (dict) – 其他的可选项参数。
type

类变量,表征该绘制图形的名称,如 Polygon,Point,Rectangle 等。

_is_list_of_pairs(points)

@classmethod

参数:points (tuple) – 坐标点。

返回bool值,该方法通过调用is_valid_format()和is_valid_value()来对输入点进行校验。

is_valid_format(point)

@classmethod

参数:point (tuple) – 坐标点。

返回bool值,该方法用来判断输入点的类型是否为列表或者元祖,且长度是否为2,若满足则返回 True ,否则为 False

is_valid_value(point)

@classmethod

参数:point (tuple) – 坐标点。

返回bool值,该方法用来判断输入点中元素是否是整数(或者为可以转换成整数的字符串)。

_equal_points(p1, p2)

@classmethod

参数:
  • p1 (tuple) – 坐标点。
  • p2 (tuple) – 坐标点。

返回bool值,该方法用来判断输入的两个点是否完全相等(x,y是否分别对应相等)。

_is_valid_coordinates(coordinates)
参数:coordinates (list) – 坐标列表。

对传入的坐标参数进行校验,默认返回 True ,子类根据具体图形实现自己的校验方式。

normalize(coordinates)
参数:coordinates (list) – 表示图形的坐标点的集合。

该方法是将输入的坐标进行格式化(将其元素中的浮点数转换为 int ,并将列表转换为 tuple )。返回一个内部元素为元祖的列表。

set_color(color)
参数:color (str) – 颜色。

设置绘制颜色,如果输入的color为 None,则使用默认的颜色,否则使用输入的颜色。

color()

@property

返回该图形的绘制颜色。 需要注意的是,因为 OpenCV 中(R, G, B)是反向的,如果 self._colorlist 或者 tuple 则对其逆序。

draw_on(im)
参数:im (object) – OpenCV 图形对象。

图形被绘制的主要接口,定义了绘制时的默认行为,如果 self._filledTrue , 则使用填充方式绘制,否则使用绘制轮廓。

_fill(im)

abstract

参数:im (object) – OpenCV 图形对象。

该方法用来在被标注对象文件上使用图像上的颜色填充形状,为预留接口,子类必须继承并且实现该方法。

_outline(im)

abstract

参数:im (object) – OpenCV 图形对象。

该方法用来在被标注对象文件上绘制轮廓的形状,为预留接口,子类必须继承并且实现该方法。

class moose.toolbox.image.drawer.Point(BaseShape)

Point 是BaseShape的子类,它定义了图形 的具体实现。

type

默认值为 “Point”

radius

定义绘制图形点的半径,默认值为 settings.DRAWER_RADIUS

_is_valid_coordinates(coordinates)
参数:coordinates (tuple) – 表示图形点的坐标点的集合。

判断输入格式是否为 [x, y] 的形式。

draw_on(im)
参数:im (object) – OpenCV 图形对象。

调用 cv2.circle 进行绘制,具体实现如下::

cv2.circle(im, self._coordinates, self.radius, self.color, -1)
class moose.toolbox.image.drawer.LineString(BaseShape)

LineString 是BaseShape的子类,它定义了图形 线 的具体实现。

type

默认值为 “LineString”

_is_valid_coordinates(coordinates)
参数:coordinates (list) – 表示图形线的坐标点的集合。

判断坐标是否按照 [[x0, y0], [x1, y1]][[x0, y0], [x1, y1], [x2, y2]] 的格式传入。

draw_on(im)
参数:im (object) – OpenCV 图形对象。

调用 cv2.line 进行绘制,具体实现如下::

for start, end in zip(self._coordinates[:-1], self._coordinates[1:]):
    cv2.line(im, start, end, self.color, self._thickness)
class moose.toolbox.image.drawer.Polygon(BaseShape)

Polygon 是BaseShape的子类,它定义了图形 多边形 的具体实现。与 线 不同的是,在绘制时既可以填充也可以绘制轮廓,默认情况下,我们使用填充的方式进行绘制。

type

默认值为 “Polygon”

is_closed

定义该类绘制图形的形状是否是封闭的。

drawn_filled

默认为 True ,即绘制时默认使用填充的方式。

_is_valid_coordinates(coordinates)
参数:coordinates (list) – 坐标。

判断输入点是否是 [[x1, y1], [x2, y2] ... [xn, yn], [x1, y1]] 的格式。

to_nparray()

返回将坐标点转换成 np.array 对象的表示,其中每个元素类型为 np.int32

_fill(im)

调用 cv2.fillPoly 进行绘制,具体实现如下::

cv2.fillPoly(im, [self.to_nparray()], self.color)
_outline(im)

调用 cv2.polylines 进行绘制,具体实现如下::

cv2.polylines(im, [self.to_nparray()], self.is_closed, self.color, self._thickness)
class moose.toolbox.image.drawer.Rectangle(BaseShape)

Rectangle 是BaseShape的子类,它定义了图形 矩形 的具体实现。与 线 不同的是,在绘制时既可以填充也可以绘制轮廓,默认情况下,我们使用绘制轮廓的方式进行绘制。

type

默认值为 “Rectangle”

drawn_filled=False

默认为 False ,即绘制时默认使用绘制轮廓的方式。

_is_valid_coordinates(coordinates)
参数:coordinates (list) – 坐标。

判断输入点是否为 [[x1, y1], [x2, y2]] 的形式。

from_region(region, label, **options)

@classmethod

参数:
  • region (list) – 坐标。
  • label (str) – 标签。
  • options (dict) – 其他可选参数。

当坐标点格式为 [x, y, w, h] 调用此类方法来实例化。

from_points(points, label, **options)

@classmethod

参数:
  • coordinates (tuple) – 坐标。
  • label (str) – 标签。
  • options (dict) – 其他可选参数。

当坐标点格式为 [[x1, y1], [x1, y2], [x2, y2,], [x2, y1], [x1, y1]] 调用此类方法来实例化。

to_points()

输出按照 [[x1, y1], [x1, y2], [x2, y2,], [x2, y1], [x1, y1]] 形式的坐标点表示。

_outline(im)

调用 cv2.rectangle 进行绘制,具体实现如下::

cv2.rectangle(im, tuple(self._coordinates[0]), tuple(self._coordinates[1]), self.color, self._thickness)
_fill(im)

调用 cv2.rectangle 进行绘制,具体实现如下::

cv2.rectangle(im, tuple(self._coordinates[0]), tuple(self._coordinates[1]), self.color, -1)

Painter(绘制器)

class moose.toolbox.image.drawer.GeneralPainter(image_path, pallet=None, autofill=False, use_default=False, persistent=True)

GeneralPainter 提供了整合多个图形,对图像按照多种方式进行绘制的能力。为 GeneralPainter 提供图片的完整路径和待绘制的 Shapes 列表,即可输出效果图(draw),掩模图(masking) 和合成图(blend)。对于输出图形的颜色,既可以在实例化 Shapes 的时候定义,也可以向 GeneralPainter 提供一个pallet(调色板),指定每个label和color的映射关系,来控制 图形绘制颜色的选择。

参数:
  • image_path (str) – 图片路径,如果不存在会抛出 IOError
  • pallet (dict) – 标签和颜色的映射表,提供了 shape 的label对应的颜色。
  • autofill (bool) – 当 shape 的label在 pallet 中不存在时,是否随机生成 一个颜色,如果为 False 时,会抛出 ImproperlyConfigured
  • use_default (bool) – 为 True 时,使用 shape 的color属性的值作为绘制的 颜色。
  • persistent (bool) – 当 autofillTrue 时,是否所有图形绘制使用相同的 label到color的映射。
shape_line_cls

指定实例化图形 线 的类,默认为 LineString

shape_point_cls

指定实例化图形 的类,默认为 Point

shape_polygon_cls

指定实例化图形 多边形 的类,默认为 Polygon

shape_rectangle_cls

指定实例化图形 矩形 的类,默认为 Rectangle

get_color(label)
参数:label (str) – 图形标签

返回图形标签对应的颜色,该值受 palletautofilluse_default 的影响,有以下三种可能:

  1. use_defaultTrue 的时候,返回 None
  2. use_defaultFalsepallet 包含该label对应的color,返回该值;
  3. use_defaultFalsepallet 不包含该label,且 autofillTrue 时,随机返回一种颜色;
add_color(label, color)
参数:
  • label (str) – 图形标签
  • color (tuple) – 颜色

添加一对标签和颜色的对应关系到 pallet 中。

update_pallet(pallet)
参数:pallet (dict) – 标签和颜色的映射表

更新 pallet 的标签和颜色的映射。

add_shape(shape)
参数:shape (object) – 图形对象

添加一个待绘制的图形对象。

from_shapes(shapes)
参数:shapes (list) – 图形对象列表或生成器

添加多个待绘制的图形对象。

clear()

清空待绘制的图形对象列表。

add_line(p1, p2, label, **options)
参数:
  • p1 (tuple) – 点坐标
  • p2 (tuple) – 点坐标
  • label (str) – 图形标签
  • options (dict) – 参数

实例化并添加线图形对象。

add_point(p, lable, **options)
参数:
  • p (tuple) – 点坐标
  • label (str) – 图形标签
  • options (dict) – 参数

实例化并添加点图形对象。

add_rectangle(p1, p2, label, **options)
参数:
  • p1 (tuple) – 点坐标
  • p2 (tuple) – 点坐标
  • label (str) – 图形标签
  • options (dict) – 参数

实例化并添加矩形图形对象。

add_polygon(pts, label, **options)
参数:
  • pts (tuple) – 点坐标
  • label (str) – 图形标签
  • options (dict) – 参数

实例化并添加多边形图形对象。

render(canvas)
参数:canvas (object) – 待绘制的目标图像文件

绘制图形列表中的图形到canvas上::

for shape in self._shapes:
    shape.set_color(self.get_color(shape._label))
    shape.draw_on(canvas)
return canvas
draw(filename)
参数:filename (str) – 图片对象名称

将图形列表中的图形绘制到原始图像中。

masking(filename)
参数:filename (str) – 图片对象名称

将图形列表中的图形绘制到与原始图像等尺寸的所有值为 (0, 0, 0) 的图像上。

blend(filename, alpha=0.7, gamma=0.0)
参数:
  • filename (str) – 图片对象名称
  • alpha (int) – 第一个数组元素的权重值
  • gamma (int) – 标量,在按位与计算中将标量加到每个和中,调整整体颜色

叠加原始图片和mask图,按照如下公式生成::

dst = alpha * src1 + (1 - alpha) * src2 + gamma

配置(settings)

异常

命令行工具
通过Moose的命令行工具来管理你的Moose项目。
动作(Actions)
编写Action实现你的业务逻辑。
模型(Model)
通过Model抽象你的数据。
订单(Order)
定义接口,创建订单。
Application模板
打包app,复用代码结构。
连接(Connection)
连接Azure,MongoDB,SQLServer/MySQL。
图像/音频处理库
便捷和统一的多媒体处理类和函数。
配置(settings)
了解Moose的各个配置项。
异常
异常抛出说明和推荐使用惯例。

相关主题

业务逻辑

Moose 提供了一系列的动作(action)支撑我们的日常工作,其中最常使用的就是上传 upload 和导出 export 了。这一节中我们着重解释这两类任务背后的工作原理,以期帮助大家理解在诸多的upload和export类中共通的流程。

上传(upload)

_images/upload-overview.png

如上图所示,上传过程包含以下步骤:

  1. 将想要标注的文件由本地上传到 Azure Blob Storage 。微软官方提供了教程解释如何使用 blob ,其中除了 Python SDK 外,还包括了一系列其他的方法。我们对此进行了封装,并提供了 连接(Connection) 模块帮助用户通过几行代码完成上传;
  2. 将已上传的文件列表按照标注模板需要的格式进行组织,形成索引文件。索引文件提供了被标注对象的相关信息——如存储路径、文件标题、音频内容、音频时长等;
  3. 利用平台的接口导入索引文件。后台通过解析索引文件,在数据库中插入相关信息;
  4. 标注人员开始标注。这一过程将会首先从后台提取相应信息,前端页面解析后加载并从 blob 中提取原始文件。

需要注意的是,为了方便处理,我们定义了一套惯例:针对每一期任务都有一个唯一的 task ID ,这个 task ID 既对应着标注平台上手动创建的一期任务,也对应着后台MongoDB中存储该期索引和标注信息的一个 database 的名称 ,同时也是 blob 中存储该期标注文件的容器名。 因此索引文件中,如果我们只记录文件在容器中的名称(意即 myblob 而非 http://myaccount.blob.core.chinacloudapi.cn/mycontainer/myblob 这样的完整的地址),系统会自动使用对应 task ID 的账号和容器来扩展url。

导出(export)

_images/export-overview.png

如上图所示,导出过程包含以下步骤:

  1. 从后端的 SQL Server 数据库中提取对应的 task ID 符合条件的记录。其中,每条原始数据和其标注结果都分别对应着一个 guid ,分别称为 SourceGuidDataGuid ,;
  2. 从后端的 MongoDB 中提取对应的 task ID 所有的索引和标注数据。注意,此时的索引和标注数据分别是两个 collection (名称分别为 sourceresult ),他们各自间的记录还没有建立一一对应的关系;
  3. 通过提取到的 SourceGuidDataGuid ,将 sourceresult 的记录建立一一对应的关系,并提供给数据工程师进行后续的数据提取、映射和转换等处理;
  4. 将结果生成文本文件。

我们可以发现,将 SQL ServerMongoDB 中的数据联系起来的是每条记录的 guid 。其中 SQL Server 主要用于存储结构化的信息,诸如数据的导入时间、标注时间、标注人员等,而 MongoDB 则用于存储非结构化的数据,如标注文件的地址和名称、标注对象的标签和坐标点。因此我们主要利用 SQL Server 筛选(通过强大的 where 表达式)提取出符合记录的 guid ,再在 MongoDB 中提取该 guid 对应的原始信息(通过索引文件导入)和标注信息,再进行后续的加工和处理。

业务逻辑
与上传和导出工作相关的业务逻辑。

其他

参与Moose的开发

GitLab Workfow

我们遵循基于GitLab的工作流,所以在参与到Moose的开发之前,最好先了解下 GitLab Workflow: An Overview 以尽快熟悉基本流程。简而言之,GitLab Workflow将软件开发分解成10个步骤,我们在此之上略有调整以适合Moose的开发工作模式,分别是:

  1. IDEA。
  2. ISSUE:讨论一个idea的最好办法就是为它创建一个issue,你的团队和同事会帮你一起在 Issue Tracker 中打磨和完善它;
  3. PLAN:一旦讨论达成一致,就是时间开始编码了。但是!我们需要根据优先级以及前置条件来调整我们的工作顺序,在 Issue Board 中来制定开发计划;
  4. CODE:创建该issue的feature分支和Merge Request,拉取到本地并开始编码;
  5. COMMIT:在该feature分支中提交你的代码;
  6. TEST:编写脚本或单元测试在 GitLab CI 中测试你的代码,确保所有单元测试能通过;
  7. REVIEW:当代码可以工作,单元测试和编译都能通过时,就可以开始进入代码审查阶段了。代码审查主要是为了保证编码风格、模块设计等非功能性问题和整体是否一致,没有问题则合并;
  8. FEEDBACK:检查各阶段在整个流程中的时间开销,研究是否有提升的地方。

GitLab Workflow: An Overview 详细解释了GitLab如何支持以上工作流,我们不再赘述,只挑非通用的地方进行说明。

参与讨论

Moose所有的讨论都在 Issue Tracker 中进行,使用该工具创建并跟踪你的idea,bug fix或是相关文档的进展。

  • 标签为 Bug 的issue意味这是一个需要修复的问题,描述清楚bug产生的步骤、期望行为以及实际结果,有助于尽快定位问题原因(选择template -> Bug 会提示需要描述的内容)。我们将优先级由上至下分为 Top PriorityP1P2 和无,按照重要性和紧急程度予以标识;

注解

书写良好的bug报告可以帮助开发者尽快定位问题,减少重现bug和解决的时间。在报告bug时请遵照以下原则:

  • 检查 Open Issues 中是否已有你发现的bug,如果已经有了,请查看报告信息和comments,如果你有新的发现,可以留下comment,或者考虑直接提交解决该bug的 Merge Request
  • Issues 中添加一个 完整的,可再现的,特定bug的报告 。这个bug的测试用例越小越好,请注意附上相关文件以保证其他开发者能重现。可以参考StackOverflow上关于 如何创建一个最小的-完整的-可验证的例子 的指导来编写你的issue;
  • 最好的方式是提供一个可以重现这个bug的单元测试(并失败)的 Merge Request
  • 标签为 Proposal 的issue意味这是一个有待实现的想法,是一个尚未开始的功能。描述这个想法希望达到的目的、旨在解决的问题,给出使用的场景和示例,如果可以的话还可以提供实现它的关键代码(template -> Proposal);
  • 标签为 Enhancement 的issue意味这是一个对旧有功能进行增强或修改的需求,通常是因为旧有功能不再适应新的业务场景,频繁的修改、重复的代码、工作量的增加都意味着代码需要重构,又或者只是需要添加单元测试。所有对旧有模块的修改和补充都应该打上`Enhancement`标签并且标上优先级 (template -> Enhancement);
  • 标签为 Document 的issue意味着这是一个说明性的文档的修改和添加,描述这篇文档要说明的对象、编写的目的和关键点、最好能够列出大纲,这能帮助其他人在编写文档的时候按照你想的方向前进(template -> Document);

计划安排

  • 如果一个issue足够简单或者已经经过充分的讨论,提出者了解所有细节或是希望用代码进行说明:那么将该Issue的Assignee指给自己,并进入代码提交流程;

  • 如果尚无issue的解决思路,只是发现某个bug或在功能上提出新的需求,那么:

    • 指定Assignee给Maintainer,并cc给 dataproc 组(通过指令 /cc @dataproc );
    • Maintainer负责回复和组织对该issue的讨论,安排开发计划和时间周期,将Assignee指给相应的负责人,并将标签置为 Todo
    • 对于不需要安排开发计划的issue,(可能是Issue本身描述不清、无法实现、被其他计划包含或不在功能设计之内等),说明理由并关掉。

代码提交

开发人员收到新的开发任务时(有issue的Assignee是自己),就进入到编码阶段,此时:

  • 修改Issue标签从 TodoDoing
  • 参照分支命名风格,在本地创建分支,并在任一commit中提及该 issue number (#xxx 的形式);
  • 完成编码后创建Merge Request,并将MR的Assignee指给Maintainer;
  • 注意在提交MR之前,除了功能代码外,也需提供相应的单元测试证明代码work,并确保所有的单元测试能跑通;

Coding style

  • 除非特殊声明的情况,Moose的编码风格遵照 PEP 8
  • Moose Cookbook提供了一些Moose使用上的建议和惯例说明,除了用户,开发者也应该了解它,确保你的想法(反映在风格上)和设计者是一致的;

分支命名风格

  • 分支创建分为功能增加和bug修改两种,两者都基于master分支创建;
  • 分支名应该足够简短且具有代表性,包含1~4个单词,使用中横线分割,表示本次修改主要做什么(可以省略动词)。例如:empty-nil-blank、rm-default-type-submit、rebuild-model-properties;
  • 如果该分支的创建是为了增加新功能,加上 feature/* 前缀;
  • 如果该分支的创建是为了修复临时的紧急的bug,加上hotfix/* 前缀,并将修改结果应用到上个patch中(通过 cherry-pick );

提交操作

详细操作流程如下:

  1. 更新你本地的代码(git pull);
  2. 创建你的 feature或bugfix 分支 (git checkout -b feature/my-new-feature);
  3. 提交修改 (git commit -am ‘Add some feature; fixed #23’);
  4. 推送到你的远端分支上 (git push origin feature/my-new-feature)
  5. 创建 Merge Request,并指定Assignee为Maintainer;

测试

运行单元测试,linux 和 macOS下:

$ ./runtest.sh tests/tests_dirs/

windows下:

$ runtest.bat tests/tests_dirs/

或者使用tox:

$ tox -e new

Release notes

Moose 0.9.6Beta (2018-04-04)

  • 添加了文档中 example.rst 的部分;
  • 修改了 Model 的初始化参数,默认传入关键字参数 **context 作为上下文环境;
  • 解决了在windows环境下使用python3安装Moose会失败的bug;

Moose 0.9.5Beta (2018-03-13)

  • 移除了对 azure>=2.0.0rc 的依赖,改为对 azure-storage-blob>=1.1.0 依赖;
  • 添加了 action.upload.VideosUpload 以支持对视频拆帧的图片的通用上传;
  • 添加了 process.video 模块,增加对视频的处理;

Moose 0.9.3Beta (2017-12-29)

  • 由于 MySQLdb 在Windows和Mac OSX上安装经常失败导致Moose安装中断,因此我们移除了对 MySQLdbpysmb 的依赖声明。依赖这两个包的模块依然存在,但由于使用较少基本不会造成较大影响,但如果需要时请尝试手动安装。

Moose 0.9.0Beta (2017-12-06)

作为发布的第一个版本,Moose提供了基本核心的功能,包括:

  • 创建项目(project)和应用(app);
  • 生成和编辑订单(order);
  • 运行App;
  • 抽象了常用的连接;
  • 内置常见的行为类;
参与Moose的开发
参与到Moose的开发中来!
Release notes
更新日志。