Пакет Python для распознавания рукописного ввода

В настоящее время я пишу диссертацию бакалавра о распознавании рукописного ввода в режиме онлайн. Это не OCR, потому что у меня есть информация о том, как символ записывается как список координат траектории пера (x, y).

Я называю этот hwrt - инструментарий распознавания рукописного ввода. Он имеет документацию , а мой друг получил «первые шаги» для работы на своем компьютере.

Однако в первый раз я написал пакет Python, который я надеюсь, что другие могут его использовать. Я надеюсь получить общую информацию об этом проекте.

Проект размещен в GitHub и имеет следующую структуру:

.
├── bin
├── dist
├── docs
├── hwrt
│   ├── misc
│   └── templates
└── tests
    └── symbols

У меня есть некоторые носететы (недостаточно, я над этим работаю).

Один из файлов в bin - view.py. Это позволяет пользователям просматривать ранее загруженные данные (см. «Первые шаги» в моей документации).

setup.py

try:
    from setuptools import setup
except ImportError:
    from distutils.core import setup

config = {
    'name': 'hwrt',
    'version': '0.1.125',
    'author': 'Martin Thoma',
    'author_email': '[email protected]',
    'packages': ['hwrt'],
    'scripts': ['bin/backup.py', 'bin/view.py', 'bin/download.py',
                'bin/test.py', 'bin/train.py', 'bin/analyze_data.py',
                'bin/hwrt', 'bin/record.py'],
    'package_data': {'hwrt': ['templates/*', 'misc/*']},
    'url': 'https://github.com/MartinThoma/hwrt',
    'license': 'MIT',
    'description': 'Handwriting Recognition Tools',
    'long_description': """A tookit for handwriting recognition. It was
    developed as part of the bachelors thesis of Martin Thoma.""",
    'install_requires': [
        "argparse",
        "theano",
        "nose",
        "natsort",
        "PyYAML",
        "matplotlib",
        "shapely"
    ],
    'keywords': ['HWRT', 'recognition', 'handwriting', 'on-line'],
    'download_url': 'https://github.com/MartinThoma/hwrt',
    'classifiers': ['Development Status :: 3 - Alpha',
                    'Environment :: Console',
                    'Intended Audience :: Developers',
                    'Intended Audience :: Science/Research',
                    'License :: OSI Approved :: MIT License',
                    'Natural Language :: English',
                    'Programming Language :: Python :: 2.7',
                    'Programming Language :: Python :: 3',
                    'Topic :: Scientific/Engineering :: Artificial Intelligence',
                    'Topic :: Software Development',
                    'Topic :: Utilities'],
    'zip_safe': False,
    'test_suite': 'nose.collector'
}

setup(**config)

view.py

#!/usr/bin/env python
"""
Display a recorded handwritten symbol as well as the preprocessing methods
and the data multiplication steps that get applied.
"""

import sys
import os
import logging
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
                    level=logging.DEBUG,
                    stream=sys.stdout)
import yaml
try:  # Python 2
    import cPickle as pickle
except ImportError:  # Python 3
    import pickle

# My modules
import hwrt
from hwrt import HandwrittenData
sys.modules['HandwrittenData'] = HandwrittenData
import hwrt.utils as utils
import hwrt.preprocessing as preprocessing
import hwrt.features as features
import hwrt.data_multiplication as data_multiplication


def _fetch_data_from_server(raw_data_id):
    """Get the data from raw_data_id from the server.
    :returns: The ``data`` if fetching worked, ``None`` if it failed."""
    import MySQLdb
    import MySQLdb.cursors

    # Import configuration file
    cfg = utils.get_database_configuration()
    if cfg is None:
        return None

    # Establish database connection
    connection = MySQLdb.connect(host=cfg[args.mysql]['host'],
                                 user=cfg[args.mysql]['user'],
                                 passwd=cfg[args.mysql]['passwd'],
                                 db=cfg[args.mysql]['db'],
                                 cursorclass=MySQLdb.cursors.DictCursor)
    cursor = connection.cursor()

    # Download dataset
    sql = ("SELECT `id`, `data` "
           "FROM `wm_raw_draw_data` WHERE `id`=%i") % raw_data_id
    cursor.execute(sql)
    return cursor.fetchone()


def _get_data_from_rawfile(path_to_data, raw_data_id):
    """Get a HandwrittenData object that has ``raw_data_id`` from a pickle file
       ``path_to_data``.
       :returns: The HandwrittenData object if ``raw_data_id`` is in
                 path_to_data, otherwise ``None``."""
    loaded = pickle.load(open(path_to_data))
    raw_datasets = loaded['handwriting_datasets']
    for raw_dataset in raw_datasets:
        if raw_dataset['handwriting'].raw_data_id == raw_data_id:
            return raw_dataset['handwriting']
    return None


def _list_ids(path_to_data):
    """List raw data IDs grouped by symbol ID from a pickle file
       ``path_to_data``."""
    loaded = pickle.load(open(path_to_data))
    raw_datasets = loaded['handwriting_datasets']
    raw_ids = {}
    for raw_dataset in raw_datasets:
        raw_data_id = raw_dataset['handwriting'].raw_data_id
        if raw_dataset['formula_id'] not in raw_ids:
            raw_ids[raw_dataset['formula_id']] = [raw_data_id]
        else:
            raw_ids[raw_dataset['formula_id']].append(raw_data_id)
    for symbol_id in sorted(raw_ids):
        print("%i: %s" % (symbol_id, sorted(raw_ids[symbol_id])))


def _get_description(prev_description):
    """Get the parsed description file (a dictionary) from another
       parsed description file."""
    current_desc_file = os.path.join(utils.get_project_root(),
                                     prev_description['data-source'],
                                     "info.yml")
    if not os.path.isfile(current_desc_file):
        logging.error("You are probably not in the folder of a model, because "
                      "%s is not a file.", current_desc_file)
        sys.exit(-1)
    with open(current_desc_file, 'r') as ymlfile:
        current_description = yaml.load(ymlfile)
    return current_description


def _get_system(model_folder):
    """Return the preprocessing description, the feature description and the
       model description."""

    # Get model description
    model_description_file = os.path.join(model_folder, "info.yml")
    if not os.path.isfile(model_description_file):
        logging.error("You are probably not in the folder of a model, because "
                      "%s is not a file. (-m argument)",
                      model_description_file)
        sys.exit(-1)
    with open(model_description_file, 'r') as ymlfile:
        model_desc = yaml.load(ymlfile)

    # Get the feature and the preprocessing description
    feature_desc = _get_description(model_desc)
    preprocessing_desc = _get_description(feature_desc)

    return (preprocessing_desc, feature_desc, model_desc)


def display_data(raw_data_string, raw_data_id, model_folder):
    """Print ``raw_data_id`` with the content ``raw_data_string`` after
       applying the preprocessing of ``model_folder`` to it."""
    print("## Raw Data (ID: %i)" % raw_data_id)
    print("```")
    print(raw_data_string)
    print("```")

    preprocessing_desc, feature_desc, _ = _get_system(model_folder)

    # Print model
    print("## Model")
    print("%s\n" % model_folder)

    # Print preprocessing queue
    print("## Preprocessing")
    print("```")
    tmp = preprocessing_desc['queue']
    preprocessing_queue = preprocessing.get_preprocessing_queue(tmp)
    for algorithm in preprocessing_queue:
        print("* " + str(algorithm))
    print("```")

    feature_list = features.get_features(feature_desc['features'])
    input_features = sum(map(lambda n: n.get_dimension(), feature_list))
    print("## Features (%i)" % input_features)
    print("```")
    for algorithm in feature_list:
        print("* %s" % str(algorithm))
    print("```")

    # Get Handwriting
    recording = HandwrittenData.HandwrittenData(raw_data_string,
                                                raw_data_id=raw_data_id)

    # Get the preprocessing queue
    tmp = preprocessing_desc['queue']
    preprocessing_queue = preprocessing.get_preprocessing_queue(tmp)
    recording.preprocessing(preprocessing_queue)

    # Get feature values as list of floats, rounded to 3 decimal places
    tmp = feature_desc['features']
    feature_list = features.get_features(tmp)
    feature_values = recording.feature_extraction(feature_list)
    feature_values = [round(el, 3) for el in feature_values]
    print("Features:")
    print(feature_values)

    # Get the list of data multiplication algorithms
    mult_queue = data_multiplication.get_data_multiplication_queue(
        feature_desc['data-multiplication'])

    # Multiply traing_set
    training_set = [recording]
    for algorithm in mult_queue:
        new_trning_set = []
        for recording in training_set:
            samples = algorithm(recording)
            for sample in samples:
                new_trning_set.append(sample)
        training_set = new_trning_set

    # Display it
    for recording in training_set:
        recording.show()


def get_parser():
    """Return the parser object for this script."""
    from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
    parser = ArgumentParser(description=__doc__,
                            formatter_class=ArgumentDefaultsHelpFormatter)
    parser.add_argument("-i", "--id", dest="id", default=292293,
                        type=int,
                        help="which RAW_DATA_ID do you want?")
    parser.add_argument("--mysql", dest="mysql", default='mysql_online',
                        help="which mysql configuration should be used?")
    parser.add_argument("-m", "--model",
                        dest="model",
                        help="where is the model folder (with a info.yml)?",
                        metavar="FOLDER",
                        type=lambda x: utils.is_valid_folder(parser, x),
                        default=utils.default_model())
    parser.add_argument("-l", "--list",
                        dest="list",
                        help="list all raw data IDs / symbol IDs",
                        action='store_true',
                        default=False)
    parser.add_argument("-s", "--server",
                        dest="server",
                        help="contact the MySQL server",
                        action='store_true',
                        default=False)
    return parser

if __name__ == '__main__':
    args = get_parser().parse_args()
    if args.list:
        preprocessing_desc, _, _ = _get_system(args.model)
        raw_datapath = os.path.join(utils.get_project_root(),
                                    preprocessing_desc['data-source'])
        _list_ids(raw_datapath)
    else:
        if args.server:
            data = _fetch_data_from_server(args.id)
            print("hwrt version: %s" % hwrt.__version__)
            display_data(data['data'], data['id'], args.model)
        else:
            logging.info("RAW_DATA_ID %i does not exist or "
                         "database connection did not work.", args.id)
            # The data was not on the server / the connection to the server did
            # not work. So try it again with the model data
            preprocessing_desc, _, _ = _get_system(args.model)
            raw_datapath = os.path.join(utils.get_project_root(),
                                        preprocessing_desc['data-source'])
            handwriting = _get_data_from_rawfile(raw_datapath, args.id)
            if handwriting is None:
                logging.info("Recording with ID %i was not found in %s",
                             args.id,
                             raw_datapath)
            else:
                print("hwrt version: %s" % hwrt.__version__)
                display_data(handwriting.raw_data_json,
                             handwriting.formula_id,
                             args.model)

Как я уже писал, я хотел бы получить общую обратную связь о проекте. Однако у меня нет опыта в упаковке, поэтому я скопировал setup.py. Я особенно не уверен, был ли правильный выбор zip_safe: False.

Я думаю, что я следую PEP8 всюду, и я использую pylint для улучшения моего кода. Однако для view.py я не понимаю следующие предупреждения о стиле /не знаю, как их исправить (в хорошем смысле):

 W:115, 4: Redefining name 'preprocessing_desc' from outer scope (line 218) (redefined-outer-name)
W:128, 4: Redefining name 'preprocessing_desc' from outer scope (line 218) (redefined-outer-name)
R:120, 0: Too many local variables (19/15) (too-many-locals)
C:216, 4: Invalid constant name "args" (invalid-name)
C:218, 8: Invalid constant name "preprocessing_desc" (invalid-name)
C:219, 8: Invalid constant name "raw_datapath" (invalid-name)
C:224,12: Invalid constant name "data" (invalid-name)
C:232,12: Invalid constant name "preprocessing_desc" (invalid-name)
C:233,12: Invalid constant name "raw_datapath" (invalid-name)
C:235,12: Invalid constant name "handwriting" (invalid-name)
37 голосов | спросил Martin Thoma 31 +03002014-10-31T16:57:52+03:00312014bEurope/MoscowFri, 31 Oct 2014 16:57:52 +0300 2014, 16:57:52

1 ответ


25

В целом я думаю, что это хорошее качество, если немного плотный код, которым я означает, что, поскольку там есть много функциональности, это не самый простой код для подражания. Одно из предложений, которое у меня для этого, - это больше группировки по темам. Так, например, у коммунальных служб может быть только пара разделы для файлов, форматирование строк, вызов внешних программ так на.

Поскольку вы работаете с большим количеством внешних файлов, я представляю себе объект-оболочка для обработки проекта или так может работать.

(Честно говоря, я могу просто отправить запросы на тянуть сейчас, это проще, хе.)

Код

При запуске тестов я обнаружил, что импорт open из future.builtins не работает (Python 2.7.9), так как нет такого module /future_builtins не имеет open, что означает, что nntoolkit не может быть загружен и serve.py:19 также выдает ошибку. я не делайте этого, потому что у вас уже есть настройка Travis CI, я увижу если я могу получить основную причину этого.

IMO pickle - не самый приятный формат для долговременных файлов данных; однако на этот момент для меня это скорее рефлекс, если он работает для вас, чем конечно, почему бы и нет (хотя у вас уже есть хотя бы одно обходное решение, sys.modules, поэтому помните, что я думаю).

Для скорости вы можете использовать ujson как замену json.

В data_analyzation_metrics.py:119 необработанная escape-строка для цвета (?) является используемый. Я также хотел бы, чтобы глобальный флаг отключил цвета, и это было бы полезно использовать библиотеку для форматирования (я видел colorterm и termcolor; могут быть и другие).

Для чего-то вроде "%s" % str(x) не требуется str.

Вы уже делаете это в некоторых местах, поэтому я бы предложил использовать with open("foo") as file: все время (если возможно).

Вместо self.__repr__() repr(self) выглядит более чистым.

В features.py:174 должны быть извлечены факторы 2 и 3 например что-то вроде draw_width = 3 if self.pen_down else 2 или так; в общее извлечение общих подвыражений (даже len(x)) может устранить много кода, поэтому я не буду искать здесь других примеров.

В общем случае, если у вас есть if foo: return, вам не нужен else, просто удалите этот отступ; также возвращение рано может удалить много отступа.

Для HandwrittenData.py:208 весь метод можно просто уменьшить до:

def __eq__(self, other):
    return isinstance(other, self.__class__) \
        and self.__dict__ == other.__dict__

euclidean_distance из preprocessing.py:30 также определяется как scipy.spatial.distance.euclidean, поэтому, если в некоторых случаях вы запускаете этот более чем несколько элементов, которые вы могли бы использовать.

preprocessing.py:497 может быть более аккуратным с for index, point in enumerate(pointlist):.

which в selfcheck.py должен уже существовать где-то ...?

Вместо использования zip вы также можете использовать itertools.izip, генератор вариант, чтобы использовать меньше памяти, когда это возможно, то есть когда выполняется только итерация что-то с циклом for вместо сохранения результирующего списка.

Упаковка

setup.py имеет номер версии, но в репозитории Git нет соответствующие теги /релизы. Если вы начинаете сейчас и добавляете, например, 0.1.207 as первый выпуск (или так), который упростит обращение к конкретным версии, то есть из сценариев установки.

long_description имеет новую строку, которая может выглядеть странно в некоторых обстоятельства.

install_requires перечисляет хотя бы один пакет, который отсутствует в requirements.txt, поэтому я бы синхронизировал их, если только это действительно не требуется для установки, в этом случае этот вопрос является спорным.

Ключевые слова выглядят хорошо, за исключением того, что я сомневаюсь, что в том числе 'HWRT' помогает, если пакет уже назван таким образом и 'on-line', вероятно не требуется дефис (он делает, см. комментарии).

Классификаторы хороши; вы также можете указать более конкретные версии Python 3.

requirements.txt не имеет требований к версии. Я бы сказал, что по меньшей мере, некоторые нижние границы (т. е. установленные вами пакеты или так далее) было бы полезно иметь. Конечно, вы не можете точно знать, какие версии для поиска, но для тех, кто пытается ее запустить, это тем не менее, быть полезным.

Я точно не знаю процесс внешних требований, но вы может только заметить, что ImageMagick является зависимостью.

Вы также исправили материал PEP8, так что теперь есть только несколько слишком длинных строк оставил; вы также можете добавить pep8 как предварительную фиксациюкрючок, так что вы не смог бы пройти регистрацию без каких-либо исправлений; Я использую это для библиотечный код как минимум. То же самое касается pylint; Возможно, я отключу несколько (и добавьте его в Makefile или снова как pre-commit крюк).

Испытания

Отлично! Существует немного дублирования, например. compare_pointlists - это три раза. Если возможно, я переместил бы это (еще одно, heh) utils пакет, чтобы убрать его с пути.

Использование nosetests и добавление Makefile также хорошо.

Будущие идеи

Ну, мне нравится PostgreSQL, поэтому я думаю, что это произойдет в какой-то момент; если у вас нет насущной причины использовать MySQL исключительно, используя независимая от базы данных библиотека была бы классной.

ответил ferada 16 TueEurope/Moscow2014-12-16T20:51:01+03:00Europe/Moscow12bEurope/MoscowTue, 16 Dec 2014 20:51:01 +0300 2014, 20:51:01

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132