Computer Vision — PYTHON https://chel-center.ru/python-yfc курс молодого бойца Mon, 24 May 2021 04:20:47 +0000 ru-RU hourly 1 https://wordpress.org/?v=5.9.3 https://chel-center.ru/python-yfc/wp-content/uploads/sites/15/2020/06/cropped-kmb-icon-2-1-32x32.png Computer Vision — PYTHON https://chel-center.ru/python-yfc 32 32 Как оформлять код https://chel-center.ru/python-yfc/2019/10/01/kak-oformlyat-kod/ https://chel-center.ru/python-yfc/2019/10/01/kak-oformlyat-kod/#respond Tue, 01 Oct 2019 01:35:18 +0000 http://chel-center.ru/python-yfc/?p=502 Читать далее «Как оформлять код»

]]>
Этот документ описывает соглашение о том, как писать код для языка python, включая стандартную библиотеку, входящую в состав python.

PEP 8 (Python Enhancement Proposals, англ. или предложения по улучшению Python, рус.) создан на основе рекомендаций Гуидо ван Россума с добавлениями от Барри. Если где-то возникал конфликт, мы выбирали стиль Гуидо. И, конечно, этот PEP может быть неполным (фактически, он, наверное, никогда не будет закончен).

Ключевая идея Гуидо такова: код читается намного больше раз, чем пишется. Собственно, рекомендации о стиле написания кода направлены на то, чтобы улучшить читаемость кода и сделать его согласованным между большим числом проектов. В идеале, весь код будет написан в едином стиле, и любой сможет легко его прочесть.

Это руководство о согласованности и единстве. Согласованность с этим руководством очень важна. Согласованность внутри одного проекта еще важнее. А согласованность внутри модуля или функции — самое важное. Но важно помнить, что иногда это руководство неприменимо, и понимать, когда можно отойти от рекомендаций. Когда вы сомневаетесь, просто посмотрите на другие примеры и решите, какой выглядит лучше.

Две причины для того, чтобы нарушить данные правила:

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

Внешний вид кода

Отступы

Используйте 4 пробела на каждый уровень отступа.

Продолжительные строки должны выравнивать обернутые элементы либо вертикально, используя неявную линию в скобках (круглых, квадратных или фигурных), либо с использованием висячего отступа. При использовании висячего отступа следует применять следующие соображения: на первой линии не должно быть аргументов, а остальные строки должны четко восприниматься как продолжение линии.

Правильно:

# Выровнено по открывающему разделителю
foo = long_function_name(var_one, var_two,
                         var_three, var_four)

# Больше отступов включено для отличения его от остальных
def long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)

Неправильно:

# Аргументы на первой линии запрещены, если не используется вертикальное выравнивание
foo = long_function_name(var_one, var_two,
    var_three, var_four)

# Больше отступов требуется, для отличения его от остальных
def long_function_name(
    var_one, var_two, var_three,
    var_four):
    print(var_one)

Опционально:

# Нет необходимости в большем количестве отступов.
foo = long_function_name(
  var_one, var_two,
  var_three, var_four)

Закрывающие круглые/квадратные/фигурные скобки в многострочных конструкциях могут находиться под первым непробельным символом последней строки списка, например:

my_list = [
    1, 2, 3,
    4, 5, 6,
    ]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
    )

либо быть под первым символом строки, начинающей многострочную конструкцию:

my_list = [
    1, 2, 3,
    4, 5, 6,
]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
)

Табуляция или пробелы?

Пробелы — самый предпочтительный метод отступов.

Табуляция должна использоваться только для поддержки кода, написанного с отступами с помощью табуляции.

Python 3 запрещает смешивание табуляции и пробелов в отступах.

Python 2 пытается преобразовать табуляцию в пробелы.

Когда вы вызываете интерпретатор Python 2 в командной строке с параметром -t, он выдает предупреждения (warnings) при использовании смешанного стиля в отступах, а запустив интерпретатор с параметром -tt, вы получите в этих местах ошибки (errors). Эти параметры очень рекомендуются!

Максимальная длина строки

Ограничьте длину строки максимум 79 символами.

Для более длинных блоков текста с меньшими структурными ограничениями (строки документации или комментарии), длину строки следует ограничить 72 символами.

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

Некоторые команды предпочитают большую длину строки. Для кода, поддерживающегося исключительно или преимущественно этой группой, в которой могут прийти к согласию по этому вопросу, нормально увеличение длины строки с 80 до 100 символов (фактически увеличивая максимальную длину до 99 символов), при условии, что комментарии и строки документации все еще будут 72 символа.

Стандартная библиотека Python консервативна и требует ограничения длины строки в 79 символов (а строк документации/комментариев в 72).

Предпочтительный способ переноса длинных строк является использование подразумеваемых продолжений строк Python внутри круглых, квадратных и фигурных скобок. Длинные строки могут быть разбиты на несколько строк, обернутые в скобки. Это предпочтительнее использования обратной косой черты для продолжения строки.

Обратная косая черта все еще может быть использована время от времени. Например, длинная конструкция with не может использовать неявные продолжения, так что обратная косая черта является приемлемой:

with open('/path/to/some/file/you/want/to/read') as file_1, \
        open('/path/to/some/file/being/written', 'w') as file_2:
    file_2.write(file_1.read())

Ещё один случай — assert.

Сделайте правильные отступы для перенесённой строки. Предпочтительнее вставить перенос строки после логического оператора, но не перед ним. Например:

class Rectangle(Blob):

    def __init__(self, width, height,
                 color='black', emphasis=None, highlight=0):
        if (width == 0 and height == 0 and
                color == 'red' and emphasis == 'strong' or
                highlight > 100):
            raise ValueError("sorry, you lose")
        if width == 0 and height == 0 and (color == 'red' or
                                           emphasis is None):
            raise ValueError("I don't think so -- values are %s, %s" %
                             (width, height))
        Blob.__init__(self, width, height,
                      color, emphasis, highlight)

Пустые строки

Отделяйте функции верхнего уровня и определения классов двумя пустыми строками.

Определения методов внутри класса разделяются одной пустой строкой.

Дополнительные пустые строки возможно использовать для разделения различных групп похожих функций. Пустые строки могут быть опущены между несколькими связанными однострочниками (например, набор фиктивных реализаций).

Используйте пустые строки в функциях, чтобы указать логические разделы.

Python расценивает символ control+L как незначащий (whitespace), и вы можете использовать его, потому что многие редакторы обрабатывают его как разрыв страницы — таким образом логические части в файле будут на разных страницах. Однако, не все редакторы распознают control+L и могут на его месте отображать другой символ.

Кодировка исходного файла

Кодировка Python должна быть UTF-8 (ASCII в Python 2).

Файлы в ASCII (Python 2) или UTF-8 (Python 3) не должны иметь объявления кодировки.

В стандартной библиотеке, нестандартные кодировки должны использоваться только для целей тестирования, либо когда комментарий или строка документации требует упомянуть имя автора, содержащего не ASCII символы; в остальных случаях использование \x, \u, \U или \N — наиболее предпочтительный способ включить не ASCII символы в строковых литералах.

Начиная с версии python 3.0 в стандартной библиотеке действует следующее соглашение: все идентификаторы обязаны содержать только ASCII символы, и означать английские слова везде, где это возможно (во многих случаях используются сокращения или неанглийские технические термины). Кроме того, строки и комментарии тоже должны содержать лишь ASCII символы. Исключения составляют: (а) test case, тестирующий не-ASCII особенности программы, и (б) имена авторов. Авторы, чьи имена основаны не на латинском алфавите, должны транслитерировать свои имена в латиницу.

Проектам с открытым кодом для широкой аудитории также рекомендуется использовать это соглашение.

Импорты

  • Каждый импорт, как правило, должен быть на отдельной строке.

    Правильно:

    import os
    import sys

    Неправильно:

    import sys, os

    В то же время, можно писать так:

    from subprocess import Popen, PIPE
  • Импорты всегда помещаются в начале файла, сразу после комментариев к модулю и строк документации, и перед объявлением констант.

    Импорты должны быть сгруппированы в следующем порядке:

    1. импорты из стандартной библиотеки
    2. импорты сторонних библиотек
    3. импорты модулей текущего проекта

    Вставляйте пустую строку между каждой группой импортов.

    Указывайте спецификации __all__ после импортов.

  • Рекомендуется абсолютное импортирование, так как оно обычно более читаемо и ведет себя лучше (или, по крайней мере, даёт понятные сообщения об ошибках) если импортируемая система настроена неправильно (например, когда каталог внутри пакета заканчивается на sys.path):

    import mypkg.sibling
    from mypkg import sibling
    from mypkg.sibling import example

    Тем не менее, явный относительный импорт является приемлемой альтернативой абсолютному импорту, особенно при работе со сложными пакетами, где использование абсолютного импорта было бы излишне подробным:

    from . import sibling
    from .sibling import example

    В стандартной библиотеке следует избегать сложной структуры пакетов и всегда использовать абсолютные импорты.

    Неявные относительно импорты никогда не должны быть использованы, и были удалены в Python 3.

  • Когда вы импортируете класс из модуля, вполне можно писать вот так:

    from myclass import MyClass
    from foo.bar.yourclass import YourClass

    Если такое написание вызывает конфликт имен, тогда пишите:

    import myclass
    import foo.bar.yourclass

    И используйте «myclass.MyClass» и «foo.bar.yourclass.YourClass».

  • Шаблоны импортов (from import *) следует избегать, так как они делают неясным то, какие имена присутствуют в глобальном пространстве имён, что вводит в заблуждение как читателей, так и многие автоматизированные средства. Существует один оправданный пример использования шаблона импорта, который заключается в опубликовании внутреннего интерфейса как часть общественного API (например, переписав реализацию на чистом Python в модуле акселератора (и не будет заранее известно, какие именно функции будут перезаписаны).

Пробелы в выражениях и инструкциях

Избегайте использования пробелов в следующих ситуациях:

  • Непосредственно внутри круглых, квадратных или фигурных скобок.

    Правильно:

    spam(ham[1], {eggs: 2})

    Неправильно:

    spam( ham[ 1 ], { eggs: 2 } )
  • Непосредственно перед запятой, точкой с запятой или двоеточием:

    Правильно:

    if x == 4: print(x, y); x, y = y, x

    Неправильно:

    if x == 4 : print(x , y) ; x , y = y , x
  • Сразу перед открывающей скобкой, после которой начинается список аргументов при вызове функции:

    Правильно:

    spam(1)

    Неправильно:

    spam (1)
  • Сразу перед открывающей скобкой, после которой следует индекс или срез:

    Правильно:

    dict['key'] = list[index]

    Неправильно:

    dict ['key'] = list [index]
  • Использование более одного пробела вокруг оператора присваивания (или любого другого) для того, чтобы выровнять его с другим:

    Правильно:

    x = 1
    y = 2
    long_variable = 3

    Неправильно:

    x             = 1
    y             = 2
    long_variable = 3

Другие рекомендации

  • Всегда окружайте эти бинарные операторы одним пробелом с каждой стороны: присваивания (=, +=, -= и другие), сравнения (==, <, >, !=, <>, <=, >=, in, not in, is, is not), логические (and, or, not).
  • Если используются операторы с разными приоритетами, попробуйте добавить пробелы вокруг операторов с самым низким приоритетом. Используйте свои собственные суждения, однако, никогда не используйте более одного пробела, и всегда используйте одинаковое количество пробелов по обе стороны бинарного оператора.

    Правильно:

    i = i + 1
    submitted += 1
    x = x*2 - 1
    hypot2 = x*x + y*y
    c = (a+b) * (a-b)

    Неправильно:

    i=i+1
    submitted +=1
    x = x * 2 - 1
    hypot2 = x * x + y * y
    c = (a + b) * (a - b)
  • Не используйте пробелы вокруг знака =, если он используется для обозначения именованного аргумента или значения параметров по умолчанию.

    Правильно:

    def complex(real, imag=0.0):
        return magic(r=real, i=imag)

    Неправильно:

    def complex(real, imag = 0.0):
        return magic(r = real, i = imag)
  • Не используйте составные инструкции (несколько команд в одной строке).

    Правильно:

    if foo == 'blah':
        do_blah_thing()
    do_one()
    do_two()
    do_three()

    Неправильно:

    if foo == 'blah': do_blah_thing()
    do_one(); do_two(); do_three()
  • Иногда можно писать тело циклов while, for или ветку if в той же строке, если команда короткая, но если команд несколько, никогда так не пишите. А также избегайте длинных строк!

    Точно неправильно:

    if foo == 'blah': do_blah_thing()
    for x in lst: total += x
    while t < 10: t = delay()

    Вероятно, неправильно:

    if foo == 'blah': do_blah_thing()
    else: do_non_blah_thing()
    
    try: something()
    finally: cleanup()
    
    do_one(); do_two(); do_three(long, argument,
                                 list, like, this)
    
    if foo == 'blah': one(); two(); three()

Комментарии

Комментарии, противоречащие коду, хуже, чем отсутствие комментариев. Всегда исправляйте комментарии, если меняете код!

Комментарии должны являться законченными предложениями. Если комментарий — фраза или предложение, первое слово должно быть написано с большой буквы, если только это не имя переменной, которая начинается с маленькой буквы (никогда не изменяйте регистр переменной!).

Если комментарий короткий, можно опустить точку в конце предложения. Блок комментариев обычно состоит из одного или более абзацев, составленных из полноценных предложений, поэтому каждое предложение должно оканчиваться точкой.

Ставьте два пробела после точки в конце предложения.

Программисты, которые не говорят на английском языке, пожалуйста, пишите комментарии на английском, если только вы не уверены на 120%, что ваш код никогда не будут читать люди, не знающие вашего родного языка.

Блоки комментариев

Блок комментариев обычно объясняет код (весь, или только некоторую часть), идущий после блока, и должен иметь тот же отступ, что и сам код. Каждая строчка такого блока должна начинаться с символа # и одного пробела после него (если только сам текст комментария не имеет отступа).

Абзацы внутри блока комментариев разделяются строкой, состоящей из одного символа #.

«Встрочные» комментарии

Старайтесь реже использовать подобные комментарии.

Такой комментарий находится в той же строке, что и инструкция. «Встрочные» комментарии должны отделяться по крайней мере двумя пробелами от инструкции. Они должны начинаться с символа # и одного пробела.

Комментарии в строке с кодом не нужны и только отвлекают от чтения, если они объясняют очевидное. Не пишите вот так:

x = x + 1                 # Increment x

Впрочем, такие комментарии иногда полезны:

x = x + 1                 # Компенсация границы

Строки документации

  • Пишите документацию для всех публичных модулей, функций, классов, методов. Строки документации необязательны для приватных методов, но лучше написать, что делает метод. Комментарий нужно писать после строки с def.
  • PEP 257 объясняет, как правильно и хорошо документировать. Заметьте, очень важно, чтобы закрывающие кавычки стояли на отдельной строке. А еще лучше, если перед ними будет ещё и пустая строка, например:

    """Return a foobang
    
    Optional plotz says to frobnicate the bizbaz first.
    
    """
  • Для однострочной документации можно оставить закрывающие кавычки на той же строке.

Контроль версий

Если вам нужно использовать Subversion, CVS или RCS в ваших исходных кодах, делайте вот так:

__version__ = "$Revision: 1a40d4eaa00b $"
# $Source$

Вставляйте эти строки после документации модуля перед любым другим кодом и отделяйте их пустыми строками по одной до и после.

Соглашения по именованию

Соглашения по именованию переменных в python немного туманны, поэтому их список никогда не будет полным — тем не менее, ниже мы приводим список рекомендаций, действующих на данный момент. Новые модули и пакеты должны быть написаны согласно этим стандартам, но если в какой-либо уже существующей библиотеке эти правила нарушаются, предпочтительнее писать в едином с ней стиле.

Главный принцип

Имена, которые видны пользователю как часть общественного API должны следовать конвенциям, которые отражают использование, а не реализацию.

Описание: Стили имен

Существует много разных стилей. Поможем вам распознать, какой стиль именования используется, независимо от того, для чего он используется.

Обычно различают следующие стили:

  • b (одиночная маленькая буква)
  • B (одиночная заглавная буква)
  • lowercase (слово в нижнем регистре)
  • lower_case_with_underscores (слова из маленьких букв с подчеркиваниями)
  • UPPERCASE (заглавные буквы)
  • UPPERCASE_WITH_UNDERSCORES (слова из заглавных букв с подчеркиваниями)
  • CapitalizedWords (слова с заглавными буквами, или CapWords, или CamelCase). Замечание: когда вы используете аббревиатуры в таком стиле, пишите все буквы аббревиатуры заглавными — HTTPServerError лучше, чем HttpServerError.
  • mixedCase (отличается от CapitalizedWords тем, что первое слово начинается с маленькой буквы)
  • Capitalized_Words_With_Underscores (слова с заглавными буквами и подчеркиваниями — уродливо!)

Ещё существует стиль, в котором имена, принадлежащие одной логической группе, имеют один короткий префикс. Этот стиль редко используется в python, но мы упоминаем его для полноты. Например, функция os.stat() возвращает кортеж, имена в котором традиционно имеют вид st_mode, st_size, st_mtime и так далее. (Так сделано, чтобы подчеркнуть соответствие этих полей структуре системных вызовов POSIX, что помогает знакомым с ней программистам).

В библиотеке X11 используется префикс Х для всех public-функций. В python этот стиль считается излишним, потому что перед полями и именами методов стоит имя объекта, а перед именами функций стоит имя модуля.

В дополнение к этому, используются следующие специальные формы записи имен с добавлением символа подчеркивания в начало или конец имени:

  • _single_leading_underscore: слабый индикатор того, что имя используется для внутренних нужд. Например, from M import * не будет импортировать объекты, чьи имена начинаются с символа подчеркивания.
  • single_trailing_underscore_: используется по соглашению для избежания конфликтов с ключевыми словами языка python, например:

    Tkinter.Toplevel(master, class_='ClassName')
  • __double_leading_underscore: изменяет имя атрибута класса, то есть в классе FooBar поле __boo становится _FooBar__boo.
  • __double_leading_and_trailing_underscore__ (двойное подчеркивание в начале и в конце имени): магические методы или атрибуты, которые находятся в пространствах имен, управляемых пользователем. Например, __init__, __import__ или __file__. Не изобретайте такие имена, используйте их только так, как написано в документации.

Предписания: соглашения по именованию

Имена, которых следует избегать

Никогда не используйте символы l (маленькая латинская буква «эль»), O (заглавная латинская буква «о») или I (заглавная латинская буква «ай») как однобуквенные идентификаторы.

В некоторых шрифтах эти символы неотличимы от цифры один и нуля. Если очень нужно l, пишите вместо неё заглавную L.

Имена модулей и пакетов

Модули должны иметь короткие имена, состоящие из маленьких букв. Можно использовать символы подчеркивания, если это улучшает читабельность. То же самое относится и к именам пакетов, однако в именах пакетов не рекомендуется использовать символ подчёркивания.

Так как имена модулей отображаются в имена файлов, а некоторые файловые системы являются нечувствительными к регистру символов и обрезают длинные имена, очень важно использовать достаточно короткие имена модулей — это не проблема в Unix, но, возможно, код окажется непереносимым в старые версии Windows, Mac, или DOS.

Когда модуль расширения, написанный на С или C++, имеет сопутствующий python-модуль (содержащий интерфейс высокого уровня), С/С++ модуль начинается с символа подчеркивания, например, _socket.

Имена классов

Имена классов должны обычно следовать соглашению CapWords.

Вместо этого могут использоваться соглашения для именования функций, если интерфейс документирован и используется в основном как функции.

Обратите внимание, что существуют отдельные соглашения о встроенных именах: большинство встроенных имен — одно слово (либо два слитно написанных слова), а соглашение CapWords используется только для именования исключений и встроенных констант.

Имена исключений

Так как исключения являются классами, к исключениями применяется стиль именования классов. Однако вы можете добавить Error в конце имени (если, конечно, исключение действительно является ошибкой).

Имена глобальных переменных

Будем надеяться, что глобальные переменные используются только внутри одного модуля. Руководствуйтесь теми же соглашениями, что и для имен функций.

Добавляйте в модули, которые написаны так, чтобы их использовали с помощью from M import *, механизм __all__, чтобы предотвратить экспортирование глобальных переменных. Или же, используйте старое соглашение, добавляя перед именами таких глобальных переменных один символ подчеркивания (которым вы можете обозначить те глобальные переменные, которые используются только внутри модуля).

Имена функций

Имена функций должны состоять из маленьких букв, а слова разделяться символами подчеркивания — это необходимо, чтобы увеличить читабельность.

Стиль mixedCase допускается в тех местах, где уже преобладает такой стиль, для сохранения обратной совместимости.

Аргументы функций и методов

Всегда используйте self в качестве первого аргумента метода экземпляра объекта.

Всегда используйте cls в качестве первого аргумента метода класса.

Если имя аргумента конфликтует с зарезервированным ключевым словом python, обычно лучше добавить в конец имени символ подчеркивания, чем исказить написание слова или использовать аббревиатуру. Таким образом, class_ лучше, чем clss. (Возможно, хорошим вариантом будет подобрать синоним).

Имена методов и переменных экземпляров классов

Используйте тот же стиль, что и для имен функций: имена должны состоять из маленьких букв, а слова разделяться символами подчеркивания.

Используйте один символ подчёркивания перед именем для непубличных методов и атрибутов.

Чтобы избежать конфликтов имен с подклассами, используйте два ведущих подчеркивания.

Python искажает эти имена: если класс Foo имеет атрибут с именем __a, он не может быть доступен как Foo.__a. (Настойчивый пользователь все еще может получить доступ, вызвав Foo._Foo__a.) Вообще, два ведущих подчеркивания должны использоваться только для того, чтобы избежать конфликтов имен с атрибутами классов, предназначенных для наследования.

Примечание: есть некоторые разногласия по поводу использования __ имена (см. ниже).

Константы

Константы обычно объявляются на уровне модуля и записываются только заглавными буквами, а слова разделяются символами подчеркивания. Например: MAX_OVERFLOW, TOTAL.

Проектирование наследования

Обязательно решите, каким должен быть метод класса или экземпляра класса (далее — атрибут) — публичный или непубличный. Если вы сомневаетесь, выберите непубличный атрибут. Потом будет проще сделать его публичным, чем наоборот.

Публичные атрибуты — это те, которые будут использовать другие программисты, и вы должны быть уверены в отсутствии обратной несовместимости. Непубличные атрибуты, в свою очередь, не предназначены для использования третьими лицами, поэтому вы можете не гарантировать, что не измените или не удалите их.

Мы не используем термин «приватный атрибут», потому что на самом деле в python таких не бывает.

Другой тип атрибутов классов принадлежит так называемому API подклассов (в других языках они часто называются protected). Некоторые классы проектируются так, чтобы от них наследовали другие классы, которые расширяют или модифицируют поведение базового класса. Когда вы проектируете такой класс, решите и явно укажите, какие атрибуты являются публичными, какие принадлежат API подклассов, а какие используются только базовым классом.

Теперь сформулируем рекомендации:

  • Открытые атрибуты не должны иметь в начале имени символа подчеркивания.
  • Если имя открытого атрибута конфликтует с ключевым словом языка, добавьте в конец имени один символ подчеркивания. Это более предпочтительно, чем аббревиатура или искажение написания (однако, у этого правила есть исключение — аргумента, который означает класс, и особенно первый аргумент метода класса (class method) должен иметь имя cls).
  • Назовите простые публичные атрибуты понятными именами и не пишите сложные методы доступа и изменения (accessor/mutator, get/set, — прим. перев.) Помните, что в python очень легко добавить их потом, если потребуется. В этом случае используйте свойства (properties), чтобы скрыть функциональную реализацию за синтаксисом доступа к атрибутам.

    Примечание 1: Свойства (properties) работают только в классах нового стиля (в Python 3 все классы являются таковыми).

    Примечание 2: Постарайтесь избавиться от побочных эффектов, связанным с функциональным поведением; впрочем, такие вещи, как кэширование, вполне допустимы.

    Примечание 3: Избегайте использования вычислительно затратных операций, потому что из-за записи с помощью атрибутов создается впечатление, что доступ происходит (относительно) быстро.

  • Если вы планируете класс таким образом, чтобы от него наследовались другие классы, но не хотите, чтобы подклассы унаследовали некоторые атрибуты, добавьте в имена два символа подчеркивания в начало, и ни одного — в конец. Механизм изменения имен в python сработает так, что имя класса добавится к имени такого атрибута, что позволит избежать конфликта имен с атрибутами подклассов.

    Примечание 1: Будьте внимательны: если подкласс будет иметь то же имя класса и имя атрибута, то вновь возникнет конфликт имен.

    Примечание 2: Механизм изменения имен может затруднить отладку или работу с __getattr__(), однако он хорошо документирован и легко реализуется вручную.

    Примечание 3: Не всем нравится этот механизм, поэтому старайтесь достичь компромисса между необходимостью избежать конфликта имен и возможностью доступа к этим атрибутам.

Общие рекомендации

  • Код должен быть написан так, чтобы не зависеть от разных реализаций языка (PyPy, Jython, IronPython, Pyrex, Psyco и пр.).

    Например, не полагайтесь на эффективную реализацию в CPython конкатенации строк в выражениях типа a+=b или a=a+b. Такие инструкции выполняются значительно медленнее в Jython. В критичных к времени выполнения частях программы используйте ».join() — таким образом склеивание строк будет выполнено за линейное время независимо от реализации python.

  • Сравнения с None должны обязательно выполняться с использованием операторов is или is not, а не с помощью операторов сравнения. Кроме того, не пишите if x, если имеете в виду if x is not None — если, к примеру, при тестировании такая переменная может принять значение другого типа, отличного от None, но при приведении типов может получиться False!
  • При реализации методов сравнения, лучше всего реализовать все 6 операций сравнения (__eq__, __ne__, __lt__, __le__, __gt__, __ge__), чем полагаться на то, что другие программисты будут использовать только конкретный вид сравнения.

    Для минимизации усилий можно воспользоваться декоратором functools.total_ordering() для реализации недостающих методов.

    PEP 207 указывает, что интерпретатор может поменять y > х на х < y, y >= х на х <= y, и может поменять местами аргументы х == y и х != y. Гарантируется, что операции sort() и min() используют оператор <, а max() использует оператор >. Однако, лучше всего осуществить все шесть операций, чтобы не возникало путаницы в других местах.

  • Всегда используйте выражение def, а не присваивание лямбда-выражения к имени.

    Правильно:

    def f(x): return 2*x

    Неправильно:

    f = lambda x: 2*x
  • Наследуйте свой класс исключения от Exception, а не от BaseException. Прямое наследование от BaseException зарезервировано для исключений, которые не следует перехватывать.
  • Используйте цепочки исключений соответствующим образом. В Python 3, «raise X from Y» следует использовать для указания явной замены без потери отладочной информации.

    Когда намеренно заменяется исключение (использование «raise X» в Python 2 или «raise X from None» в Python 3.3+), проследите, чтобы соответствующая информация передалась в новое исключение (такие, как сохранение имени атрибута при преобразовании KeyError в AttributeError или вложение текста исходного исключения в новом).

  • Когда вы генерируете исключение, пишите raise ValueError(‘message’) вместо старого синтаксиса raise ValueError, message.

    Старая форма записи запрещена в python 3.

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

  • Когда код перехватывает исключения, перехватывайте конкретные ошибки вместо простого выражения except:.

    К примеру, пишите вот так:

    try:
        import platform_specific_module
    except ImportError:
        platform_specific_module = None

    Простое написание «except:» также перехватит и SystemExit, и KeyboardInterrupt, что породит проблемы, например, сложнее будет завершить программу нажатием control+C. Если вы действительно собираетесь перехватить все исключения, пишите «except Exception:».

    Хорошим правилом является ограничение использования «except:», кроме двух случаев:

    1. Если обработчик выводит пользователю всё о случившейся ошибке; по крайней мере, пользователь будет знать, что произошла ошибка.
    2. Если нужно выполнить некоторый код после перехвата исключения, а потом вновь «бросить» его для обработки где-то в другом месте. Обычно же лучше пользоваться конструкцией «try…finally».
  • При связывании перехваченных исключений с именем, предпочитайте явный синтаксис привязки, добавленный в Python 2.6:

    try:
        process_data()
    except Exception as exc:
        raise DataProcessingFailedError(str(exc))

    Это единственный синтаксис, поддерживающийся в Python 3, который позволяет избежать проблем неоднозначности, связанных с более старым синтаксисом на основе запятой.

  • При перехвате ошибок операционной системы, предпочитайте использовать явную иерархию исключений, введенную в Python 3.3, вместо анализа значений errno.
  • Постарайтесь заключать в каждую конструкцию try…except минимум кода, чтобы легче отлавливать ошибки. Опять же, это позволяет избежать замаскированных ошибок.

    Правильно:

    try:
        value = collection[key]
    except KeyError:
        return key_not_found(key)
    else:
        return handle_value(value)

    Неправильно:

    try:
        # Здесь много действий!
        return handle_value(collection[key])
    except KeyError:
        # Здесь также перехватится KeyError, который может быть сгенерирован handle_value()
        return key_not_found(key)
  • Когда ресурс является локальным на участке кода, используйте выражение with для того, чтобы после выполнения он был очищен оперативно и надёжно.
  • Менеджеры контекста следует вызывать с помощью отдельной функции или метода, всякий раз, когда они делают что-то другое, чем получение и освобождение ресурсов. Например:

    Правильно:

    with conn.begin_transaction():
        do_stuff_in_transaction(conn)

    Неправильно:

    with conn:
        do_stuff_in_transaction(conn)

    Последний пример не дает никакой информации, указывающей на то, что __enter__ и __exit__ делают что-то кроме закрытия соединения после транзакции. Быть явным важно в данном случае.

  • Используйте строковые методы вместо модуля string — они всегда быстрее и имеют тот же API для unicode-строк. Можно отказаться от этого правила, если необходима совместимость с версиями python младше 2.0.

    В Python 3 остались только строковые методы.

  • Пользуйтесь ».startswith() и ».endswith() вместо обработки срезов строк для проверки суффиксов или префиксов.

    startswith() и endswith() выглядят чище и порождают меньше ошибок. Например:

    Правильно:

    if foo.startswith('bar'):

    Неправильно:

    if foo[:3] == 'bar':
  • Сравнение типов объектов нужно делать с помощью isinstance(), а не прямым сравнением типов:

    Правильно:

    if isinstance(obj, int):

    Неправильно:

    if type(obj) is type(1):

    Когда вы проверяете, является ли объект строкой, обратите внимание на то, что строка может быть unicode-строкой. В python 2 у str и unicode есть общий базовый класс, поэтому вы можете написать:

    if isinstance(obj, basestring):

    Отметим, что в Python 3, unicode и basestring больше не существуют (есть только str) и bytes больше не является своего рода строкой (это последовательность целых чисел).

  • Для последовательностей (строк, списков, кортежей) используйте тот факт, что пустая последовательность есть false:

    Правильно:

    if not seq:
    if seq:

    Неправильно:

    if len(seq)
    if not len(seq)
  • Не пользуйтесь строковыми константами, которые имеют важные пробелы в конце — они невидимы, а многие редакторы (а теперь и reindent.py) обрезают их.
  • Не сравнивайте логические типы с True и False с помощью ==:

    Правильно:

    if greeting:

    Неправильно:

    if greeting == True:

    Совсем неправильно:

    if greeting is True:
  • Оригинал

    Специальные требования для оформления текстов для моих курсантов

    В самом начале скрипта с кодом Python необходимо указывать следующие атрибуты:

    #имя проекта: numpy-example
    #номер версии: 1.0
    #имя файла: example_2.py
    #автор и его учебная группа: Е. Волков, ЭУ-142
    #дата создания: 20.03.2019
    #дата последней модификации: 25.03.2019
    #связанные файлы: пакеты numpy, matplotlib
    #описание: простейшие статистические вычисления
    #версия Python: 3.6
    ]]> https://chel-center.ru/python-yfc/2019/10/01/kak-oformlyat-kod/feed/ 0 Нескучный NumPy https://chel-center.ru/python-yfc/2019/10/01/neskuchnyj-numpy/ https://chel-center.ru/python-yfc/2019/10/01/neskuchnyj-numpy/#respond Tue, 01 Oct 2019 02:29:31 +0000 http://chel-center.ru/python-yfc/?p=29028 Читать далее «Нескучный NumPy»

    ]]>

    Содержание

    Что такое NumPy?

    Это библиотека с открытым исходным кодом, некогда отделившаяся от проекта SciPy. NumPy является наследником Numeric и NumArray. Основан NumPy на библиотеке LAPAC, которая написана на Fortran. Не-python альтернативой для NumPy является Matlab.

    В силу того, что NumPy базируется на Fortran это быстрая библиотека. А в силу того, что поддерживает векторные операции с многомерными массивами — крайне удобная.

    Кроме базового варианта (многомерные массивы в базовом варианте) NumPy включает в себя набор пакетов для решения специализированных задач, например:

    • numpy.linalg — реализует операции линейной алгебры (простое умножение векторов и матриц есть в базовом варианте);
    • numpy.random — реализует функции для работы со случайными величинами;
    • numpy.fft — реализует прямое и обратное преобразование Фурье.

    Итак, я предлагаю рассмотреть подробно всего несколько возможностей NumPy и примеров их использования, которых будет достаточно, чтобы вы поняли, на сколько мощный этот инструмент!

    Но, прежде чем продолжить чтение этой статьи загрузите IDLE Python, где в режиме интерактивного интерпретатора повторяйте приведённые здесь скрипы и «в живую» смотрите на результат их выполнения. Но, не забудьте в начале сеанса записать:

    import numpy
    

    Создание массива

    Создать массив можно несколькими способами:

    1. преобразовать список в массив:
      A = np.array([ [1, 2, 3], [4, 5, 6] ])
      A
      Out:
      array([ [1, 2, 3],
             [4, 5, 6] ])
      
    2. скопировать массив (копия и глубокая копия обязательна!!!):
      B = A.copy()
      B
      Out:
      array([ [1, 2, 3],
             [4, 5, 6] ])
      
    3. создать нулевой или единичный массив заданного размера:
      A = np.zeros((2, 3))
      A
      Out:
      array([ [0., 0., 0.],
             [0., 0., 0.] ])
      B = np.ones((3, 2))
      B
      Out:
      array([ [1., 1.],
             [1., 1.],
             [1., 1.] ])
      
      B = np.ones((3, 2))
      B
      Out:
      array([ [1., 1.],
             [1., 1.],
             [1., 1.] ])
      

      Либо взять размеры уже существующего массива:

      A = np.array([ [1, 2, 3], [4, 5, 6] ])
      B = np.zeros_like(A)
      B
      Out:
      array([ [0, 0, 0],
             [0, 0, 0] ])
      
      A = np.array([ [1, 2, 3], [4, 5, 6] ])
      B = np.ones_like(A)
      B
      Out:
      array([ [1, 1, 1],
             [1, 1, 1] ])
      
    4. при создании двумерного квадратного массива можете сделать его единичной диагональной матрицей:
      A = np.eye(3)
      A
      Out:
      array([ [1., 0., 0.],
             [0., 1., 0.],
             [0., 0., 1.] ])
      
    5. построить массив чисел от From (включая) до To (не включая) с шагом Step:
      From = 2.5
      To = 7
      Step = 0.5
      A = np.arange(From, To, Step)
      A
      Out:
      array([2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. , 6.5])
      

      По умолчанию from = 0, step = 1, поэтому возможен вариант с одним параметром, интерпретируемым как To:

      A = np.arange(5)
      A
      Out:
      array([0, 1, 2, 3, 4])
      

      Либо с двумя — как From и To:

      A = np.arange(10, 15)
      A
      Out:
      array([10, 11, 12, 13, 14])
      

    Обратите внимание, что в методе №3 размеры массива передавались в качестве одного параметра (кортеж размеров). Вторым параметром в способах №3 и №4 можно указать желаемый тип элементов массива:

    A = np.zeros((2, 3), 'int')
    A
    Out:
    array([ [0, 0, 0],
           [0, 0, 0] ])
    
    B = np.ones((3, 2), 'complex')
    B
    Out:
    array([ [1.+0.j, 1.+0.j],
           [1.+0.j, 1.+0.j],
           [1.+0.j, 1.+0.j] ])
    

    Используя метод astype, можно привести массив к другому типу. В качестве параметра указывается желаемый тип:

    A = np.ones((3, 2))
    B = A.astype('str')
    B
    Out:
    array([ ['1.0', '1.0'],
           ['1.0', '1.0'],
           ['1.0', '1.0'] ], dtype='<U32')
    

    Все доступные типы можно найти в словаре sctypes:

    np.sctypes
    Out:
    {'int': [numpy.int8, numpy.int16, numpy.int32, numpy.int64],
     'uint': [numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64],
     'float': [numpy.float16, numpy.float32, numpy.float64, numpy.float128],
     'complex': [numpy.complex64, numpy.complex128, numpy.complex256],
     'others': [bool, object, bytes, str, numpy.void]}
    

     <наверх>

    Доступ к элементам, срезы

    Доступ к элементам массива осуществляется по целочисленным индексами, начинается отсчет с 0:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    A[1, 1]
    Out:
    5
    

    Если представить многомерный массив как систему вложенных одномерных массивов (линейный массив, элементы которого могут быть линейными массивами), становится очевидной возможность получать доступ к подмассивам с использованием неполного набора индексов:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    A[1]
    Out:
    array([4, 5, 6])
    

    С учетом этой парадигмы, можем переписать пример доступа к одному элементу:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    A[1][1]
    Out:
    5
    

    При использовании неполного набора индексов, недостающие индексы неявно заменяются списком всех возможных индексов вдоль соответствующей оси. Сделать это явным образом можно, поставив «:«. Предыдущий пример с одним индексом можно переписать в следующем виде:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    A[1, :]
    Out:
    array([4, 5, 6])
    

    «Пропустить» индекс можно вдоль любой оси или осей, если за «пропущенной» осью последуют оси с индексацией, то «:» обязательно:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    A[:, 1]
    Out:
    array([2, 5])
    

    Индексы могут принимать отрицательные целые значения. В этом случае отсчет ведется от конца массива:

    A = np.arange(5)
    print(A)
    A[-1]
    Out:
    [0 1 2 3 4]
    4
    

    Можно использовать не одиночные индексы, а списки индексов вдоль каждой оси:

    A = np.arange(5)
    print(A)
    A[ [0, 1, -1] ]
    Out:
    [0 1 2 3 4]
    array([0, 1, 4])
    

    Либо диапазоны индексов в виде «From:To:Step». Такая конструкция называется срезом. Выбираются все элементы по списку индексов начиная с индекса From включительно, до индекса To не включая с шагом Step:

    A = np.arange(5)
    print(A)
    A[0:4:2]
    Out:
    [0 1 2 3 4]
    array([0, 2])
    

    Шаг индекса имеет значение по умолчанию 1 и может быть пропущен:

    A = np.arange(5)
    print(A)
    A[0:4]
    Out:
    [0 1 2 3 4]
    array([0, 1, 2, 3])
    

    Значения From и To тоже имеют дефолтные значения: 0 и размер массива по оси индексации соответственно:

    A = np.arange(5)
    print(A)
    A[:4]
    Out:
    [0 1 2 3 4]
    array([0, 1, 2, 3])
    
    A = np.arange(5)
    print(A)
    A[-3:]
    Out:
    [0 1 2 3 4]
    array([2, 3, 4])
    

    Если вы хотите использовать From и To по умолчанию (все индексы по данной оси) а шаг отличный от 1, то вам необходимо использовать две пары двоеточий, чтобы интерпретатор смог идентифицировать единственный параметр как Step. Следующий код «разворачивает» массив вдоль второй оси, а вдоль первой не меняет:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    B = A[:, ::-1]
    print("A", A)
    print("B", B)
    Out:
    A [ [1 2 3]
     [4 5 6] ]
    B [ [3 2 1]
     [6 5 4] ]
    

    А теперь выполним

    print(A)
    B[0, 0] = 0
    print(A)
    Out:
    [ [1 2 3]
     [4 5 6] ]
    [ [1 2 0]
     [4 5 6] ]
    

    Как видите, через B мы изменили данные в A. Вот почему в реальных задачах важно использовать копии. Пример выше должен был бы выглядеть так:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    B = A.copy()[:, ::-1]
    print("A", A)
    print("B", B)
    Out:
    A [ [1 2 3]
     [4 5 6] ]
    B [ [3 2 1]
     [6 5 4] ]
    

    В NumPy также реализована возможность доступа ко множеству элементов массива через булев индексный массив. Индексный массив должен совпадать по форме с индексируемым.

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    I = np.array([ [False, False, True], [ True, False, True] ])
    A[I]
    Out:
    array([3, 4, 6])
    

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

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    I = np.array([ [False, False, True], [ True, False, True] ])
    A[I] = 0
    print(A)
    Out:
    [ [1 2 0]
     [0 5 0] ]
    

    Над индексирующими булевыми массивами определены логические операции logical_and, logical_or и logical_not выполняющие логические операции И, ИЛИ и НЕ поэлементно:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    I1 = np.array([ [False, False, True], [True, False, True] ])
    I2 = np.array([ [False, True, False], [False, False, True] ])
    B = A.copy()
    C = A.copy()
    D = A.copy()
    B[np.logical_and(I1, I2)] = 0
    C[np.logical_or(I1, I2)] = 0
    D[np.logical_not(I1)] = 0
    print('B\n', B)
    print('\nC\n', C)
    print('\nD\n', D)
    Out:
    B
     [ [1 2 3]
     [4 5 0] ]
    C
     [ [1 0 0]
     [0 5 0] ]
    D
     [ [0 0 3]
     [4 0 6] ]
    

    logical_and и logical_or принимают 2 операнда, logical_not — один. Можно использовать операторы &, | и ~ для выполнения И, ИЛИ и НЕ соответственно с любым количеством операндов:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    I1 = np.array([ [False, False, True], [True, False, True] ])
    I2 = np.array([ [False, True, False], [False, False, True] ])
    A[I1 & (I1 | ~ I2)] = 0
    print(A)
    Out:
    [ [1 2 0]
     [0 5 0] ]
    

    Что эквивалентно применению только I1.

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

    Найдем индексирующий массив I элементов, которые больше, чем 3, а элементы со значениями меньше чем 2 и больше 4 — обнулим:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    print('A before\n', A)
    I = A > 3
    print('I\n', I)
    A[np.logical_or(A < 2, A > 4)] = 0
    print('A after\n', A)
    Out:
    A before
     [ [1 2 3]
     [4 5 6] ]
    I
     [ [False False False]
     [ True  True  True] ]
    A after
     [ [0 2 3]
     [4 0 0] ]
    

     <наверх>

    Форма массива и ее изменение

    Многомерный массив можно представить как одномерный массив максимальной длины, нарезанный на фрагменты по длине самой последней оси и уложенный слоями по осям, начиная с последних.

    Для наглядности рассмотрим пример:

    A = np.arange(24)
    B = A.reshape(4, 6)
    C = A.reshape(4, 3, 2)
    print('B\n', B)
    print('\nC\n', C)
    Out:
    B
     [ [ 0  1  2  3  4  5]
     [ 6  7  8  9 10 11]
     [12 13 14 15 16 17]
     [18 19 20 21 22 23] ]
    C
     [ [ [ 0  1]
      [ 2  3]
      [ 4  5] ]
     [ [ 6  7]
      [ 8  9]
      [10 11] ]
     [ [12 13]
      [14 15]
      [16 17] ]
     [ [18 19]
      [20 21]
      [22 23] ] ]
    

    В этом примере мы из одномерного массива длиной 24 элемента сформировали 2 новых массива. Массив B, размером 4 на 6. Если посмотреть на порядок значений, то видно, что вдоль второго измерения идут цепочки последовательных значений.

    В массиве C, размером 4 на 3 на 2, непрерывные значения идут вдоль последней оси. Вдоль второй оси идут последовательно блоки, объединение которых дало бы в результате строки вдоль второй оси массива B.

    А учитывая, что мы не делали копии, становится понятно, что это разные формы преставления одного и того же массива данных. Поэтому можно легко и быстро менять форму массива, не изменяя самих данных.

    Чтобы узнать размерность массива (количество осей), можно использовать поле ndim (число), а чтобы узнать размер вдоль каждой оси — shape (кортеж). Размерность можно также узнать и по длине shape. Чтобы узнать полное количество элементов в массиве можно воспользоваться значением size:

    A = np.arange(24)
    C = A.reshape(4, 3, 2)
    print(C.ndim, C.shape, len(C.shape), A.size)
    Out:
    3 (4, 3, 2) 3 24
    

    Обратите внимание, что ndim и shape — это атрибуты, а не методы!
    Чтобы увидеть массив одномерным, можно воспользоваться функцией ravel:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    A.ravel()
    Out:
    array([1, 2, 3, 4, 5, 6])
    

    Чтобы поменять размеры вдоль осей или размерность используется метод reshape:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    A.reshape(3, 2)
    Out:
    array([ [1, 2],
           [3, 4],
           [5, 6] ])
    

    Важно, чтобы количество элементов сохранилось. Иначе возникнет ошибка:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    A.reshape(3, 3)
    Out:
    ValueError                                Traceback (most recent call last)
    <ipython-input-73-d204e18427d9> in <module>
          1 A = np.array([ [1, 2, 3], [4, 5, 6] ])
    ----> 2 A.reshape(3, 3)
    ValueError: cannot reshape array of size 6 into shape (3,3)
    

    Учитывая, что количество элементов постоянно, размер вдоль одной любой оси при выполнении reshape может быть вычислен из значений длины вдоль других осей. Размер вдоль одной оси можно обозначить -1 и тогда он будет вычислен автоматически:

    A = np.arange(24)
    B = A.reshape(4, -1)
    C = A.reshape(4, -1, 2)
    print(B.shape, C.shape)
    Out:
    (4, 6) (4, 3, 2)
    

    Можно reshape использовать вместо ravel:

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    B = A.reshape(-1)
    print(B.shape)
    Out:
    (6,)
    

    Рассмотрим практическое применение некоторых возможностей для обработки изображений. В качестве объекта исследования будем использовать фотографию:

    Исходная фотография для упражнений
    Исходная фотография для упражнений

    Попробуем ее загрузить и визуализировать средствами Python. Для этого нам понадобятся OpenCV и Matplotlib (ext-1.py):

    import cv2
    from matplotlib import pyplot as plt
    
    I = cv2.imread('susu-new-year.jpg')[:, :, ::-1]
    flg = plt.figure(num=None, figsize=(15, 15), dpi=80, facecolor='w', edgecolor='k')
    plt.imshow(I)
    plt.show()
    
    flg.savefig('./output-images/ext-1.jpg')
    

    Результат будет такой:

    Результат загрузки фото
    Результат загрузки фото

    Обратите внимание на строку загрузки:

    I = cv2.imread('susu-new-year.jpg')[:, :, ::-1]
    print(I.shape)
    Out:
    (1280, 1920, 3)
    

    OpenCV работает с изображениями в формате BGR, а нам привычен RGB. Мы меняем порядок байтов вдоль оси цвета без обращения к функциям OpenCV, используя конструкцию [:, :, ::-1].

    Уменьшим изображение в 2 раза по каждой оси. Наше изображение имеет четные размеры по осям, соответственно, может быть уменьшено без интерполяции (ext-2.py):

    I_ = I.reshape(I.shape[0] // 2, 2, I.shape[1] // 2, 2, -1)
    print(I_.shape)
    plt.figure(num=None, figsize=(10, 10), dpi=80, facecolor='w', edgecolor='k')
    plt.imshow(I_[:, 0, :, 0])
    plt.show()
    
    Уменьшим изображение в 2 раза по каждой оси
    Уменьшим изображение в 2 раза по каждой оси

    Поменяв форму массива, мы получили 2 новые оси, по 2 значения в каждой, им соответствуют кадры, составленные из нечетных и четных строк и столбцов исходного изображения.

     <наверх>

    Перестановка осей и транспонирование

    В кроме изменения формы массива при неизменном порядке единиц данных, часто встречается необходимость изменить порядок следования осей, что естественным образом повлечет перестановки блоков данных.

    Примером такого преобразования может быть транспонирование матрицы: взаимозамена строк и столбцов.

    A = np.array([ [1, 2, 3], [4, 5, 6] ])
    print('A\n', A)
    print('\nA data\n', A.ravel())
    B = A.T
    print('\nB\n', B)
    print('\nB data\n', B.ravel())
    Out:
    A
     [ [1 2 3]
     [4 5 6] ]
    A data
     [1 2 3 4 5 6]
    B
     [ [1 4]
     [2 5]
     [3 6] ]
    B data
     [1 4 2 5 3 6]
    

    В этом примере для транспонирования матрицы A использовалась конструкция A.T. Оператор транспонирования инвертирует порядок осей. Рассмотрим еще один пример с тремя осями:

    C = np.arange(24).reshape(4, -1, 2)
    print(C.shape, np.transpose(C).shape)
    print()
    print(C[0])
    print()
    print(C.T[:, :, 0])
    Out:
    [ [0 1]
     [2 3]
     [4 5] ]
    [ [0 2 4]
     [1 3 5] ]
    

    У этой короткой записи есть более длинный аналог: np.transpose(A). Это более универсальный инструмент для замены порядка осей. Вторым параметром можно задать кортеж номеров осей исходного массива, определяющий порядок их положения в результирующем массиве.

    Для примера переставим первые две оси изображения. Картинка должна перевернуться, но цветовую ось оставим без изменения (ext-3.py):

    I_ = np.transpose(I, (1, 0, 2))
    plt.figure(num=None, figsize=(15, 15), dpi=80, facecolor='w', edgecolor='k')
    plt.imshow(I_)
    plt.show()
    
    Переставим первые две оси изображения
    Переставим первые две оси изображения

    Для этого примера можно было применить другой инструмент swapaxes. Этот метод переставляет местами две оси, указанные в параметрах. Пример выше можно было реализовать так:

    I_ = np.swapaxes(I, 0, 1)
    

     <наверх>

    Объединение массивов

    Объединяемые массивы должны иметь одинаковое количество осей. Объединять массивы можно с образованием новой оси, либо вдоль уже существующей.

    Для объединения с образованием новой оси исходные массивы должны иметь одинаковые размеры вдоль всех осей:

    A = np.array([ [1, 2, 3, 4], [5, 6, 7, 8] ])
    B = A[::-1]
    C = A[:, ::-1]
    D = np.stack((A, B, C))
    print(D.shape)
    D
    Out:
    (3, 2, 4)
    array([ [ [1, 2, 3, 4],
            [5, 6, 7, 8] ],
           [ [5, 6, 7, 8],
            [1, 2, 3, 4] ],
           [ [4, 3, 2, 1],
            [8, 7, 6, 5] ] ])
    

    Как видно из примера, массивы-операнды стали подмассивами нового объекта и выстроились вдоль новой оси, которая стоит самой первой по порядку.

    Для объединения массивов вдоль существующей оси, они должны иметь одинаковый размер по всем осям, кроме выбранной для объединения, а по ней могут иметь произвольные размеры:

    A = np.ones((2, 1, 2))
    B = np.zeros((2, 3, 2))
    C = np.concatenate((A, B), 1)
    print(C.shape)
    C
    Out:
    (2, 4, 2)
    array([ [ [1., 1.],
            [0., 0.],
            [0., 0.],
            [0., 0.] ],
           [ [1., 1.],
            [0., 0.],
            [0., 0.],
            [0., 0.] ] ])
    

    Для объединения по первой или второй оси можно использовать методы vstack и hstack соответственно. Покажем это на примере изображений. vstack объединяет изображения одинаковой ширины по высоте, а hsstack объединяет одинаковые по высоте картинки в одно широкое (ext-4.py):

    import numpy as np
    import cv2
    from matplotlib import pyplot as plt
    
    I = cv2.imread('susu-new-year.jpg')[:, :, ::-1]
    I_ = I.reshape(I.shape[0] // 2, 2, I.shape[1] // 2, 2, -1)
    Ih = np.hstack((I_[:, 0, :, 0], I_[:, 0, :, 1]))
    Iv = np.vstack((I_[:, 0, :, 0], I_[:, 1, :, 0]))
    flg = plt.figure(num=None, figsize=(10, 10), dpi=80, facecolor='w', edgecolor='k')
    plt.imshow(Ih)
    plt.show()
    flg.savefig('./output-images/ext-41.jpg')
    
    flg = plt.figure(num=None, figsize=(10, 10), dpi=80, facecolor='w', edgecolor='k')
    plt.imshow(Iv)
    plt.show()
    flg.savefig('./output-images/ext-42.jpg')
    
    Объединение изображений по горизонтали
    Объединение изображений по горизонтали
    Объединение изображений по вертикали
    Объединение изображений по вертикали

    Обратите внимание на то, что во всех примерах этого раздела объединяемые массивы передаются одним параметром (кортежем). Количество операндов может быть любым, а не обязательно только 2.

    Также обратите внимание на то, что происходит с памятью, при объединении массивов:

    A = np.array([ [1, 2, 3, 4], [5, 6, 7, 8] ])
    B = A[::-1]
    C = A[:, ::-1]
    D = np.stack((A, B, C))
    D[0, 0, 0] = 0
    print(A)
    Out:
    [ [1 2 3 4]
     [5 6 7 8] ]
    

    Так как создается новый объект, данные в него копируются из исходных массивов, поэтому изменения в новых данных не влияют на исходные.

     <наверх>

    Клонирование данных

    Оператор np.repeat(A, n) вернет одномерный массив с элементами массива A, каждый из которых будет повторен n раз.

    A = np.array([ [1, 2, 3, 4], [5, 6, 7, 8] ])
    print(np.repeat(A, 2))
    Out:
    [1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8]
    

    После этого преобразования, можно перестроить геометрию массива и собрать повторяющиеся данные в одну ось:

    A = np.array([ [1, 2, 3, 4], [5, 6, 7, 8] ])
    B = np.repeat(A, 2).reshape(A.shape[0], A.shape[1], -1)
    print(B)
    Out:
    [ [ [1 1]
      [2 2]
      [3 3]
      [4 4] ]
     [ [5 5]
      [6 6]
      [7 7]
      [8 8] ] ]
    

    Этот вариант отличается от объединения массива с самим собой оператором stack только положением оси, вдоль которой стоят одинаковые данные. В примере выше это последняя ось, если использовать stack — первая:

    A = np.array([ [1, 2, 3, 4], [5, 6, 7, 8] ])
    B = np.stack((A, A))
    print(B)
    Out:
    [ [ [1 2 3 4]
      [5 6 7 8] ]
     [ [1 2 3 4]
      [5 6 7 8] ] ]
    

    Как бы ни было выполнено клонирование данных, следующим шагом можно переместить ось, вдоль которой стоят одинаковые значения, в любую позицию с системе осей:

    A = np.array([ [1, 2, 3, 4], [5, 6, 7, 8] ])
    B = np.transpose(np.stack((A, A)), (1, 0, 2))
    C = np.transpose(np.repeat(A, 2).reshape(A.shape[0], A.shape[1], -1), (0, 2, 1))
    print('B\n', B)
    print('\nC\n', C)
    Out:
    B
     [ [ [1 2 3 4]
      [1 2 3 4] ]
     [ [5 6 7 8]
      [5 6 7 8] ]]
    C
     [ [[1 2 3 4]
      [1 2 3 4] ]
     [ [5 6 7 8]
      [5 6 7 8] ] ]
    

    Если же мы хотим «растянуть» какую либо ось, используя повторение элементов, то ось с одинаковыми значениями надо поставить после растягиваемой (используя transpose), а затем объединить эти две оси (используя reshape). Рассмотрим пример с растяжением изображения вдоль вертикальной оси за счет дублирования строк (ext-5.py):

    import numpy as np
    import cv2
    from matplotlib import pyplot as plt
    
    I0 = cv2.imread('susu-new-year.jpg')[:, :, ::-1]     # загрузили большое изображение
    I1 = I0.reshape(I0.shape[0] // 2, 2, I0.shape[1] // 2, 2, -1)[:, 0, :, 0]  # уменьшили вдвое по каждому измерению
    I2 = np.repeat(I1, 2)  # склонировали данные
    I3 = I2.reshape(I1.shape[0], I1.shape[1], I1.shape[2], -1)
    I4 = np.transpose(I3, (0, 3, 1, 2)) # поменяли порядок осей
    I5 = I4.reshape(-1, I1.shape[1], I1.shape[2]) # объединили оси
    
    print('I0', I0.shape)
    print('I1', I1.shape)
    print('I2', I2.shape)
    print('I3', I3.shape)
    print('I4', I4.shape)
    print('I5', I5.shape)
    
    flg = plt.figure(num=None, figsize=(10, 10), dpi=80, facecolor='w', edgecolor='k')
    plt.imshow(I5)
    plt.show()
    flg.savefig('./output-images/ext-5.jpg')
    
    Пример с растяжением изображения вдоль вертикальной оси за счет дублирования строк
    Пример с растяжением изображения вдоль вертикальной оси за счет дублирования строк

     <наверх>

    Математические операции над элементами массива

    Если A и B массивы одинакового размера, то их можно складывать, умножать, вычитать, делить и возводить в степень. Эти операции выполняются поэлементно, результирующий массив будет совпадать по геометрии с исходными массивами, а каждый его элемент будет результатом выполнения соответствующей операции над парой элементов из исходных массивов:

    A = np.array([ [-1., 2., 3.], [4., 5., 6.], [7., 8., 9.] ])
    B = np.array([ [1., -2., -3.], [7., 8., 9.], [4., 5., 6.], ])
    C = A + B
    D = A - B
    E = A * B
    F = A / B
    G = A ** B
    print('+\n', C, '\n')
    print('-\n', D, '\n')
    print('*\n', E, '\n')
    print('/\n', F, '\n')
    print('**\n', G, '\n')
    Out:
    +
     [ [ 0.  0.  0.]
     [11. 13. 15.]
     [11. 13. 15.] ] 
    -
     [ [-2.  4.  6.]
     [-3. -3. -3.]
     [ 3.  3.  3.] ] 
    *
     [ [-1. -4. -9.]
     [28. 40. 54.]
     [28. 40. 54.] ] 
    /
     [ [-1.         -1.         -1.        ]
     [ 0.57142857  0.625       0.66666667]
     [ 1.75        1.6         1.5       ] ] 
    **
     [ [-1.0000000e+00  2.5000000e-01  3.7037037e-02]
     [ 1.6384000e+04  3.9062500e+05  1.0077696e+07]
     [ 2.4010000e+03  3.2768000e+04  5.3144100e+05] ]
    

    Можно выполнить любую операцию из приведенных выше над массивом и числом. В этом случае операция также выполнится над каждым из элементов массива:

    A = np.array([ [-1., 2., 3.], [4., 5., 6.], [7., 8., 9.] ])
    B = -2.
    
    C = A + B
    D = A - B
    E = A * B
    F = A / B
    G = A ** B
    print('+\n', C, '\n')
    print('-\n', D, '\n')
    print('*\n', E, '\n')
    print('/\n', F, '\n')
    print('**\n', G, '\n')
    Out:
    +
     [ [-3.  0.  1.]
     [ 2.  3.  4.]
     [ 5.  6.  7.] ] 
    -
     [ [ 1.  4.  5.]
     [ 6.  7.  8.]
     [ 9. 10. 11.] ] 
    *
     [ [  2.  -4.  -6.]
     [ -8. -10. -12.]
     [-14. -16. -18.] ] 
    /
     [ [ 0.5 -1.  -1.5]
     [-2.  -2.5 -3. ]
     [-3.5 -4.  -4.5] ] 
    **
     [ [1.         0.25       0.11111111]
     [0.0625     0.04       0.02777778]
     [0.02040816 0.015625   0.01234568] ] 
    

    Учитывая, что многомерный массив можно рассматривать как плоский массив (первая ось), элементы которого — массивы (остальные оси), возможно выполнение рассматриваемых операций над массивами A и B в случае, когда геометрия B совпадает с геометрией подмассивов A при фиксированном значении по первой оси. Иными словами, при совпадающем количестве осей и размерах A[i] и B. Этом случае каждый из массивов A[i] и B будут операндами для операций, определенных над массивами.

    A = np.array([ [1., 2., 3.], [4., 5., 6.], [7., 8., 9.] ])
    B = np.array([-1.1, -1.2, -1.3])
    C = A.T + B
    D = A.T - B
    E = A.T * B
    F = A.T / B
    G = A.T ** B
    print('+\n', C, '\n')
    print('-\n', D, '\n')
    print('*\n', E, '\n')
    print('/\n', F, '\n')
    print('**\n', G, '\n')
    Out:
    +
     [ [-0.1  2.8  5.7]
     [ 0.9  3.8  6.7]
     [ 1.9  4.8  7.7] ] 
    -
     [ [ 2.1  5.2  8.3]
     [ 3.1  6.2  9.3]
     [ 4.1  7.2 10.3] ] 
    *
     [ [ -1.1  -4.8  -9.1]
     [ -2.2  -6.  -10.4]
     [ -3.3  -7.2 -11.7] ] 
    /
     [ [-0.90909091 -3.33333333 -5.38461538]
     [-1.81818182 -4.16666667 -6.15384615]
     [-2.72727273 -5.         -6.92307692] ] 
    **
     [ [1.         0.18946457 0.07968426]
     [0.4665165  0.14495593 0.06698584]
     [0.29865282 0.11647119 0.05747576] ] 
    

    В этом примере массив B подвергается операции с каждой строкой массива A. При необходимости умножения/деления/сложения/вычитания/возведения степень подмассивов вдоль другой оси, необходимо использовать транспонирование, чтобы поставить нужную ось на место первой, а затем вернуть ее на свое место. Рассмотри пример выше, но с умножением на вектор B столбцов массива A:

    A = np.array([ [1., 2., 3.], [4., 5., 6.], [7., 8., 9.] ])
    B = np.array([-1.1, -1.2, -1.3])
    C = (A.T + B).T
    D = (A.T - B).T
    E = (A.T * B).T
    F = (A.T / B).T
    G = (A.T ** B).T
    print('+\n', C, '\n')
    print('-\n', D, '\n')
    print('*\n', E, '\n')
    print('/\n', F, '\n')
    print('**\n', G, '\n')
    Out:
    +
     [ [-0.1  0.9  1.9]
     [ 2.8  3.8  4.8]
     [ 5.7  6.7  7.7] ] 
    -
     [ [ 2.1  3.1  4.1]
     [ 5.2  6.2  7.2]
     [ 8.3  9.3 10.3] ] 
    *
     [ [ -1.1  -2.2  -3.3]
     [ -4.8  -6.   -7.2]
     [ -9.1 -10.4 -11.7] ] 
    /
     [ [-0.90909091 -1.81818182 -2.72727273]
     [-3.33333333 -4.16666667 -5.        ]
     [-5.38461538 -6.15384615 -6.92307692] ] 
    **
     [ [1.         0.4665165  0.29865282]
     [0.18946457 0.14495593 0.11647119]
     [0.07968426 0.06698584 0.05747576] ] 
    

    Для более сложных функций (например, для тригонометрических, экспоненты, логарифма, преобразования между градусами и радианами, модуля, корня квадратного и.д.) в NumPy есть реализация. Рассмотрим на примере экспоненты и логарифма:

    A = np.array([ [1., 2., 3.], [4., 5., 6.], [7., 8., 9.] ])
    B = np.exp(A)
    C = np.log(B)
    print('A', A, '\n')
    print('B', B, '\n')
    print('C', C, '\n')
    Out:
    A [ [1. 2. 3.]
     [4. 5. 6.]
     [7. 8. 9.] ] 
    B [ [2.71828183e+00 7.38905610e+00 2.00855369e+01]
     [5.45981500e+01 1.48413159e+02 4.03428793e+02]
     [1.09663316e+03 2.98095799e+03 8.10308393e+03] ] 
    C [ [1. 2. 3.]
     [4. 5. 6.]
     [7. 8. 9.] ] 
    

    С полным списком математических операций в NumPy можно ознакомиться тут.

     <наверх>

    Матричное умножение

    Описанная выше операция произведения массивов выполняется поэлементно. А при необходимости выполнения операций по правилам линейной алгебры над массивами как над тензорами можно воспользоваться методом dot(A, B). В зависимости от вида операндов, функция выполнит:

    • если аргументы скаляры (числа), то выполнится умножение;
    • если аргументы вектор (одномерный массив) и скаляр, то выполнится умножение массива на число;
    • если аргументы вектора, то выполнится скалярное умножение (сумма поэлементных произведений);
    • если аргументы тензор (многомерный массив) и скаляр, то выполнится умножение вектора на число;
    • если аргументы тензора, то выполнится произведение тензоров по последней оси первого аргумента и предпоследней — второго;
    • если аргументы матрицы, то выполнится произведение матриц (это частный случай произведения тензоров);
    • если аргументы матрица и вектор, то выполнится произведение матрицы и вектора (это тоже частный случай произведения тензоров).

    Для выполнения операций должны совпадать соответсвующие размеры: для векторов длины, для тензоров — длины вдоль осей, по которым будет происходить суммирование поэлементных произведений.

    Рассмотрим примеры со скалярами и векторами:

    # скаляры
    A = 2
    B = 3
    print(np.dot(A, B), '\n')
    # вектор и скаляр
    A = np.array([2., 3., 4.])
    B = 3
    print(np.dot(A, B), '\n')
    # вектора
    A = np.array([2., 3., 4.])
    B = np.array([-2., 1., -1.])
    print(np.dot(A, B), '\n')
    # тензор и скаляр
    A = np.array([ [2., 3., 4.], [5., 6., 7.] ])
    B = 2
    print(np.dot(A, B), '\n')
    Out:
    6 
    [ 6.  9. 12.] 
    -5.0 
    [ [ 4.  6.  8.]
     [10. 12. 14.] ] 
    

    С тензорами посмотрим только на то, как меняется размер геометрия результирующего массива:

    # матрица (тензор 2) и вектор (тензор 1)
    A = np.ones((5, 6))
    B = np.ones(6)
    print('A:', A.shape, '\nB:', B.shape, '\nresult:', np.dot(A, B).shape, '\n\n')
    # матрицы (тензора 2)
    A = np.ones((5, 6))
    B = np.ones((6, 7))
    print('A:', A.shape, '\nB:', B.shape, '\nresult:', np.dot(A, B).shape, '\n\n')
    # многомерные тензора
    A = np.ones((5, 6, 7, 8))
    B = np.ones((1, 2, 3, 8, 4))
    print('A:', A.shape, '\nB:', B.shape, '\nresult:', np.dot(A, B).shape, '\n\n')
    Out:
    A: (5, 6) 
    B: (6,) 
    result: (5,) 
    A: (5, 6) 
    B: (6, 7) 
    result: (5, 7) 
    A: (5, 6, 7, 8) 
    B: (1, 2, 3, 8, 4) 
    result: (5, 6, 7, 1, 2, 3, 4) 
    

    Для выполнения произведения тензоров с использованием других осей, вместо определенных для dot можно воспользоваться tensordot с явным указанием осей:

    A = np.ones((1, 3, 7, 4))
    B = np.ones((5, 7, 6, 7, 8))
    print('A:', A.shape, '\nB:', B.shape, '\nresult:', np.tensordot(A, B, [2, 1]).shape, '\n\n')
    Out:
    A: (1, 3, 7, 4) 
    B: (5, 7, 6, 7, 8) 
    result: (1, 3, 4, 5, 6, 7, 8)  
    

    Мы явно указали, используем третью ось первого массива и вторую — второго (размеры по этим осям должны совпадать).

     <наверх>

    Агрегаторы

    Агрегаторы — это методы NumPy позволяющие заменять данные интегральными характеристиками вдоль некоторых осей. Например, можно посчитать среднее значение, максимальное, минимальное, вариацию или еще какую-то характеристику вдоль какой-либо оси или осей и сформировать из этих данных новый массив. Форма нового массива будет содержать все оси исходного массива, кроме тех, вдоль которых подсчитывался агрегатор.

    Для примера, сформируем массив со случайными значениями. Затем найдем минимальное, максимальное и среднее значение в его столбцах:

    A = np.random.rand(4, 5)
    print('A\n', A, '\n')
    print('min\n', np.min(A, 0), '\n')
    print('max\n', np.max(A, 0), '\n')
    print('mean\n', np.mean(A, 0), '\n')
    print('average\n', np.average(A, 0), '\n')
    Out:
    A
     [ [0.58481838 0.32381665 0.53849901 0.32401355 0.05442121]
     [0.34301843 0.38620863 0.52689694 0.93233065 0.73474868]
     [0.09888225 0.03710514 0.17910721 0.05245685 0.00962319]
     [0.74758173 0.73529492 0.58517879 0.11785686 0.81204847] ] 
    min
     [0.09888225 0.03710514 0.17910721 0.05245685 0.00962319] 
    max
     [0.74758173 0.73529492 0.58517879 0.93233065 0.81204847] 
    mean
     [0.4435752  0.37060634 0.45742049 0.35666448 0.40271039] 
    average
     [0.4435752  0.37060634 0.45742049 0.35666448 0.40271039]
    

    При таком использовании mean и average выглядят синонимами. Но эти функции обладают разным набором дополнительных параметров. У нах разные возможности по маскированию и взвешиванию усредняемых данных.

    Можно подсчитать интегральные характеристики и по нескольким осям:

    A = np.ones((10, 4, 5))
    print('sum\n', np.sum(A, (0, 2)), '\n')
    print('min\n', np.min(A, (0, 2)), '\n')
    print('max\n', np.max(A, (0, 2)), '\n')
    print('mean\n', np.mean(A, (0, 2)), '\n')
    Out:
    sum
     [50. 50. 50. 50.] 
    min
     [1. 1. 1. 1.] 
    max
     [1. 1. 1. 1.] 
    mean
     [1. 1. 1. 1.] 
    

    В этом примере рассмотрена еще одна интегральная характеристика sum — сумма.

    Список агрегаторов выглядит примерно так:

    • сумма: sum и nansum — вариант корректно обходящийся с nan;
    • произведение: prod и nanprod;
    • среднее и матожидание: average и mean (nanmean), nanaverage нету;
    • медиана: median и nanmedian;
    • перцентиль: percentile и nanpercentile;
    • вариация: var и nanvar;
    • стандартное отклонение (квадратный корень из вариации): std и nanstd;
    • минимальное значение: min и nanmin;
    • максимальное значение: max и nanmax;
    • индекс элемента, имеющего минимальное значение: argmin и nanargmin;
    • индекс элемента, имеющего максимальное значение: argmax и nanargmax.

    В случае использования argmin и argmax (соответственно, и nanargmin, и nanargmax) необходимо указывать одну ось, вдоль которой будет считаться характеристика.

    Если не указать оси, то по умолчанию все рассматриваемые характеристики считаются по всему массиву. В этом случае argmin и argmax тоже корректно отработают и найдут индекс максимального или минимального элемента так, как буд-то все данные в массиве вытянуты вдоль одной оси командой ravel().

    Еще следует отметить, агрегирующие методы определены не только как методы модуля NumPy, но и для самих массивов: запись np.aggregator(A, axes) эквивалентна записи A.aggregator(axes), где под aggregator подразумевается одна из рассмотренных выше функций, а под axes — индексы осей.

    A = np.ones((10, 4, 5))
    print('sum\n', A.sum((0, 2)), '\n')
    print('min\n', A.min((0, 2)), '\n')
    print('max\n', A.max((0, 2)), '\n')
    print('mean\n', A.mean((0, 2)), '\n')
    Out:
    sum
     [50. 50. 50. 50.] 
    min
     [1. 1. 1. 1.] 
    max
     [1. 1. 1. 1.] 
    mean
     [1. 1. 1. 1.] 
    

     <наверх>

    Вместо заключения — пример

    Давайте построим алгоритм линейной низкочастотной фильтрации изображения.

    Для начала загрузим зашумленное изображение.

    Рассмотрите это изображения, открыв в соседнем окне
    Рассмотрите это изображения, открыв в соседнем окне

    Фильтровать изображение будем с использованием гауссова фильтра. Но вместо выполнения свертки непосредственно (с итерированием), применим взвешенное усреднение срезов изображения, сдвинутых относительно друг друга(ext-6.py):

    def smooth(I):
        J = I.copy()
    
        J[1:-1] = (J[1:-1] // 2 + J[:-2] // 4 + J[2:] // 4)
        J[:, 1:-1] = (J[:, 1:-1] // 2 + J[:, :-2] // 4 + J[:, 2:] // 4)
    
        return J
    

    Применим эту функцию к нашему изображению единожды, дважды и трижды:

    I_noise = cv2.imread('susu-new-year-noise.jpg')
    
    I_denoise_1 = smooth(I_noise)
    I_denoise_2 = smooth(I_denoise_1)
    I_denoise_3 = smooth(I_denoise_2)
    
    cv2.imwrite('./output-images/susu-new-year-noise_1.jpg', I_denoise_1)
    cv2.imwrite('./output-images/susu-new-year-noise_2.jpg', I_denoise_2)
    cv2.imwrite('./output-images/susu-new-year-noise_3.jpg', I_denoise_3)
    

    Получаем следующие результаты:

    Делай раз
    Делай раз

    при однократном применении фильтра;

    Делай два
    Делай два

    при двукратном;

    Делай три
    Делай три

    при трехкратном.

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

    Теперь давайте посмотрим, сверткам с какими ядрами эквивалентна наша фильтрация. Для этого подвергнем аналогичным преобразованиям одиночный единичный импульс и визуализируем. На самом деле импульс будет не единичным, а равным по амплитуде 255, так как само смешивание оптимизировано под целочисленные данные. Но это не мешает оценить общий вид ядер:

    M = np.zeros((11, 11))
    
    M[5, 5] = 255
    M1 = smooth(M)
    M2 = smooth(M1)
    M3 = smooth(M2)
    
    flg = plt.figure(num=None, figsize=(10, 10), dpi=80, facecolor='w', edgecolor='k')
    plt.subplot(1, 3, 1)
    plt.imshow(M1)
    
    plt.subplot(1, 3, 2)
    plt.imshow(M2)
    
    plt.subplot(1, 3, 3)
    plt.imshow(M3)
    
    plt.show()
    flg.savefig('./output-images/ext-7.jpg')
    

    Мы рассмотрели далеко не полный набор возможностей NumPy, надеюсь, этого было достаточно для демонстрации всей мощи и красоты этого инструмента!

    Скрипты этой статьи, посвященные работе с картинками расположены на Gihub

    Основано на материалах Habr

    ]]>
    https://chel-center.ru/python-yfc/2019/10/01/neskuchnyj-numpy/feed/ 0
    Фильтрация фона видео с OpenCV https://chel-center.ru/python-yfc/2019/11/18/filtracija-fona-video-s-opencv/ https://chel-center.ru/python-yfc/2019/11/18/filtracija-fona-video-s-opencv/#respond Mon, 18 Nov 2019 04:36:20 +0000 http://waksoft.susu.ru/?p=28133 Читать далее «Фильтрация фона видео с OpenCV»

    ]]>
    Для своих приложений компьютерного зрения у меня нет значительных вычислительных мощностей. Поэтому приходится довольствоваться малым и использовать простые, но эффективные методы.

    Здесь мы рассмотрим один из таких методов для выделения движущихся объектов на фоне видео-сцены статически установленной камеры.

    Этот сценарий не редкость. Например, подобные используют камеры видеонаблюдения ГИБДД.

    Временная Медианная Фильтрация

    Чтобы понять суть идеи, которую мы собираемся описать, рассмотрим более простую задачу в одномерном пространстве 1D.

    Проведем эксперимент — каждые 10 миллисекунд мы что-то измеряем (скажем, температуру в комнате).

    Допустим, температура в помещении составляет 70°F по Фаренгейту.

    На приведенном выше рисунке мы показали измерения от двух термометров — хорошего и плохого
    На приведенном выше рисунке мы показали измерения от двух термометров — хорошего и плохого

    Хороший термометр, слева, показывает 70°F с некоторым гауссовским шумом. Чтобы получить более точную оценку температуры, мы можем просто усреднить значения в течение нескольких секунд. Поскольку шум является гауссовым с положительными и отрицательными значениями, то при вычислении среднего произойдет взаимная компенсация. Действительно, среднее значение в данном конкретном случае составляет 70,01°F.

    С другой стороны, плохой термометр большую часть времени ведет себя как хороший, но иногда цифры совершенно неверны.

    На самом деле, если мы вычислим среднее показаний плохого термометра, то получим 71,07°F. Это явно завышенная оценка.

    А мы сможем получить хорошую оценку температуры по этим показаниям?

    Мой ответ — ДА. Когда данные содержат выбросы, медиана является более надежной оценкой значения, которое нам надо.

    Медиана (статистика) — это значение в середине ряда отсортированных в порядке возрастания или убывания данных.

    Медиана наших показаний составляет 70,05°F, что гораздо лучше, чем 71,07°F.

    Единственным недостатком является то, что вычисление медианы является более медленным алгоритмом в сравнении с вычислением средне-взвешенного значения.

    Использование медианы для оценки фона

    Теперь давайте вернемся к проблеме оценки фона, когда камера установлена статично и не меняет своего положения.

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

    Для видеоряда мы можем произвольно выбрать несколько кадров (скажем, 25 кадров).

    Другими словами, для каждого пикселя мы теперь имеем 25 оценок его принадлежности к фону. До тех пор, пока пиксель не будет накрыт автомобилем или другим движущимся объектом более чем в 50% случаев, медиана пикселя над этими 25 кадрами даст хорошую оценку принадлежности фону в этом фрагменте.

    Мы можем повторить это для каждого пикселя и, таким образом, восстановить весь фон.

    import numpy as np
    import cv2
    from skimage import data, filters
    
    # Open Video
    cap = cv2.VideoCapture('video.mp4')
    
    # Randomly select 25 frames
    frameIds = cap.get(cv2.CAP_PROP_FRAME_COUNT) * np.random.uniform(size=25)
    
    # Store selected frames in an array
    frames = []
    for fid in frameIds:
        cap.set(cv2.CAP_PROP_POS_FRAMES, fid)
        ret, frame = cap.read()
        frames.append(frame)
    
    # Calculate the median along the time axis
    medianFrame = np.median(frames, axis=0).astype(dtype=np.uint8)    
    
    # Display median frame
    cv2.imshow('frame', medianFrame)
    cv2.waitKey(0)
    

    Как вы можете видеть, мы случайным образом выбираем 25 кадров и вычисляем медиану каждого пикселя над 25 кадрами. Этот медианный кадр является хорошей оценкой фона, если по крайней мере в 50% случаев пиксель принадлежащий фону не перекрыт.

    Результат показан ниже

    Оценка фона — медианный кадр, вычисленный путем нахождения медианного значения каждого пикселя на 25 кадрах
    Оценка фона — медианный кадр, вычисленный путем нахождения медианного значения каждого пикселя на 25 кадрах

    Разделение кадров

    Следующий очевидный вопрос заключается в том, можем ли мы создать маску для каждого кадра, которая выделит движущиеся части изображения.

    Это достигается за несколько шагов.

    1. Преобразуем кадр в оттенки серого.
    2. По циклу всех кадров в видео. Извлекаем текущий кадр и преобразуйте его в оттенки серого.
    3. Вычисляем абсолютную разницу между текущим кадром и медианным кадром.
    4. Вычисляем пороговое значение приведенного выше изображение для удаления шума и приводим выходной сигнала к бинарному виду.

    Посмотрим код.

    # Reset frame number to 0
    cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
    
    # Convert background to grayscale
    grayMedianFrame = cv2.cvtColor(medianFrame, cv2.COLOR_BGR2GRAY)
    
    # Loop over all frames
    ret = True
    while(ret):
    
      # Read frame
      ret, frame = cap.read()
      # Convert current frame to grayscale
      frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
      # Calculate absolute difference of current frame and 
      # the median frame
      dframe = cv2.absdiff(frame, grayMedianFrame)
      # Treshold to binarize
      th, dframe = cv2.threshold(dframe, 30, 255, cv2.THRESH_BINARY)
      # Display image
      cv2.imshow('frame', dframe)
      cv2.waitKey(20)
    
    # Release video object
    cap.release()
    
    # Destroy all windows
    cv2.destroyAllWindows()
    

    Результаты

    На видео ниже показан вывод оценки фона и дифференцирования кадров.

     

    Источник вдохновения: Simple Background Estimation in Videos using OpenCV (C++/Python)

    ]]>
    https://chel-center.ru/python-yfc/2019/11/18/filtracija-fona-video-s-opencv/feed/ 0
    Усреднённое лицо и OpenCV для Python https://chel-center.ru/python-yfc/2019/11/19/usrednjonnoe-lico-i-opencv-dlja-python/ https://chel-center.ru/python-yfc/2019/11/19/usrednjonnoe-lico-i-opencv-dlja-python/#respond Tue, 19 Nov 2019 03:34:26 +0000 http://waksoft.susu.ru/?p=27851 Читать далее «Усреднённое лицо и OpenCV для Python»

    ]]>
    В этой статье вы узнаете, как сгенерировать усредненное изображение лица при помощи библиотеки OpenCV. Но прежде, чем читать рекомендую ознакомиться со статьёй 5 вариантов среднего или какая средняя температура в больнице? — могут появиться очень интересные аналогии…

    Девушка, изображенная на рисунке 1 ниже, большинству читателей покажется симпатичной. Но вы сможете угадать ее национальность? Почему у нее такая ровная кожа? Правильно – этой девушки нет. Но и нельзя сказать, что это полностью виртуальное изображение. Это усредненный портрет всех сотрудниц компании Sight Commerce Inc. примерно в 2011 году. Благодаря разнообразию в компании, её национальность сложно определить, так как здесь работают девушки с европейскими, латиноамериканскими, восточноазиатскими и индийскими корнями!

    Рисунок 1 — искусственный портрет девушки
    Рисунок 1 — искусственный портрет девушки

    История усреднения лиц забавна

    Все началось с исследований Френсиса Гальтона (племянник Чарльза Дарвина), который еще в 1878 году изобрел новый фотографический прием: научился комбинировать лица и составлять первые фотороботы. Он полагал, что, комбинируя лица преступников, можно смоделировать «прототипическое» лицо уголовника и впоследствии распознавать потенциальных преступников по чертам лиц. Оказалось, что эта гипотеза ошибочна: рассмотрев чью-либо фотографию, невозможно определить его склонность к преступлениям.

    Однако, Гальтон заметил, что усредненное лицо всегда выглядит привлекательнее всех «составляющих» его лиц. В одном поразительном эксперименте исследователи «сложили» лица всех 22 финалисток конкурса «Мисс Германия — 2002». Опрошенные оценили получившийся портрет выше, чем любую из конкурсанток, даже выше «мисс Берлин», которая тогда оказалась победительницей. Уф! Оказывается, Джессика Альба такая хорошенькая именно потому, что ее лицо близко к среднему.

    Можно ли приравнять «среднее» к «посредственному»? Почему усредненное лицо кажется нам привлекательным? Согласно эволюционной гипотезе под названием «койнофилия», особи в активном репродуктивном возрасте ищут партнеров с усредненными чертами, поскольку отклонения от среднего могут свидетельствовать о вредных мутациях. Кроме того, среднее лицо симметрично, поскольку вариации в левой и правой части лица взаимно сглаживаются.

    Как сгенерировать усредненное лицо в OpenCV?

    Рисунок 2 — усредненный портрет президентов США от Картера до Обамы
    Рисунок 2 — усредненный портрет президентов США от Картера до Обамы

    Проверенный код и изображения к статье можно скачать здесь.

    Далее дано пошаговое описание, как сгенерировать усредненное лицо, имея вышеприведенный набор изображений. При этом мы не учитываем размер самих изображений или размер лица на каждом портрете.

    Делай 1: Обнаружение черт лица

    Рисунок 3 — Пример обнаружения черт лица
    Рисунок 3 — Пример обнаружения черт лица

    Для каждого портрета мы рассчитываем 68 «контрольных точек» при помощи библиотеки dlib. О том, как установить и использовать dlib, подробно рассказано в другом месте Facial Feature Detection. На портрете Обамы расставлено 68 контрольных точек.

    Делай 2: Преобразование координат

    На входе размер изображений лиц может сильно отличаться. Поэтому нам придется их нормализовать и привести к одной системе отсчета. Для этого мы деформируем все изображения лиц до размера 600×600, чтобы левый угол левого глаза находился в точке с координатами (180, 200), а правый угол правого глаза – в точке (420, 200). Давайте назовем эту систему отсчета «конечной координатной системой», а координаты исходных изображений – «начальной координатной системой».

    Как я выбрал вышеуказанные точки? Я хотел гарантировать, что эти точки будут расположены на одной горизонтальной линии, и эта линия будет пролегать примерно в трети пути от верхнего до нижнего края картинки. Итак, я добивался, чтобы кончики глазниц находились в точках с координатами ( 0.3 x ширина, высота / 3 ) и ( 0.7 x ширина, высота / 3 ).

    Нам также известно, где расположены уголки глаз на исходных изображениях – соответственно, в контрольных точках 36 и 45. Затем мы можем вычислить преобразование подобия (вращение, перенос, масштабирование) и перевести точки из начальной координатной системы в конечную.

    Рисунок 4 — Преобразование подобия используется для превращения исходного изображения размером 3000×2300 в конечное размером 600×600
    Рисунок 4 — Преобразование подобия используется для превращения исходного изображения размером 3000×2300 в конечное размером 600×600

    Что такое преобразование подобия? Преобразование подобия – это матрица размером 2×3, позволяющая изменять расположение точек (x, y) или целого изображения. Два первых столбца этой матрицы кодируют вращение и масштабирование, а последний — перенос (т.e. смещение). Допустим, вы преобразуете (перемещаете) четыре угла квадрата таким образом, что квадрат масштабируется в направлении x и y в sx и sy раз соответственно. В то же время он поворачивается на угол θ и переносится (перемещается) на tx и ty в направлениях x и y. Преобразование подобия при этом можно записать так:

    Исходя из точки (x, y), вышеописанное преобразование подобия переносит эту точку в (xt, yt) в соответствии со следующим уравнением:

    Преобразование подобия можно выполнить при помощи estimateRigidTransform

    # Python 
     
    # inPts and outPts are numpy arrays of tuples
    # The last parameter indicates we do not want a similarity 
    # transform and not a full affine transform. 
     
    cv2.estimateRigidTransform(inPts, outPts, False);
    

    Однако, здесь есть одна небольшая проблема. OpenCV требует, чтобы вы задали как минимум три пары точек. Это глупо, поскольку преобразование подобия вполне можно сделать, располагая всего двумя точками. Поэтому можно просто вообразить третью точку, таким образом, чтобы она и две известные нам точки образовывали равносторонний треугольник. Затем используем estimateRigidTransform так, как будто у нас три пары точек.

    Вычислив преобразование подобия, можно с его помощью превратить исходное изображение и его контрольные точки в конечные координаты. Изображение преобразуется при помощи warpAffine, а точки – при помощи transform.

    Делай 3: Выравнивание лица

    Рисунок 5 — Результат упрощенного усреднения лица
    Рисунок 5 — Результат упрощенного усреднения лица

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

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

    Этот процесс подробнее описан в моем посте Face Morphing, а в общих чертах – ниже.

    Вычисляем средние лицевые точки

    Чтобы вычислить, как будет выглядеть среднее лицо, все черты которого выровнены, для начала нужно рассчитать среднее от всех преобразованных контрольных точек в конечном изображении. Для этого просто усредняем значения x и y всех контрольных точек в координатах конечного изображения.

    Вычисление триангуляции Делоне

    Рисунок 6 — Вычисление триангуляции Делоне для усредненных контрольных точек
    Рисунок 6 — Вычисление триангуляции Делоне для усредненных контрольных точек

    На предыдущем этапе мы получили положения контрольных точек для усредненного лица в конечных координатах. Можно использовать эти 68 точек (показаны синим цветом На рисунке 6) и 8 точек на границе конечного изображения (показаны зеленым) для расчета триангуляции Делоне (показана красным). Подробнее триангуляция Делоне описана здесь.

    Триангуляция Делоне позволяет разбить изображение на треугольники. В результате такой триангуляции получаем список треугольников, представленных в виде массива из индексов 76 точек (68 точек на лице + 8 граничных точек). В примере триангуляции, показанном ниже, заметно, что контрольные точки 62, 68 и 60 образуют треугольник, 32, 50 и 49 – другой треугольник и т.д.

    Деформация треугольников

    Пример триангуляции

    [
    62 68 60
    32 50 49
    15 16 72
    9 8 58
    53 35 36
    …  ]
    

    На предыдущем этапе мы вычислили среднее расположение контрольных точек на лице и, опираясь на эти данные, выполнили триангуляцию Делоне, чтобы поделить изображение на треугольники. На рисунке 7 видим треугольники Делоне, наложенные на преобразованное исходное изображение, а на том изображении, что находится в середине, показана триангуляция усредненных контрольных точек. Обратите внимание: треугольник 1 на изображении слева соответствует треугольнику 1 на среднем изображении. Зная три вершины треугольника 1, расположенного на левом изображении и соответствующие им три вершины треугольника со среднего изображения, можно рассчитать аффинное преобразование. Повторив эту процедуру для каждого из треугольников с левого изображения, получаем правое изображение. Итак, правое изображение — результат деформации левого до состояния усредненного лица.

    Рисунок 7 — Деформация изображения на базе триангуляции Делоне
    Рисунок 7 — Деформация изображения на базе триангуляции Делоне

    Делай 4: Усреднение лица

    Применив манипуляции из предыдущего этапа ко всем исходным изображениям, получаем конечные изображения, деформированные именно так, чтобы результат совпадал с усредненными конечными точками. Чтобы вычислить усредненное изображение, можно просто сложить значения интенсивности пикселов всех деформированных изображений и разделить эту сумму на количество изображений. На рисунке 2 показан результат такого усреднения. Он выглядит гораздо лучше, чем то «среднее», что было На рисунке 5.
    Как по-вашему выглядит «средний» президент США? По-моему — отечески и мило.

    Результаты усреднения лица

    Рисунок 8 — Усредненное лицо Марка Цукерберга, Ларри Пейджа, Илона Маска и Джеффа Безоса
    Рисунок 8 — Усредненное лицо Марка Цукерберга, Ларри Пейджа, Илона Маска и Джеффа Безоса
    Рисунок 9 — Усредненное лицо Бри Ларсон, Джулианны Мур, Кейт Бланшетт и Дженнифер Лоуренс
    Рисунок 9 — Усредненное лицо Бри Ларсон, Джулианны Мур, Кейт Бланшетт и Дженнифер Лоуренс

    Как выглядит усредненный ведущий предприниматель-технарь? На рисунке 8 показано усредненное лицо Марка Цукерберга, Ларри Пейджа, Илона Маска и Джеффа Безоса. Не могу сказать об этом «среднем предпринимателе» ничего особенного кроме того, что у него все-таки просматривается шевелюра (несмотря на отрицательный вклад Джеффа Безоса).

    Как выглядит усредненная оскароносная актриса? На рисунке 9 показано усредненное лицо Бри Ларсон, Джулианны Мур, Кейт Бланшетт и Дженнифер Лоуренс. Итак, средняя кинозвезда очень симпатичная. И зубы у нее получше, чем у успешного предпринимателя. Ничего удивительного.

    Также можно сделать симметричное лицо, усреднив его с зеркальным изображением. Пример показан ниже.

    Рисунок 10— Симметричный президент Обама (в центре) полученный усреднением его фотографии (слева) с его же зеркальным отражением (справа)
    Рисунок 10— Симметричный президент Обама (в центре) полученный усреднением его фотографии (слева) с его же зеркальным отражением (справа)

    Перевод: Average Face: OpenCV (C++/Python) Tutorial

    ]]>
    https://chel-center.ru/python-yfc/2019/11/19/usrednjonnoe-lico-i-opencv-dlja-python/feed/ 0
    Обнаружение объекта на изображении методом цветовой сегментации (Python) https://chel-center.ru/python-yfc/2019/11/26/obnaruzhenie-obekta-na-izobrazhenii-opirajas-na-cvetovuju-segmentacii-python/ https://chel-center.ru/python-yfc/2019/11/26/obnaruzhenie-obekta-na-izobrazhenii-opirajas-na-cvetovuju-segmentacii-python/#respond Tue, 26 Nov 2019 07:19:43 +0000 http://waksoft.susu.ru/?p=27166 Читать далее «Обнаружение объекта на изображении методом цветовой сегментации (Python)»

    ]]>
    Прежде чем говорить о сути вещей, договоримся о терминах . . .

    Контур — просто непрерывная кривая, разделяющая на изображении все точки с одинаковым цветом или интенсивность. Контур является полезным инструментом для анализа форм, а также для обнаружения и распознавания объектов.

    Порог. Применение порогового значения к изображению в градациях серого делает его двоичным черно/белым изображением. Из разумных соображений устанавливается пороговое значение, при котором все значения ниже этого порога становятся черными, а все значения выше становятся белыми.

    Начинаем наши упражнения

    Теперь у нас есть все для начала и сразу на простом примере уясним для себя понятие цветовая сегментация. Но пока мы доберемся до более интересных вещей, немного потерпите.

     Круг Омбре - изображение, сделанное с помощью фотошопа
    Круг Омбре — изображение, сделанное с помощью фотошопа

    Для своих практических упражнений заберите это изображение здесь.

    Разделим изображение на 17 колец по уровням серого, а затем измерим площадь каждого кольца, ограниченного контуром.

    import cv2
    import numpy as np
    
    def viewImage(image):
        cv2.namedWindow('Display', cv2.WINDOW_NORMAL)
        cv2.imshow('Display', image)
        cv2.waitKey(0)
        cv2.destroyAllWindows()
    
    def grayscale_17_levels (image):
        high = 255
        while(true):  
            low = high - 15
            col_to_be_changed_low = np.array([low])
            col_to_be_changed_high = np.array([high])
            curr_mask = cv2.inRange(gray, col_to_be_changed_low,col_to_be_changed_high)
            gray[curr_mask > 0] = (high)
            high -= 15
            if(low == 0 ):
                break
    
    image = cv2.imread('./path/to/image')
    viewImage(image)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    grayscale_17_levels(gray)
    viewImage(gray)
    
    То же изображение разделено на 17 уровней серого
    То же изображение разделено на 17 уровней серого
    def get_area_of_each_gray_level(im):
    
    ## convert image to gray scale (must br done before contouring)
        image = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
        output = []
        high = 255
        first = True
        while(true):
    
            low = high - 15
            if(first == False):
    
                ## making values that are of a greater gray level black 
                ## so it won't get detected  
                to_be_black_again_low = np.array([high])
                to_be_black_again_high = np.array([255])
                curr_mask = cv2.inRange(image, to_be_black_again_low, 
                to_be_black_again_high)
                image[curr_mask > 0] = (0)
                
            # making values of this gray level white so we can calculate
            # it's area
            ret, threshold = cv2.threshold(image, low, 255, 0)
            contours, hirerchy = cv2.findContours(threshold, 
            cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
    
            if(len(contours) > 0):
                output.append([cv2.contourArea(contours[0])])
                cv2.drawContours(im, contours, -1, (0,0,255), 3)
    
            high -= 15
            first = False
            if(low == 0 ):
    break
    return output
    

    В этой функции я просто преобразую диапазон (интенсивностей) серого, который хочу оконтурить (выделить) на соответствующей итерации, объединяя все, что находятся в этом диапазоне, в одно поле с заданной интенсивностью. Я включаю любую интенсивность кроме черной (в том числе и наибольшую, и наименьшую интенсивности).

    Второй шаг — получение порогового (бинарного) изображения. Теперь поле, которое я хочу обвести контуром надо сделать белым, а все остальные — чёрными. Этот шаг не сильно оригинален, но он необходим, потому как оконтуривание лучше всего делать по черно-белым (пороговым, бинарным) изображениям.

    Получим (пороговое, бинарное) изображение, где белое кольцо заменит серое с интенсивностью 10-го уровня (255–15 * 10), а все остальные кольца сделаем чёрными.

    Выделяем единственный, 10-й сегмент, для вычисления его площади
    Выделяем единственный, 10-й сегмент, для вычисления его площади

    image = cv2.imread('./path/to/image')
    print(get_area_of_each_gray_level(image))
    viewImage(image)
    

     Контуры 17 уровней серого на исходном изображении
    Контуры 17 уровней серого на исходном изображении


     

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

    Зачем всё это?

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

    На факультете компьютерной инженерии MTI, с которым у нас активное сотрудничество, ведутся работы над проектом под названием «Машинное обучение» для интеллектуального обнаружения и идентификации опухолей.

    В этом проекте в полный рост используется сегментация цветного изображения для того, чтобы научить компьютер обнаруживать опухоль. При работе с МРТ (Магнитно-резонансная томография) программа должна определять стадию развития рака, что достигается сегментированием сканированных изображений по различным уровням оттенков серого, где самые темные участки наиболее заполнены раковыми клетками, а близкие к белому соответствуют более здоровым частям тела. Далее вычисляется степень развития опухоли, которой соответствует некоторый уровень серого. Именно эта информация позволяет локализовать и определить стадию развития опухоли.

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

    Обнаружение объекта

    Фото Лукаса из Pexels
    Фото Лукаса из Pexels

    Для своих упражнений заберём изображение отсюда, на Pexels, которое нужно будет просто немного обрезать.

    На этом изображении необходимо получить только контур листа. Текстура изображения очень нерегулярна и неровна, хотя цветов не так уж и много. Кроме того, интенсивность зеленого ещё и меняет свою яркость. Поэтому лучше всего будет объединить все оттенки зеленого в один цвет. Таким образом, когда будет построен контур, с листом можно будет работать как с единым целым.

     ПРИМЕЧАНИЕ  Вот результат оконтуриавания без какой-либо предварительной обработки. Здесь я просто хочу показать, как неровномерная природа листа обманывает OpenCV и не позволяет ему понять, что это всего лишь один объект. Кстати, очень полезный мануал по OpenCV

    Контур без предварительной обработки, обнаружен 531 контур
    Контур без предварительной обработки, обнаружен 531 контур

    import cv2
    import numpy as np
    
    def viewImage(image):
        cv2.namedWindow('Display', cv2.WINDOW_NORMAL)
        cv2.imshow('Display', image)
        cv2.waitKey(0)
        cv2.destroyAllWindows()
    

    Первое, что вам необходимо знать — HSV (цветовая модель) ваших цветов, которую можно узнать, сделав преобразование его из RGB в HSV, использовав нижеследующий код.

    ## getting green HSV color representation
    green = np.uint8([0, 255, 0])
    green_hsv = cv2.cvtColor(green,cv2.COLOR_BGR2HSV)
    print( green_hsv)
    
    Зеленый цвет в HSV-представлении
    Зеленый цвет в HSV-представлении

    Преобразование изображения в HSV-представление: с HSV значительно проще получить полный диапазон одного цвета. В HSV H — тон (например, красный, зелёный или сине-голубой), S — насыщенность (варьируется в пределах 0—100 или 0—1. Чем больше этот параметр, тем «чище» цвет, поэтому этот параметр иногда называют чистотой цвета. А чем ближе этот параметр к нулю, тем ближе цвет к нейтральному серому), V — значение или Brightness — яркость (задаётся в пределах 0—100 или 0—1). Мы уже знаем, что зеленому цвету в HSV соответствует [60, 255, 255]. Все зеленые цвета мира расположены в пределах от [45, 100, 50] до [75, 255, 255], то есть от [60–15, 100, 50] до [60+15 , 255, 255]. 15 — только приблизительное значение.

    Мы берем этот диапазон и преобразуем его в [75, 255, 200 ] или любой другой цвет из диапазона зелёных (3-е значение, яркость, должно быть относительно большим), это значение, когда мы перейдём к бинарному изображению, станет белым.

    image = cv2.imread('./path/to/image.jpg')
    hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
    viewImage(hsv_img) ## 1
    
    green_low = np.array([45 , 100, 50] )
    green_high = np.array([75, 255, 255])
    curr_mask = cv2.inRange(hsv_img, green_low, green_high)
    hsv_img[curr_mask > 0] = ([75,255,200])
    viewImage(hsv_img) ## 2
    
    ## converting the HSV image to Gray inorder to be able to apply 
    ## contouring
    RGB_again = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2RGB)
    gray = cv2.cvtColor(RGB_again, cv2.COLOR_RGB2GRAY)
    viewImage(gray) ## 3
    
    ret, threshold = cv2.threshold(gray, 90, 255, 0)
    viewImage(threshold) ## 4
    
    contours, hierarchy =  cv2.findContours(threshold,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
    cv2.drawContours(image, contours, -1, (0, 0, 255), 3)
    viewImage(image) ## 5
    

    Изображение сразу после преобразования в HSV (1)
    Изображение сразу после преобразования в HSV (1)

    Изображение после нанесения маски (нормализация цвета) (2)
    Изображение после нанесения маски (нормализация цвета) (2)

    Изображение после преобразования из HSV в серый (3)
    Изображение после преобразования из HSV в серый (3)

    Бинарное изображение, последний шаг (4)
    Бинарное изображение, последний шаг (4)

    Конечный контур (5)
    Конечный контур (5)

    Поскольку, похоже, на заднем плане также есть неровномерности, применяя это преобразование мы сможем получить самый большой контур. Самый большой контур, конечно, лист.

    Мы можем вычислить индекс контура листа в массиве выделенных контуров по наибольшей площади и центру, близкому к центру листа.

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

    def findGreatesContour(contours):
        largest_area = 0
        largest_contour_index = -1
        i = 0
        total_contours = len(contours)
        while (i  largest_area):
                largest_area = area
                largest_contour_index = i
            i+=1
                
        return largest_area, largest_contour_index
    
    # to get the center of the contour
    cnt = contours[13]
    M = cv2.moments(cnt)
    cX = int(M["m10"] / M["m00"])
    cY = int(M["m01"] / M["m00"])
    
    largest_area, largest_contour_index = findGreatesContour(contours)
    
    print(largest_area)
    print(largest_contour_index)
    
    print(len(contours))
    
    print(cX)
    print(cY)
    

    Результат печати
    Результат печати

     

    Некоторые примеры кода из этой статьи можно загрузить здесь.
     

    По мотивам Object detection via color-based image segmentation using python

    ]]>
    https://chel-center.ru/python-yfc/2019/11/26/obnaruzhenie-obekta-na-izobrazhenii-opirajas-na-cvetovuju-segmentacii-python/feed/ 0
    OpenCV: шпаргалка https://chel-center.ru/python-yfc/2020/02/22/opencv-shpargalka/ https://chel-center.ru/python-yfc/2020/02/22/opencv-shpargalka/#respond Sat, 22 Feb 2020 03:14:17 +0000 http://waksoft.susu.ru/?p=27158 Читать далее «OpenCV: шпаргалка»

    ]]>
    Что такое OpenCV?

    Библиотека компьютерного зрения и машинного обучения с открытым исходным кодом. В неё входят более 2500 алгоритмов, в которых есть как классические, так и современные алгоритмы для компьютерного зрения и машинного обучения. Эта библиотека имеет интерфейсы на различных языках, среди которых есть Python (в этой статье используем его), Java, C++ и Matlab.

    Содержание

    Установка

    Инструкцию по установке на Windows можно посмотреть здесь, а на Linux — здесь.

    Импорт и просмотр изображения

    import cv2
    image = cv2.imread("./путь/к/изображению.расширение")
    cv2.imshow("Image", image)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    

    Примечание:
    При чтении способом выше изображение находится в цветовом пространстве не RGB (как все привыкли), а BGR. Возможно, в начале это не так важно, но как только вы начнёте работать с цветом — стоит знать об этой особенности. Есть 2 пути решения:

    1. Поменять местами 1-й канал (R — красный) с 3-м каналом (B — синий), и тогда красный цвет будет (0,0,255), а не (255,0,0).
    2. Поменять цветовое пространство на RGB:
      rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
      

    И тогда в коде работать уже не с image, а с rgb_image.

    На протяжении статьи для вывода изображений будет использоваться следующий код:

    import cv2
    def viewImage(image, name_of_window):
        cv2.namedWindow(name_of_window, cv2.WINDOW_NORMAL)
        cv2.imshow(name_of_window, image)
        cv2.waitKey(0)
        cv2.destroyAllWindows()
    

    Кадрирование

    Кадрирование — оригинальное изображениеф
    Кадрирование — оригинальное изображениеф
    Пёсик после кадрирования
    Пёсик после кадрирования
    import cv2
    cropped = image[10:500, 500:2000]
    viewImage(cropped, "Пёсик после кадрирования")
    

    Где image[10:500, 500:2000] — это image[y:y + высота, x:x + ширина].

    Изменение размера

    Изменение размера — исходное изображение
    Изменение размера — исходное изображение
    Изменение размера на 20%
    Изменение размера на 20%
    import cv2
    scale_percent = 20 # Процент от изначального размера
    width = int(img.shape[1] * scale_percent / 100)
    height = int(img.shape[0] * scale_percent / 100)
    dim = (width, height)
    resized = cv2.resize(img, dim, interpolation = cv2.INTER_AREA)
    viewImage(resized, "После изменения размера на 20 %")
    

    Эта функция учитывает соотношение сторон оригинального изображения. Другие функции изменения размера изображений можно увидеть здесь.

    Поворот

    Фото Джонатана Мейера из Pexels.
    Фото Джонатана Мейера из Pexels.
    Пёсик  после поворота на 180 градусов.
    Пёсик после поворота на 180 градусов.
    import cv2
    (h, w, d) = image.shape
    center = (w // 2, h // 2)
    M = cv2.getRotationMatrix2D(center, 180, 1.0)
    rotated = cv2.warpAffine(image, M, (w, h))
    viewImage(rotated, "Пёсик после поворота на 180 градусов")
    

    image.shape возвращает высоту, ширину и каналы. M — матрица поворота — поворачивает изображение на 180 градусов вокруг центра. -ve — это угол поворота изображения по часовой стрелке, а +ve, соответственно, против часовой.

    Перевод в градации серого и в чёрно-белое изображение по порогу

    Оригинал: Pexels
    Оригинал: Pexels
    Градации серого
    Градации серого
    Чёрно-белое. Порог
    Чёрно-белое. Порог
    import cv2
    gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    ret, threshold_image = cv2.threshold(im, 127, 255, 0)
    viewImage(gray_image, "Пёсик в градациях серого")
    viewImage(threshold_image, "Чёрно-белый пёсик")
    

    gray_image — это одноканальная версия изображения.

    Функция threshold возвращает изображение, в котором все пиксели, которые темнее (меньше) 127 заменены на 0, а все, которые ярче (больше) 127, — на 255.

    Для ясности другой пример:

    ret, threshold = cv2.threshold(im, 150, 200, 10)
    

    Здесь всё, что темнее, чем 150, заменяется на 10, а всё, что ярче, — на 200.

    Остальные threshold-функции описаны здесь.

    Размытие/сглаживание

    Исходное изображение с Pixabay
    Исходное изображение с Pixabay
    Размытый пёсик
    Размытый пёсик
    import cv2
    blurred = cv2.GaussianBlur(image, (51, 51), 0)
    viewImage(blurred, "Размытый пёсик")
    

    Функция GaussianBlur (размытие по Гауссу) принимает 3 параметра:

    1. Исходное изображение.
    2. Кортеж из 2 положительных нечётных чисел. Чем больше числа, тем больше сила сглаживания.
    3. sigmaX и sigmaY. Если эти параметры оставить равными 0, то их значение будет рассчитано автоматически.

    Больше про размытие здесь.

    Рисование прямоугольников

    Рисуем прямоугольники — исходное изображение
    Рисуем прямоугольники — исходное изображение
    Обводим мордочку жёлтым прямоугольником
    Обводим мордочку жёлтым прямоугольником
    import cv2
    output = image.copy()
    cv2.rectangle(output, (2600, 800), (4100, 2400), (0, 255, 255), 10)
    viewImage(output, "Обводим прямоугольником лицо пёсика")
    

    Эта функция принимает 5 параметров:

    1. Само изображение.
    2. Координата верхнего левого угла (x1, y1).
    3. Координата нижнего правого угла (x2, y2).
    4. Цвет прямоугольника (GBR/RGB в зависимости от выбранной цветовой модели).
    5. Толщина линии прямоугольника.

    Рисование линий

    Изображение с pexels
    Изображение с pexels
    Двух пёсиков разделим линией
    Двух пёсиков разделим линией
    import cv2
    output = image.copy()
    cv2.line(output, (60, 20), (400, 200), (0, 0, 255), 5)
    viewImage(output, "2 пёсика, разделённые линией")
    

    Функция line принимает 5 параметров:

    1. Само изображение, на котором рисуется линия.
    2. Координата первой точки (x1, y1).
    3. Координата второй точки (x2, y2).
    4. Цвет линии (GBR/RGB в зависимости от выбранной цветовой модели).
    5. Толщина линии.

    Текст на изображении

    Исходное изображение для надписи
    Исходное изображение для надписи
    Надпись на изображении
    Надпись на изображении
    import cv2
    output = image.copy()
    cv2.putText(output, "We <3 Dogs", (1500, 3600),cv2.FONT_HERSHEY_SIMPLEX, 15, (30, 105, 210), 40) 
    viewImage(output, "Изображение с текстом")
    

    Функция putText принимает 7 параметров:

    1. Непосредственно изображение.
    2. Текст для изображения.
    3. Координата нижнего левого угла начала текста (x, y).
    4. Используемый шрифт.
    5. Размер шрифта.
    6. Цвет текста (GBR/RGB в зависимости от выбранной цветовой модели).
    7. Толщина линий букв.

    Распознавание лиц

    На этот раз без пёсиков.

    <a href="https://pixabay.com/photos/young-woman-female-youth-healthy-1208208/" class="bo de js jt ju jv" target="_blank" rel="noopener">Фото с Pixabay</a>
    Фото с Pixabay
    Обнаружено два лица
    Обнаружено два лица
    import cv2
    image_path = "./путь/к/фото.расширение"
    face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
    image = cv2.imread(image_path)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    faces = face_cascade.detectMultiScale(
        gray,
        scaleFactor= 1.1,a
        minNeighbors= 5,
        minSize=(10, 10)
    )
    faces_detected = "Лиц обнаружено: " + format(len(faces))
    print(faces_detected)
    # Рисуем квадраты вокруг лиц
    for (x, y, w, h) in faces:
        cv2.rectangle(image, (x, y), (x+w, y+h), (255, 255, 0), 2)
    viewImage(image,faces_detected)
    

    detectMultiScale — общая функция для распознавания как лиц, так и объектов. Чтобы функция искала именно лица, мы передаём ей соответствующий каскад.

    Функция detectMultiScale принимает 4 параметра:

    1. Обрабатываемое изображение в градации серого.
    2. Параметр scaleFactor. Некоторые лица могут быть больше других, поскольку находятся ближе, чем остальные. Этот параметр компенсирует перспективу.
    3. Алгоритм распознавания использует скользящее окно во время распознавания объектов. Параметр minNeighbors определяет количество объектов вокруг лица. То есть чем больше значение этого параметра, тем больше аналогичных объектов необходимо алгоритму, чтобы он определил текущий объект, как лицо. Слишком маленькое значение увеличит количество ложных срабатываний, а слишком большое сделает алгоритм более требовательным.
    4. minSize — непосредственно размер этих областей.

    Contours — распознавание объектов

    Распознавание объектов производится с помощью цветовой сегментации изображения. Для этого есть две функции: cv2.findContours и cv2.drawContours.

    В этой статье детально описано обнаружение объектов с помощью цветовой сегментации. Всё, что вам нужно для неё, находится там.

    Сохранение изображения

    import cv2
    image = cv2.imread("./импорт/путь.расширение")
    cv2.imwrite("./экспорт/путь.расширение", image)
    

    Заключение

    OpenCV — отличная библиотека с лёгкими алгоритмами, которые могут использоваться в 3D-рендере, продвинутом редактировании изображений и видео, отслеживании и идентификации объектов и людей на видео, поиске идентичных изображений из набора и для много-много чего ещё.

    Эта библиотека очень важна для тех, кто разрабатывает проекты, связанные с машинным обучением в области изображений.

    Требуется регистрация для доступа к контенту. Регистрация, если Вы уже зарегистрированы — подключитесь

    ]]>
    https://chel-center.ru/python-yfc/2020/02/22/opencv-shpargalka/feed/ 0
    Декораторы в Python https://chel-center.ru/python-yfc/2020/03/08/dekoratory-v-python/ https://chel-center.ru/python-yfc/2020/03/08/dekoratory-v-python/#respond Sun, 08 Mar 2020 05:45:09 +0000 http://chel-center.ru/python-yfc/?p=29913 Читать далее «Декораторы в Python»

    ]]>
    Тема декораторов довольно часто, однако, обсуждается и эта статья (выросшая из одного вопроса на stackoverflow) наиболее полно, по-моему мнению, раскрывает тему и, что немаловажно, является «пошаговым руководством» для использованию декораторов, позволяющим новичку овладеть этой техникой сразу на достойном уровне.

    Итак, что же такое «декоратор»?

    Впереди достаточно длинная статья, так что, если кто-то спешит — вот пример того, как работают декораторы:

    def makebold(fn):
        def wrapped():
            return "" + fn() + ""
        return wrapped
     
    def makeitalic(fn):
        def wrapped():
            return "" + fn() + ""
        return wrapped
     
    @makebold
    @makeitalic
    def hello():
        return "hello habr"
     
    print hello() ## выведет hello habr
    


    Те же из вас, кто готов потратить немного времени, приглашаются прочесть длиииинный пост.

    Функции в Python’e являются объектами

    Для того, чтобы понять, как работают декораторы, в первую очередь следует осознать, что в Python’е функции — это тоже объекты.

    Давайте посмотрим, что из этого следует:

    def shout(word="да"):
        return word.capitalize()+"!"
     
    print shout()
    # выведет: 'Да!'
     
    # Так как функция - это объект, вы связать её с переменнной,
    # как и любой другой объект
    scream = shout
     
    # Заметьте, что мы не используем скобок: мы НЕ вызываем функцию "shout",
    # мы связываем её с переменной "scream". Это означает, что теперь мы
    # можем вызывать "shout" через "scream":
     
    print scream()
    # выведет: 'Да!'
    
    # Более того, это значит, что мы можем удалить "shout", и функция всё ещё
    # будет доступна через переменную "scream"
     
    del shout
    try:
        print shout()
    except NameError, e:
        print e
        #выведет: "name 'shout' is not defined"
     
    print scream()
    # выведет: 'Да!'
    

    Ссылки на функции

    Ну что, вы всё ещё здесь?:)

    Теперь мы знаем, что функции являются полноправными объектами, а значит:

    • могут быть связаны с переменной;
    • могут быть определены одна внутри другой.

    Что ж, а это значит, что одна функция может вернуть другую функцию!
    Давайте посмотрим:

    def getTalk(type="shout"):
     
        # Мы определяем функции прямо здесь
        def shout(word="да"):
            return word.capitalize()+"!"
     
        def whisper(word="да") :
            return word.lower()+"...";
     
        # Затем возвращаем необходимую
        if type == "shout":
            # Заметьте, что мы НЕ используем "()", нам нужно не вызвать функцию,
            # а вернуть объект функции
            return shout
        else:
            return whisper
     
    # Как использовать это непонятное нечто?
    # Возьмём функцию и свяжем её с переменной
    talk = getTalk()
     
    # Как мы можем видеть, "talk" теперь - объект "function":
    print talk
    # выведет: 
     
    # Который можно вызывать, как и функцию, определённую "обычным образом":
    print talk()
     
    # Если нам захочется - можно вызвать её напрямую из возвращаемого значения:
    print getTalk("whisper")()
    # выведет: да...
    
    

    Подождите, раз мы можем возвращать функцию, значит, мы можем и передавать её другой функции, как параметр:

    def doSomethingBefore(func):
        print "Я делаю что-то ещё, перед тем как вызвать функцию, которую ты мне передал"
        print func()
     
    doSomethingBefore(scream)
    #выведет:
    # Я делаю что-то ещё, перед тем как вызвать функцию, которую ты мне передал
    # Да!
    

    Ну что, теперь у нас есть все необходимые знания для того, чтобы понять, как работают декораторы.

    Как вы могли догадаться, декораторы — это, по сути, просто своеобразные «обёртки», которые дают нам возможность делать что-либо до и после того, что сделает декорируемая функция, не изменяя её.

    Создадим свой декоратор «вручную»

    # Декоратор - это функция, ожидающая ДРУГУЮ функцию в качестве параметра
    def my_shiny_new_decorator(a_function_to_decorate):
        # Внутри себя декоратор определяет функцию-"обёртку".
    
        # Она будет (что бы вы думали?..) обёрнута вокруг декорируемой,
        # получая возможность исполнять произвольный код до и после неё.
    
    
        def the_wrapper_around_the_original_function():
            # Поместим здесь код, который мы хотим запускать ДО вызова
            # оригинальной функции
            print "Я - код, который отработает до вызова функции"
     
            # ВЫЗОВЕМ саму декорируемую функцию
            a_function_to_decorate()
    
            # А здесь поместим код, который мы хотим запускать ПОСЛЕ вызова
            # оригинальной функции
            print "А я - код, срабатывающий после"
    
        # На данный момент функция "a_function_to_decorate" НЕ ВЫЗЫВАЛАСЬ НИ РАЗУ
    
        # Теперь, вернём функцию-обёртку, которая содержит в себе
        # декорируемую функцию, и код, который необходимо выполнить до и после.
    
        # Всё просто!
        return the_wrapper_around_the_original_function
    
    # Представим теперь, что у нас есть функция, которую мы не планируем больше трогать.
    
    def a_stand_alone_function():
        print "Я простая одинокая функция, ты ведь не посмеешь меня изменять?.."
     
    a_stand_alone_function()
    # выведет: Я простая одинокая функция, ты ведь не посмеешь меня изменять?..
    
     
    # Однако, чтобы изменить её поведение, мы можем декорировать её, то есть
    # Просто передать декоратору, который обернет исходную функцию в любой код,
    # который нам потребуется, и вернёт новую, готовую к использованию функцию:
     
    a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
    a_stand_alone_function_decorated()
    #выведет:
    # Я - код, который отработает до вызова функции
    # Я простая одинокая функция, ты ведь не посмеешь меня изменять?..
    
    # А я - код, срабатывающий после
    

    Наверное, теперь мы бы хотели, чтобы каждый раз, во время вызова a_stand_alone_function, вместо неё вызывалась a_stand_alone_function_decorated. Нет ничего проще, просто перезапишем a_stand_alone_function функцией, которую нам вернул my_shiny_new_decorator:

    a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function)
    a_stand_alone_function()
    #выведет:
    # Я - код, который отработает до вызова функции
    # Я простая одинокая функция, ты ведь не посмеешь меня изменять?..
    
    # А я - код, срабатывающий после
    

    Вы ведь уже догадались, что это ровно тоже самое, что делают @декораторы.:)

    Разрушаем ореол таинственности вокруг декораторов

    Вот так можно было записать предыдущий пример, используя синтаксис декораторов:

    @my_shiny_new_decorator
    def another_stand_alone_function():
        print "Оставь меня в покое"
     
    another_stand_alone_function()
    #выведет:
    # Я - код, который отработает до вызова функции
    # Оставь меня в покое
    # А я - код, срабатывающий после
    

    Да, всё действительно так просто! decorator — просто синтаксический сахар для конструкций вида:

    another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)
    

    Декораторы — это просто pythonic-реализация паттерна проектирования «Декоратор». В Python включены некоторые классические паттерны проектирования, такие как рассматриваемые в этой статье декораторы, или привычные любому пайтонисту итераторы.

    Конечно, можно вкладывать декораторы друг в друга, например так:

    def bread(func):
        def wrapper():
            print ""
            func()
            print "<\______/>"
        return wrapper
     
    def ingredients(func):
        def wrapper():
            print "#помидоры#"
            func()
            print "~салат~"
        return wrapper
     
    def sandwich(food="--ветчина--"):
        print food
     
    sandwich()
    #выведет: --ветчина--
    sandwich = bread(ingredients(sandwich))
    sandwich()
    #выведет:
    # 
    # #помидоры#
    # --ветчина--
    # ~салат~
    # <\______/>
    

    И используя синтаксис декораторов:

    @bread
    @ingredients
    def sandwich(food="--ветчина--"):
        print food
     
    sandwich()
    #выведет:
    # 
    # #помидоры#
    # --ветчина--
    # ~салат~
    # <\______/>
    

    Следует помнить о том, что порядок декорирования ВАЖЕН:

    @ingredients
    @bread
    def sandwich(food="--ветчина--"):
        print food
     
    sandwich()
    #выведет:
    # #помидоры#
    # 
    # --ветчина--
    # <\______/>
    # ~салат~
    

    На этом моменте Вы можете счастливо уйти, с осознанием того, что вы поняли, что такое декораторы и с чем их едят.

    Для тех же, кто хочет помучать ещё немного свой мозг, вторая часть статьи, посвящённая продвинутому использованию декораторов.

    Декораторы, которые мы до этого рассматривали не имели одного очень важного функционала — передачи аргументов декорируемой функции.

    Что ж, исправим это недоразумение!

    Передача («проброс») аргументов в декорируемую функцию

    Никакой чёрной магии, всё, что нам необходимо — собственно, передать аргументы дальше!

    def a_decorator_passing_arguments(function_to_decorate):
        def a_wrapper_accepting_arguments(arg1, arg2): # аргументы прибывают отсюда
            print "Смотри, что я получил:", arg1, arg2
            function_to_decorate(arg1, arg2)
        return a_wrapper_accepting_arguments
     
    # Теперь, когда мы вызываем функцию, которую возвращает декоратор,
    # мы вызываем её "обёртку", передаём ей аргументы и уже в свою очередь
    # она передаёт их декорируемой функции
     
    @a_decorator_passing_arguments
    def print_full_name(first_name, last_name):
        print "Меня зовут", first_name, last_name
     
    print_full_name("Питер", "Венкман")
    # выведет:
    # Смотри, что я получил: Питер Венкман
    # Меня зовут Питер Венкман
    # *
    

    * — Прим. переводчика: Питер Венкман — имя одного из Охотников за приведениями, главного героя одноименного культового фильма.

    Декорирование методов

    Один из важных фактов, которые следует понимать, заключается в том, что функции и методы в Python’e — это практически одно и то же, за исключением того, что методы всегда ожидают первым параметром ссылку на сам объект (self). Это значит, что мы можем создавать декораторы для методов так же, как и для функций, просто не забывая про self.

    def method_friendly_decorator(method_to_decorate):
        def wrapper(self, lie):
            lie = lie - 3 # действительно, дружелюбно - снизим возраст ещё сильней :-)
            return method_to_decorate(self, lie)
        return wrapper
     
     
    class Lucy(object):
     
        def __init__(self):
            self.age = 32
     
        @method_friendly_decorator
        def sayYourAge(self, lie):
            print "Мне %s, а ты бы сколько дал?" % (self.age + lie)
     
    l = Lucy()
    l.sayYourAge(-3)
    # выведет: Мне 26, а ты бы сколько дал?
    

    Конечно, если мы создаём максимально общий декоратор и хотим, чтобы его можно было применить к любой функции или методу, то стоит воспользоваться тем, что *args распаковывает список args, а **kwargs распаковывает словарь kwargs:

    def a_decorator_passing_arbitrary_arguments(function_to_decorate):
        # Данная "обёртка" принимает любые аргументы
        def a_wrapper_accepting_arbitrary_arguments(*args, **kwargs):
            print "Передали ли мне что-нибудь?:"
            print args
            print kwargs
            # Теперь мы распакуем *args и **kwargs
            # Если вы не слишком хорошо знакомы с распаковкой, можете прочесть следующую статью:
            # http://www.saltycrane.com/blog/2008/01/how-to-use-args-and-kwargs-in-python/
            function_to_decorate(*args, **kwargs)
        return a_wrapper_accepting_arbitrary_arguments
     
    @a_decorator_passing_arbitrary_arguments
    def function_with_no_argument():
        print "Python is cool, no argument here." # оставлено без перевода, хорошая игра слов:)
     
    function_with_no_argument()
    # выведет:
    # Передали ли мне что-нибудь?:
    # ()
    # {}
    # Python is cool, no argument here.
     
    @a_decorator_passing_arbitrary_arguments
    def function_with_arguments(a, b, c):
        print a, b, c
     
    function_with_arguments(1,2,3)
    # выведет:
    # Передали ли мне что-нибудь?:
    # (1, 2, 3)
    # {}
    # 1 2 3
     
    @a_decorator_passing_arbitrary_arguments
    def function_with_named_arguments(a, b, c, platypus="Почему нет?"):
        print "Любят ли %s, %s и %s утконосов? %s" %\
        (a, b, c, platypus)
     
    function_with_named_arguments("Билл", "Линус", "Стив", platypus="Определенно!")
    # выведет:
    # Передали ли мне что-нибудь?:
    # ('Билл', 'Линус', 'Стив')
    # {'platypus': 'Определенно!'}
    # Любят ли Билл, Линус и Стив утконосов? Определенно!
     
    class Mary(object):
     
        def __init__(self):
            self.age = 31
     
        @a_decorator_passing_arbitrary_arguments
        def sayYourAge(self, lie=-3): # Теперь мы можем указать значение по умолчанию
            print "Мне %s, а ты бы сколько дал?" % (self.age + lie)
     
    m = Mary()
    m.sayYourAge()
    # выведет:
    # Передали ли мне что-нибудь?:
    # (<__main__ .Mary object at 0xb7d303ac>,)
    # {}
    # Мне 28, а ты бы сколько дал?
    

    Вызов декоратора с различными аргументами

    Отлично, с этим разобрались. Что вы теперь скажете о том, чтобы попробовать вызывать декораторы с различными аргументами?

    Это не так просто, как кажется, поскольку декоратор должен принимать функцию в качестве аргумента, и мы не можем просто так передать ему что либо ещё.

    Так что, перед тем, как показать вам решение, я бы хотел освежить в памяти то, что мы уже знаем:

    # Декораторы - это просто функции
    def my_decorator(func):
        print "Я обычная функция"
        def wrapper():
            print "Я - функция, возвращаемая декоратором"
            func()
        return wrapper
     
    # Так что, мы можем вызывать её, не используя "@"-синтаксис:
     
    def lazy_function():
        print "zzzzzzzz"
     
    decorated_function = my_decorator(lazy_function)
    # выведет: Я обычная функция
     
    # Данный код выводит "Я обычная функция", потому что это ровно то, что мы сделали:
    # вызвали функцию. Ничего сверхъестественного
     
    @my_decorator
    def lazy_function():
        print "zzzzzzzz"
     
    # выведет: Я обычная функция
    

    Как мы видим, это два аналогичных действия. Когда мы пишем @my_decorator — мы просто говорим интерпретатору «вызвать функцию, под названием my_decorator». Это важный момент, потому что данное название может как привести нас напрямую к декоратору… так и нет!
    Давайте сделаем нечто страшное!:)

    def decorator_maker():
     
        print "Я создаю декораторы! Я буду вызван только раз: "+\
              "когда ты попросишь меня создать тебе декоратор."
     
        def my_decorator(func):
     
            print "Я - декоратор! Я буду вызван только раз: в момент декорирования функции."
     
            def wrapped():
                print ("Я - обёртка вокруг декорируемой функции. "
                      "Я буду вызвана каждый раз когда ты вызываешь декорируемую функцию. "
                      "Я возвращаю результат работы декорируемой функции.")
                return func()
     
            print "Я возвращаю обёрнутую функцию."
     
            return wrapped
     
        print "Я возвращаю декоратор."
        return my_decorator
     
    # Давайте теперь создадим декоратор. Это всего лишь ещё один вызов функции
    new_decorator = decorator_maker()
    # выведет:
    # Я создаю декораторы! Я буду вызван только раз: когда ты попросишь меня создать тебе декоратор. 
    # Я возвращаю декоратор.
     
    # Теперь декорируем функцию
     
    def decorated_function():
        print "Я - декорируемая функция."
     
    decorated_function = new_decorator(decorated_function)
    # выведет:
    # Я - декоратор! Я буду вызван только раз: в момент декорирования функции.
    # Я возвращаю обёрнутую функцию.
     
    # Теперь наконец вызовем функцию:
    decorated_function()
    # выведет:
    # Я - обёртка вокруг декорируемой функции. Я буду вызвана каждый раз когда ты вызываешь декорируемую функцию.
    # Я возвращаю результат работы декорируемой функции.
    # Я - декорируемая функция.
    
    

    Длинно? Длинно. Перепишем данный код без использования промежуточных переменных:

    def decorated_function():
        print "Я - декорируемая функция."
    decorated_function = decorator_maker()(decorated_function)
    # выведет:
    # Я создаю декораторы! Я буду вызван только раз: когда ты попросишь меня создать тебе декоратор. 
    # Я возвращаю декоратор.
    # Я - декоратор! Я буду вызван только раз: в момент декорирования функции.
    # Я возвращаю обёрнутую функцию.
     
    # Наконец:
    decorated_function()
    # выведет:
    # Я - обёртка вокруг декорируемой функции. Я буду вызвана каждый раз когда ты вызываешь декорируемую функцию.
    # Я возвращаю результат работы декорируемой функции.
    # Я - декорируемая функция.
    
    

    А теперь ещё раз, ещё короче:

    @decorator_maker()
    def decorated_function():
        print "I am the decorated function."
    # выведет:
    # Я создаю декораторы! Я буду вызван только раз: когда ты попросишь меня создать тебе декоратор. 
    # Я возвращаю декоратор.
    # Я - декоратор! Я буду вызван только раз: в момент декорирования функции.
    # Я возвращаю обёрнутую функцию.
     
    # И снова:
    decorated_function()
    # выведет:
    # Я - обёртка вокруг декорируемой функции. Я буду вызвана каждый раз когда ты вызываешь декорируемую функцию.
    # Я возвращаю результат работы декорируемой функции.
    # Я - декорируемая функция.
    
    

    Вы заметили, что мы вызвали функцию, после знака «@»?:)

    Вернёмся, наконец, к аргументам декораторов, ведь если мы используем функцию, чтобы создавать декораторы «на лету», мы можем передавать ей любые аргументы, верно?

    def decorator_maker_with_arguments(decorator_arg1, decorator_arg2):
     
        print "Я создаю декораторы! И я получил следующие аргументы:", decorator_arg1, decorator_arg2
     
        def my_decorator(func):
            print "Я - декоратор. И ты всё же смог передать мне эти аргументы:", decorator_arg1, decorator_arg2
     
            # Не перепутайте аргументы декораторов с аргументами функций!
            def wrapped(function_arg1, function_arg2) :
                print ("Я - обёртка вокруг декорируемой функции.\n"
                      "И я имею доступ ко всем аргументам: \n"
                      "\t- и декоратора: {0} {1}\n"
                      "\t- и функции: {2} {3}\n"
                      "Теперь я могу передать нужные аргументы дальше"
                      .format(decorator_arg1, decorator_arg2,
                              function_arg1, function_arg2))
                return func(function_arg1, function_arg2)
     
            return wrapped
     
        return my_decorator
     
    @decorator_maker_with_arguments("Леонард", "Шелдон")
    def decorated_function_with_arguments(function_arg1, function_arg2):
        print ("Я - декорируемая функция и я знаю только о своих аргументах: {0}"
               " {1}".format(function_arg1, function_arg2))
     
    decorated_function_with_arguments("Раджеш", "Говард")
    # выведет:
    # Я создаю декораторы! И я получил следующие аргументы: Леонард Шелдон
    # Я - декоратор. И ты всё же смог передать мне эти аргументы: Леонард Шелдон
    # Я - обёртка вокруг декорируемой функции.
    # И я имею доступ ко всем аргументам: 
    #   - и декоратора: Леонард Шелдон
    #   - и функции: Раджеш Говард
    # Теперь я могу передать нужные аргументы дальше
    # Я - декорируемая функция и я знаю только о своих аргументах: Раджеш Говард
    

    * — Прим. переводчика: в данном примере автор упоминает имена главных героев популярного сериала «Теория Большого взрыва».
    Вот он, искомый декоратор, которому можно передавать произвольные аргументы.

    Безусловно, аргументами могут быть любые переменные:

    c1 = "Пенни"
    c2 = "Лесли"
     
    @decorator_maker_with_arguments("Леонард", c1)
    def decorated_function_with_arguments(function_arg1, function_arg2):
        print ("Я - декорируемая функция и я знаю только о своих аргументах: {0}"
               " {1}".format(function_arg1, function_arg2))
     
    decorated_function_with_arguments(c2, "Говард")
    # выведет:
    # Я создаю декораторы! И я получил следующие аргументы: Леонард Пенни
    # Я - декоратор. И ты всё же смог передать мне эти аргументы: Леонард Пенни
    # Я - обёртка вокруг декорируемой функции.
    # И я имею доступ ко всем аргументам: 
    #   - и декоратора: Леонард Пенни
    #   - и функции: Лесли Говард
    # Теперь я могу передать нужные аргументы дальше
    # Я - декорируемая функция и я знаю только о своих аргументах: Лесли Говард
    

    Таким образом, мы можем передавать декоратору любые аргументы, как обычной функции. Мы можем использовать и распаковку через *args и **kwargs в случае необходимости.

    Но необходимо всегда держать в голове, что декоратор вызывается ровно один раз. Ровно в момент, когда Python импортирует Ваш скрипт. После этого мы уже не можем никак изменить аргументы, с которыми.

    Когда мы пишем «import x» все функции из x декорируются сразу же, и мы уже не сможем ничего изменить.

    Немного практики: напишем декоратор декорирующий декоратор

    Если вы дочитали до этого момента и ещё в строю — вот вам бонус от меня.

    Это небольшая хитрость позволит вам превратить любой обычный декоратор в декоратор, принимающий аргументы.

    Изначально, чтобы получить декоратор, принимающий аргументы, мы создали его с помощью другой функции.

    Мы обернули наш декоратор.

    Есть ли у нас что-нибудь, чем можно обернуть функцию?
    Точно, декораторы!

    Давайте же немного развлечёмся и напишем декоратор для декораторов:

    def decorator_with_args(decorator_to_enhance):
        """
        Эта функция задумывается КАК декоратор и ДЛЯ декораторов.
        Она должна декорировать другую функцию, которая должна быть декоратором.
        Лучше выпейте чашку кофе.
        Она даёт возможность любому декоратору принимать произвольные аргументы,
        избавляя Вас от головной боли о том, как же это делается, каждый раз, когда этот функционал необходим.
        """
     
        # Мы используем тот же трюк, который мы использовали для передачи аргументов:
        def decorator_maker(*args, **kwargs):
     
            # создадим на лету декоратор, который принимает как аргумент только 
            # функцию, но сохраняет все аргументы, переданные своему "создателю"
            def decorator_wrapper(func):
     
                # Мы возвращаем то, что вернёт нам изначальный декоратор, который, в свою очередь
                # ПРОСТО ФУНКЦИЯ (возвращающая функцию).
                # Единственная ловушка в том, что этот декоратор должен быть именно такого
                # decorator(func, *args, **kwargs)
                # вида, иначе ничего не сработает
                return decorator_to_enhance(func, *args, **kwargs)
     
            return decorator_wrapper
     
        return decorator_maker
    

    Это может быть использовано так:

    # Мы создаём функцию, которую будем использовать как декоратор и декорируем её :-)
    # Не стоит забывать, что она должна иметь вид "decorator(func, *args, **kwargs)"
    @decorator_with_args
    def decorated_decorator(func, *args, **kwargs):
        def wrapper(function_arg1, function_arg2):
            print "Мне тут передали...:", args, kwargs
            return func(function_arg1, function_arg2)
        return wrapper
     
    # Теперь декорируем любую нужную функцию нашим новеньким, ещё блестящим декоратором:
     
    @decorated_decorator(42, 404, 1024)
    def decorated_function(function_arg1, function_arg2):
        print "Привет", function_arg1, function_arg2
     
    decorated_function("Вселенная и", "всё прочее")
    # выведет:
    # Мне тут передали...: (42, 404, 1024) {}
    # Привет Вселенная и всё прочее
     
    # Уфффффф!
    

    Думаю, я знаю, что Вы сейчас чувствуете.

    Последний раз Вы испытывали это ощущение, слушая, как вам говорят: «Чтобы понять рекурсию необходимо для начала понять рекурсию».

    Но ведь теперь Вы рады, что разобрались с этим?;)

    Рекомендации для работы с декораторами

    • Декораторы были введены в Python 2.4, так что узнавайте, на чём будет выполняться Ваш код.
    • Декораторы несколько замедляют вызов функции, не забывайте об этом.
    • Вы не можете «раздекорировать» функцию. Безусловно, существуют трюки, позволяющие создать декоратор, который можно отсоединить от функции, но это плохая практика. Правильней будет запомнить, что если функция декорирована — это не отменить.
    • Декораторы оборачивают функции, что может затруднить отладку.

    Последняя проблема частично решена в Python 2.5, добавлением в стандартную библиотеку модуля functools включающего в себя functools.wraps, который копирует всю информацию об оборачиваемой функции (её имя, из какого она райомодуля, её docstrings и т.п.) в функцию-обёртку.

    Забавным фактом является то, что functools.wraps — сам по себе декоратор.

    # Во время отладки, в трассировочную информацию выводится __name__ функции.
    def foo():
        print "foo"
     
    print foo.__name__
    # выведет: foo
     
    # Однако, декораторы мешают нормальному ходу дел:
    def bar(func):
        def wrapper():
            print "bar"
            return func()
        return wrapper
     
    @bar
    def foo():
        print "foo"
     
    print foo.__name__
    # выведет: wrapper
     
    # "functools" может нам с этим помочь
     
    import functools
     
    def bar(func):
        # Объявляем "wrapper" оборачивающим "func"
        # и запускаем магию:
        @functools.wraps(func)
        def wrapper():
            print "bar"
            return func()
        return wrapper
     
    @bar
    def foo():
        print "foo"
     
    print foo.__name__
    # выведет: foo
    

    Как можно использовать декораторы?

    И в заключение, я бы хотел ответить на вопрос, который я часто слышу: зачем же нужны декораторы? Как их можно использовать?
    Декораторы могут быть использованы для расширения возможностей функций из сторонних библиотек (код которых мы не можем изменять), или для упрощения отладки (мы не хотим изменять код, который ещё не устоялся).

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

    def benchmark(func):
        """
        Декоратор, выводящий время, которое заняло
        выполнение декорируемой функции.
        """
        import time
        def wrapper(*args, **kwargs):
            t = time.clock()
            res = func(*args, **kwargs)
            print func.__name__, time.clock() - t
            return res
        return wrapper
     
     
    def logging(func):
        """
        Декоратор, логирующий работу кода.
        (хорошо, он просто выводит вызовы, но тут могло быть и логирование!)
        """
        def wrapper(*args, **kwargs):
            res = func(*args, **kwargs)
            print func.__name__, args, kwargs
            return res
        return wrapper
     
     
    def counter(func):
        """
        Декоратор, считающий и выводящий количество вызовов
        декорируемой функции.
        """
        def wrapper(*args, **kwargs):
            wrapper.count += 1
            res = func(*args, **kwargs)
            print "{0} была вызвана: {1}x".format(func.__name__, wrapper.count)
            return res
        wrapper.count = 0
        return wrapper
     
     
    @benchmark
    @logging
    @counter
    def reverse_string(string):
        return str(reversed(string))
     
    print reverse_string("А роза упала на лапу Азора")
    print reverse_string("A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!")
     
    # выведет:
    # reverse_string ('А роза упала на лапу Азора',) {}
    # wrapper 0.0
    # reverse_string была вызвана: 1x
    # арозА упал ан алапу азор А
    # reverse_string ('A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!',) {}
    # wrapper 0.0
    # reverse_string была вызвана: 2x
    # !amanaP :lanac a ,noep a ,stah eros ,raj a ,hsac ,oloR a ,tur a ,mapS ,snip ,eperc a ,)lemac a ro( niaga gab ananab a ,gat a ,nat a ,gab ananab a ,gag a ,inoracam ,elacrep ,epins ,spam ,arutaroloc a ,shajar ,soreh ,atsap ,eonac a ,nalp a ,nam A
    

    Таким образом, декораторы можно применить к любой функции, расширив её функционал и не переписывая ни строчки кода!

    import httplib
     
    @benchmark
    @logging
    @counter
    def get_random_futurama_quote():
        conn = httplib.HTTPConnection("slashdot.org:80")
        conn.request("HEAD", "/index.html")
        for key, value in conn.getresponse().getheaders():
            if key.startswith("x-b") or key.startswith("x-f"):
                return value
        return "Эх, нет... не могу!"
     
    print get_random_futurama_quote()
    print get_random_futurama_quote()
     
    #outputs:
    #get_random_futurama_quote () {}
    #wrapper 0.02
    #get_random_futurama_quote была вызвана: 1x
    #The laws of science be a harsh mistress.
    #get_random_futurama_quote () {}
    #wrapper 0.01
    #get_random_futurama_quote была вызвана: 2x
    #Curse you, merciful Poseidon!
    

    В Python включены такие декораторы как property, staticmethod и т.д.

    В Django декораторы используются для управления кешированием, контроля за правами доступа и определения обработчиков адресов. В Twisted — для создания поддельных асинхронных inline-вызовов.

    Декораторы открывают широчайший простор для экспериментов! И надеюсь, что данная статья поможет Вам в его освоении!
    Спасибо за внимание!

    Источники вдохновения:

    ]]>
    https://chel-center.ru/python-yfc/2020/03/08/dekoratory-v-python/feed/ 0
    Как сделать бота для Instagram https://chel-center.ru/python-yfc/2020/04/12/kak-sdelat-bota-dlya-instagram/ https://chel-center.ru/python-yfc/2020/04/12/kak-sdelat-bota-dlya-instagram/#respond Sun, 12 Apr 2020 06:57:30 +0000 http://chel-center.ru/python-yfc/?p=30153 Читать далее «Как сделать бота для Instagram»

    ]]>
    В этом уроке:

    Что делают и имеют общего SocialCaptain, Kicksta, Instavast и многие другие компании? Все они добиваются для своих клиентов большей аудитории, больше подписчиков и больше лайков в Instagram, в то время как вы ещё палец о палец не ударили. Они делают все это с помощью автоматизации и люди платят им за это немалые деньги. Но то же самое вы можете сделать абсолютно бесплатно, используя InstaPy!

    В этом уроке вы узнаете, как создать бота с помощью Python и InstaPy, библиотеки Тима Гросманна, которая автоматизирует ваши действия в Instagram, чтобы вы получили больше подписчиков и лайков.

    Попутно вы узнаете об автоматизации браузера с помощью Selenium и Шаблонов объектов страницы, который служит основой InstaPy.

    В этом уроке вы узнаете:

    • Как работают боты в Instagram;
    • Как автоматизировать браузер с помощью Selenium;
    • Как использовать Шаблон объекта страницы, чтобы сделать ваш код более удобным и тестируемым;
    • Как использовать InstaPy для создания базового бота в Instagram.

    Прежде чем создавать бота, начнём с изучения того, как они работают в Instagram.

    Это важно: Прежде чем внедрять какие-либо средства автоматизации обязательно ознакомьтесь с Условиями использования Instagram

    Как работают боты в Instagram

    Каким образом скрипт автоматизации может принести вам больше лайков и фалловеров? Прежде чем ответить на этот вопрос, подумайте о том, как реальный человек получает увеличивает количество лайков и фалловеров.

    Человек постоянно активен на платформе. Он часто публикуют сообщения, подписываются на других людей, а также любит оставлять комментарии к постам других людей. Боты работают точно так же: они подписываются, постоянно комментируют в соответствии с установленными вами правилами.

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

    Например, если вы продаете женскую одежду в Instagram, то можете указать своему боту, что он должен ставить лайки, комментировать и следить в основном за женщинами или профилями, посты которых содержат хэштеги #beauty, #fashion или #одежда. Это повышает вероятность того, что ваша целевая аудитория заметит вас, последует за вами и начнет читать и отвечать на ваши публикациями.

    Как это работает с технической стороны? Вы не можете использовать API разработчика Instagram, поскольку он довольно ограничен для этой цели. Обратите внимание на автоматизацию браузера. Это работает лучше:

    1. Вы отдаёте ему свои полномочия.
    2. Вы опрееляее по каким критериям становиться фалловером, какие комментарии оставлять и какой тип постов нравиться.
    3. Ваш бот открывает браузер, в адресной строке запиывает https://instagram.com, входит в систему со вашими учетными данными и начинает выполнять действия, которые ему поручили.

    Давайте создадим первую версию своего бота в Instagram, которая автоматически войдет в ваш профиль. Обратите внимание, InstaPy ещё не будете использоваться.

    Как автоматизировать браузер

    Для этой версии вашего бота в Instagram вы будете использовать Selenium, инструмент, который InstaPy использует под капотом.

    Сначала установите Selenium. Во время установки убедитесь, что вы уже установили Firefox WebDriver. Начиная с последней версии, InstaPy прекратила поддержку Chrome. Поэтому, на вашем компьютере должен быть установлен браузер Firefox.

    Теперь создайте файл Python и напишите в нем следующий код:

    from time import sleep
    from selenium import webdriver
    
    browser = webdriver.Firefox()
    
    browser.get('https://www.instagram.com/')
    
    sleep(5)
    browser.close()
    

    Запустите код, и вы увидите, что браузер Firefox открывается и перенаправляет вас на страницу входа в Instagram. Вот построчный комментарий кода:

    • В строках 1 и 2 импортируется sleep и webdriver.
    • Строка 4 инициализирует и устанавливает драйвер Firefox в браузере.
    • Строка 6 записывает в адресной строке https://www.instagram.com/ и нажиает клавишу Enter.
    • В строке 8 пяти секундное ожидание для просмотра результата. Иначе браузер мгоновенно закроется.
    • Строка 10 закрывает браузер.

    Это Hello, World в версии Selenium. Теперь вы готовы добавить код для входа в свой профиль Instagram. Но сначала задумайтесь, как вы заходите в свой профиль вручную. Вы бы сделали следующее:

    1. Переход по адресу https://www.instagram.com/.
    2. Нажимаем на ссылку login.
    3. Вводим учётные данные.
    4. Нажимаем кнопку login.

    Первый шаг уже сделан в коде выше. Теперь измените его так, чтобы он нажимал на ссылку для входа на главной странице Instagram:

    from time import sleep
    from selenium import webdriver
    
    browser = webdriver.Firefox()
    browser.implicitly_wait(5)
    
    browser.get('https://www.instagram.com/')
    
    login_link = browser.find_element_by_xpath("//a[text()='Log in']")
    login_link.click()
    
    sleep(5)
    
    browser.close()
    

    Прокомментироем строчки:

    • строка 5 устанавливает пять секунд ожидания. Если Selenium не может найти элемент, то он ждет пять секунд для полной загрузки страницы и повторяет попытку.
    • строка9 находит элемент <a>, текст которого соответствует Login. Он делает это с помощью XPath, но есть и несколько других методов, которые можно использовать.
    • строка 10 щелкает на найденной ссылке <a> для входа в систему.

    Запустите скрипт, и вы увидите свой скрипт в действии. Откроется браузер, зайдите в Instagram и нажмите на ссылку для входа, чтобы перейти на страницу входа.

    На странице входа в систему есть три важных элемента:

    1. Ввод имени пользователя;
    2. Ввод пароля;
    3. Нажатие кнопки подключения.

    Затем измените сценарий так, чтобы он находил эти элементы, вводил ваши учетные данные и нажимал кнопку входа в систему:

    from time import sleep
    from selenium import webdriver
    
    browser = webdriver.Firefox()
    browser.implicitly_wait(5)
    
    browser.get('https://www.instagram.com/')
    
    login_link = browser.find_element_by_xpath("//a[text()='Log in']")
    login_link.click()
    
    sleep(2)
    
    username_input = browser.find_element_by_css_selector("input[name='username']")
    password_input = browser.find_element_by_css_selector("input[name='password']")
    
    username_input.send_keys("<your username>")
    password_input.send_keys("<your password>")
    
    login_button = browser.find_element_by_xpath("//button[@type='submit']")
    login_button.click()
    
    sleep(5)
    
    browser.close()
    

    Вот комментарий для изменений:

    1. строка 12 ожидает две секунды для загрузки страницы.
    2. строки 14 и 15 находит поля ввода имени пользователя и пароля с помощью CSS. Вы могли бы использовать любой другой метод, который нравится.
    3. строки 17 и 18 записывают ваше имя пользователя и пароль в соответствующие профилю. Не забудьте заполнить <your username> и <your password>!
    4. строка 20 находит кнопку входа по XPath.
    5. строка 21 нажимает на кнопку входа в систему.

    Запустите скрипт, и вы автоматически войдете в свой профиль Instagram.

    Вы хорошо начали со своим ботом Instagram. Если вы продолжите писать этот сценарий, то все остальное будет выглядеть очень похоже. Чтобы найти понравившиеся сообщения, прокрутите вниз свой канал, найдите кнопку «Нравится» с помощью CSS, нажмите на нее, найдите раздел комментариев, оставьте комментарий и продолжайте.

    Хорошей новостью является то, что все эти шаги могут быть выполнены InstaPy. Но прежде чем перейти к использованию Instapy, следует еще кое-что узнать, чтобы лучше понять, как работает InstaPy: Шаблон объекта страницы.

    Как использовать шаблон объекта страницы

    Теперь, когда вы написали код входа в систему, как бы вы написали тест для него? А будет это выглядеть примерно так:

    def test_login_page(browser):
        browser.get('https://www.instagram.com/accounts/login/')
        username_input = browser.find_element_by_css_selector("input[name='username']")
        password_input = browser.find_element_by_css_selector("input[name='password']")
        username_input.send_keys("<your username>")
        password_input.send_keys("<your password>")
        login_button = browser.find_element_by_xpath("//button[@type='submit']")
        login_button.click()
    
        errors = browser.find_elements_by_css_selector('#error_message')
        assert len(errors) == 0
    

    Вы видите, что с этим кодом что-то не так? Он не соответствует DRY principle. То есть код дублируется как в приложении, так и в тесте.

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

    С помощью этого шаблона для наиболее важных страниц или фрагментов вы создаете классы объектов страницы, которые предоставляют интерфейсы, которые просты в программировании и которые скрывают основной виджет в окне. Имея это в виду, вы можете переписать приведенный выше код и создать класс HomePage и класс LoginPage:

    from time import sleep
    
    class LoginPage:
        def __init__(self, browser):
            self.browser = browser
    
        def login(self, username, password):
            username_input = self.browser.find_element_by_css_selector("input[name='username']")
            password_input = self.browser.find_element_by_css_selector("input[name='password']")
            username_input.send_keys(username)
            password_input.send_keys(password)
            login_button = browser.find_element_by_xpath("//button[@type='submit']")
            login_button.click()
            sleep(5)
    
    class HomePage:
        def __init__(self, browser):
            self.browser = browser
            self.browser.get('https://www.instagram.com/')
    
        def go_to_login_page(self):
            self.browser.find_element_by_xpath("//a[text()='Log in']").click()
            sleep(2)
            return LoginPage(self.browser)
    

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

    Еще одна вещь, на которую следует обратить внимание, это то, что при переходе на другую страницу с помощью объекта страницы он возвращает объект страницы для новой страницы. Обратите внимание на возвращаемое значение go_to_log_in_page(). Если у вас есть другой класс с именем FeedPage, то login() класса LoginPage вернет экземпляр этого: return FeedPage().

    Вот как вы можете использовать шаблон объекта Page:

    from selenium import webdriver
    
    browser = webdriver.Firefox()
    browser.implicitly_wait(5)
    
    home_page = HomePage(browser)
    login_page = home_page.go_to_login_page()
    login_page.login("<your username>", "<your password>")
    
    browser.close()
    

    Это выглядит намного лучше, и тест выше можно переписать так:

    def test_login_page(browser):
        home_page = HomePage(browser)
        login_page = home_page.go_to_login_page()
        login_page.login("<your username>", "<your password>")
    
        errors = browser.find_elements_by_css_selector('#error_message')
        assert len(errors) == 0
    

    С этими изменениями вам не придется трогать свои тесты, если что-то изменится в пользовательском интерфейсе.

    Для получения дополнительной информации о шаблоне объектов страницы см. официальную документацию и в Статье Мартина Фаулера.

    Теперь, когда вы знакомы как с Selenium, так и с шаблоном объектов страницы, вы будете чувствовать себя как дома с InstaPy. Далее вы сделаем базового бота.

    Примечание:

    Как Selenium, так и Pattern Object Page широко используются не только для Instagram, но и для других веб-сайтов.

    Как создать Instagram-бот с помощью InstaPy

    В этом разделе вы будете использовать InstaPy для создания бота в Instagram, который будет автоматически добавлять, отслеживать и комментировать различные сообщения. Во-первых, вам нужно установить InstaPy:

    $ python3 -m pip install instapy
    

    Таким образом вы установите instapy в своей системе.

    Примечание:

    Рекомендуется использовать виртуальные среды для каждого проекта, чтобы изолировать зависимости.

    Основные функции

    Теперь, чтобы вы могли сравнить два варианта, можно переписать код выше с InstaPy. Сначала создайте другой файл Python и поместите в него следующий код:

    from instapy import InstaPy
    
    InstaPy(username="<your_username>", password="<your_password>").login()
    

    Замените имя пользователя и пароль своим, запустите скрипт и вуаля! Используя всего одну строку кода, вы добились того же результата.

    Даже если ваши результаты одинаковы, вы сможете увидеть, что поведение не совсем одинакво. Помимо простого входа в свой профиль, InstaPy выполняет и другие действия, такие как проверка вашего интернет-соединения и состояния серверов Instagram. Это можно наблюдать прямо в браузере или в логах:

    INFO [2019-12-17 22:03:19] [username]  -- Connection Checklist [1/3] (Internet Connection Status)
    INFO [2019-12-17 22:03:20] [username]  - Internet Connection Status: ok
    INFO [2019-12-17 22:03:20] [username]  - Current IP is "17.283.46.379" and it's from "Germany/DE"
    INFO [2019-12-17 22:03:20] [username]  -- Connection Checklist [2/3] (Instagram Server Status)
    INFO [2019-12-17 22:03:26] [username]  - Instagram WebSite Status: Currently Up
    

    Для одной строки кода не плохо, не так ли? Теперь пришло время заставить скрипт делать более интересные вещи, чем просто вход в систему.

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

    Во-первых, вам могут понравиться некоторые сообщения с тегами #bmw или #mercedes, используя like_by_tags():

    from instapy import InstaPy
    
    session = InstaPy(username="<your_username>", password="<your_password>")
    session.login()
    session.like_by_tags(["bmw", "mercedes"], amount=5)
    

    Здесь вы передали методу список понравившихся тегов и количество понравившихся сообщений для каждого тега. В этом случае вы указали, что ему должно понравиться десять постов, по пять для каждого из двух тегов. Но посмотрите, что происходит после запуска скрипта:

    INFO [2019-12-17 22:15:58] [username]  Tag [1/2]
    INFO [2019-12-17 22:15:58] [username]  --> b'bmw'
    INFO [2019-12-17 22:16:07] [username]  desired amount: 14  |  top posts [disabled]: 9  |  possible posts: 43726739
    INFO [2019-12-17 22:16:13] [username]  Like# [1/14]
    INFO [2019-12-17 22:16:13] [username]  https://www.instagram.com/p/B6MCcGcC3tU/
    INFO [2019-12-17 22:16:15] [username]  Image from: b'mattyproduction'
    INFO [2019-12-17 22:16:15] [username]  Link: b'https://www.instagram.com/p/B6MCcGcC3tU/'
    INFO [2019-12-17 22:16:15] [username]  Description: b'Mal etwas anderes \xf0\x9f\x91\x80\xe2\x98\xba\xef\xb8\x8f Bald ist das komplette Video auf YouTube zu finden (n\xc3\xa4here Infos werden folgen). Vielen Dank an @patrick_jwki @thehuthlife  und @christic_  f\xc3\xbcr das bereitstellen der Autos \xf0\x9f\x94\xa5\xf0\x9f\x98\x8d#carporn#cars#tuning#bagged#bmw#m2#m2competition#focusrs#ford#mk3#e92#m3#panasonic#cinematic#gh5s#dji#roninm#adobe#videography#music#bimmer#fordperformance#night#shooting#'
    INFO [2019-12-17 22:16:15] [username]  Location: b'K\xc3\xb6ln, Germany'
    INFO [2019-12-17 22:16:51] [username]  --> Image Liked!
    INFO [2019-12-17 22:16:56] [username]  --> Not commented
    INFO [2019-12-17 22:16:57] [username]  --> Not following
    INFO [2019-12-17 22:16:58] [username]  Like# [2/14]
    INFO [2019-12-17 22:16:58] [username]  https://www.instagram.com/p/B6MDK1wJ-Kb/
    INFO [2019-12-17 22:17:01] [username]  Image from: b'davs0'
    INFO [2019-12-17 22:17:01] [username]  Link: b'https://www.instagram.com/p/B6MDK1wJ-Kb/'
    INFO [2019-12-17 22:17:01] [username]  Description: b'Someone said cloud? \xf0\x9f\xa4\x94\xf0\x9f\xa4\xad\xf0\x9f\x98\x88 \xe2\x80\xa2\n\xe2\x80\xa2\n\xe2\x80\xa2\n\xe2\x80\xa2\n#bmw #bmwrepost #bmwm4 #bmwm4gts #f82 #bmwmrepost #bmwmsport #bmwmperformance #bmwmpower #bmwm4cs #austinyellow #davs0 #mpower_official #bmw_world_ua #bimmerworld #bmwfans #bmwfamily #bimmers #bmwpost #ultimatedrivingmachine #bmwgang #m3f80 #m5f90 #m4f82 #bmwmafia #bmwcrew #bmwlifestyle'
    INFO [2019-12-17 22:17:34] [username]  --> Image Liked!
    INFO [2019-12-17 22:17:37] [username]  --> Not commented
    INFO [2019-12-17 22:17:38] [username]  --> Not following
    

    По умолчанию InstaPy понравится первые девять лучших постов в дополнение к вашему значению amount. Таким образом, общее число лайков на тег увеличивается до четырнадцати (девять лучших постов плюс пять, указанных вами в amount).

    Обратите внимание, что InstaPy регистрирует каждое действие, которое он предпринимает. Как вы можете видеть выше, в нем упоминается, какая публикация ему понравилась, а также ссылка наа неё, описание, местоположение и то, прокомментировал ли бот публикацию или подписался на автора.

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

    Теперь вы, вероятно, не хотите, чтобы ваш бот обращал внимание на неподходящие посты. Чтобы этого не произошло, вы можете использовать set_dont_like ():

    from instapy import InstaPy
    
    session = InstaPy(username="<your_username>", password="<your_password>")
    session.login()
    session.like_by_tags(["bmw", "mercedes"], amount=5)
    session.set_dont_like(["naked", "nsfw"])
    

    С этим изменением посты, которые содержат в описаниях слова naked или nsfw, будут проигнорированы. Здесь можно написать любые другие слова, чтобы ваш бот их избегал.

    Далее, вы можете сказать боту, чтобы он не только обращал внимание на посты, но и следил за некоторыми авторами этих постов. Вы можете сделать это с помощью set_do_follow():

    from instapy import InstaPy
    
    session = InstaPy(username="<your_username>", password="<your_password>")
    session.login()
    session.like_by_tags(["bmw", "mercedes"], amount=5)
    session.set_dont_like(["naked", "nsfw"])
    session.set_do_follow(True, percentage=50)
    

    Если вы запустите скрипт сейчас, то бот будет следить за 50% пользователей, чьи посты ему понравились. Как обычно, каждое действие будет зарегистрировано.

    Вы также можете оставить некоторые комментарии к сообщениям. Есть две вещи, которые нужно сделать. Сначала включите комментирование с помощью set_do_comment():

    from instapy import InstaPy
    
    session = InstaPy(username="<your_username>", password="<your_password>")
    session.login()
    session.like_by_tags(["bmw", "mercedes"], amount=5)
    session.set_dont_like(["naked", "nsfw"])
    session.set_do_follow(True, percentage=50)
    session.set_do_comment(True, percentage=50)
    

    Затем скажите боту, какие комментарии оставить с помощью set_comments():

    from instapy import InstaPy
    
    session = InstaPy(username="<your_username>", password="<your_password>")
    session.login()
    session.like_by_tags(["bmw", "mercedes"], amount=5)
    session.set_dont_like(["naked", "nsfw"])
    session.set_do_follow(True, percentage=50)
    session.set_do_comment(True, percentage=50)
    session.set_comments(["Nice!", "Sweet!", "Beautiful :heart_eyes:"])
    

    Запустите скрипт, и бот оставит один из трех комментариев на половине сообщений, с которыми он взаимодействует.

    Теперь, когда вы закончили с основными настройками, рекомендуется завершить сеанс с помощью end():

    from instapy import InstaPy
    
    session = InstaPy(username="<your_username>", password="<your_password>")
    session.login()
    session.like_by_tags(["bmw", "mercedes"], amount=5)
    session.set_dont_like(["naked", "nsfw"])
    session.set_do_follow(True, percentage=50)
    session.set_do_comment(True, percentage=50)
    session.set_comments(["Nice!", "Sweet!", "Beautiful :heart_eyes:"])
    session.end()
    

    Браузер закроется, сохранив журналы. а так же подготовит полный отчет, который вы увидите в консоли.

    Дополнительные функции в InstaPy

    InstaPy — крупный проект, который имеет множество тщательно документированных функций. Хорошей новостью является то, что если вы чувствуете себя комфортно с функциями, которые использовали выше, то c остальные должны чувствовать себя примерно так же. В этом разделе будут описаны некоторые из наиболее полезных функций InstaPy.

    Контролер ограничений

    Вы не можете чистить Instagram весь день и каждый день. Служба быстро заметит, что работает бот и запретит некоторые из его действий. Вот почему было бы неплохо устанавливать граничения на некоторые действия. Возьмем, к примеру, следующее:

    session.set_quota_supervisor(enabled=True, peak_comments_daily=240, peak_comments_hourly=21)
    

    Бот будет выпонять комментирование до тех пор, пока не достигнет часового и дневного лимита. Он возобновит комментирование после истечения периода ограничения.

    Headless Browser

    Эта функция позволяет запускать вашего бота без графического интерфейса браузера, что очень полезно, если вы хотите развернуть своего бота на сервере, где вам нужен графический интерфейс или его просто нет. Кроме того, при этом требуется меньше ресурсов процессора, что повышает производительность. Для этого можно записать:

    session = InstaPy(username='test', password='test', headless_browser=True)
    

    Обратите внимание, что этот флаг устанавливается при инициализации объекта InstaPy.

    Использование искусственного интеллека при анализе постов

    Ранее вы видели, как игнорировать посты, содержащие неуместные слова в своих описаниях. Что если описание хорошее, а само изображение неуместное? Вы можете интегрировать своего бота InstaPy с ClarifAI, который предлагает услуги по распознаванию изображений и видео:

    session.set_use_clarifai(enabled=True, api_key='<your_api_key>')
    session.clarifai_check_img_for(['nsfw'])
    

    Теперь ваш бот не будет обращать внимание или комментировать изображения, которые ClarifAI считает NSFW. Вы получаете 5 000 бесплатных API-вызовов в месяц.

    Границы отношений

    Зачастую общение с сообщениями людей, у которых много подписчиков, является пустой тратой времени. В таких случаях рекомендуется установить некоторые границы отношений, чтобы ваш бот не тратил свои драгоценные вычислительные ресурсы:

    session.set_relationship_bounds(enabled=True, max_followers=8500)
    

    В этом случае ваш бот не будет взаимодействовать с сообщениями пользователей, у которых более 8 500 подписчиков.

    Чтобы узнать о многих других функциях и конфигурациях в InstaPy, ознакомьтесь с документацией.

    Заключение

    С минимальными усилиями автоматизировать свои действия в Instagram позволяет InstaPy. Это очень гибкий инструмент с множеством полезных функций.

    В этом уроке вы узнали:

    • Как работают боты в Instagram
    • Как автоматизировать браузер с помощью Selenium
    • Как использовать Шаблон объекта страницы, чтобы сделать ваш код более удобным и тестируемым
    • Как использовать InstaPy для создания базового бота в Instagram

    Read the InstaPy documentation and experiment with your bot a little bit. Soon you’ll start getting new followers and likes with a minimal amount of effort. I gained a few new followers myself while writing this tutorial. If you prefer video tutorials, there is also a Udemy course by the creator of InstaPy Tim Großmann.

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

    По мотивам How to Make an Instagram Bot With Python and InstaPy

    ]]>
    https://chel-center.ru/python-yfc/2020/04/12/kak-sdelat-bota-dlya-instagram/feed/ 0
    Как записать свой экрана с помощью Python https://chel-center.ru/python-yfc/2021/03/02/kak-zapisat-svoj-ekrana-s-pomoshhyu-python/ https://chel-center.ru/python-yfc/2021/03/02/kak-zapisat-svoj-ekrana-s-pomoshhyu-python/#respond Tue, 02 Mar 2021 06:17:10 +0000 http://chel-center.ru/python-yfc/?p=32258 Читать далее «Как записать свой экрана с помощью Python»

    ]]>
    Запись экрана позволяет создавать демонстрационные видеоролики, записывать игровые достижения и создавать видеоролики, которые можно публиковать в Интернете в социальных сетях. Однако существует множество промышленных программ, которые могут помочь вам в этом очень легко. В этом руководстве вы узнаете, как создать свой собственный простой экранный рекордер на Python, который вы можете расширить для своих нужд.

    Давайте начнем, сначала установим необходимые зависимости для этого урока:

    pip3 install numpy opencv-python pyautogui
    

    Процесс выглядит следующим образом:

    • Сделайте снимок экрана с помощью pyautogui.
    • Преобразуйте этот снимок экрана в массив numpy.
    • Запишите этот массив numpy в файл с правильным форматом с помощью средства записи видео в OpenCV.

    Импортируем необходимые модули:

    import cv2
    import numpy as np
    import pyautogui
    

    Давайте инициализируем формат, который мы будем использовать для записи нашего видеофайла (с именем «output.avi»):

    # разрешение экрана дисплея, получите его в настройках вашей ОС
    SCREEN_SIZE = (1920, 1080)
    # определяем кодек
    fourcc = cv2.VideoWriter_fourcc(*"XVID")
    # создаем объект записи видео
    out = cv2.VideoWriter("output.avi", fourcc, 20.0, (SCREEN_SIZE))
    

    Примечание: необходимо получить правильный SCREEN_SIZE из вашей операционной системы, то есть разрешение экрана, иначе запись в файл не будет работать (в качестве альтернативы вы можете использовать функцию pyautogui.size(), чтобы получить размер основного монитора).

    fourcc — это библиотека видеокодеков, которую OpenCV будет использовать для записи видеофайла, здесь мы указали XVID. Значение с плавающей запятой 20.0, переданное в качестве третьего параметра в cv2.VideoWriter, соответствует FPS (кадрам в секунду).

    Теперь нам нужно продолжать делать снимки экрана и записывать в файл в цикле, пока пользователь не нажмет кнопку q, вот основной цикл для этого:

    while True:
        # сделать скриншот
        img = pyautogui.screenshot()
        # преобразовываем эти пиксели в правильный массив numpy для работы с OpenCV
        frame = np.array(img)
        # конвертировать цвета из BGR в RGB
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        # пишем фрейм
        out.write(frame)
        # показать рамку
        cv2.imshow("screenshot", frame)
        # если пользователь нажимает q, он выходит
        if cv2.waitKey(1) == ord("q"):
            break
    
    # убедитесь, что все закрыто при выходе
    cv2.destroyAllWindows()
    out.release()
    

    Сначала мы используем функцию screenshot(), которая возвращает объект изображения, поэтому нам нужно преобразовать его в правильный массив numpy. После этого нам нужно преобразовать этот кадр в RGB, потому что OpenCV по умолчанию использует BGR.

    Как упоминалось в официальной документации pyautogui, вы также можете записывать только области экрана, путем передачи аргумента ключевого слова региона, который представляет собой кортеж из четырех целых чисел, представляющий верхнюю, левую, ширину и высоту области для захвата, вот как это делается:

    img = pyautogui.screenshot(region=(0, 0, 300, 400))
    

    После того, как вы закончите запись, просто нажмите q, это разрушит окно и завершит запись в файл, попробуйте!

    Кроме того, вы можете заменить оператор while True циклом for следующим образом:

    for i in range(200):
        # сделать скриншот
        img = pyautogui.screenshot()
        # остальной код ...
    

    С помощью этого кода ваш экран будет записываться в течение 10 секунд, потому что мы установили FPS на 20 (что имеет смысл, потому что 200 равно 20 умножить на 10).

    Хорошо, есть бесконечное количество идей, которые можно использовать для расширения этого. Например, вы можете объединить это с аудиорекордером, и вы получите инструмент Python, который одновременно записывает ваш экран и голос, нужно будет использовать поток, который записывает звук, а другой — для средства записи экрана, сообщите нам о своем прогрессе в комментариях ниже!

    Или вы можете создать сочетания клавиш, которые запускают, приостанавливают и останавливают запись, это руководство может вам помочь.

    По мотивам How to Make a Screen Recorder in Python

    ]]>
    https://chel-center.ru/python-yfc/2021/03/02/kak-zapisat-svoj-ekrana-s-pomoshhyu-python/feed/ 0
    Обработка изображений с использованием OpenCV в Python https://chel-center.ru/python-yfc/2021/05/23/obrabotka-izobrazhenij-s-ispolzovaniem-opencv-v-python/ https://chel-center.ru/python-yfc/2021/05/23/obrabotka-izobrazhenij-s-ispolzovaniem-opencv-v-python/#respond Sun, 23 May 2021 12:03:07 +0000 https://chel-center.ru/python-yfc/?p=33331 Читать далее «Обработка изображений с использованием OpenCV в Python»

    ]]>
    Узнайте методы выполнения преобразований плоского изображения, такие как перемещение изображения, отражение, поворот, масштабирование, обрезка и нарезка с помощью библиотеки OpenCV в Python.

    Введение   

    По существу, трансформация изображения — это его отображение из одной системы координат в другою, она сопоставляет некоторые координаты точки (x, y) в одной системе с точкой координатами (x', y') в другой системе координат.

    Например, если у нас есть точка с координатами (2, 3) в системе координате x-y и мы строим ту же точку в системе координате u-v, естественно, она будет выглядеть ​​по-разному, как и показано на рисунке ниже:


    Содержание

    Для чего неоходимо преобразовывать изображения?   

    На изображении ниже геометрическое соотношение между комиксом и изображением справа основано на преобразовании подобия (поворот, перемещение и масштабирование). Если нам нужно обучить модель машинного обучения, которая находит этот комикс, то нам нужно ввести изображение в другой форме и под другим углом.

    Методы преобразования изображений очень нам помогут на этапе предварительной обработки изображений в машинном обучении.

    Изображение может быть представлено как матрица. Каждое значение в матрице — это цвет пикселя в определенной координате. Преобразование изображения может быть выполнено с использованием матричного умножения. Математики разработали несколько видов матриц, которые можно использовать для выполнения определенных операций преобразования.

    Перенос изображений   

    Трансляция изображения или перенос — это прямолинейный сдвиг изображения из одного места в другое, поэтому сдвиг объекта называется переносом. Приведенная ниже матрица используется для переноса изображения:

    \begin{bmatrix}x' \\ y' \\ 1\end{bmatrix} = \begin{bmatrix}0 & 0 & B_x \\ 0 & 1 & B_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix}x \\ y \\ 1\end{bmatrix}

    Значение b_x определяет, насколько изображение будет перемещено по оси x, а значение b_y определяет перемещение изображения по оси y:

    Теперь, когда вы поняли перенос изображений, давайте взглянем на код Python. В OpenCV есть две встроенные функции для выполнения такого преобразований:

    • cv2.warpPerspective, которая принимает в качестве входных данных матрицу преобразования (3×3).
    • cv2.warpAffine принимает матрицу преобразования (2×3) в качестве входных данных.

    Обе функции принимают три входных параметра:

    • Входное изображение.
    • Матрица трансформации.
    • Кортеж высоты и ширины изображения.

    В этом уроке будем использовать функцию cv2.warpPerspective().

    Приведенный ниже код считывает входное изображение (если вам важна точность, получите изображение из этого урока здесь и поместите его в текущий рабочий каталог), переносит и показывает:

    import numpy as np
    import cv2
    import matplotlib.pyplot as plt
    
    # читать входное изображение
    img = cv2.imread("chelyabinsk.jpg")
    # преобразовать из BGR в RGB, чтобы можно было построить график с помощью matplotlib
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    # отключить оси x и y
    plt.axis('off')
    # показать изображение
    plt.imshow(img)
    plt.show()
    # получить форму изображения
    rows, cols, dim = img.shape
    # матрица преобразования для перевода
    M = np.float32([ [1, 0, 50],
                    [0, 1, 50],
                    [0, 0, 1] ])
    # применяем плоское преобразование к изображению
    translated_img = cv2.warpPerspective(img, M, (cols, rows))
    # отключить оси x и y
    plt.axis('off')
    # показать получившееся изображение
    plt.imshow(translated_img)
    plt.show()
    # сохраняем получившееся изображение на диск
    plt.imsave("chelyabinsk_translated.jpg", translated_img)
    

    Обратите внимание, что мы используем plt.axis('off'), поскольку мы не хотим выводить значения оси, и мы показываем изображение с помощью функции imshow() из matplotlib.

    Также использована функцию plt.imsave() для локального сохранения изображения.

    Исходное изображение:

    Результат переноса:

    Масштабирование изображения   

    Масштабирование изображения — это процесс, используемый для изменения размера цифрового изображения. OpenCV имеет встроенную функцию cv2.resize(), но мы будем выполнять преобразование, используя умножение матриц, как и раньше. Матрица, используемая для масштабирования, показана ниже.

    \begin{bmatrix}x' \\ y' \\ 1\end{bmatrix} = \begin{bmatrix}S_x & 0 & 0 \\ 0 & S_y & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix}x \\ y \\ 1\end{bmatrix}

    S_x и S_y — коэффициенты масштабирования для оси x и оси y соответственно.

    Приведенный ниже код отвечает за чтение того же изображения, определение матрицы преобразования для масштабирования и показывает получившееся изображение:

    import numpy as np
    import cv2
    import matplotlib.pyplot as plt
    
    # читать входное изображение
    img = cv2.imread("chelyabinsk.jpg")
    # преобразовать из BGR в RGB, чтобы можно было построить график с помощью matplotlib
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    # отключить оси x и y
    plt.axis('off')
    # показать изображение
    plt.imshow(img)
    plt.show()
    # получить форму изображения
    rows, cols, dim = img.shape
    # матрица преобразования для масштабирования
    M = np.float32([ [1.5, 0  , 0],
                	[0,   1.8, 0],
                	[0,   0,   1] ])
    # применяем плоское преобразование к изображению
    scaled_img = cv2.warpPerspective(img,M,(cols*2,rows*2))
    # отключить оси x и y
    plt.axis('off')
    # показать получившееся изображение
    plt.imshow(scaled_img)
    plt.show()
    # сохраняем получившееся изображение на диск
    plt.imsave("chelyabinsk_scaled.jpg", scaled_img)
    

    Вот результат:

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

    Сдвиг изображения

    Карта сдвига — это линейная карта, которая смещает каждую точку в фиксированном направлении, она заменяет каждую точку по горизонтали или вертикали определенным значением, пропорциональным ее координатам x или y, есть два типа эффектов сдвига.

    Сдвиг в направление оси x   

    Когда сдвиг выполняется в направлении оси x, границы изображения, параллельные оси x, сохраняют свое положение, а края, параллельные оси y, меняют свое положение в зависимости от коэффициента сдвига:

    Сдвиг в направление оси Y   

    Когда сдвиг выполняется в направлении оси y, границы изображения, параллельные оси y, сохраняют свое положение, а края, параллельные оси x, меняют свое положение в зависимости от коэффициента сдвига.

    Матрица для сдвига показана на рисунке ниже:

    \begin{bmatrix}x' \\ y' \\ 1\end{bmatrix} = \begin{bmatrix}sh_x & 0 & 0 \\ 0 & sh_y & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix}x \\ y \\ 1\end{bmatrix}

    Ниже приведен код, отвечающий за сдвиг:

    import numpy as np
    import cv2
    import matplotlib.pyplot as plt
    
    # читать входное изображение
    img = cv2.imread("chelyabinsk.jpg")
    # конвертировать из BGR в RGB, чтобы мы могли построить график с помощью matplotlib
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    # отключить оси x и y
    plt.axis('off')
    # показать изображение
    plt.imshow(img)
    plt.show()
    # получить форму изображения
    rows, cols, dim = img.shape
    # матрицы преобразования для сдвига
    # сдвиг, примененный к оси x
    M = np.float32([ [1, 0.5, 0],
                 	[0, 1  , 0],
                	[0, 0  , 1] ])
    # сдвиг, примененный к оси Y
    # M = np.float32([ [1,   0, 0],
    #             	  [0.5, 1, 0],
    #             	  [0,   0, 1] ])
    # применяем плоское преобразование к изображению
    sheared_img = cv2.warpPerspective(img,M,(int(cols*1.5),int(rows*1.5)))
    # отключить оси x и y
    plt.axis('off')
    # show the resulting image
    plt.imshow(sheared_img)
    plt.show()
    # сохраняем получившееся изображение на диск
    plt.imsave("chelyabinsk_sheared.jpg", sheared_img)
    

    Первая матрица — это сдвиг, примененный к оси x, если вы хотите ось y, то закомментируйте первую матрицу и раскомментируйте вторую.

    Изображение, сдвинутое по оси x:

    Изображение, сдвинутое по оси y:

    Отражение изображения   

    Отражение изображения (или зеркальное отображение) переворачивает изображение как по вертикали, так и по горизонтали, это частный случай масштабирования. Для отражения по оси x мы устанавливаем значение S_y равным -1, а S_x равным 1 и наоборот для отражения по оси y.

    Матричные преобразования для отражения показаны ниже:

    \begin{bmatrix}x' \\ y' \\ 1\end{bmatrix} = \begin{bmatrix}1 & 0 & 0 \\ 0 & -1 & rows \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix}x \\ y \\ 1\end{bmatrix}

    \begin{bmatrix}x' \\ y' \\ 1\end{bmatrix} = \begin{bmatrix}-1 & 0 & cols \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix}x \\ y \\ 1\end{bmatrix}

    Вот код Python для размышлений:

    import numpy as np
    import cv2
    import matplotlib.pyplot as plt
    
    # read the input image
    img = cv2.imread("chelyabinsk.jpg")
    # convert from BGR to RGB so we can plot using matplotlib
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    # disable x & y axis
    plt.axis('off')
    # show the image
    plt.imshow(img)
    plt.show()
    # get the image shape
    rows, cols, dim = img.shape
    # transformation matrix for x-axis reflection 
    M = np.float32([ [1,  0, 0   ],
                    [0, -1, rows],
                    [0,  0, 1   ] ])
    # transformation matrix for y-axis reflection
    # M = np.float32([ [-1, 0, cols],
    #                 [ 0, 1, 0   ],
    #                 [ 0, 0, 1   ] ])
    # apply a perspective transformation to the image
    reflected_img = cv2.warpPerspective(img,M,(int(cols),int(rows)))
    # disable x & y axis
    plt.axis('off')
    # show the resulting image
    plt.imshow(reflected_img)
    plt.show()
    # save the resulting image to disk
    plt.imsave("chelyabinsk_reflected.jpg", reflected_img)
    

    Как и раньше, сначала будет переворот относительно ось x (вертикальное отображение). Если хотите, раскомментируйте вторую матрицу и закомментируйте первую, получите переворот относительно оси y (вертикальное отображение).

    Отраженное изображение по оси x:

    А это отраженное изображение по оси y:

    Поворот изображения   

    Вращение — это понятие в математике, которое представляет собой движение определенного пространства, которое сохраняет хотя бы одну точку. Поворот изображения — это обычная процедура обработки изображений с приложениями для сопоставления, выравнивания и других алгоритмов на основе изображений, а также для увеличения данных, особенно когда речь идет о классификации изображений.

    Матрица преобразования вращения показана на рисунке ниже, где \theta — угол поворота:

    \begin{bmatrix}x' \\ y' \\ 1\end{bmatrix} = \begin{bmatrix}\cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0\\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix}x \\ y \\ 1\end{bmatrix}

    Ниже приведен код Python для поворота изображения:

    import numpy as np
    import cv2
    import matplotlib.pyplot as plt
    
    # читать входное изображение
    img = cv2.imread("chelyabinsk.jpg")
    # преобразовать из BGR в RGB, чтобы можно было построить график с помощью matplotlib
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    # отключить оси x и y
    plt.axis('off')
    # показать изображение
    plt.imshow(img)
    plt.show()
    # получить форму изображения
    rows, cols, dim = img.shape
    # угол от градуса до радиана
    angle = np.radians(10)
    # матрица преобразования для вращения
    M = np.float32([ [np.cos(angle), -(np.sin(angle)), 0],
                	[np.sin(angle), np.cos(angle), 0],
                	[0, 0, 1] ])
    # применяем плоское преобразование к изображению
    rotated_img = cv2.warpPerspective(img, M, (int(cols),int(rows)))
    # отключить оси x и y
    plt.axis('off')
    # показать получившееся изображение
    plt.imshow(rotated_img)
    plt.show()
    # сохраняем получившееся изображение на диск
    plt.imsave("chelyabinsk_rotated.jpg", rotated_img)
    

    Выходное изображение:

    Он был повернут на 10° (np.radians(10)), вы можете редактировать его по своему усмотрению!

    Обрезка изображения   

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

    import numpy as np
    import cv2
    import matplotlib.pyplot as plt
    
    # читать входное изображение
    img = cv2.imread("chelyabinsk.jpg")
    # преобразовать из BGR в RGB, чтобы можно было построить график с помощью matplotlib
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    # отключить оси x и y
    plt.axis('off')
    # показать изображение
    plt.imshow(img)
    plt.show()
    # получаем 200 пикселей от 100 до 300 по оси x и оси y
    # измените это, если хотите, просто убедитесь, что вы не превышаете столбцы и строки
    cropped_img = img[100:300, 100:300]
    # отключить оси x и y
    plt.axis('off')
    # показать получившееся изображение
    plt.imshow(cropped_img)
    plt.show()
    # сохраняем получившееся изображение на диск
    plt.imsave("chelyabinsk_cropped.jpg", cropped_img)
    

    Поскольку OpenCV загружает изображение в виде массива numpy, то можно обрезать изображение, просто указав нужные индексы нарезки, в нашем случае мы решили получить изображение размером 200 х 200 пикселей, вырезанное из исходного с 100 по 300 пиксель по обеим осям. Вот результат:

    Заключение   

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

    Вы можете получить все коды здесь.

    По материалам Image Transformations using OpenCV in Python

    ]]>
    https://chel-center.ru/python-yfc/2021/05/23/obrabotka-izobrazhenij-s-ispolzovaniem-opencv-v-python/feed/ 0