Oracle фактически ликвидирует Sun |
Избегайте этой ловушки, не следует придавать антропоморфные черты Ларри Эллисону.
Брайэн Кантрилл
Похоже, что в Oracle приняли решение окончательно избавиться от трудовых ресурсов, составляющих костяк Sun Microsystems. Массовые увольнения затронули около 2500 сотрудников, работающих над операционной системой Solaris, платформой SPARC и системами хранения данных ZFS Storage Appliance.
Это не рядовая трансформация — оптимизация, а настоящая бойня. По мнению создателя системы динамической отладки Dtrace
Брайэна Кантрилла (Bryan Cantrill) на сей раз нанесен непоправимый ущерб, в результате потери 90% производственных кадров подразделения Solaris, включая все руководство.
В 2009 г. Oracle приобрел испытывающую серьезнейшие трудности на рынке Sun Microsystems за 5.6 млрд. долларов США. Компания теряла позиции на рынке вследствие лавинообразного распространения Linux в качестве серверной ОС, успеха платформы amd64 и невнятной стратегии по взаимоотношениям с сообществом открытого ПО. Solaris стал открытым слишком поздно — лишь в 2005 г., причем открытым не полностью, отдельные элементы ОС, такие как локализация и некоторые драйвера, оставались проприетарными. Затем появился OpenSolaris, однако точкой сбора сообщества он не сумел стать. То ли дело была в лицензии CDDL, то ли проблема была в том, что Sun пыталась манипулировать проектом. Трудно сказать почему именно, но не взлетел.
Kicked butt, had fun, didn't cheat, loved our customers, changed computing forever. Scott McNealy
Эпитафия Скота МакНили как нельзя лучше отражает жизненный путь компании — отлично развлекались, не дурили головы своим заказчикам и навсегда изменили ИТ. Довольно быстро стало очевидно, что Solaris Ораклу попросту не нужен, и развивать его он не намерен. Затем 13 августе 2010 г. случилось одно из самых позорных событий в истории открытого ПО — компания втихую закрыла исходный код OS Solaris. Никаких официальных заявлений на сей счет не последовало.
We will distribute updates to approved CDDL or other open source-licensed code following full releases of our enterprise Solaris operating system. In this manner, new technology innovations will show up in our releases before anywhere else. We will no longer distribute source code for the entirety of the Solaris operating system in real-time while it is developed, on a nightly basis.
Это всего лишь отрывок из внутреннего циркуляра для сотрудников компании, который естественно сразу же просочился в прессу. Тут речь идет о том, что исходный код будут выкладывать только во время релиза новой версии ОС, а обновления будут только бинарными. Но это оказалось неправдой, после выхода Solaris 11 исходный код не выложили.
Для тех, кто владеет английским очень рекомендую посмотреть выступление Брайэна Кантрилла на конференции Usenix. Эпиграф к статье — одна из его цитат, вот еще несколько.
О принципах руководства компании. Оставшись без мудрого руководства манагеров инженеры выдали гору инноваций: ZFS, DTrace, Zones и много других.
Sun управлялась со стороны враждующих группировок во главе с атаманами, как Сомали.
О закрытии исходного кода OpenSolaris.
Это ОТВРАТИТЕЛЬНАЯ выходка со стороны корпорации. Вот из-за такого поведения мы становимся циничными и подозрительными.
О последствиях закрытия исходников OpenSolaris для нового проекта ОС illumos — полностью открытого форка OpenSolaris.
Мы готовимся к моменту Судного Дня в лицензиях открытого исходного кода, у нас есть такие сценарии, они работают и это здорово.
Вскоре после этого из Oracle ушли все разработчики DTrace, создатели ZFS, команда zones и сетевики. Вся разработка и инновация на этих направлениях далее происходила операционной системе illumos, где осела диаспора программистов из Sun Solaris. Благодаря особенностям открытых лицензий, в том числе CDDL, в рамках которой шла разработка OpenSolaris, Oracle не может претендовать на все последующие улучшения в коде illumos. То есть может, но только в рамках своего же проекта с открытым кодом. Сценарии Судного Дня работают как надо.
Для полноты картины стоит добавить, что Oracle активно участвует в разработке ядра Linux, где традиционно входит в десятку наиболее активных компаний.
NetBeans
Apache Foundation, верное решение.Sun Microsystems еще недавно — живая легенда и лучшее, что когда-либо было в Unix. Вот лишь небольшая часть их наследия.
Компания Oracle имела все возможности для того, чтобы развивать и поддерживать OpenSolaris, но вместо этого закрыла исходники и с тех пор Solaris уже не имел будущего. Когда тяжба с компанией Google за использования Java в мобильной ОС Андроид закончилась пшиком в Oracle потеряли к активам Sun Microsystems всякий интерес. Вместо этого компания будет продавать ПО на основе собственной операционной системы — Unbreakable Linux.
Метки: author temujin управление сообществом управление разработкой управление проектами oracle sun microsystems solaris opensolaris illumos |
Blockchain Life 2017: от золотой пиццы к криптовалютной лихорадке |
Метки: author megapost финансы в it биткоины |
Blockchain Life 2017: от золотой пиццы к криптовалютной лихорадке |
Метки: author megapost финансы в it биткоины |
Apache Ignite 2.1 — теперь со вкусом Persistence |
Метки: author artemshitov программирование java big data блог компании gridgain apache ignite data grid compute grid highload gridgain |
Apache Ignite 2.1 — теперь со вкусом Persistence |
Метки: author artemshitov программирование java big data блог компании gridgain apache ignite data grid compute grid highload gridgain |
Эзотерический язык, транслирующийся в шаблоны C++ |
constexpr
, а мне хотелось остаться в области извращённых развлечений и использовать минимум возможностей языка. Замечательные статьи Жизнь во время компиляции от HurrTheDurr и Интерпретация во время компиляции, или Альтернативное понимание лямбд в C++11 от ilammy (вторая — более хардкорная и без using template
с constexpr
) описывали практически всё, что извращённому уму нужно было знать. В итоге я оставил статью в черновиках, но не оставил желания метапрограммировать. И сегодня я возвращаюсь с заново написанным текстом и новыми идеями.std::vector
принимает метазначение T
и возвращает метазначение std::vector
. Важно, что у метазначения есть набор метаполей (с точки зрения C++ — вложенные классы, псевдонимы типов, статические константные поля), с помощью которых можно сделать самое интересное.value
, и понимать его как значение, которое возвращает метафункция. Метафункции над целыми числами запишутся довольно просто:template
struct square {
static const int value = x * x;
};
square<5>::value
.struct One;
struct Zero;
one
и zero
только объявлены и практически бесполезны. Достаточно ли этого? Да. Чему же тогда они равны и как их использовать? Равны они, что важно, только самим себе. Это своего рода абстрактные символы, которые можно задействовать в символьных вычислениях (почти как в Mathematica и др.). Метапрограмма будет вычислять значения метавыражений различной сложности. Рассмотрим сначала эти выражения, а несколько позже займёмся интерпретацией символов и выводом результатов на экран.template
struct Not {
typedef Zero value;
};
template <>
struct Not {
typedef One value;
};
Not
принимает некоторое значение и возвращает единицу, если это значение — ноль. Во всех остальных случаях она возвратит ноль. Таким образом, за счёт специализации шаблонов, мы имеем паттерн-матчинг (сравнение с образцом) в зачаточной стадии: можем описывать отдельно поведение функции для одного или нескольких аргументов, имеющих конкретные значения, а компилятор C++, отметив соответствие параметров шаблона одному из образцов, подставит нужную специализацию. Пользуясь этим, мы могли бы уже написать что-то рекурсивное (например факториал, разделив описание на fac<0>
и fac<всё остальное>
).value
, можно представить себе, как выглядят многозначные функции. Запишем конструктор списка Cons
и пустой список Nil
, хорошо знакомые знатокам функционального программирования:template
struct Cons {
typedef h head;
typedef t tail;
};
struct Nil;
Cons
в ФП — функция, конструирующая список из первого элемента (головы) и списка остальных элементов (хвост). Обычному списку будет соответствовать Cons>>
. Поскольку для работы со списком нужно уметь получать его составные части, мы сделаем Cons
многозначной функцией, возвращающей голову (Cons<...,...>::head
), и хвост (Cons<...,...>::tail
). Любители ООП могут представить, что Cons
— это конструктор, а head
и tail
— геттеры. В ФП все присваивания заменяются на // отрицание непустого списка
template
struct negate {
private: // инкапсулируем промежуточные вычисления
typedef typename list::head h; // голова
typedef typename Not::value not_head; // отрицание головы
typedef typename list::tail t; // хвост
typedef typename negate::value not_tail; // отрицание хвоста
public:
// список из отрицаний элементов - это отрицание головы,
// соединённое с хвостом отрицаний
typedef Cons value;
};
// отрицание пустого списка
template <>
struct negate {
// пустой список - сам себе отрицание
typedef Nil value;
};
f (x+y) (g y) z
.-- список - либо слитые голова и хвост (причём хвост - сам список),
-- либо пустота
data List a = Cons a List | Nil
-- хелперы для получения головы и хвоста
head (Cons x _) = x -- возвращает голову
tail (Cons _ xs) = xs -- возвращает хвост
-- отрицание списка
negate (Cons x xs) = Cons (Not x) (negate xs)
negate Nil = Nil
negate::value
, конечно, не сработает, но если One
будет иметь метаполя head
и tail
, он вполне подойдёт. Впрочем, пока negate
не «разыменовали» с помощью ::value
, программа продолжает -- Преобразованная голова соединяется с преобразованным хвостом
map f (Cons x xs) = Cons (f x) (map f xs)
-- Пустой список преобразовывать не нужно
map f Nil = Nil
std::transform
. В нашем же языке метафункция map
объявляется с помощью параметризации шаблона шаблоном:// преобразование непустого списка
// f - шаблон с одним аргументом - унарная метафункция
template class f, typename list>
struct map {
private:
typedef typename list::head h; // голова
typedef typename f::value new_head; // преобразованная голова
typedef typename list::tail t; // хвост
typedef typename map::value new_tail; // преобразованный хвост
public:
// Преобразованная голова соединяется с преобразованным хвостом
typedef Cons value;
};
// преобразование пустого списка
template class f>
struct map {
// пустой список преобразовывать не нужно
typedef Nil value;
};
f
сюда можем подставить описанную ранее функцию Not
и посчитать список отрицаний:typedef map>>>::value list;
// list эквивалентно Cons>>
typedef
— это некий эквивалент оператора присваивания. Или, что звучит более корректно для функциональных языков, — операция задания соответствии имени и выражения.using x = Not::value;
template
using g = Cons;
value
, когда метафункция возвращает одно метазначение. Это может быть более удобным, если программисту лень явно указывать ::value
, а также накладывать требование вычислимости.negate
компилируется, а negate::value
— нет. Пользуясь громоздкими метаполями можно написать метафункцию ветвления, которая вычисляет ::value
только для одной своей ветки в зависимости от условия и возвращает это значение. Получится, что одно из выражений никогда не вычислится: все операции выполняются только при получении ::value
, а его никто не трогал:// рассмотрим только одну из специализаций
template
struct If {
// expr1, expr2 вычислены, а expr1::value и expr2::value - нет
typedef typename expr1::value value;
}
// If, destroy>::value не разрушит мир
g
имеет смысл уже вычисленного f::value
. И если ветвиться между двумя рекурсивными вариантами, вычисления будут бесконечными — аналог переполнения стека на этапе компиляции.template
struct If {
// expr1, expr2 вычислены
typedef expr1 value;
};
// If::value>::value разрушит мир
f x
возвращает унарную функцию g
, неявно использующую аргумент x
функции f
.f x = g
where g xs = Cons x xs
-- использование: (f x) xs или, что полезнее, map (f x) list
Cons
, но g
как унарную можно передать в уже написанную функцию map
, а как бинарную — Cons
нет!this
которого содержит весь нужный контекст, а operator ()
принимает требуемые интерфейсом аргументы. Красивая концепция из ФП заменяется эквивалентной по мощности концепцией ООП. Даже стандартный шаблон std::function
предусмотрен!// до C++11: функциональный объект, захвативший переменную
struct f {
f(something& x) : x(x) {}
something_else operator () (something_else& xs) { // xs - явный аргумент
return Cons(x, xs);
}
private:
something x; // неявный аргумент
};
// C++11 и выше: лямбда-функция, захватившая переменную
something x;
auto f = [x](something_else& xs) { return Cons(x, xs); };
int(int)
) на функциональные объекты (например, std::function
) программист C++ получает полноценный аналог замыканий.template
struct f {
template
struct g {
typedef Cons value;
};
};
// использование: f::g::value или map::g, list>
f
«вылезает» символ g
, но в остальном — честное замыкание. Выражение f::g
может быть использовано вместо обычной унарной функции (например, Not
).Cons
и Nil
. Кроме того, это может оказаться даже более простым ходом. Чтобы передать в метафункцию два списка, достаточно… передать два списка: f
, для вариадических же шаблонов требуется задавать обёртку, захватывающую все списки, кроме последнего: f, y1, y2, y3>
, так как список аргументов произвольной длины должен быть один, и несколько списков, записанные подряд, просто «слипнутся». По завершении вычислений приходится возвращать обёртку, т.к. в C++ нельзя затайпдефить список из нескольких типов. А чтобы «выковырить» список из обёртки и передать в метафункцию (скажем, f
), придётся использовать метаколлбеки: реализовать у обёртки метаметод принимающий эту метафункцию f
и передающий ей содержимое списка:typename
struct list_wrapper {
// передаём в list_wrapper<...>::call функцию,
// которой передадут наши аргументы xs
struct call class f> {
typedef typename f::value value;
};
};
negate
нужно сконструировать вспомогательную унарную метафункцию f
, которая принимает результат рекурсивного применения negate
к хвосту списка, вычисляет отрицание головы и возвращает обёртку для собранного вместе списка:typename
struct negate {
template
struct f {
typedef list_wrapper::value, ys> value;
};
typedef typename negate::template call::value;
};
typename <>
struct negate<> {
typedef list_wrapper<> value;
};
negate
выглядит более громоздко даже по сравнению с версией для Cons
/Nil
. Здесь требуются и более серьёзные измышления, когда как для написания «обычной» версии достаточно основ ФП и механической замены Haskell -> C++. Поэтому с помощью вариадических шаблонов лучше написать обёртку для преобразования последовательности параметров в список Cons
/Nil
, а затем при реализации программы пользоваться уже им. Так мы сможем и задавать списки приятным перечислением через запятую, и мыслить более простыми категориями.void One::print();
), либо — внутри метафункции, если возможностей паттерн-матчинга хватает для рассмотрения всех вариантов. Вот, например, метафункция print
, распечатывающая свой аргумент (единицу, ноль или список) на этапе конструирования экземпляра print<...>
:template
struct print {
print () {
std::cout << "unknown number" << std::endl;
}
};
template <>
struct print {
print () {
std::cout << "1" << std::endl;
}
};
template <>
struct print {
print () {
std::cout << "0" << std::endl;
}
};
// print::value>() выведет "1"
One
и Zero
и построить сложение, умножение,…decltype
никак нельзя было провести чисто функциональные вычисления над типами, создать переменные и объекты C++ соответствующих типов, провести вычисления над ними, а потом снова вернуться к вычислениям над типами.sum, three> // тип - корректно
sum, three>() // значение - компилируется
do_something(sum(), three()) // значения - компилируется
sum(), three()> // нельзя посчитать,
// т.к. нет перехода от значений к типам
sum()), decltype(three())> // C++11; компилируется
decltype
типы были аналогией чистоты, а значения — сущностями императивного мира: параметром шаблона мог быть только тип, а тип мог быть получен только преобразованиями типа; один раз создав значение, нельзя было вернуться обратно к типам.decltype
. Впрочем, decltype
свой аргумент не исполняет, а лишь отвечает на вопрос «какой бы был тип выражения, если бы мы его начали считать», отчего не нарушает функциональной чистоты. Поэтому, пока выражение над переменными не покидает скобки decltype
, чистота сохраняется. С точки зрения языка шаблонов, decltype
выгодно использовать вместе с перегрузкой операторов. В следующем примере выражения эквивалентны, но нижнее выглядит менее громоздко:typedef sum, three> x;
typedef decltype(one() + two() + three()) x;
auto v = one() + two() + three(); // v выполнится как императивная конструкция
typedef decltype(v) x;
::value
, скобочки и typename
сильно выматывают программиста и растягивают код программы. Конечно, логика говорит, что решивший программировать в таком стиле должен страдать, но… это одно из тех извращённых развлечений, к которым зачастую склоняется мозг айтишника. Хочется попробовать сделать так, чтобы извращённость сохранилась, но в той степени, когда страдания ещё можно терпеть.One;
Zero;
Not (val x) = Zero; // общий случай
Not (Zero) = One; // частный случай
And(val x, val y); // для x,y != One,Zero And не определена
And(val x, One) = x; // One - константа, val x - аргумент
And(val x, Zero) = Zero;
// использование: Not(One) или And(One, Zero)
typename
), другой шаблон (template class
) и т.д., и это следует указывать. А значит, создавая метаФВП, одним typename
не обойтись и требование указания типа переходит также в мой язык. По умолчанию задан тип val
, соответствующий обычному метазначению (структура или обычный тип в C++). Для описания функций можно комбенировать типы с помощью стрелки (->
). Например, val -> val
— унарная функция (шаблон, принимающий один параметр), (val, val) -> val
— бинарная функция (шаблон, принимающий два параметра) и так далее.#type
можно задавать синонимы, чтобы ещё чуть-чуть сократить запись и прояснить смысл за счёт уместного именования:#type number = val;
#type list = val;
#type unary = number -> number;
#type map_t = (unary, list) -> list;
// map_t раскроется в template class, typename> class
#type
как аналог #define
в C, задающий текстовые преобразования, которые нехитрым способом можно провести и вручную.Nil;
Cons(val x, val y) {
head = x;
tail = y;
}
::value
. То есть f(x)
эквивалентно f::value
. Но у Cons
сгенерируется только два метаполя head
и tail
. Предотвращать раскрытие ::value
требуется явно с помощью апострофа: Cons(x, xs)'
.::value
как одна из частых конструкций должна была раскрываться автоматически, но должна была быть возможность (а) передать нераскрытое значение (см. проблему функции ветвления и бесконечной рекурсии выше под спойлером) и (б) использовать другие метаполя, кроме value
. В итоге я остановился на «экранировании», записи метаполей через точку и ввёл обратный для экранирования оператор "!", раскрывающий ::value
:Cons(x, xs)'.head; // x
Cons(x, xs)'.tail; // xs
Not(Zero); // One
Not(Zero)'!; // One
::value
может вполне сосуществовать со множественным возвратом. Реализуя отрицание списка negate
на C++, мы создавали локальные метапеременные, которые вполне можно было возвратить. Здесь — аналогично, только все значения публичны:// отрицание непустого списка:
// список из отрицаний элементов - это отрицание головы,
// соединённое с хвостом отрицаний
negate (list list) = Cons(not_head, not_tail)' {
h = list.head; // голова
not_head = Not(h); // отрицание головы
t = list.tail; // хвост
not_tail = negate(t); // отрицание хвоста
} // точку с запятой можно опустить!
// пустой список - сам себе отрицание
negate (Nil) = Nil;
// преобразование непустого списка
map (unary f, list list) = Cons(new_head, new_tail)' {
h = list.head; // голова
new_head = f(h); // преобразованная голова
t = list.tail; // хвост
new_tail = map(f, t); // преобразованный хвост
}
// преобразование пустого списка
map (unary f, Nil) = Nil;
unary
и list
можно было сразу указать val -> val
и val
соответственно, но запись оказалась бы более длинной и менее наглядной.f(val x) = {
g(val xs) = Cons(x, xs)';
}
// использование: f(x)'.g(list) или map(f(x)'.g, list)
value
и возвращать value
. Одноимённый с классом член в C++ уже отдан конструктору, из-за чего задание ему нового смысла через typedef
приводит к ошибке компиляции:template
struct f {
template
struct value { // value!
typedef Cons value; // value!
};
};
f (val x) = g(Not(x)) {
g (val x) = x; // будет заменено на g (x1) = x1
// т.к. x станет параметром template f и не допустит x как параметр template g
}
::value
, нигде не сказано, что f(x)
— это f::value
, а не call::result
. Это знание хранилось внутри транслятора, и его использование в программе прорывало бы абстракцию:f(val x) = One;
f(Zero) = Zero;
main {
print::value>(); // почему так?
}
impure { код }
может появляться вместо объявления значения/функции, а также справа после знака "=" при объявлении функции. В этих случаях код на C++ вставляется в соответствующее место программы. Для экспортирования выражения перед ним ставится ключевое слово impure
. С точки зрения C++ это эквивалентно конструированию объекта типа, описываемого выражением.impure
приготовим «распечатыватель» списка:print(val list) {
head = list.head;
tail = list.tail;
impure {
// после typedef-ов head, tail расположится следующий блок кода:
print() {
impure print(head); // преобразуется в "print();"
std::cout << ", ";
impure print(tail);
}
}
}
print(Zero) = impure { // аналогично print(Zero) { impure { ..., но короче
print() {
std::cout << "0";
}
}
print(One) = impure {
print() {
std::cout << "1";
}
}
print(Nil) = impure {
print() {
std::cout << std::endl;
}
}
namespace
и using namespace
:namespace ns1 {
namespace ns2 {
f(val x) = x;
}
}
namespace ns3 {
using namespace ns1.ns2;
g(val x) = f(x);
}
using namespace
также является вынужденной мерой. В С++ в некоторых случаях недостаточно записи вида f::y
. Если f
, x
или z
— не конкретные типы/шаблоны, а параметры шаблона, то ::y
становится выходом чёрного ящика. Нужно указывать, что мы получаем при вычислении ::y
— тип или шаблон (например, typename f::y
или f::template y
). Я не автоматизировал эти указания и не реализовал синтаксический сахар для более простого описания вручную, поэтому каждое использование точки вызывает появление «typename». Т.е. f::y
оттранслируется во что-то некорректное вида typename typename f::value::typename y::value
. В случае пространства имён это излишне, using namespace
позволяет обойтись без вставки «typename».f (val x) = y(x) { y(x) = g(x); } // было
f = lambda (val x) -> y(x) { y(x) = g(x); } // стало
template struct f {}; // пусть была метафункция f
using g = f; // нельзя описать эквивалентность шаблона шаблону
// нужно описать эквивалентность выражений, соответствующих типам
template using g = f; // g - тип, f - тип
map(lambda(val x) -> y, xs)
придётся модифицировать язык и генерировать шаблоны с временными именами.value::value
— это конструктор, поэтому не получается напрямую реализовать лямбду, которая возвращает лямбду. Для возврата функций из лямбд и разрешения записей вида g = f
нужно использовать другие концепции и почти полностью переписать транслятор.impure { private: }
в код или элементарной модификацией языка.fMap(fCurry(fCons).fApply(vXs), vObj.vYs)
— при доступе к метаполю подставится template
, typename
или ничего, если первым символом имени будет «f», «v» или «n» соответственно).// объявляем значение x
struct x {
typedef internal::value type; // тип значения x
};
// объявляем функцию f(x) = x
struct f {
typedef internal::function type; // тип функции f
template
struct value {
typedef typename x::type type; // тип результата функции f
struct result { // обёртка для того, чтобы избежать value::value
typedef x value;
};
};
};
internal
со списком типов и метафункциями для работы с ними; за счёт дополнительной обёртки struct result
можно будет избавиться от value::value
и возвращать безымянные функции из функций. Также из-за того, что любое значение или функция будет выражаться в виде структуры (то есть просто типа, а не типа или шаблона), можно будет задавать синонимы функций (с помощью typedef
), отменить обязательное описание типов аргументов функции, устранить ограничения на лямбды и возврат функций из ФВП.value
использовать два значения так, чтобы при увеличении глубины вложенности использовалось другое значение. Получится, что вместо value::value
будет value1::value2
и никаких попыток переопределения конструктора не будет:template
struct f {
typedef internal::True uses_value1; // использует value1
template
struct value1 {
typedef internal::False uses_value1; // использует value2
typedef Cons value2;
};
};
uses_value1
будет выбирать, доступаться до value1
или до value2
.val
дополнительных типов для чисел (int
, uint
, ...) и массивов ([val]
, [int]
, [uint]
, ...). При использовании метаполей придётся решить описанную выше проблему с указанием template
и typename
, т.к. для чисел ничего не требуется указывать, а для типов и шаблонов — требуется.main
:// строим списки:
my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
negated_list = negate(my_list);
negated_list2 = map(Not, my_list);
impure {
int main() {
// печатаем списки:
std::cout << "my list is ";
impure print(my_list);
std::cout << "negated list is ";
impure print(negated_list);
std::cout << "negated list is also ";
impure print(negated_list2);
}
}
my list is 1, 1, 0,
negated list is 0, 0, 1,
negated list is also 0, 0, 1,
impure {
#include
}
One;
Zero;
Not (val x) = Zero; // общий случай
Not (Zero) = One; // частный случай
And(val x, val y); // для x,y != One,Zero And не определена
And(val x, One) = x; // One - константа, val x - аргумент
And(val x, Zero) = Zero;
#type number = val;
#type list = val;
#type unary = number -> number;
#type map_t = (unary, list) -> list;
Nil;
Cons(val x, val y) {
head = x;
tail = y;
}
// отрицание непустого списка:
negate (list list) = Cons(not_head, not_tail)' {
h = list.head;
not_head = Not(h);
t = list.tail;
not_tail = negate(t);
}
// пустой список - сам себе отрицание
negate (Nil) = Nil;
// преобразование непустого списка
map (unary f, list list) = Cons(new_head, new_tail)' {
h = list.head;
new_head = f(h);
t = list.tail;
new_tail = map(f, t);
}
// преобразование пустого списка
map (unary f, Nil) = Nil;
print(val list) {
head = list.head;
tail = list.tail;
impure {
print() {
impure print(head);
std::cout << ", ";
impure print(tail);
}
}
}
print(Zero) = impure {
print() {
std::cout << "0";
}
}
print(One) = impure {
print() {
std::cout << "1";
}
}
print(Nil) = impure {
print() {
std::cout << std::endl;
}
}
my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
negated_list = negate(my_list);
negated_list2 = map(Not, my_list);
impure {
int main() {
std::cout << "my list is ";
impure print(my_list);
std::cout << "negated list is ";
impure print(negated_list);
std::cout << "negated list is also ";
impure print(negated_list2);
}
}
#include
struct One;
struct Zero;
template
struct Not {
typedef Zero _value;
};
template <>
struct Not {
typedef One _value;
};
template
struct And;
template
struct And {
typedef x _value;
};
template
struct And {
typedef Zero _value;
};
struct Nil;
template
struct Cons {
typedef x head;
typedef y tail;
};
template
struct negate {
typedef typename list::head h;
typedef typename Not ::_value not_head;
typedef typename list::tail t;
typedef typename negate ::_value not_tail;
typedef Cons _value;
};
template <>
struct negate {
typedef Nil _value;
};
template class f, typename list>
struct map {
typedef typename list::head h;
typedef typename f ::_value new_head;
typedef typename list::tail t;
typedef typename map ::_value new_tail;
typedef Cons _value;
};
template class f>
struct map {
typedef Nil _value;
};
template
struct print {
typedef typename list::head head;
typedef typename list::tail tail;
print() {
print ();
std::cout << ", ";
print ();
}
};
template <>
struct print {
print() {
std::cout << "0";
}
};
template <>
struct print {
print() {
std::cout << "1";
}
};
template <>
struct print {
print() {
std::cout << std::endl;
}
};
typedef Cons > > my_list;
typedef typename negate ::_value negated_list;
typedef typename map ::_value negated_list2;
int main() {
std::cout << "my list is ";
print ();
std::cout << "negated list is ";
print ();
std::cout << "negated list is also ";
print ();
}
node src/compile <исходник> # запуск
|
Эзотерический язык, транслирующийся в шаблоны C++ |
constexpr
, а мне хотелось остаться в области извращённых развлечений и использовать минимум возможностей языка. Замечательные статьи Жизнь во время компиляции от HurrTheDurr и Интерпретация во время компиляции, или Альтернативное понимание лямбд в C++11 от ilammy (вторая — более хардкорная и без using template
с constexpr
) описывали практически всё, что извращённому уму нужно было знать. В итоге я оставил статью в черновиках, но не оставил желания метапрограммировать. И сегодня я возвращаюсь с заново написанным текстом и новыми идеями.std::vector
принимает метазначение T
и возвращает метазначение std::vector
. Важно, что у метазначения есть набор метаполей (с точки зрения C++ — вложенные классы, псевдонимы типов, статические константные поля), с помощью которых можно сделать самое интересное.value
, и понимать его как значение, которое возвращает метафункция. Метафункции над целыми числами запишутся довольно просто:template
struct square {
static const int value = x * x;
};
square<5>::value
.struct One;
struct Zero;
one
и zero
только объявлены и практически бесполезны. Достаточно ли этого? Да. Чему же тогда они равны и как их использовать? Равны они, что важно, только самим себе. Это своего рода абстрактные символы, которые можно задействовать в символьных вычислениях (почти как в Mathematica и др.). Метапрограмма будет вычислять значения метавыражений различной сложности. Рассмотрим сначала эти выражения, а несколько позже займёмся интерпретацией символов и выводом результатов на экран.template
struct Not {
typedef Zero value;
};
template <>
struct Not {
typedef One value;
};
Not
принимает некоторое значение и возвращает единицу, если это значение — ноль. Во всех остальных случаях она возвратит ноль. Таким образом, за счёт специализации шаблонов, мы имеем паттерн-матчинг (сравнение с образцом) в зачаточной стадии: можем описывать отдельно поведение функции для одного или нескольких аргументов, имеющих конкретные значения, а компилятор C++, отметив соответствие параметров шаблона одному из образцов, подставит нужную специализацию. Пользуясь этим, мы могли бы уже написать что-то рекурсивное (например факториал, разделив описание на fac<0>
и fac<всё остальное>
).value
, можно представить себе, как выглядят многозначные функции. Запишем конструктор списка Cons
и пустой список Nil
, хорошо знакомые знатокам функционального программирования:template
struct Cons {
typedef h head;
typedef t tail;
};
struct Nil;
Cons
в ФП — функция, конструирующая список из первого элемента (головы) и списка остальных элементов (хвост). Обычному списку будет соответствовать Cons>>
. Поскольку для работы со списком нужно уметь получать его составные части, мы сделаем Cons
многозначной функцией, возвращающей голову (Cons<...,...>::head
), и хвост (Cons<...,...>::tail
). Любители ООП могут представить, что Cons
— это конструктор, а head
и tail
— геттеры. В ФП все присваивания заменяются на // отрицание непустого списка
template
struct negate {
private: // инкапсулируем промежуточные вычисления
typedef typename list::head h; // голова
typedef typename Not::value not_head; // отрицание головы
typedef typename list::tail t; // хвост
typedef typename negate::value not_tail; // отрицание хвоста
public:
// список из отрицаний элементов - это отрицание головы,
// соединённое с хвостом отрицаний
typedef Cons value;
};
// отрицание пустого списка
template <>
struct negate {
// пустой список - сам себе отрицание
typedef Nil value;
};
f (x+y) (g y) z
.-- список - либо слитые голова и хвост (причём хвост - сам список),
-- либо пустота
data List a = Cons a List | Nil
-- хелперы для получения головы и хвоста
head (Cons x _) = x -- возвращает голову
tail (Cons _ xs) = xs -- возвращает хвост
-- отрицание списка
negate (Cons x xs) = Cons (Not x) (negate xs)
negate Nil = Nil
negate::value
, конечно, не сработает, но если One
будет иметь метаполя head
и tail
, он вполне подойдёт. Впрочем, пока negate
не «разыменовали» с помощью ::value
, программа продолжает -- Преобразованная голова соединяется с преобразованным хвостом
map f (Cons x xs) = Cons (f x) (map f xs)
-- Пустой список преобразовывать не нужно
map f Nil = Nil
std::transform
. В нашем же языке метафункция map
объявляется с помощью параметризации шаблона шаблоном:// преобразование непустого списка
// f - шаблон с одним аргументом - унарная метафункция
template class f, typename list>
struct map {
private:
typedef typename list::head h; // голова
typedef typename f::value new_head; // преобразованная голова
typedef typename list::tail t; // хвост
typedef typename map::value new_tail; // преобразованный хвост
public:
// Преобразованная голова соединяется с преобразованным хвостом
typedef Cons value;
};
// преобразование пустого списка
template class f>
struct map {
// пустой список преобразовывать не нужно
typedef Nil value;
};
f
сюда можем подставить описанную ранее функцию Not
и посчитать список отрицаний:typedef map>>>::value list;
// list эквивалентно Cons>>
typedef
— это некий эквивалент оператора присваивания. Или, что звучит более корректно для функциональных языков, — операция задания соответствии имени и выражения.using x = Not::value;
template
using g = Cons;
value
, когда метафункция возвращает одно метазначение. Это может быть более удобным, если программисту лень явно указывать ::value
, а также накладывать требование вычислимости.negate
компилируется, а negate::value
— нет. Пользуясь громоздкими метаполями можно написать метафункцию ветвления, которая вычисляет ::value
только для одной своей ветки в зависимости от условия и возвращает это значение. Получится, что одно из выражений никогда не вычислится: все операции выполняются только при получении ::value
, а его никто не трогал:// рассмотрим только одну из специализаций
template
struct If {
// expr1, expr2 вычислены, а expr1::value и expr2::value - нет
typedef typename expr1::value value;
}
// If, destroy>::value не разрушит мир
g
имеет смысл уже вычисленного f::value
. И если ветвиться между двумя рекурсивными вариантами, вычисления будут бесконечными — аналог переполнения стека на этапе компиляции.template
struct If {
// expr1, expr2 вычислены
typedef expr1 value;
};
// If::value>::value разрушит мир
f x
возвращает унарную функцию g
, неявно использующую аргумент x
функции f
.f x = g
where g xs = Cons x xs
-- использование: (f x) xs или, что полезнее, map (f x) list
Cons
, но g
как унарную можно передать в уже написанную функцию map
, а как бинарную — Cons
нет!this
которого содержит весь нужный контекст, а operator ()
принимает требуемые интерфейсом аргументы. Красивая концепция из ФП заменяется эквивалентной по мощности концепцией ООП. Даже стандартный шаблон std::function
предусмотрен!// до C++11: функциональный объект, захвативший переменную
struct f {
f(something& x) : x(x) {}
something_else operator () (something_else& xs) { // xs - явный аргумент
return Cons(x, xs);
}
private:
something x; // неявный аргумент
};
// C++11 и выше: лямбда-функция, захватившая переменную
something x;
auto f = [x](something_else& xs) { return Cons(x, xs); };
int(int)
) на функциональные объекты (например, std::function
) программист C++ получает полноценный аналог замыканий.template
struct f {
template
struct g {
typedef Cons value;
};
};
// использование: f::g::value или map::g, list>
f
«вылезает» символ g
, но в остальном — честное замыкание. Выражение f::g
может быть использовано вместо обычной унарной функции (например, Not
).Cons
и Nil
. Кроме того, это может оказаться даже более простым ходом. Чтобы передать в метафункцию два списка, достаточно… передать два списка: f
, для вариадических же шаблонов требуется задавать обёртку, захватывающую все списки, кроме последнего: f, y1, y2, y3>
, так как список аргументов произвольной длины должен быть один, и несколько списков, записанные подряд, просто «слипнутся». По завершении вычислений приходится возвращать обёртку, т.к. в C++ нельзя затайпдефить список из нескольких типов. А чтобы «выковырить» список из обёртки и передать в метафункцию (скажем, f
), придётся использовать метаколлбеки: реализовать у обёртки метаметод принимающий эту метафункцию f
и передающий ей содержимое списка:typename
struct list_wrapper {
// передаём в list_wrapper<...>::call функцию,
// которой передадут наши аргументы xs
struct call class f> {
typedef typename f::value value;
};
};
negate
нужно сконструировать вспомогательную унарную метафункцию f
, которая принимает результат рекурсивного применения negate
к хвосту списка, вычисляет отрицание головы и возвращает обёртку для собранного вместе списка:typename
struct negate {
template
struct f {
typedef list_wrapper::value, ys> value;
};
typedef typename negate::template call::value;
};
typename <>
struct negate<> {
typedef list_wrapper<> value;
};
negate
выглядит более громоздко даже по сравнению с версией для Cons
/Nil
. Здесь требуются и более серьёзные измышления, когда как для написания «обычной» версии достаточно основ ФП и механической замены Haskell -> C++. Поэтому с помощью вариадических шаблонов лучше написать обёртку для преобразования последовательности параметров в список Cons
/Nil
, а затем при реализации программы пользоваться уже им. Так мы сможем и задавать списки приятным перечислением через запятую, и мыслить более простыми категориями.void One::print();
), либо — внутри метафункции, если возможностей паттерн-матчинга хватает для рассмотрения всех вариантов. Вот, например, метафункция print
, распечатывающая свой аргумент (единицу, ноль или список) на этапе конструирования экземпляра print<...>
:template
struct print {
print () {
std::cout << "unknown number" << std::endl;
}
};
template <>
struct print {
print () {
std::cout << "1" << std::endl;
}
};
template <>
struct print {
print () {
std::cout << "0" << std::endl;
}
};
// print::value>() выведет "1"
One
и Zero
и построить сложение, умножение,…decltype
никак нельзя было провести чисто функциональные вычисления над типами, создать переменные и объекты C++ соответствующих типов, провести вычисления над ними, а потом снова вернуться к вычислениям над типами.sum, three> // тип - корректно
sum, three>() // значение - компилируется
do_something(sum(), three()) // значения - компилируется
sum(), three()> // нельзя посчитать,
// т.к. нет перехода от значений к типам
sum()), decltype(three())> // C++11; компилируется
decltype
типы были аналогией чистоты, а значения — сущностями императивного мира: параметром шаблона мог быть только тип, а тип мог быть получен только преобразованиями типа; один раз создав значение, нельзя было вернуться обратно к типам.decltype
. Впрочем, decltype
свой аргумент не исполняет, а лишь отвечает на вопрос «какой бы был тип выражения, если бы мы его начали считать», отчего не нарушает функциональной чистоты. Поэтому, пока выражение над переменными не покидает скобки decltype
, чистота сохраняется. С точки зрения языка шаблонов, decltype
выгодно использовать вместе с перегрузкой операторов. В следующем примере выражения эквивалентны, но нижнее выглядит менее громоздко:typedef sum, three> x;
typedef decltype(one() + two() + three()) x;
auto v = one() + two() + three(); // v выполнится как императивная конструкция
typedef decltype(v) x;
::value
, скобочки и typename
сильно выматывают программиста и растягивают код программы. Конечно, логика говорит, что решивший программировать в таком стиле должен страдать, но… это одно из тех извращённых развлечений, к которым зачастую склоняется мозг айтишника. Хочется попробовать сделать так, чтобы извращённость сохранилась, но в той степени, когда страдания ещё можно терпеть.One;
Zero;
Not (val x) = Zero; // общий случай
Not (Zero) = One; // частный случай
And(val x, val y); // для x,y != One,Zero And не определена
And(val x, One) = x; // One - константа, val x - аргумент
And(val x, Zero) = Zero;
// использование: Not(One) или And(One, Zero)
typename
), другой шаблон (template class
) и т.д., и это следует указывать. А значит, создавая метаФВП, одним typename
не обойтись и требование указания типа переходит также в мой язык. По умолчанию задан тип val
, соответствующий обычному метазначению (структура или обычный тип в C++). Для описания функций можно комбенировать типы с помощью стрелки (->
). Например, val -> val
— унарная функция (шаблон, принимающий один параметр), (val, val) -> val
— бинарная функция (шаблон, принимающий два параметра) и так далее.#type
можно задавать синонимы, чтобы ещё чуть-чуть сократить запись и прояснить смысл за счёт уместного именования:#type number = val;
#type list = val;
#type unary = number -> number;
#type map_t = (unary, list) -> list;
// map_t раскроется в template class, typename> class
#type
как аналог #define
в C, задающий текстовые преобразования, которые нехитрым способом можно провести и вручную.Nil;
Cons(val x, val y) {
head = x;
tail = y;
}
::value
. То есть f(x)
эквивалентно f::value
. Но у Cons
сгенерируется только два метаполя head
и tail
. Предотвращать раскрытие ::value
требуется явно с помощью апострофа: Cons(x, xs)'
.::value
как одна из частых конструкций должна была раскрываться автоматически, но должна была быть возможность (а) передать нераскрытое значение (см. проблему функции ветвления и бесконечной рекурсии выше под спойлером) и (б) использовать другие метаполя, кроме value
. В итоге я остановился на «экранировании», записи метаполей через точку и ввёл обратный для экранирования оператор "!", раскрывающий ::value
:Cons(x, xs)'.head; // x
Cons(x, xs)'.tail; // xs
Not(Zero); // One
Not(Zero)'!; // One
::value
может вполне сосуществовать со множественным возвратом. Реализуя отрицание списка negate
на C++, мы создавали локальные метапеременные, которые вполне можно было возвратить. Здесь — аналогично, только все значения публичны:// отрицание непустого списка:
// список из отрицаний элементов - это отрицание головы,
// соединённое с хвостом отрицаний
negate (list list) = Cons(not_head, not_tail)' {
h = list.head; // голова
not_head = Not(h); // отрицание головы
t = list.tail; // хвост
not_tail = negate(t); // отрицание хвоста
} // точку с запятой можно опустить!
// пустой список - сам себе отрицание
negate (Nil) = Nil;
// преобразование непустого списка
map (unary f, list list) = Cons(new_head, new_tail)' {
h = list.head; // голова
new_head = f(h); // преобразованная голова
t = list.tail; // хвост
new_tail = map(f, t); // преобразованный хвост
}
// преобразование пустого списка
map (unary f, Nil) = Nil;
unary
и list
можно было сразу указать val -> val
и val
соответственно, но запись оказалась бы более длинной и менее наглядной.f(val x) = {
g(val xs) = Cons(x, xs)';
}
// использование: f(x)'.g(list) или map(f(x)'.g, list)
value
и возвращать value
. Одноимённый с классом член в C++ уже отдан конструктору, из-за чего задание ему нового смысла через typedef
приводит к ошибке компиляции:template
struct f {
template
struct value { // value!
typedef Cons value; // value!
};
};
f (val x) = g(Not(x)) {
g (val x) = x; // будет заменено на g (x1) = x1
// т.к. x станет параметром template f и не допустит x как параметр template g
}
::value
, нигде не сказано, что f(x)
— это f::value
, а не call::result
. Это знание хранилось внутри транслятора, и его использование в программе прорывало бы абстракцию:f(val x) = One;
f(Zero) = Zero;
main {
print::value>(); // почему так?
}
impure { код }
может появляться вместо объявления значения/функции, а также справа после знака "=" при объявлении функции. В этих случаях код на C++ вставляется в соответствующее место программы. Для экспортирования выражения перед ним ставится ключевое слово impure
. С точки зрения C++ это эквивалентно конструированию объекта типа, описываемого выражением.impure
приготовим «распечатыватель» списка:print(val list) {
head = list.head;
tail = list.tail;
impure {
// после typedef-ов head, tail расположится следующий блок кода:
print() {
impure print(head); // преобразуется в "print();"
std::cout << ", ";
impure print(tail);
}
}
}
print(Zero) = impure { // аналогично print(Zero) { impure { ..., но короче
print() {
std::cout << "0";
}
}
print(One) = impure {
print() {
std::cout << "1";
}
}
print(Nil) = impure {
print() {
std::cout << std::endl;
}
}
namespace
и using namespace
:namespace ns1 {
namespace ns2 {
f(val x) = x;
}
}
namespace ns3 {
using namespace ns1.ns2;
g(val x) = f(x);
}
using namespace
также является вынужденной мерой. В С++ в некоторых случаях недостаточно записи вида f::y
. Если f
, x
или z
— не конкретные типы/шаблоны, а параметры шаблона, то ::y
становится выходом чёрного ящика. Нужно указывать, что мы получаем при вычислении ::y
— тип или шаблон (например, typename f::y
или f::template y
). Я не автоматизировал эти указания и не реализовал синтаксический сахар для более простого описания вручную, поэтому каждое использование точки вызывает появление «typename». Т.е. f::y
оттранслируется во что-то некорректное вида typename typename f::value::typename y::value
. В случае пространства имён это излишне, using namespace
позволяет обойтись без вставки «typename».f (val x) = y(x) { y(x) = g(x); } // было
f = lambda (val x) -> y(x) { y(x) = g(x); } // стало
template struct f {}; // пусть была метафункция f
using g = f; // нельзя описать эквивалентность шаблона шаблону
// нужно описать эквивалентность выражений, соответствующих типам
template using g = f; // g - тип, f - тип
map(lambda(val x) -> y, xs)
придётся модифицировать язык и генерировать шаблоны с временными именами.value::value
— это конструктор, поэтому не получается напрямую реализовать лямбду, которая возвращает лямбду. Для возврата функций из лямбд и разрешения записей вида g = f
нужно использовать другие концепции и почти полностью переписать транслятор.impure { private: }
в код или элементарной модификацией языка.fMap(fCurry(fCons).fApply(vXs), vObj.vYs)
— при доступе к метаполю подставится template
, typename
или ничего, если первым символом имени будет «f», «v» или «n» соответственно).// объявляем значение x
struct x {
typedef internal::value type; // тип значения x
};
// объявляем функцию f(x) = x
struct f {
typedef internal::function type; // тип функции f
template
struct value {
typedef typename x::type type; // тип результата функции f
struct result { // обёртка для того, чтобы избежать value::value
typedef x value;
};
};
};
internal
со списком типов и метафункциями для работы с ними; за счёт дополнительной обёртки struct result
можно будет избавиться от value::value
и возвращать безымянные функции из функций. Также из-за того, что любое значение или функция будет выражаться в виде структуры (то есть просто типа, а не типа или шаблона), можно будет задавать синонимы функций (с помощью typedef
), отменить обязательное описание типов аргументов функции, устранить ограничения на лямбды и возврат функций из ФВП.value
использовать два значения так, чтобы при увеличении глубины вложенности использовалось другое значение. Получится, что вместо value::value
будет value1::value2
и никаких попыток переопределения конструктора не будет:template
struct f {
typedef internal::True uses_value1; // использует value1
template
struct value1 {
typedef internal::False uses_value1; // использует value2
typedef Cons value2;
};
};
uses_value1
будет выбирать, доступаться до value1
или до value2
.val
дополнительных типов для чисел (int
, uint
, ...) и массивов ([val]
, [int]
, [uint]
, ...). При использовании метаполей придётся решить описанную выше проблему с указанием template
и typename
, т.к. для чисел ничего не требуется указывать, а для типов и шаблонов — требуется.main
:// строим списки:
my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
negated_list = negate(my_list);
negated_list2 = map(Not, my_list);
impure {
int main() {
// печатаем списки:
std::cout << "my list is ";
impure print(my_list);
std::cout << "negated list is ";
impure print(negated_list);
std::cout << "negated list is also ";
impure print(negated_list2);
}
}
my list is 1, 1, 0,
negated list is 0, 0, 1,
negated list is also 0, 0, 1,
impure {
#include
}
One;
Zero;
Not (val x) = Zero; // общий случай
Not (Zero) = One; // частный случай
And(val x, val y); // для x,y != One,Zero And не определена
And(val x, One) = x; // One - константа, val x - аргумент
And(val x, Zero) = Zero;
#type number = val;
#type list = val;
#type unary = number -> number;
#type map_t = (unary, list) -> list;
Nil;
Cons(val x, val y) {
head = x;
tail = y;
}
// отрицание непустого списка:
negate (list list) = Cons(not_head, not_tail)' {
h = list.head;
not_head = Not(h);
t = list.tail;
not_tail = negate(t);
}
// пустой список - сам себе отрицание
negate (Nil) = Nil;
// преобразование непустого списка
map (unary f, list list) = Cons(new_head, new_tail)' {
h = list.head;
new_head = f(h);
t = list.tail;
new_tail = map(f, t);
}
// преобразование пустого списка
map (unary f, Nil) = Nil;
print(val list) {
head = list.head;
tail = list.tail;
impure {
print() {
impure print(head);
std::cout << ", ";
impure print(tail);
}
}
}
print(Zero) = impure {
print() {
std::cout << "0";
}
}
print(One) = impure {
print() {
std::cout << "1";
}
}
print(Nil) = impure {
print() {
std::cout << std::endl;
}
}
my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
negated_list = negate(my_list);
negated_list2 = map(Not, my_list);
impure {
int main() {
std::cout << "my list is ";
impure print(my_list);
std::cout << "negated list is ";
impure print(negated_list);
std::cout << "negated list is also ";
impure print(negated_list2);
}
}
#include
struct One;
struct Zero;
template
struct Not {
typedef Zero _value;
};
template <>
struct Not {
typedef One _value;
};
template
struct And;
template
struct And {
typedef x _value;
};
template
struct And {
typedef Zero _value;
};
struct Nil;
template
struct Cons {
typedef x head;
typedef y tail;
};
template
struct negate {
typedef typename list::head h;
typedef typename Not ::_value not_head;
typedef typename list::tail t;
typedef typename negate ::_value not_tail;
typedef Cons _value;
};
template <>
struct negate {
typedef Nil _value;
};
template class f, typename list>
struct map {
typedef typename list::head h;
typedef typename f ::_value new_head;
typedef typename list::tail t;
typedef typename map ::_value new_tail;
typedef Cons _value;
};
template class f>
struct map {
typedef Nil _value;
};
template
struct print {
typedef typename list::head head;
typedef typename list::tail tail;
print() {
print ();
std::cout << ", ";
print ();
}
};
template <>
struct print {
print() {
std::cout << "0";
}
};
template <>
struct print {
print() {
std::cout << "1";
}
};
template <>
struct print {
print() {
std::cout << std::endl;
}
};
typedef Cons > > my_list;
typedef typename negate ::_value negated_list;
typedef typename map ::_value negated_list2;
int main() {
std::cout << "my list is ";
print ();
std::cout << "negated list is ";
print ();
std::cout << "negated list is also ";
print ();
}
node src/compile <исходник> # запуск
|
Эзотерический язык, транслирующийся в шаблоны C++ |
constexpr
, а мне хотелось остаться в области извращённых развлечений и использовать минимум возможностей языка. Замечательные статьи Жизнь во время компиляции от HurrTheDurr и Интерпретация во время компиляции, или Альтернативное понимание лямбд в C++11 от ilammy (вторая — более хардкорная и без using template
с constexpr
) описывали практически всё, что извращённому уму нужно было знать. В итоге я оставил статью в черновиках, но не оставил желания метапрограммировать. И сегодня я возвращаюсь с заново написанным текстом и новыми идеями.std::vector
принимает метазначение T
и возвращает метазначение std::vector
. Важно, что у метазначения есть набор метаполей (с точки зрения C++ — вложенные классы, псевдонимы типов, статические константные поля), с помощью которых можно сделать самое интересное.value
, и понимать его как значение, которое возвращает метафункция. Метафункции над целыми числами запишутся довольно просто:template
struct square {
static const int value = x * x;
};
square<5>::value
.struct One;
struct Zero;
one
и zero
только объявлены и практически бесполезны. Достаточно ли этого? Да. Чему же тогда они равны и как их использовать? Равны они, что важно, только самим себе. Это своего рода абстрактные символы, которые можно задействовать в символьных вычислениях (почти как в Mathematica и др.). Метапрограмма будет вычислять значения метавыражений различной сложности. Рассмотрим сначала эти выражения, а несколько позже займёмся интерпретацией символов и выводом результатов на экран.template
struct Not {
typedef Zero value;
};
template <>
struct Not {
typedef One value;
};
Not
принимает некоторое значение и возвращает единицу, если это значение — ноль. Во всех остальных случаях она возвратит ноль. Таким образом, за счёт специализации шаблонов, мы имеем паттерн-матчинг (сравнение с образцом) в зачаточной стадии: можем описывать отдельно поведение функции для одного или нескольких аргументов, имеющих конкретные значения, а компилятор C++, отметив соответствие параметров шаблона одному из образцов, подставит нужную специализацию. Пользуясь этим, мы могли бы уже написать что-то рекурсивное (например факториал, разделив описание на fac<0>
и fac<всё остальное>
).value
, можно представить себе, как выглядят многозначные функции. Запишем конструктор списка Cons
и пустой список Nil
, хорошо знакомые знатокам функционального программирования:template
struct Cons {
typedef h head;
typedef t tail;
};
struct Nil;
Cons
в ФП — функция, конструирующая список из первого элемента (головы) и списка остальных элементов (хвост). Обычному списку будет соответствовать Cons>>
. Поскольку для работы со списком нужно уметь получать его составные части, мы сделаем Cons
многозначной функцией, возвращающей голову (Cons<...,...>::head
), и хвост (Cons<...,...>::tail
). Любители ООП могут представить, что Cons
— это конструктор, а head
и tail
— геттеры. В ФП все присваивания заменяются на // отрицание непустого списка
template
struct negate {
private: // инкапсулируем промежуточные вычисления
typedef typename list::head h; // голова
typedef typename Not::value not_head; // отрицание головы
typedef typename list::tail t; // хвост
typedef typename negate::value not_tail; // отрицание хвоста
public:
// список из отрицаний элементов - это отрицание головы,
// соединённое с хвостом отрицаний
typedef Cons value;
};
// отрицание пустого списка
template <>
struct negate {
// пустой список - сам себе отрицание
typedef Nil value;
};
f (x+y) (g y) z
.-- список - либо слитые голова и хвост (причём хвост - сам список),
-- либо пустота
data List a = Cons a List | Nil
-- хелперы для получения головы и хвоста
head (Cons x _) = x -- возвращает голову
tail (Cons _ xs) = xs -- возвращает хвост
-- отрицание списка
negate (Cons x xs) = Cons (Not x) (negate xs)
negate Nil = Nil
negate::value
, конечно, не сработает, но если One
будет иметь метаполя head
и tail
, он вполне подойдёт. Впрочем, пока negate
не «разыменовали» с помощью ::value
, программа продолжает -- Преобразованная голова соединяется с преобразованным хвостом
map f (Cons x xs) = Cons (f x) (map f xs)
-- Пустой список преобразовывать не нужно
map f Nil = Nil
std::transform
. В нашем же языке метафункция map
объявляется с помощью параметризации шаблона шаблоном:// преобразование непустого списка
// f - шаблон с одним аргументом - унарная метафункция
template class f, typename list>
struct map {
private:
typedef typename list::head h; // голова
typedef typename f::value new_head; // преобразованная голова
typedef typename list::tail t; // хвост
typedef typename map::value new_tail; // преобразованный хвост
public:
// Преобразованная голова соединяется с преобразованным хвостом
typedef Cons value;
};
// преобразование пустого списка
template class f>
struct map {
// пустой список преобразовывать не нужно
typedef Nil value;
};
f
сюда можем подставить описанную ранее функцию Not
и посчитать список отрицаний:typedef map>>>::value list;
// list эквивалентно Cons>>
typedef
— это некий эквивалент оператора присваивания. Или, что звучит более корректно для функциональных языков, — операция задания соответствии имени и выражения.using x = Not::value;
template
using g = Cons;
value
, когда метафункция возвращает одно метазначение. Это может быть более удобным, если программисту лень явно указывать ::value
, а также накладывать требование вычислимости.negate
компилируется, а negate::value
— нет. Пользуясь громоздкими метаполями можно написать метафункцию ветвления, которая вычисляет ::value
только для одной своей ветки в зависимости от условия и возвращает это значение. Получится, что одно из выражений никогда не вычислится: все операции выполняются только при получении ::value
, а его никто не трогал:// рассмотрим только одну из специализаций
template
struct If {
// expr1, expr2 вычислены, а expr1::value и expr2::value - нет
typedef typename expr1::value value;
}
// If, destroy>::value не разрушит мир
g
имеет смысл уже вычисленного f::value
. И если ветвиться между двумя рекурсивными вариантами, вычисления будут бесконечными — аналог переполнения стека на этапе компиляции.template
struct If {
// expr1, expr2 вычислены
typedef expr1 value;
};
// If::value>::value разрушит мир
f x
возвращает унарную функцию g
, неявно использующую аргумент x
функции f
.f x = g
where g xs = Cons x xs
-- использование: (f x) xs или, что полезнее, map (f x) list
Cons
, но g
как унарную можно передать в уже написанную функцию map
, а как бинарную — Cons
нет!this
которого содержит весь нужный контекст, а operator ()
принимает требуемые интерфейсом аргументы. Красивая концепция из ФП заменяется эквивалентной по мощности концепцией ООП. Даже стандартный шаблон std::function
предусмотрен!// до C++11: функциональный объект, захвативший переменную
struct f {
f(something& x) : x(x) {}
something_else operator () (something_else& xs) { // xs - явный аргумент
return Cons(x, xs);
}
private:
something x; // неявный аргумент
};
// C++11 и выше: лямбда-функция, захватившая переменную
something x;
auto f = [x](something_else& xs) { return Cons(x, xs); };
int(int)
) на функциональные объекты (например, std::function
) программист C++ получает полноценный аналог замыканий.template
struct f {
template
struct g {
typedef Cons value;
};
};
// использование: f::g::value или map::g, list>
f
«вылезает» символ g
, но в остальном — честное замыкание. Выражение f::g
может быть использовано вместо обычной унарной функции (например, Not
).Cons
и Nil
. Кроме того, это может оказаться даже более простым ходом. Чтобы передать в метафункцию два списка, достаточно… передать два списка: f
, для вариадических же шаблонов требуется задавать обёртку, захватывающую все списки, кроме последнего: f, y1, y2, y3>
, так как список аргументов произвольной длины должен быть один, и несколько списков, записанные подряд, просто «слипнутся». По завершении вычислений приходится возвращать обёртку, т.к. в C++ нельзя затайпдефить список из нескольких типов. А чтобы «выковырить» список из обёртки и передать в метафункцию (скажем, f
), придётся использовать метаколлбеки: реализовать у обёртки метаметод принимающий эту метафункцию f
и передающий ей содержимое списка:typename
struct list_wrapper {
// передаём в list_wrapper<...>::call функцию,
// которой передадут наши аргументы xs
struct call class f> {
typedef typename f::value value;
};
};
negate
нужно сконструировать вспомогательную унарную метафункцию f
, которая принимает результат рекурсивного применения negate
к хвосту списка, вычисляет отрицание головы и возвращает обёртку для собранного вместе списка:typename
struct negate {
template
struct f {
typedef list_wrapper::value, ys> value;
};
typedef typename negate::template call::value;
};
typename <>
struct negate<> {
typedef list_wrapper<> value;
};
negate
выглядит более громоздко даже по сравнению с версией для Cons
/Nil
. Здесь требуются и более серьёзные измышления, когда как для написания «обычной» версии достаточно основ ФП и механической замены Haskell -> C++. Поэтому с помощью вариадических шаблонов лучше написать обёртку для преобразования последовательности параметров в список Cons
/Nil
, а затем при реализации программы пользоваться уже им. Так мы сможем и задавать списки приятным перечислением через запятую, и мыслить более простыми категориями.void One::print();
), либо — внутри метафункции, если возможностей паттерн-матчинга хватает для рассмотрения всех вариантов. Вот, например, метафункция print
, распечатывающая свой аргумент (единицу, ноль или список) на этапе конструирования экземпляра print<...>
:template
struct print {
print () {
std::cout << "unknown number" << std::endl;
}
};
template <>
struct print {
print () {
std::cout << "1" << std::endl;
}
};
template <>
struct print {
print () {
std::cout << "0" << std::endl;
}
};
// print::value>() выведет "1"
One
и Zero
и построить сложение, умножение,…decltype
никак нельзя было провести чисто функциональные вычисления над типами, создать переменные и объекты C++ соответствующих типов, провести вычисления над ними, а потом снова вернуться к вычислениям над типами.sum, three> // тип - корректно
sum, three>() // значение - компилируется
do_something(sum(), three()) // значения - компилируется
sum(), three()> // нельзя посчитать,
// т.к. нет перехода от значений к типам
sum()), decltype(three())> // C++11; компилируется
decltype
типы были аналогией чистоты, а значения — сущностями императивного мира: параметром шаблона мог быть только тип, а тип мог быть получен только преобразованиями типа; один раз создав значение, нельзя было вернуться обратно к типам.decltype
. Впрочем, decltype
свой аргумент не исполняет, а лишь отвечает на вопрос «какой бы был тип выражения, если бы мы его начали считать», отчего не нарушает функциональной чистоты. Поэтому, пока выражение над переменными не покидает скобки decltype
, чистота сохраняется. С точки зрения языка шаблонов, decltype
выгодно использовать вместе с перегрузкой операторов. В следующем примере выражения эквивалентны, но нижнее выглядит менее громоздко:typedef sum, three> x;
typedef decltype(one() + two() + three()) x;
auto v = one() + two() + three(); // v выполнится как императивная конструкция
typedef decltype(v) x;
::value
, скобочки и typename
сильно выматывают программиста и растягивают код программы. Конечно, логика говорит, что решивший программировать в таком стиле должен страдать, но… это одно из тех извращённых развлечений, к которым зачастую склоняется мозг айтишника. Хочется попробовать сделать так, чтобы извращённость сохранилась, но в той степени, когда страдания ещё можно терпеть.One;
Zero;
Not (val x) = Zero; // общий случай
Not (Zero) = One; // частный случай
And(val x, val y); // для x,y != One,Zero And не определена
And(val x, One) = x; // One - константа, val x - аргумент
And(val x, Zero) = Zero;
// использование: Not(One) или And(One, Zero)
typename
), другой шаблон (template class
) и т.д., и это следует указывать. А значит, создавая метаФВП, одним typename
не обойтись и требование указания типа переходит также в мой язык. По умолчанию задан тип val
, соответствующий обычному метазначению (структура или обычный тип в C++). Для описания функций можно комбенировать типы с помощью стрелки (->
). Например, val -> val
— унарная функция (шаблон, принимающий один параметр), (val, val) -> val
— бинарная функция (шаблон, принимающий два параметра) и так далее.#type
можно задавать синонимы, чтобы ещё чуть-чуть сократить запись и прояснить смысл за счёт уместного именования:#type number = val;
#type list = val;
#type unary = number -> number;
#type map_t = (unary, list) -> list;
// map_t раскроется в template class, typename> class
#type
как аналог #define
в C, задающий текстовые преобразования, которые нехитрым способом можно провести и вручную.Nil;
Cons(val x, val y) {
head = x;
tail = y;
}
::value
. То есть f(x)
эквивалентно f::value
. Но у Cons
сгенерируется только два метаполя head
и tail
. Предотвращать раскрытие ::value
требуется явно с помощью апострофа: Cons(x, xs)'
.::value
как одна из частых конструкций должна была раскрываться автоматически, но должна была быть возможность (а) передать нераскрытое значение (см. проблему функции ветвления и бесконечной рекурсии выше под спойлером) и (б) использовать другие метаполя, кроме value
. В итоге я остановился на «экранировании», записи метаполей через точку и ввёл обратный для экранирования оператор "!", раскрывающий ::value
:Cons(x, xs)'.head; // x
Cons(x, xs)'.tail; // xs
Not(Zero); // One
Not(Zero)'!; // One
::value
может вполне сосуществовать со множественным возвратом. Реализуя отрицание списка negate
на C++, мы создавали локальные метапеременные, которые вполне можно было возвратить. Здесь — аналогично, только все значения публичны:// отрицание непустого списка:
// список из отрицаний элементов - это отрицание головы,
// соединённое с хвостом отрицаний
negate (list list) = Cons(not_head, not_tail)' {
h = list.head; // голова
not_head = Not(h); // отрицание головы
t = list.tail; // хвост
not_tail = negate(t); // отрицание хвоста
} // точку с запятой можно опустить!
// пустой список - сам себе отрицание
negate (Nil) = Nil;
// преобразование непустого списка
map (unary f, list list) = Cons(new_head, new_tail)' {
h = list.head; // голова
new_head = f(h); // преобразованная голова
t = list.tail; // хвост
new_tail = map(f, t); // преобразованный хвост
}
// преобразование пустого списка
map (unary f, Nil) = Nil;
unary
и list
можно было сразу указать val -> val
и val
соответственно, но запись оказалась бы более длинной и менее наглядной.f(val x) = {
g(val xs) = Cons(x, xs)';
}
// использование: f(x)'.g(list) или map(f(x)'.g, list)
value
и возвращать value
. Одноимённый с классом член в C++ уже отдан конструктору, из-за чего задание ему нового смысла через typedef
приводит к ошибке компиляции:template
struct f {
template
struct value { // value!
typedef Cons value; // value!
};
};
f (val x) = g(Not(x)) {
g (val x) = x; // будет заменено на g (x1) = x1
// т.к. x станет параметром template f и не допустит x как параметр template g
}
::value
, нигде не сказано, что f(x)
— это f::value
, а не call::result
. Это знание хранилось внутри транслятора, и его использование в программе прорывало бы абстракцию:f(val x) = One;
f(Zero) = Zero;
main {
print::value>(); // почему так?
}
impure { код }
может появляться вместо объявления значения/функции, а также справа после знака "=" при объявлении функции. В этих случаях код на C++ вставляется в соответствующее место программы. Для экспортирования выражения перед ним ставится ключевое слово impure
. С точки зрения C++ это эквивалентно конструированию объекта типа, описываемого выражением.impure
приготовим «распечатыватель» списка:print(val list) {
head = list.head;
tail = list.tail;
impure {
// после typedef-ов head, tail расположится следующий блок кода:
print() {
impure print(head); // преобразуется в "print();"
std::cout << ", ";
impure print(tail);
}
}
}
print(Zero) = impure { // аналогично print(Zero) { impure { ..., но короче
print() {
std::cout << "0";
}
}
print(One) = impure {
print() {
std::cout << "1";
}
}
print(Nil) = impure {
print() {
std::cout << std::endl;
}
}
namespace
и using namespace
:namespace ns1 {
namespace ns2 {
f(val x) = x;
}
}
namespace ns3 {
using namespace ns1.ns2;
g(val x) = f(x);
}
using namespace
также является вынужденной мерой. В С++ в некоторых случаях недостаточно записи вида f::y
. Если f
, x
или z
— не конкретные типы/шаблоны, а параметры шаблона, то ::y
становится выходом чёрного ящика. Нужно указывать, что мы получаем при вычислении ::y
— тип или шаблон (например, typename f::y
или f::template y
). Я не автоматизировал эти указания и не реализовал синтаксический сахар для более простого описания вручную, поэтому каждое использование точки вызывает появление «typename». Т.е. f::y
оттранслируется во что-то некорректное вида typename typename f::value::typename y::value
. В случае пространства имён это излишне, using namespace
позволяет обойтись без вставки «typename».f (val x) = y(x) { y(x) = g(x); } // было
f = lambda (val x) -> y(x) { y(x) = g(x); } // стало
template struct f {}; // пусть была метафункция f
using g = f; // нельзя описать эквивалентность шаблона шаблону
// нужно описать эквивалентность выражений, соответствующих типам
template using g = f; // g - тип, f - тип
map(lambda(val x) -> y, xs)
придётся модифицировать язык и генерировать шаблоны с временными именами.value::value
— это конструктор, поэтому не получается напрямую реализовать лямбду, которая возвращает лямбду. Для возврата функций из лямбд и разрешения записей вида g = f
нужно использовать другие концепции и почти полностью переписать транслятор.impure { private: }
в код или элементарной модификацией языка.fMap(fCurry(fCons).fApply(vXs), vObj.vYs)
— при доступе к метаполю подставится template
, typename
или ничего, если первым символом имени будет «f», «v» или «n» соответственно).// объявляем значение x
struct x {
typedef internal::value type; // тип значения x
};
// объявляем функцию f(x) = x
struct f {
typedef internal::function type; // тип функции f
template
struct value {
typedef typename x::type type; // тип результата функции f
struct result { // обёртка для того, чтобы избежать value::value
typedef x value;
};
};
};
internal
со списком типов и метафункциями для работы с ними; за счёт дополнительной обёртки struct result
можно будет избавиться от value::value
и возвращать безымянные функции из функций. Также из-за того, что любое значение или функция будет выражаться в виде структуры (то есть просто типа, а не типа или шаблона), можно будет задавать синонимы функций (с помощью typedef
), отменить обязательное описание типов аргументов функции, устранить ограничения на лямбды и возврат функций из ФВП.value
использовать два значения так, чтобы при увеличении глубины вложенности использовалось другое значение. Получится, что вместо value::value
будет value1::value2
и никаких попыток переопределения конструктора не будет:template
struct f {
typedef internal::True uses_value1; // использует value1
template
struct value1 {
typedef internal::False uses_value1; // использует value2
typedef Cons value2;
};
};
uses_value1
будет выбирать, доступаться до value1
или до value2
.val
дополнительных типов для чисел (int
, uint
, ...) и массивов ([val]
, [int]
, [uint]
, ...). При использовании метаполей придётся решить описанную выше проблему с указанием template
и typename
, т.к. для чисел ничего не требуется указывать, а для типов и шаблонов — требуется.main
:// строим списки:
my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
negated_list = negate(my_list);
negated_list2 = map(Not, my_list);
impure {
int main() {
// печатаем списки:
std::cout << "my list is ";
impure print(my_list);
std::cout << "negated list is ";
impure print(negated_list);
std::cout << "negated list is also ";
impure print(negated_list2);
}
}
my list is 1, 1, 0,
negated list is 0, 0, 1,
negated list is also 0, 0, 1,
impure {
#include
}
One;
Zero;
Not (val x) = Zero; // общий случай
Not (Zero) = One; // частный случай
And(val x, val y); // для x,y != One,Zero And не определена
And(val x, One) = x; // One - константа, val x - аргумент
And(val x, Zero) = Zero;
#type number = val;
#type list = val;
#type unary = number -> number;
#type map_t = (unary, list) -> list;
Nil;
Cons(val x, val y) {
head = x;
tail = y;
}
// отрицание непустого списка:
negate (list list) = Cons(not_head, not_tail)' {
h = list.head;
not_head = Not(h);
t = list.tail;
not_tail = negate(t);
}
// пустой список - сам себе отрицание
negate (Nil) = Nil;
// преобразование непустого списка
map (unary f, list list) = Cons(new_head, new_tail)' {
h = list.head;
new_head = f(h);
t = list.tail;
new_tail = map(f, t);
}
// преобразование пустого списка
map (unary f, Nil) = Nil;
print(val list) {
head = list.head;
tail = list.tail;
impure {
print() {
impure print(head);
std::cout << ", ";
impure print(tail);
}
}
}
print(Zero) = impure {
print() {
std::cout << "0";
}
}
print(One) = impure {
print() {
std::cout << "1";
}
}
print(Nil) = impure {
print() {
std::cout << std::endl;
}
}
my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
negated_list = negate(my_list);
negated_list2 = map(Not, my_list);
impure {
int main() {
std::cout << "my list is ";
impure print(my_list);
std::cout << "negated list is ";
impure print(negated_list);
std::cout << "negated list is also ";
impure print(negated_list2);
}
}
#include
struct One;
struct Zero;
template
struct Not {
typedef Zero _value;
};
template <>
struct Not {
typedef One _value;
};
template
struct And;
template
struct And {
typedef x _value;
};
template
struct And {
typedef Zero _value;
};
struct Nil;
template
struct Cons {
typedef x head;
typedef y tail;
};
template
struct negate {
typedef typename list::head h;
typedef typename Not ::_value not_head;
typedef typename list::tail t;
typedef typename negate ::_value not_tail;
typedef Cons _value;
};
template <>
struct negate {
typedef Nil _value;
};
template class f, typename list>
struct map {
typedef typename list::head h;
typedef typename f ::_value new_head;
typedef typename list::tail t;
typedef typename map ::_value new_tail;
typedef Cons _value;
};
template class f>
struct map {
typedef Nil _value;
};
template
struct print {
typedef typename list::head head;
typedef typename list::tail tail;
print() {
print ();
std::cout << ", ";
print ();
}
};
template <>
struct print {
print() {
std::cout << "0";
}
};
template <>
struct print {
print() {
std::cout << "1";
}
};
template <>
struct print {
print() {
std::cout << std::endl;
}
};
typedef Cons > > my_list;
typedef typename negate ::_value negated_list;
typedef typename map ::_value negated_list2;
int main() {
std::cout << "my list is ";
print ();
std::cout << "negated list is ";
print ();
std::cout << "negated list is also ";
print ();
}
node src/compile <исходник> # запуск
|
[Перевод] JavaScript: методы асинхронного программирования |
console.log('1')
console.log('2')
console.log('3')
console.log('1')
setTimeout(function afterTwoSeconds() {
console.log('2')
}, 2000)
console.log('3')
setTimeout
. Коллбэк будет вызвана, в данном примере, через 2 секунды. Приложение при этом не остановится, ожидая, пока истекут эти две секунды. Вместо этого его исполнение продолжится, а когда сработает таймер, будет вызвана функция afterTwoSeconds
.XMLHttpRequest
(XHR), но вы вполне можете использовать тут jQuery ($.ajax
), или более современный стандартный подход, основанный на использовании функции fetch
. И то и другое сводится к использованию промисов. Код, в зависимости от похода, будет меняться, но вот, для начала, такой пример:// аргумент url может быть чем-то вроде 'https://api.github.com/users/daspinola/repos'
function request(url) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Код обработки успешного завершения запроса
} else {
// Обрабатываем ответ с сообщением об ошибке
}
}
}
xhr.ontimeout = function () {
// Ожидание ответа заняло слишком много времени, тут будет код, который обрабатывает подобную ситуацию
}
xhr.open('get', url, true)
xhr.send();
}
// Вызовем функцию "doThis" с другой функцией в качестве параметра, в данном случае - это функция "andThenThis". Функция "doThis" исполнит код, находящийся в ней, после чего, в нужный момент, вызовет функцию "andThenThis".
doThis(andThenThis)
// Внутри "doThis" обращение к переданной ей функции осуществляется через параметр "callback" , фактически, это просто переменная, которая хранит ссылку на функцию
function andThenThis() {
console.log('and then this')
}
// Назвать параметр, в котором окажется функция обратного вызова, можно как угодно, "callback" - это просто распространённый вариант
function doThis(callback) {
console.log('this first')
// Для того, чтобы функция, ссылка на которую хранится в переменной, была вызвана, нужно поместить после имени переменной скобки, '()', иначе ничего не получится
callback()
}
request
:function request(url, callback) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr.response)
} else {
callback(xhr.status, null)
}
}
}
xhr.ontimeout = function () {
console.log('Timeout')
}
xhr.open('get', url, true)
xhr.send();
}
callback
, поэтому, после выполнения запроса и получения ответа сервера, коллбэк будет вызван и в случае ошибки, и в случае успешного завершения операции.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
request(userGet, function handleUsersList(error, users) {
if (error) throw error
const list = JSON.parse(users).items
list.forEach(function(user) {
request(user.repos_url, function handleReposList(err, repos) {
if (err) throw err
//Здесь обработаем список репозиториев
})
})
})
handleUsersList
;SON.parse
, преобразовываем его, для удобства, в объект;repos_url
— это URL для наших следующих запросов, и получили мы его из первого запроса.handleReposList
. Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.try {
request(userGet, handleUsersList)
} catch (e) {
console.error('Request boom! ', e)
}
function handleUsersList(error, users) {
if (error) throw error
const list = JSON.parse(users).items
list.forEach(function(user) {
request(user.repos_url, handleReposList)
})
}
function handleReposList(err, repos) {
if (err) throw err
// Здесь обрабатываем список репозиториев
console.log('My very few repos', repos)
}
forEach
, здесь три, заключается в том, что такой код тяжело читать и поддерживать. Подобная проблема существует, пожалуй, со дня появления функций обратного вызова, она широко известна как ад коллбэков.const myPromise = new Promise(function(resolve, reject) {
// Здесь будет код
if (codeIsFine) {
resolve('fine')
} else {
reject('error')
}
})
myPromise
.then(function whenOk(response) {
console.log(response)
return response
})
.catch(function notOk(err) {
console.error(err)
})
resolve
и reject
;Promise
. Если код будет выполнен успешно, вызывают метод resolve
, если нет — reject
;resolve
, будет исполнен метод .then
для объекта Promise
, аналогично, если будет вызван reject
, будет исполнен метод .catch
.resolve
и reject
принимают только один параметр, в результате, например, при выполнении команды вида resolve('yey', 'works')
, коллбэку .then
будет передано лишь 'yey'
;.then
, в конце соответствующих коллбэков следует всегда использовать return
, иначе все они будут выполнены одновременно, а это, очевидно, не то, чего вы хотите достичь;reject
, если следующим в цепочке идёт .then
, он будет выполнен (вы можете считать .then
выражением, которое выполняется в любом случае);.then
в каком-то из них возникнет ошибка, следующие за ним будут пропущены до тех пор, пока не будет найдено выражение .catch
;resolve
или reject
, а также состояния «resolved» и «rejected», которые соответствуют успешному, с вызовом resolve
, и неуспешному, с вызовом reject
, завершению работы промиса. Когда промис оказывается в состоянии «resolved» или «rejected», оно уже не может быть изменено.function request(url) {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr.status)
}
}
}
xhr.ontimeout = function () {
reject('timeout')
}
xhr.open('get', url, true)
xhr.send();
})
}
request
, возвращено будет примерно следующее.request
, перепишем остальной код.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const myPromise = request(userGet)
console.log('will be pending when logged', myPromise)
myPromise
.then(function handleUsersList(users) {
console.log('when resolve is found it comes here with the response, in this case users ', users)
const list = JSON.parse(users).items
return Promise.all(list.map(function(user) {
return request(user.repos_url)
}))
})
.then(function handleReposList(repos) {
console.log('All users repos in an array', repos)
})
.catch(function handleErrors(error) {
console.log('when a reject is executed it will come here ignoring the then statement ', error)
})
.then
при успешном разрешении промиса. У нас имеется список пользователей. Во второе выражение .then
мы передаём массив с репозиториями. Если что-то пошло не так, мы окажемся в выражении .catch
.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const userRequest = request(userGet)
// Если просто прочитать эту часть программы вслух, можно сразу понять что именно делает код
userRequest
.then(handleUsersList)
.then(repoRequest)
.then(handleReposList)
.catch(handleErrors)
function handleUsersList(users) {
return JSON.parse(users).items
}
function repoRequest(users) {
return Promise.all(users.map(function(user) {
return request(user.repos_url)
}))
}
function handleReposList(repos) {
console.log('All users repos in an array', repos)
}
function handleErrors(error) {
console.error('Something went wrong ', error)
}
.then
раскрывает смысл вызова userRequest
. С кодом легко работать, его легко читать.function
. С помощью генераторов асинхронный код можно сделать очень похожим на синхронный. Например, выглядеть это может так:function* foo() {
yield 1
const args = yield 2
console.log(args)
}
var fooIterator = foo()
console.log(fooIterator.next().value) // выведет 1
console.log(fooIterator.next().value) // выведет 2
fooIterator.next('aParam') // приведёт к вызову console.log внутри генератора и к выводу 'aParam'
return
, используют выражение yield
, которое останавливает выполнение функции до следующего вызова .next
итератора. Это похоже на выражение .then
в промисах, которое выполняется при разрешении промиса.request
:function request(url) {
return function(callback) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr.response)
} else {
callback(xhr.status, null)
}
}
}
xhr.ontimeout = function () {
console.log('timeout')
}
xhr.open('get', url, true)
xhr.send()
}
}
url
, но вместо того, чтобы сразу выполнить запрос, мы хотим его выполнить только тогда, когда у нас будет функция обратного вызова для обработки ответа.function* list() {
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const users = yield request(userGet)
yield
for (let i = 0; i<=users.length; i++) {
yield request(users[i].repos_url)
}
}
request
, которая она принимает url
и возвращает функцию, которая ожидает коллбэк);users
, для отправки в следующий .next
;users
и ожидаем, для каждого из них, .next
, возвращая, для каждого, соответствующий коллбэк.try {
const iterator = list()
iterator.next().value(function handleUsersList(err, users) {
if (err) throw err
const list = JSON.parse(users).items
// Отправляем список пользователей итератору
iterator.next(list)
list.forEach(function(user) {
iterator.next().value(function userRepos(error, repos) {
if (error) throw repos
// Здесь обрабатываем информацию о репозиториях каждого пользователя
console.log(user, JSON.parse(repos))
})
})
})
} catch (e) {
console.error(e)
}
async
, какую функцию предполагается выполнять асинхронно, и, используя await
, сообщить системе о том, какая часть кода должна ждать разрешения соответствующего промиса.sumTwentyAfterTwoSeconds(10)
.then(result => console.log('after 2 seconds', result))
async function sumTwentyAfterTwoSeconds(value) {
const remainder = afterTwoSeconds(20)
return value + await remainder
}
function afterTwoSeconds(value) {
return new Promise(resolve => {
setTimeout(() => { resolve(value) }, 2000);
});
}
sumTwentyAfterTwoSeconds
;afterTwoSeconds
, который может завершиться вызовом resolve
или reject
;.then
, где завершается операция, отмеченная ключевым словом await
, в данном случае — это всего одна операция.request
к использовании в конструкции async/await
:function request(url) {
return new Promise(function(resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr.status)
}
}
}
xhr.ontimeout = function () {
reject('timeout')
}
xhr.open('get', url, true)
xhr.send()
})
}
async
, в которой используем ключевое слово await
:async function list() {
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const users = await request(userGet)
const usersList = JSON.parse(users).items
usersList.forEach(async function (user) {
const repos = await request(user.repos_url)
handleRepoList(user, repos)
})
}
function handleRepoList(user, repos) {
const userRepos = JSON.parse(repos)
// Обрабатываем тут репозитории для каждого пользователя
console.log(user, userRepos)
}
list
, которая обработает запрос. Ещё конструкция async/await
нам понадобится в цикле forEach
, чтобы сформировать список репозиториев. Вызвать всё это очень просто:list()
.catch(e => console.error(e))
async/await
можно почитать здесь.async/await
, как и минус генераторов, заключается в том, что эту конструкцию не поддерживают старые браузеры, а для её использования в серверной разработке нужно пользоваться Node 8. В подобной ситуации, опять же, поможет транспилятор, например — babel.async/await
. Если вы хотите как следует разобраться с тем, о чём мы говорили — поэкспериментируйте с этим кодом и со всеми рассмотренными технологиями.$.ajax
и fetch
. Если у вас есть идеи о том, как улучшить качество кода при использовании вышеописанных методик — буду благодарен, если расскажете об этом мне.
Метки: author ru_vds разработка веб-сайтов javascript блог компании ruvds.com разработка callback async await promise generator асинхронный код |
[Перевод] JavaScript: методы асинхронного программирования |
console.log('1')
console.log('2')
console.log('3')
console.log('1')
setTimeout(function afterTwoSeconds() {
console.log('2')
}, 2000)
console.log('3')
setTimeout
. Коллбэк будет вызвана, в данном примере, через 2 секунды. Приложение при этом не остановится, ожидая, пока истекут эти две секунды. Вместо этого его исполнение продолжится, а когда сработает таймер, будет вызвана функция afterTwoSeconds
.XMLHttpRequest
(XHR), но вы вполне можете использовать тут jQuery ($.ajax
), или более современный стандартный подход, основанный на использовании функции fetch
. И то и другое сводится к использованию промисов. Код, в зависимости от похода, будет меняться, но вот, для начала, такой пример:// аргумент url может быть чем-то вроде 'https://api.github.com/users/daspinola/repos'
function request(url) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Код обработки успешного завершения запроса
} else {
// Обрабатываем ответ с сообщением об ошибке
}
}
}
xhr.ontimeout = function () {
// Ожидание ответа заняло слишком много времени, тут будет код, который обрабатывает подобную ситуацию
}
xhr.open('get', url, true)
xhr.send();
}
// Вызовем функцию "doThis" с другой функцией в качестве параметра, в данном случае - это функция "andThenThis". Функция "doThis" исполнит код, находящийся в ней, после чего, в нужный момент, вызовет функцию "andThenThis".
doThis(andThenThis)
// Внутри "doThis" обращение к переданной ей функции осуществляется через параметр "callback" , фактически, это просто переменная, которая хранит ссылку на функцию
function andThenThis() {
console.log('and then this')
}
// Назвать параметр, в котором окажется функция обратного вызова, можно как угодно, "callback" - это просто распространённый вариант
function doThis(callback) {
console.log('this first')
// Для того, чтобы функция, ссылка на которую хранится в переменной, была вызвана, нужно поместить после имени переменной скобки, '()', иначе ничего не получится
callback()
}
request
:function request(url, callback) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr.response)
} else {
callback(xhr.status, null)
}
}
}
xhr.ontimeout = function () {
console.log('Timeout')
}
xhr.open('get', url, true)
xhr.send();
}
callback
, поэтому, после выполнения запроса и получения ответа сервера, коллбэк будет вызван и в случае ошибки, и в случае успешного завершения операции.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
request(userGet, function handleUsersList(error, users) {
if (error) throw error
const list = JSON.parse(users).items
list.forEach(function(user) {
request(user.repos_url, function handleReposList(err, repos) {
if (err) throw err
//Здесь обработаем список репозиториев
})
})
})
handleUsersList
;SON.parse
, преобразовываем его, для удобства, в объект;repos_url
— это URL для наших следующих запросов, и получили мы его из первого запроса.handleReposList
. Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.try {
request(userGet, handleUsersList)
} catch (e) {
console.error('Request boom! ', e)
}
function handleUsersList(error, users) {
if (error) throw error
const list = JSON.parse(users).items
list.forEach(function(user) {
request(user.repos_url, handleReposList)
})
}
function handleReposList(err, repos) {
if (err) throw err
// Здесь обрабатываем список репозиториев
console.log('My very few repos', repos)
}
forEach
, здесь три, заключается в том, что такой код тяжело читать и поддерживать. Подобная проблема существует, пожалуй, со дня появления функций обратного вызова, она широко известна как ад коллбэков.const myPromise = new Promise(function(resolve, reject) {
// Здесь будет код
if (codeIsFine) {
resolve('fine')
} else {
reject('error')
}
})
myPromise
.then(function whenOk(response) {
console.log(response)
return response
})
.catch(function notOk(err) {
console.error(err)
})
resolve
и reject
;Promise
. Если код будет выполнен успешно, вызывают метод resolve
, если нет — reject
;resolve
, будет исполнен метод .then
для объекта Promise
, аналогично, если будет вызван reject
, будет исполнен метод .catch
.resolve
и reject
принимают только один параметр, в результате, например, при выполнении команды вида resolve('yey', 'works')
, коллбэку .then
будет передано лишь 'yey'
;.then
, в конце соответствующих коллбэков следует всегда использовать return
, иначе все они будут выполнены одновременно, а это, очевидно, не то, чего вы хотите достичь;reject
, если следующим в цепочке идёт .then
, он будет выполнен (вы можете считать .then
выражением, которое выполняется в любом случае);.then
в каком-то из них возникнет ошибка, следующие за ним будут пропущены до тех пор, пока не будет найдено выражение .catch
;resolve
или reject
, а также состояния «resolved» и «rejected», которые соответствуют успешному, с вызовом resolve
, и неуспешному, с вызовом reject
, завершению работы промиса. Когда промис оказывается в состоянии «resolved» или «rejected», оно уже не может быть изменено.function request(url) {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr.status)
}
}
}
xhr.ontimeout = function () {
reject('timeout')
}
xhr.open('get', url, true)
xhr.send();
})
}
request
, возвращено будет примерно следующее.request
, перепишем остальной код.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const myPromise = request(userGet)
console.log('will be pending when logged', myPromise)
myPromise
.then(function handleUsersList(users) {
console.log('when resolve is found it comes here with the response, in this case users ', users)
const list = JSON.parse(users).items
return Promise.all(list.map(function(user) {
return request(user.repos_url)
}))
})
.then(function handleReposList(repos) {
console.log('All users repos in an array', repos)
})
.catch(function handleErrors(error) {
console.log('when a reject is executed it will come here ignoring the then statement ', error)
})
.then
при успешном разрешении промиса. У нас имеется список пользователей. Во второе выражение .then
мы передаём массив с репозиториями. Если что-то пошло не так, мы окажемся в выражении .catch
.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const userRequest = request(userGet)
// Если просто прочитать эту часть программы вслух, можно сразу понять что именно делает код
userRequest
.then(handleUsersList)
.then(repoRequest)
.then(handleReposList)
.catch(handleErrors)
function handleUsersList(users) {
return JSON.parse(users).items
}
function repoRequest(users) {
return Promise.all(users.map(function(user) {
return request(user.repos_url)
}))
}
function handleReposList(repos) {
console.log('All users repos in an array', repos)
}
function handleErrors(error) {
console.error('Something went wrong ', error)
}
.then
раскрывает смысл вызова userRequest
. С кодом легко работать, его легко читать.function
. С помощью генераторов асинхронный код можно сделать очень похожим на синхронный. Например, выглядеть это может так:function* foo() {
yield 1
const args = yield 2
console.log(args)
}
var fooIterator = foo()
console.log(fooIterator.next().value) // выведет 1
console.log(fooIterator.next().value) // выведет 2
fooIterator.next('aParam') // приведёт к вызову console.log внутри генератора и к выводу 'aParam'
return
, используют выражение yield
, которое останавливает выполнение функции до следующего вызова .next
итератора. Это похоже на выражение .then
в промисах, которое выполняется при разрешении промиса.request
:function request(url) {
return function(callback) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr.response)
} else {
callback(xhr.status, null)
}
}
}
xhr.ontimeout = function () {
console.log('timeout')
}
xhr.open('get', url, true)
xhr.send()
}
}
url
, но вместо того, чтобы сразу выполнить запрос, мы хотим его выполнить только тогда, когда у нас будет функция обратного вызова для обработки ответа.function* list() {
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const users = yield request(userGet)
yield
for (let i = 0; i<=users.length; i++) {
yield request(users[i].repos_url)
}
}
request
, которая она принимает url
и возвращает функцию, которая ожидает коллбэк);users
, для отправки в следующий .next
;users
и ожидаем, для каждого из них, .next
, возвращая, для каждого, соответствующий коллбэк.try {
const iterator = list()
iterator.next().value(function handleUsersList(err, users) {
if (err) throw err
const list = JSON.parse(users).items
// Отправляем список пользователей итератору
iterator.next(list)
list.forEach(function(user) {
iterator.next().value(function userRepos(error, repos) {
if (error) throw repos
// Здесь обрабатываем информацию о репозиториях каждого пользователя
console.log(user, JSON.parse(repos))
})
})
})
} catch (e) {
console.error(e)
}
async
, какую функцию предполагается выполнять асинхронно, и, используя await
, сообщить системе о том, какая часть кода должна ждать разрешения соответствующего промиса.sumTwentyAfterTwoSeconds(10)
.then(result => console.log('after 2 seconds', result))
async function sumTwentyAfterTwoSeconds(value) {
const remainder = afterTwoSeconds(20)
return value + await remainder
}
function afterTwoSeconds(value) {
return new Promise(resolve => {
setTimeout(() => { resolve(value) }, 2000);
});
}
sumTwentyAfterTwoSeconds
;afterTwoSeconds
, который может завершиться вызовом resolve
или reject
;.then
, где завершается операция, отмеченная ключевым словом await
, в данном случае — это всего одна операция.request
к использовании в конструкции async/await
:function request(url) {
return new Promise(function(resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr.status)
}
}
}
xhr.ontimeout = function () {
reject('timeout')
}
xhr.open('get', url, true)
xhr.send()
})
}
async
, в которой используем ключевое слово await
:async function list() {
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const users = await request(userGet)
const usersList = JSON.parse(users).items
usersList.forEach(async function (user) {
const repos = await request(user.repos_url)
handleRepoList(user, repos)
})
}
function handleRepoList(user, repos) {
const userRepos = JSON.parse(repos)
// Обрабатываем тут репозитории для каждого пользователя
console.log(user, userRepos)
}
list
, которая обработает запрос. Ещё конструкция async/await
нам понадобится в цикле forEach
, чтобы сформировать список репозиториев. Вызвать всё это очень просто:list()
.catch(e => console.error(e))
async/await
можно почитать здесь.async/await
, как и минус генераторов, заключается в том, что эту конструкцию не поддерживают старые браузеры, а для её использования в серверной разработке нужно пользоваться Node 8. В подобной ситуации, опять же, поможет транспилятор, например — babel.async/await
. Если вы хотите как следует разобраться с тем, о чём мы говорили — поэкспериментируйте с этим кодом и со всеми рассмотренными технологиями.$.ajax
и fetch
. Если у вас есть идеи о том, как улучшить качество кода при использовании вышеописанных методик — буду благодарен, если расскажете об этом мне.
Метки: author ru_vds разработка веб-сайтов javascript блог компании ruvds.com разработка callback async await promise generator асинхронный код |
[Перевод] JavaScript: методы асинхронного программирования |
console.log('1')
console.log('2')
console.log('3')
console.log('1')
setTimeout(function afterTwoSeconds() {
console.log('2')
}, 2000)
console.log('3')
setTimeout
. Коллбэк будет вызвана, в данном примере, через 2 секунды. Приложение при этом не остановится, ожидая, пока истекут эти две секунды. Вместо этого его исполнение продолжится, а когда сработает таймер, будет вызвана функция afterTwoSeconds
.XMLHttpRequest
(XHR), но вы вполне можете использовать тут jQuery ($.ajax
), или более современный стандартный подход, основанный на использовании функции fetch
. И то и другое сводится к использованию промисов. Код, в зависимости от похода, будет меняться, но вот, для начала, такой пример:// аргумент url может быть чем-то вроде 'https://api.github.com/users/daspinola/repos'
function request(url) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Код обработки успешного завершения запроса
} else {
// Обрабатываем ответ с сообщением об ошибке
}
}
}
xhr.ontimeout = function () {
// Ожидание ответа заняло слишком много времени, тут будет код, который обрабатывает подобную ситуацию
}
xhr.open('get', url, true)
xhr.send();
}
// Вызовем функцию "doThis" с другой функцией в качестве параметра, в данном случае - это функция "andThenThis". Функция "doThis" исполнит код, находящийся в ней, после чего, в нужный момент, вызовет функцию "andThenThis".
doThis(andThenThis)
// Внутри "doThis" обращение к переданной ей функции осуществляется через параметр "callback" , фактически, это просто переменная, которая хранит ссылку на функцию
function andThenThis() {
console.log('and then this')
}
// Назвать параметр, в котором окажется функция обратного вызова, можно как угодно, "callback" - это просто распространённый вариант
function doThis(callback) {
console.log('this first')
// Для того, чтобы функция, ссылка на которую хранится в переменной, была вызвана, нужно поместить после имени переменной скобки, '()', иначе ничего не получится
callback()
}
request
:function request(url, callback) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr.response)
} else {
callback(xhr.status, null)
}
}
}
xhr.ontimeout = function () {
console.log('Timeout')
}
xhr.open('get', url, true)
xhr.send();
}
callback
, поэтому, после выполнения запроса и получения ответа сервера, коллбэк будет вызван и в случае ошибки, и в случае успешного завершения операции.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
request(userGet, function handleUsersList(error, users) {
if (error) throw error
const list = JSON.parse(users).items
list.forEach(function(user) {
request(user.repos_url, function handleReposList(err, repos) {
if (err) throw err
//Здесь обработаем список репозиториев
})
})
})
handleUsersList
;SON.parse
, преобразовываем его, для удобства, в объект;repos_url
— это URL для наших следующих запросов, и получили мы его из первого запроса.handleReposList
. Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.try {
request(userGet, handleUsersList)
} catch (e) {
console.error('Request boom! ', e)
}
function handleUsersList(error, users) {
if (error) throw error
const list = JSON.parse(users).items
list.forEach(function(user) {
request(user.repos_url, handleReposList)
})
}
function handleReposList(err, repos) {
if (err) throw err
// Здесь обрабатываем список репозиториев
console.log('My very few repos', repos)
}
forEach
, здесь три, заключается в том, что такой код тяжело читать и поддерживать. Подобная проблема существует, пожалуй, со дня появления функций обратного вызова, она широко известна как ад коллбэков.const myPromise = new Promise(function(resolve, reject) {
// Здесь будет код
if (codeIsFine) {
resolve('fine')
} else {
reject('error')
}
})
myPromise
.then(function whenOk(response) {
console.log(response)
return response
})
.catch(function notOk(err) {
console.error(err)
})
resolve
и reject
;Promise
. Если код будет выполнен успешно, вызывают метод resolve
, если нет — reject
;resolve
, будет исполнен метод .then
для объекта Promise
, аналогично, если будет вызван reject
, будет исполнен метод .catch
.resolve
и reject
принимают только один параметр, в результате, например, при выполнении команды вида resolve('yey', 'works')
, коллбэку .then
будет передано лишь 'yey'
;.then
, в конце соответствующих коллбэков следует всегда использовать return
, иначе все они будут выполнены одновременно, а это, очевидно, не то, чего вы хотите достичь;reject
, если следующим в цепочке идёт .then
, он будет выполнен (вы можете считать .then
выражением, которое выполняется в любом случае);.then
в каком-то из них возникнет ошибка, следующие за ним будут пропущены до тех пор, пока не будет найдено выражение .catch
;resolve
или reject
, а также состояния «resolved» и «rejected», которые соответствуют успешному, с вызовом resolve
, и неуспешному, с вызовом reject
, завершению работы промиса. Когда промис оказывается в состоянии «resolved» или «rejected», оно уже не может быть изменено.function request(url) {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr.status)
}
}
}
xhr.ontimeout = function () {
reject('timeout')
}
xhr.open('get', url, true)
xhr.send();
})
}
request
, возвращено будет примерно следующее.request
, перепишем остальной код.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const myPromise = request(userGet)
console.log('will be pending when logged', myPromise)
myPromise
.then(function handleUsersList(users) {
console.log('when resolve is found it comes here with the response, in this case users ', users)
const list = JSON.parse(users).items
return Promise.all(list.map(function(user) {
return request(user.repos_url)
}))
})
.then(function handleReposList(repos) {
console.log('All users repos in an array', repos)
})
.catch(function handleErrors(error) {
console.log('when a reject is executed it will come here ignoring the then statement ', error)
})
.then
при успешном разрешении промиса. У нас имеется список пользователей. Во второе выражение .then
мы передаём массив с репозиториями. Если что-то пошло не так, мы окажемся в выражении .catch
.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const userRequest = request(userGet)
// Если просто прочитать эту часть программы вслух, можно сразу понять что именно делает код
userRequest
.then(handleUsersList)
.then(repoRequest)
.then(handleReposList)
.catch(handleErrors)
function handleUsersList(users) {
return JSON.parse(users).items
}
function repoRequest(users) {
return Promise.all(users.map(function(user) {
return request(user.repos_url)
}))
}
function handleReposList(repos) {
console.log('All users repos in an array', repos)
}
function handleErrors(error) {
console.error('Something went wrong ', error)
}
.then
раскрывает смысл вызова userRequest
. С кодом легко работать, его легко читать.function
. С помощью генераторов асинхронный код можно сделать очень похожим на синхронный. Например, выглядеть это может так:function* foo() {
yield 1
const args = yield 2
console.log(args)
}
var fooIterator = foo()
console.log(fooIterator.next().value) // выведет 1
console.log(fooIterator.next().value) // выведет 2
fooIterator.next('aParam') // приведёт к вызову console.log внутри генератора и к выводу 'aParam'
return
, используют выражение yield
, которое останавливает выполнение функции до следующего вызова .next
итератора. Это похоже на выражение .then
в промисах, которое выполняется при разрешении промиса.request
:function request(url) {
return function(callback) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr.response)
} else {
callback(xhr.status, null)
}
}
}
xhr.ontimeout = function () {
console.log('timeout')
}
xhr.open('get', url, true)
xhr.send()
}
}
url
, но вместо того, чтобы сразу выполнить запрос, мы хотим его выполнить только тогда, когда у нас будет функция обратного вызова для обработки ответа.function* list() {
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const users = yield request(userGet)
yield
for (let i = 0; i<=users.length; i++) {
yield request(users[i].repos_url)
}
}
request
, которая она принимает url
и возвращает функцию, которая ожидает коллбэк);users
, для отправки в следующий .next
;users
и ожидаем, для каждого из них, .next
, возвращая, для каждого, соответствующий коллбэк.try {
const iterator = list()
iterator.next().value(function handleUsersList(err, users) {
if (err) throw err
const list = JSON.parse(users).items
// Отправляем список пользователей итератору
iterator.next(list)
list.forEach(function(user) {
iterator.next().value(function userRepos(error, repos) {
if (error) throw repos
// Здесь обрабатываем информацию о репозиториях каждого пользователя
console.log(user, JSON.parse(repos))
})
})
})
} catch (e) {
console.error(e)
}
async
, какую функцию предполагается выполнять асинхронно, и, используя await
, сообщить системе о том, какая часть кода должна ждать разрешения соответствующего промиса.sumTwentyAfterTwoSeconds(10)
.then(result => console.log('after 2 seconds', result))
async function sumTwentyAfterTwoSeconds(value) {
const remainder = afterTwoSeconds(20)
return value + await remainder
}
function afterTwoSeconds(value) {
return new Promise(resolve => {
setTimeout(() => { resolve(value) }, 2000);
});
}
sumTwentyAfterTwoSeconds
;afterTwoSeconds
, который может завершиться вызовом resolve
или reject
;.then
, где завершается операция, отмеченная ключевым словом await
, в данном случае — это всего одна операция.request
к использовании в конструкции async/await
:function request(url) {
return new Promise(function(resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr.status)
}
}
}
xhr.ontimeout = function () {
reject('timeout')
}
xhr.open('get', url, true)
xhr.send()
})
}
async
, в которой используем ключевое слово await
:async function list() {
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const users = await request(userGet)
const usersList = JSON.parse(users).items
usersList.forEach(async function (user) {
const repos = await request(user.repos_url)
handleRepoList(user, repos)
})
}
function handleRepoList(user, repos) {
const userRepos = JSON.parse(repos)
// Обрабатываем тут репозитории для каждого пользователя
console.log(user, userRepos)
}
list
, которая обработает запрос. Ещё конструкция async/await
нам понадобится в цикле forEach
, чтобы сформировать список репозиториев. Вызвать всё это очень просто:list()
.catch(e => console.error(e))
async/await
можно почитать здесь.async/await
, как и минус генераторов, заключается в том, что эту конструкцию не поддерживают старые браузеры, а для её использования в серверной разработке нужно пользоваться Node 8. В подобной ситуации, опять же, поможет транспилятор, например — babel.async/await
. Если вы хотите как следует разобраться с тем, о чём мы говорили — поэкспериментируйте с этим кодом и со всеми рассмотренными технологиями.$.ajax
и fetch
. Если у вас есть идеи о том, как улучшить качество кода при использовании вышеописанных методик — буду благодарен, если расскажете об этом мне.
Метки: author ru_vds разработка веб-сайтов javascript блог компании ruvds.com разработка callback async await promise generator асинхронный код |
Метавычисления и глубокие свёрточные сети: интервью с профессором ИТМО |
Вот, например, несколько неплохих курсов:
Introduction into Deep Learning
Neural Networks and Deep Learning
Convolutional Neural Networks for Visual Recognition
Метки: author ARG89 машинное обучение big data блог компании jug.ru group deep learning |
Метавычисления и глубокие свёрточные сети: интервью с профессором ИТМО |
Вот, например, несколько неплохих курсов:
Introduction into Deep Learning
Neural Networks and Deep Learning
Convolutional Neural Networks for Visual Recognition
Метки: author ARG89 машинное обучение big data блог компании jug.ru group deep learning |
Метавычисления и глубокие свёрточные сети: интервью с профессором ИТМО |
Вот, например, несколько неплохих курсов:
Introduction into Deep Learning
Neural Networks and Deep Learning
Convolutional Neural Networks for Visual Recognition
Метки: author ARG89 машинное обучение big data блог компании jug.ru group deep learning |
Технокубок 2017-2018 |
Каждый год Министерство образования и науки РФ публикует перечень школьных олимпиад, дающих льготы при поступлении в вузы. С 2015 года в этот список входит и Технокубок — олимпиада по программированию под эгидой Mail.Ru Group.
Поучаствовать в Технокубке могут ученики 8—11-х классов. Олимпиада позволяет ребятам оценить свои силы и пообщаться с профессионалами IT-отрасли, а главное — дает шанс поступить в ведущие профильные вузы России.
В 2017/18 учебном году Технокубок стал олимпиадой II уровня. Каждый вуз определяет льготу самостоятельно — это может быть максимальный балл по информатике или зачисление без вступительных испытаний. Судить о льготах на текущий год можно исходя из прошлогодних привилегий: как правило, эти сведения доступны на сайтах вузов. Также информация есть здесь.
Зарегистрироваться на Технокубок можно до 12 ноября. Заочные отборочные раунды пройдут 17 сентября, 15 октября и 12 ноября. Для того чтобы попасть в финал, достаточно победить в одном из трех отборочных раундов. Для интересующихся — примеры и разборы задач прошлых лет:
Технокубок 2015/16
1-й и 2-й отборочный раунд
Финал
Технокубок 2016/17
1-й отборочный раунд
2-й отборочный раунд
3-й отборочный раунд
Финал
В преддверии каждого отборочного тура проводятся ознакомительные онлайн-раунды. Они нужны для того, чтобы школьники разобрались в платформе Codeforces, на базе которой проводятся основные этапы. Во время ознакомительных раундов можно потренироваться на двух простых задачах, их результаты никак не повлияют на итоговый рейтинг.
Очный финал состоится в марте 2018-го на двух московских площадках: в МГТУ им. Н. Э. Баумана и в МФТИ. Награждение победителей пройдет в офисе Mail.Ru Group.
Каждый год в Технокубке участвует около 3 тысяч школьников из России и стран СНГ. Всего за время существования олимпиады в ней приняли участие более 7 тысяч человек.
Участникам, занявшим первые три места, помимо льгот при поступлении достанутся ценные призы. Кроме того, финалисты, которые попадут в МГТУ им. Н. Э. Баумана и в МФТИ, получат привилегии при поступлении в Технопарк и Технотрек — бесплатные практико-ориентированные образовательные проекты Mail.Ru Group.
В составе оргкомитета олимпиады — председатель совета директоров и сооснователь Mail.Ru Group Дмитрий Гришин. Сопредседатели оргкомитета — ректор МФТИ Н. Н. Кудрявцев и ректор МГТУ им. Н. Э. Баумана А. А. Александров. Председатель жюри — Михаил Мирзаянов, старший преподаватель кафедры математических основ информатики и олимпиадного программирования Саратовского государственного университета, основатель и генеральный директор Codeforces, дважды серебряный призер чемпионата мира по программированию ACM-ICPC.
Зарегистрироваться, узнать подробности и задать вопросы можно на сайте олимпиады и в группе в ВКонтакте. Технокубок-2018 ждет новых героев!
Метки: author sat2707 спортивное программирование программирование блог компании mail.ru group технокубок олимпиада школьников поступление |
Технокубок 2017-2018 |
Каждый год Министерство образования и науки РФ публикует перечень школьных олимпиад, дающих льготы при поступлении в вузы. С 2015 года в этот список входит и Технокубок — олимпиада по программированию под эгидой Mail.Ru Group.
Поучаствовать в Технокубке могут ученики 8—11-х классов. Олимпиада позволяет ребятам оценить свои силы и пообщаться с профессионалами IT-отрасли, а главное — дает шанс поступить в ведущие профильные вузы России.
В 2017/18 учебном году Технокубок стал олимпиадой II уровня. Каждый вуз определяет льготу самостоятельно — это может быть максимальный балл по информатике или зачисление без вступительных испытаний. Судить о льготах на текущий год можно исходя из прошлогодних привилегий: как правило, эти сведения доступны на сайтах вузов. Также информация есть здесь.
Зарегистрироваться на Технокубок можно до 12 ноября. Заочные отборочные раунды пройдут 17 сентября, 15 октября и 12 ноября. Для того чтобы попасть в финал, достаточно победить в одном из трех отборочных раундов. Для интересующихся — примеры и разборы задач прошлых лет:
Технокубок 2015/16
1-й и 2-й отборочный раунд
Финал
Технокубок 2016/17
1-й отборочный раунд
2-й отборочный раунд
3-й отборочный раунд
Финал
В преддверии каждого отборочного тура проводятся ознакомительные онлайн-раунды. Они нужны для того, чтобы школьники разобрались в платформе Codeforces, на базе которой проводятся основные этапы. Во время ознакомительных раундов можно потренироваться на двух простых задачах, их результаты никак не повлияют на итоговый рейтинг.
Очный финал состоится в марте 2018-го на двух московских площадках: в МГТУ им. Н. Э. Баумана и в МФТИ. Награждение победителей пройдет в офисе Mail.Ru Group.
Каждый год в Технокубке участвует около 3 тысяч школьников из России и стран СНГ. Всего за время существования олимпиады в ней приняли участие более 7 тысяч человек.
Участникам, занявшим первые три места, помимо льгот при поступлении достанутся ценные призы. Кроме того, финалисты, которые попадут в МГТУ им. Н. Э. Баумана и в МФТИ, получат привилегии при поступлении в Технопарк и Технотрек — бесплатные практико-ориентированные образовательные проекты Mail.Ru Group.
В составе оргкомитета олимпиады — председатель совета директоров и сооснователь Mail.Ru Group Дмитрий Гришин. Сопредседатели оргкомитета — ректор МФТИ Н. Н. Кудрявцев и ректор МГТУ им. Н. Э. Баумана А. А. Александров. Председатель жюри — Михаил Мирзаянов, старший преподаватель кафедры математических основ информатики и олимпиадного программирования Саратовского государственного университета, основатель и генеральный директор Codeforces, дважды серебряный призер чемпионата мира по программированию ACM-ICPC.
Зарегистрироваться, узнать подробности и задать вопросы можно на сайте олимпиады и в группе в ВКонтакте. Технокубок-2018 ждет новых героев!
Метки: author sat2707 спортивное программирование программирование блог компании mail.ru group технокубок олимпиада школьников поступление |
Технокубок 2017-2018 |
Каждый год Министерство образования и науки РФ публикует перечень школьных олимпиад, дающих льготы при поступлении в вузы. С 2015 года в этот список входит и Технокубок — олимпиада по программированию под эгидой Mail.Ru Group.
Поучаствовать в Технокубке могут ученики 8—11-х классов. Олимпиада позволяет ребятам оценить свои силы и пообщаться с профессионалами IT-отрасли, а главное — дает шанс поступить в ведущие профильные вузы России.
В 2017/18 учебном году Технокубок стал олимпиадой II уровня. Каждый вуз определяет льготу самостоятельно — это может быть максимальный балл по информатике или зачисление без вступительных испытаний. Судить о льготах на текущий год можно исходя из прошлогодних привилегий: как правило, эти сведения доступны на сайтах вузов. Также информация есть здесь.
Зарегистрироваться на Технокубок можно до 12 ноября. Заочные отборочные раунды пройдут 17 сентября, 15 октября и 12 ноября. Для того чтобы попасть в финал, достаточно победить в одном из трех отборочных раундов. Для интересующихся — примеры и разборы задач прошлых лет:
Технокубок 2015/16
1-й и 2-й отборочный раунд
Финал
Технокубок 2016/17
1-й отборочный раунд
2-й отборочный раунд
3-й отборочный раунд
Финал
В преддверии каждого отборочного тура проводятся ознакомительные онлайн-раунды. Они нужны для того, чтобы школьники разобрались в платформе Codeforces, на базе которой проводятся основные этапы. Во время ознакомительных раундов можно потренироваться на двух простых задачах, их результаты никак не повлияют на итоговый рейтинг.
Очный финал состоится в марте 2018-го на двух московских площадках: в МГТУ им. Н. Э. Баумана и в МФТИ. Награждение победителей пройдет в офисе Mail.Ru Group.
Каждый год в Технокубке участвует около 3 тысяч школьников из России и стран СНГ. Всего за время существования олимпиады в ней приняли участие более 7 тысяч человек.
Участникам, занявшим первые три места, помимо льгот при поступлении достанутся ценные призы. Кроме того, финалисты, которые попадут в МГТУ им. Н. Э. Баумана и в МФТИ, получат привилегии при поступлении в Технопарк и Технотрек — бесплатные практико-ориентированные образовательные проекты Mail.Ru Group.
В составе оргкомитета олимпиады — председатель совета директоров и сооснователь Mail.Ru Group Дмитрий Гришин. Сопредседатели оргкомитета — ректор МФТИ Н. Н. Кудрявцев и ректор МГТУ им. Н. Э. Баумана А. А. Александров. Председатель жюри — Михаил Мирзаянов, старший преподаватель кафедры математических основ информатики и олимпиадного программирования Саратовского государственного университета, основатель и генеральный директор Codeforces, дважды серебряный призер чемпионата мира по программированию ACM-ICPC.
Зарегистрироваться, узнать подробности и задать вопросы можно на сайте олимпиады и в группе в ВКонтакте. Технокубок-2018 ждет новых героев!
Метки: author sat2707 спортивное программирование программирование блог компании mail.ru group технокубок олимпиада школьников поступление |
Сегментация лица на селфи без нейросетей |
Приветствую вас, коллеги. Оказывается, не все компьютерное зрение сегодня делается с использованием нейронных сетей. Хотя многие стартапы и заявляют, что у них дип лернинг везде, спешу вас разочаровать, они просто хотят хайпануть немножечко. Рассмотрим, например, задачу сегментации. В нашем слаке развернулась целая драма. Одна богатая и высокотехнологичная селфи-компания собрала датасет для сегментации селфи с помощью нейросетей (а это непростое и недешевое занятие). А другая, более бедная и не очень развитая решила, что можно подкупить людей, размечающих фотки, и
спполучить базу. В общем, страсти в этих ваших Интернетах еще те. Недавно я наткнулся на статью, где без всяких нейросетей на устройстве делают очень даже хорошую сегментацию. Для сегментации от пользователя требуется дать алгоритму несколько подсказок, но с помощью dlib и opencv такие подсказки легко автоматизируются. В качестве бонуса мы так же сгладим вырезанное лицо и перенесем на какого-нибудь рандомного человека, тем самым поймем, как работают маски во всех этих снапчятах и маскарадах. В общем, классика еще жива, и если вы хотите немного окунуться в классическое компьютерное зрение на питоне, то добро пожаловать под кат.
Кратко опишем алгоритм, а затем перейдем к его реализации по шагам. Допустим, у нас есть некоторое изображение, мы просим пользователя нарисовать на изображении две кривых. Первая (синий цвет) должна полностью принадлежать объекту интереса. Вторая (зеленый цвет) должна касаться только фона изображения.
Далее делаем следующие шаги:
Дальнейший материал будет разбавляться вставками кода на питоне, если вы планируете выполнять его по мере чтения поста, то вам понадобятся следующие импорты:
%matplotlib inline
import matplotlib
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style("dark")
plt.rcParams['figure.figsize'] = 16, 12
import pandas as pd
from PIL import Image
from tqdm import tqdm_notebook
from skimage import transform
import itertools as it
from sklearn.neighbors.kde import KernelDensity
import matplotlib.cm as cm
import queue
from skimage import morphology
import dlib
import cv2
from imutils import face_utils
from scipy.spatial import Delaunay
Идея того, как автоматизировать штрихи была навеяна приложением FaceApp, которое якобы использует нейросети для трансформации. Как мне кажется, они если и используют сети где то, то только в детектировании особых точек на лице. Взгляните на скриншот справа, они предлагают выровнять свое лицо по контуру. Вероятно, алгоритм детекции обучен примерно на таком масштабе. Как только лицо попадает в контур, сама рамка контура исчезает, значит особые точки вычислились. Позвольте вам представить сегодняшнего подопытного, а так-же напомнить, что из себя представляют эти самые особые точки на лице.
img_input = np.array(Image.open('./../data/input2.jpg'))[:500, 400:, :]
print(img_input.shape)
plt.imshow(img_input)
Теперь воспользуемся возможностями бесплатного программного обеспечения с открытым исходным кодом и найдем рамку вокруг лица и особые точки на лице, всего их 62.
# инстанцируем класс для детекции лиц (рамка)
detector = dlib.get_frontal_face_detector()
# инстанцируем класс для детекции ключевых точек
predictor = dlib.shape_predictor('./../data/shape_predictor_68_face_landmarks.dat')
# конвертируем изображение в много оттенков серого
img_gray = cv2.cvtColor(img_input, cv2.COLOR_BGR2GRAY)
# вычисляем список рамок на каждое найденное лицо
rects = detector(img_gray, 0)
# вычисляем ключевые точки
shape = predictor(img_gray, rects[0])
shape = face_utils.shape_to_np(shape)
img_tmp = img_input.copy()
for x, y in shape:
cv2.circle(img_tmp, (x, y), 1, (0, 0, 255), -1)
plt.imshow(img_tmp)
Оригинальная рамка на лице слишком мелкая (зеленый цвет), нам понадобится рамка, которая полностью содержит в себе лицо с некоторым зазором (красный цвет). Коэффициенты расширения рамки получены эмпирическим путем с помощью анализа нескольких десятков селфи разного масштаба и разных людей.
# оригинальная рамка
face_origin = sorted([(t.width()*t.height(),
(t.left(), t.top(), t.width(), t.height()))
for t in rects],
key=lambda t: t[0], reverse=True)[0][1]
# коэффициенты расширения рамки
rescale = (1.3, 2.2, 1.3, 1.3)
# расширение рамки, так чтобы она не вылезла за края
(x, y, w, h) = face_origin
cx = x + w/2
cy = y + h/2
w = min(img_input.shape[1] - x, int(w/2 + rescale[2]*w/2))
h = min(img_input.shape[0] - y, int(h/2 + rescale[3]*h/2))
fx = max(0, int(x + w/2*(1 - rescale[0])))
fy = max(0, int(y + h/2*(1 - rescale[1])))
fw = min(img_input.shape[1] - fx, int(w - w/2*(1 - rescale[0])))
fh = min(img_input.shape[0] - fy, int(h - h/2*(1 - rescale[1])))
face = (fx, fy, fw, fh)
img_tmp = cv2.rectangle(img_input.copy(), (face[0], face[1]),
(face[0] + face[2], face[1] + face[3]),
(255, 0, 0), thickness=3, lineType=8, shift=0)
img_tmp = cv2.rectangle(img_tmp, (face_origin[0], face_origin[1]),
(face_origin[0] + face_origin[2], face_origin[1] + face_origin[3]),
(0, 255, 0), thickness=3, lineType=8, shift=0)
plt.imshow(img_tmp)
Теперь у нас имеется область, которая точно не относится к лицу — всё, что вне красной рамки. Выберем оттуда некоторое количество случайных точек и будем считать их штрихами фона. Также у нас имеются 62 точки, которые точно расположены на лице. Для упрощения задачи я выберу 5 из них: по одной на уровне глаз на краю лица, по одной на уровне рта на краю лица и одну внизу посередине подбородка. Все точки внутри этого пятиугольника будут принадлежать только лицу. Опять же для простоты будем считать, что лицо вертикально расположено на изображении и потому мы можем отразить полученный пятиугольник по оси , тем самым получив восьмиугольник. Все, что внутри восьмиугольника будем считать штрихом объекта.
# выбираем вышеописанные пять точек
points = [shape[0].tolist(), shape[16].tolist()]
for ix in [4, 12, 8]:
x, y = shape[ix].tolist()
points.append((x, y))
points.append((x, points[0][1] + points[0][1] - y))
# я не особо в прототипе запариваюсь над производительностью
# так что вызываю триангуляцию Делоне,
# чтобы использовать ее как тест на то, что точка внутри полигона
# все это можно делать быстрее, т.к. точный тест не нужен
# для прототипа :good-enough:
hull = Delaunay(points)
xy_fg = []
for x, y in it.product(range(img_input.shape[0]), range(img_input.shape[1])):
if hull.find_simplex([y, x]) >= 0:
xy_fg.append((x, y))
print('xy_fg%:', len(xy_fg)/np.prod(img_input.shape))
# вычисляем количество точек для фона
# примерно равно что бы было тому, что на лице
r = face[1]*face[3]/np.prod(img_input.shape[:2])
print(r)
k = 0.1
xy_bg_n = int(k*np.prod(img_input.shape[:2]))
print(xy_bg_n)
# накидываем случайные точки
xy_bg = zip(np.random.uniform(0, img_input.shape[0], size=xy_bg_n).astype(np.int),
np.random.uniform(0, img_input.shape[1], size=xy_bg_n).astype(np.int))
xy_bg = list(xy_bg)
xy_bg = [(x, y) for (x, y) in xy_bg
if y < face[0] or y > face[0] + face[2] or x < face[1] or x > face[1] + face[3]]
print(len(xy_bg)/np.prod(img_input.shape[:2]))
img_tmp = img_input/255
for x, y in xy_fg:
img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([1, 0, 0]) * 0.5
for x, y in xy_bg:
img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([0, 0, 1]) * 0.5
plt.imshow(img_tmp)
Теперь у нас есть два набора данных: точки объекта и точки фона .
points_fg = np.array([img_input[x, y, :] for (x, y) in xy_fg])
points_bg = np.array([img_input[x, y, :] for (x, y) in xy_bg])
Посмотрим на распределение цветов по RGB каналам в каждом из множеств. Первая гистограмма — для объекта, вторая — для фона.
fig, axes = plt.subplots(nrows=2, ncols=1)
sns.distplot(points_fg[:, 0], ax=axes[0], color='r')
sns.distplot(points_fg[:, 1], ax=axes[0], color='g')
sns.distplot(points_fg[:, 2], ax=axes[0], color='b')
sns.distplot(points_bg[:, 0], ax=axes[1], color='r')
sns.distplot(points_bg[:, 1], ax=axes[1], color='g')
sns.distplot(points_bg[:, 2], ax=axes[1], color='b')
Радует, что распределения отличаются. Это значит, что если мы сможем получить функции, оценивающие вероятность принадлежности точки к нужному распределению, то мы получим нечеткие маски. И оказывается такой способ есть — kernel density estimation. Для заданного набора точек, можно построить функцию оценки плотности для новой точки следующим образом (для простоты пример для одномерного распределения):
где:
Мы для простоты будем использовать Гауссово ядро:
Хотя для скорости Гауссово ядро не лучший выбор и если взять ядро Епанечникова, то все будет считаться быстрее. Так же я буду использовать KernelDensity из sklearn, что в итоге выльется в 5 минут скоринга. Авторы этой статьи утверждают, что замена KDE на оптимальную реализацию сокращает расчеты на устройстве до одной секунды.
# инстанцируем классы KDE для объекта и фона
kde_fg = KernelDensity(kernel='gaussian',
bandwidth=1,
algorithm='kd_tree',
leaf_size=100).fit(points_fg)
kde_bg = KernelDensity(kernel='gaussian',
bandwidth=1,
algorithm='kd_tree',
leaf_size=100).fit(points_bg)
# инициализируем и вычисляем маски
score_kde_fg = np.zeros(img_input.shape[:2])
score_kde_bg = np.zeros(img_input.shape[:2])
likelihood_fg = np.zeros(img_input.shape[:2])
coodinates = it.product(range(score_kde_fg.shape[0]),
range(score_kde_fg.shape[1]))
for x, y in tqdm_notebook(coodinates,
total=np.prod(score_kde_fg.shape)):
score_kde_fg[x, y] = np.exp(kde_fg.score(img_input[x, y, :].reshape(1, -1)))
score_kde_bg[x, y] = np.exp(kde_bg.score(img_input[x, y, :].reshape(1, -1)))
n = score_kde_fg[x, y] + score_kde_bg[x, y]
if n == 0:
n = 1
likelihood_fg[x, y] = score_kde_fg[x, y]/n
В итоге у нас есть несколько масок:
score_kde_fg
— оценка вероятности быть точкой объекта score_kde_bg
— оценка вероятности быть точкой объекта likelihood_fg
— нормализированная вероятность быть точкой объекта 1 - likelihood_fg
нормализированная вероятность быть точкой фона Посмотрим на следующие распределения.
sns.distplot(score_kde_fg.flatten())
plt.show()
sns.distplot(score_kde_bg.flatten())
plt.show()
Распределение значений likelihood_fg:
sns.distplot(likelihood_fg.flatten())
plt.show()
Вселяет надежду то, что на есть два пика, и количество точек, принадлежащих лицу, явно не меньше, чем фоновых точек. Нарисуем полученные маски.
plt.matshow(score_kde_fg, cmap=cm.bwr)
plt.show()
plt.matshow(score_kde_bg, cmap=cm.bwr)
plt.show()
plt.matshow(likelihood_fg, cmap=cm.bwr)
plt.show()
plt.matshow(1 - likelihood_fg, cmap=cm.bwr)
plt.show()
К сожалению, часть косяка двери получилась частью лица. Хорошо, что косяк далеко от лица. Этим-то свойством мы и воспользуемся в следущей части.
Представим изображение как граф, узлами которого являются пиксели, а ребрами соединены точки сверху и снизу от текущей точки, а так же справа и слева от неё. Весами ребер будем считать абсолютное значение разницы вероятностей принадлежности точек к объекту или к фону:
Соответственно, чем вероятности ближе к друг другу, тем меньше вес ребра между точками. Воспользуемся алгоритмом Дейкстры для поиска наикратчайших путей и их расстояний от точки до всех остальных. Алгоритм мы вызовем два раза, подав на вход все вероятности принадлежности в объекту и затем вероятности принадлежности точек к фону. Понятие расстояния зашьем сразу в алгоритм, а расстояние между точками, принадлежащими одной группе (объекту или фону), будет равно нулю. В рамках алгоритма Дейкстры мы можем поместить все эти точки в группу посещенных вершин.
def dijkstra(start_points, w):
d = np.zeros(w.shape) + np.infty
v = np.zeros(w.shape, dtype=np.bool)
q = queue.PriorityQueue()
for x, y in start_points:
d[x, y] = 0
q.put((d[x, y], (x, y)))
for x, y in it.product(range(w.shape[0]), range(w.shape[1])):
if np.isinf(d[x, y]):
q.put((d[x, y], (x, y)))
while not q.empty():
_, p = q.get()
if v[p]:
continue
neighbourhood = []
if p[0] - 1 >= 0:
neighbourhood.append((p[0] - 1, p[1]))
if p[0] + 1 <= w.shape[0] - 1:
neighbourhood.append((p[0] + 1, p[1]))
if p[1] - 1 >= 0:
neighbourhood.append((p[0], p[1] - 1))
if p[1] + 1 < w.shape[1]:
neighbourhood.append((p[0], p[1] + 1))
for x, y in neighbourhood:
# тут вычисляется расстояние
d_tmp = d[p] + np.abs(w[x, y] - w[p])
if d[x, y] > d_tmp:
d[x, y] = d_tmp
q.put((d[x, y], (x, y)))
v[p] = True
return d
# вызываем алгоритм для двух масок
d_fg = dijkstra(xy_fg, likelihood_fg)
d_bg = dijkstra(xy_bg, 1 - likelihood_fg)
plt.matshow(d_fg, cmap=cm.bwr)
plt.show()
plt.matshow(d_bg, cmap=cm.bwr)
plt.show()
А теперь относим к объекту все те точки, от которых расстояние до объекта меньше чем расстояния до фона (можно добавить некоторый зазор).
margin = 0.0
mask = (d_fg < (d_bg + margin)).astype(np.uint8)
plt.matshow(mask)
plt.show()
Можно отправить себя в космос.
img_fg = img_input/255.0
img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
x = int(img_bg.shape[0] - img_fg.shape[0])
y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
mask_3d = np.dstack([mask, mask, mask])
img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
plt.imshow(img_bg)
Вы наверняка заметили, что маска слегка рваная на краях. Но это легко исправить методами математической морфологии.
Допустим, у нас есть структурный элемент (СЭ) типа "диск" — бинарная маска диска.
Мы воспользуемся размыканием, что сначала удалит "волосатость" по краям, а потом вернет первоначальный размер (объект "похудеет" после эрозии).
mask = morphology.opening(mask, morphology.disk(11))
plt.imshow(mask)
После применения такой маски результат станет поприятнее:
img_fg = img_input/255.0
img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
x = int(img_bg.shape[0] - img_fg.shape[0])
y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
mask_3d = np.dstack([mask, mask, mask])
img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = \
mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
plt.imshow(img_bg)
Возьмем случайную фотку из интернетов для эксперимента по переносу лица.
img_target = np.array(Image.open('./../data/target.jpg'))
img_target = (transform.rescale(img_target, scale=0.5, mode='constant')*255).astype(np.uint8)
print(img_target.shape)
plt.imshow(img_target)
Найдем на подопытном все 62 колючевые точки лица, напомню, что они будут в том же порядке как и на любом другом лице.
img_gray = cv2.cvtColor(img_target, cv2.COLOR_BGR2GRAY)
rects_target = detector(img_gray, 0)
shape_target = predictor(img_gray, rects_target[0])
shape_target = face_utils.shape_to_np(shape_target)
Чтобы перенести одно лицо на другое, нужно первое лицо отмасштабировать под новое, повернуть и передвинуть, т.е. применить некоторое аффинное преобразование к первому лицу. Оказывается, что и аффинное преобразование не некоторое, а вполне конкретное. Оно должно быть таким, которое переводит 62 точки первого лица в 62 точки второго лица. Получается, что для получения оператора аффинного преобразования нам необходимо решить задачу линейной регрессии.
Данное уравнение легко решается с помощью псевдообратной матрицы:
Так и сделаем:
# добавим справа к матрицам колонку единиц,
# иначе будет только масштабирование и поворот, без переноса
X = np.hstack((shape, np.ones(shape.shape[0])[:, np.newaxis]))
Y = np.hstack((shape_target, np.ones(shape_target.shape[0])[:, np.newaxis]))
# учим оператор
A = np.dot(np.dot(np.linalg.inv(np.dot(X.T, X)), X.T), Y)
# выбираем точки лица по искомой маске
X = np.array([(y, x, 1) for (x, y) in
it.product(range(mask.shape[0]),
range(mask.shape[1])) if mask[x, y] == 1.0])
# вычисляем новые координаты маски на целевом изображении
Y = np.dot(X, A).astype(np.int)
img_tmp = img_target.copy()
for y, x, _ in Y:
if x < 0 or x >= img_target.shape[0] or y < 0 or y >= img_target.shape[1]:
continue
img_tmp[x, y, :] = np.array([0, 0, 0])
plt.imshow(img_tmp)
img_trans = img_target.copy().astype(np.uint8)
points_face = {}
for ix in range(X.shape[0]):
y1, x1, _ = X[ix, :]
y2, x2, _ = Y[ix, :]
if x2 < 0 or x2 >= img_target.shape[0] or
y2 < 0 or y2 >= img_target.shape[1]:
continue
points_face[(x2, y2)] = img_input[x1, y1, :]
for (x, y), c in points_face.items():
img_trans[x, y, :] = c
plt.imshow(img_trans)
В качестве домашней работы вы можоте самостоятельно сделать следующие улучшения:
Ноутбук с исходниками находится тут. Приятного времяпрепровождения.
Как обычно спс bauchgefuehl за редактуру.
|
Сегментация лица на селфи без нейросетей |
Приветствую вас, коллеги. Оказывается, не все компьютерное зрение сегодня делается с использованием нейронных сетей. Хотя многие стартапы и заявляют, что у них дип лернинг везде, спешу вас разочаровать, они просто хотят хайпануть немножечко. Рассмотрим, например, задачу сегментации. В нашем слаке развернулась целая драма. Одна богатая и высокотехнологичная селфи-компания собрала датасет для сегментации селфи с помощью нейросетей (а это непростое и недешевое занятие). А другая, более бедная и не очень развитая решила, что можно подкупить людей, размечающих фотки, и
спполучить базу. В общем, страсти в этих ваших Интернетах еще те. Недавно я наткнулся на статью, где без всяких нейросетей на устройстве делают очень даже хорошую сегментацию. Для сегментации от пользователя требуется дать алгоритму несколько подсказок, но с помощью dlib и opencv такие подсказки легко автоматизируются. В качестве бонуса мы так же сгладим вырезанное лицо и перенесем на какого-нибудь рандомного человека, тем самым поймем, как работают маски во всех этих снапчятах и маскарадах. В общем, классика еще жива, и если вы хотите немного окунуться в классическое компьютерное зрение на питоне, то добро пожаловать под кат.
Кратко опишем алгоритм, а затем перейдем к его реализации по шагам. Допустим, у нас есть некоторое изображение, мы просим пользователя нарисовать на изображении две кривых. Первая (синий цвет) должна полностью принадлежать объекту интереса. Вторая (зеленый цвет) должна касаться только фона изображения.
Далее делаем следующие шаги:
Дальнейший материал будет разбавляться вставками кода на питоне, если вы планируете выполнять его по мере чтения поста, то вам понадобятся следующие импорты:
%matplotlib inline
import matplotlib
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style("dark")
plt.rcParams['figure.figsize'] = 16, 12
import pandas as pd
from PIL import Image
from tqdm import tqdm_notebook
from skimage import transform
import itertools as it
from sklearn.neighbors.kde import KernelDensity
import matplotlib.cm as cm
import queue
from skimage import morphology
import dlib
import cv2
from imutils import face_utils
from scipy.spatial import Delaunay
Идея того, как автоматизировать штрихи была навеяна приложением FaceApp, которое якобы использует нейросети для трансформации. Как мне кажется, они если и используют сети где то, то только в детектировании особых точек на лице. Взгляните на скриншот справа, они предлагают выровнять свое лицо по контуру. Вероятно, алгоритм детекции обучен примерно на таком масштабе. Как только лицо попадает в контур, сама рамка контура исчезает, значит особые точки вычислились. Позвольте вам представить сегодняшнего подопытного, а так-же напомнить, что из себя представляют эти самые особые точки на лице.
img_input = np.array(Image.open('./../data/input2.jpg'))[:500, 400:, :]
print(img_input.shape)
plt.imshow(img_input)
Теперь воспользуемся возможностями бесплатного программного обеспечения с открытым исходным кодом и найдем рамку вокруг лица и особые точки на лице, всего их 62.
# инстанцируем класс для детекции лиц (рамка)
detector = dlib.get_frontal_face_detector()
# инстанцируем класс для детекции ключевых точек
predictor = dlib.shape_predictor('./../data/shape_predictor_68_face_landmarks.dat')
# конвертируем изображение в много оттенков серого
img_gray = cv2.cvtColor(img_input, cv2.COLOR_BGR2GRAY)
# вычисляем список рамок на каждое найденное лицо
rects = detector(img_gray, 0)
# вычисляем ключевые точки
shape = predictor(img_gray, rects[0])
shape = face_utils.shape_to_np(shape)
img_tmp = img_input.copy()
for x, y in shape:
cv2.circle(img_tmp, (x, y), 1, (0, 0, 255), -1)
plt.imshow(img_tmp)
Оригинальная рамка на лице слишком мелкая (зеленый цвет), нам понадобится рамка, которая полностью содержит в себе лицо с некоторым зазором (красный цвет). Коэффициенты расширения рамки получены эмпирическим путем с помощью анализа нескольких десятков селфи разного масштаба и разных людей.
# оригинальная рамка
face_origin = sorted([(t.width()*t.height(),
(t.left(), t.top(), t.width(), t.height()))
for t in rects],
key=lambda t: t[0], reverse=True)[0][1]
# коэффициенты расширения рамки
rescale = (1.3, 2.2, 1.3, 1.3)
# расширение рамки, так чтобы она не вылезла за края
(x, y, w, h) = face_origin
cx = x + w/2
cy = y + h/2
w = min(img_input.shape[1] - x, int(w/2 + rescale[2]*w/2))
h = min(img_input.shape[0] - y, int(h/2 + rescale[3]*h/2))
fx = max(0, int(x + w/2*(1 - rescale[0])))
fy = max(0, int(y + h/2*(1 - rescale[1])))
fw = min(img_input.shape[1] - fx, int(w - w/2*(1 - rescale[0])))
fh = min(img_input.shape[0] - fy, int(h - h/2*(1 - rescale[1])))
face = (fx, fy, fw, fh)
img_tmp = cv2.rectangle(img_input.copy(), (face[0], face[1]),
(face[0] + face[2], face[1] + face[3]),
(255, 0, 0), thickness=3, lineType=8, shift=0)
img_tmp = cv2.rectangle(img_tmp, (face_origin[0], face_origin[1]),
(face_origin[0] + face_origin[2], face_origin[1] + face_origin[3]),
(0, 255, 0), thickness=3, lineType=8, shift=0)
plt.imshow(img_tmp)
Теперь у нас имеется область, которая точно не относится к лицу — всё, что вне красной рамки. Выберем оттуда некоторое количество случайных точек и будем считать их штрихами фона. Также у нас имеются 62 точки, которые точно расположены на лице. Для упрощения задачи я выберу 5 из них: по одной на уровне глаз на краю лица, по одной на уровне рта на краю лица и одну внизу посередине подбородка. Все точки внутри этого пятиугольника будут принадлежать только лицу. Опять же для простоты будем считать, что лицо вертикально расположено на изображении и потому мы можем отразить полученный пятиугольник по оси , тем самым получив восьмиугольник. Все, что внутри восьмиугольника будем считать штрихом объекта.
# выбираем вышеописанные пять точек
points = [shape[0].tolist(), shape[16].tolist()]
for ix in [4, 12, 8]:
x, y = shape[ix].tolist()
points.append((x, y))
points.append((x, points[0][1] + points[0][1] - y))
# я не особо в прототипе запариваюсь над производительностью
# так что вызываю триангуляцию Делоне,
# чтобы использовать ее как тест на то, что точка внутри полигона
# все это можно делать быстрее, т.к. точный тест не нужен
# для прототипа :good-enough:
hull = Delaunay(points)
xy_fg = []
for x, y in it.product(range(img_input.shape[0]), range(img_input.shape[1])):
if hull.find_simplex([y, x]) >= 0:
xy_fg.append((x, y))
print('xy_fg%:', len(xy_fg)/np.prod(img_input.shape))
# вычисляем количество точек для фона
# примерно равно что бы было тому, что на лице
r = face[1]*face[3]/np.prod(img_input.shape[:2])
print(r)
k = 0.1
xy_bg_n = int(k*np.prod(img_input.shape[:2]))
print(xy_bg_n)
# накидываем случайные точки
xy_bg = zip(np.random.uniform(0, img_input.shape[0], size=xy_bg_n).astype(np.int),
np.random.uniform(0, img_input.shape[1], size=xy_bg_n).astype(np.int))
xy_bg = list(xy_bg)
xy_bg = [(x, y) for (x, y) in xy_bg
if y < face[0] or y > face[0] + face[2] or x < face[1] or x > face[1] + face[3]]
print(len(xy_bg)/np.prod(img_input.shape[:2]))
img_tmp = img_input/255
for x, y in xy_fg:
img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([1, 0, 0]) * 0.5
for x, y in xy_bg:
img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([0, 0, 1]) * 0.5
plt.imshow(img_tmp)
Теперь у нас есть два набора данных: точки объекта и точки фона .
points_fg = np.array([img_input[x, y, :] for (x, y) in xy_fg])
points_bg = np.array([img_input[x, y, :] for (x, y) in xy_bg])
Посмотрим на распределение цветов по RGB каналам в каждом из множеств. Первая гистограмма — для объекта, вторая — для фона.
fig, axes = plt.subplots(nrows=2, ncols=1)
sns.distplot(points_fg[:, 0], ax=axes[0], color='r')
sns.distplot(points_fg[:, 1], ax=axes[0], color='g')
sns.distplot(points_fg[:, 2], ax=axes[0], color='b')
sns.distplot(points_bg[:, 0], ax=axes[1], color='r')
sns.distplot(points_bg[:, 1], ax=axes[1], color='g')
sns.distplot(points_bg[:, 2], ax=axes[1], color='b')
Радует, что распределения отличаются. Это значит, что если мы сможем получить функции, оценивающие вероятность принадлежности точки к нужному распределению, то мы получим нечеткие маски. И оказывается такой способ есть — kernel density estimation. Для заданного набора точек, можно построить функцию оценки плотности для новой точки следующим образом (для простоты пример для одномерного распределения):
где:
Мы для простоты будем использовать Гауссово ядро:
Хотя для скорости Гауссово ядро не лучший выбор и если взять ядро Епанечникова, то все будет считаться быстрее. Так же я буду использовать KernelDensity из sklearn, что в итоге выльется в 5 минут скоринга. Авторы этой статьи утверждают, что замена KDE на оптимальную реализацию сокращает расчеты на устройстве до одной секунды.
# инстанцируем классы KDE для объекта и фона
kde_fg = KernelDensity(kernel='gaussian',
bandwidth=1,
algorithm='kd_tree',
leaf_size=100).fit(points_fg)
kde_bg = KernelDensity(kernel='gaussian',
bandwidth=1,
algorithm='kd_tree',
leaf_size=100).fit(points_bg)
# инициализируем и вычисляем маски
score_kde_fg = np.zeros(img_input.shape[:2])
score_kde_bg = np.zeros(img_input.shape[:2])
likelihood_fg = np.zeros(img_input.shape[:2])
coodinates = it.product(range(score_kde_fg.shape[0]),
range(score_kde_fg.shape[1]))
for x, y in tqdm_notebook(coodinates,
total=np.prod(score_kde_fg.shape)):
score_kde_fg[x, y] = np.exp(kde_fg.score(img_input[x, y, :].reshape(1, -1)))
score_kde_bg[x, y] = np.exp(kde_bg.score(img_input[x, y, :].reshape(1, -1)))
n = score_kde_fg[x, y] + score_kde_bg[x, y]
if n == 0:
n = 1
likelihood_fg[x, y] = score_kde_fg[x, y]/n
В итоге у нас есть несколько масок:
score_kde_fg
— оценка вероятности быть точкой объекта score_kde_bg
— оценка вероятности быть точкой объекта likelihood_fg
— нормализированная вероятность быть точкой объекта 1 - likelihood_fg
нормализированная вероятность быть точкой фона Посмотрим на следующие распределения.
sns.distplot(score_kde_fg.flatten())
plt.show()
sns.distplot(score_kde_bg.flatten())
plt.show()
Распределение значений likelihood_fg:
sns.distplot(likelihood_fg.flatten())
plt.show()
Вселяет надежду то, что на есть два пика, и количество точек, принадлежащих лицу, явно не меньше, чем фоновых точек. Нарисуем полученные маски.
plt.matshow(score_kde_fg, cmap=cm.bwr)
plt.show()
plt.matshow(score_kde_bg, cmap=cm.bwr)
plt.show()
plt.matshow(likelihood_fg, cmap=cm.bwr)
plt.show()
plt.matshow(1 - likelihood_fg, cmap=cm.bwr)
plt.show()
К сожалению, часть косяка двери получилась частью лица. Хорошо, что косяк далеко от лица. Этим-то свойством мы и воспользуемся в следущей части.
Представим изображение как граф, узлами которого являются пиксели, а ребрами соединены точки сверху и снизу от текущей точки, а так же справа и слева от неё. Весами ребер будем считать абсолютное значение разницы вероятностей принадлежности точек к объекту или к фону:
Соответственно, чем вероятности ближе к друг другу, тем меньше вес ребра между точками. Воспользуемся алгоритмом Дейкстры для поиска наикратчайших путей и их расстояний от точки до всех остальных. Алгоритм мы вызовем два раза, подав на вход все вероятности принадлежности в объекту и затем вероятности принадлежности точек к фону. Понятие расстояния зашьем сразу в алгоритм, а расстояние между точками, принадлежащими одной группе (объекту или фону), будет равно нулю. В рамках алгоритма Дейкстры мы можем поместить все эти точки в группу посещенных вершин.
def dijkstra(start_points, w):
d = np.zeros(w.shape) + np.infty
v = np.zeros(w.shape, dtype=np.bool)
q = queue.PriorityQueue()
for x, y in start_points:
d[x, y] = 0
q.put((d[x, y], (x, y)))
for x, y in it.product(range(w.shape[0]), range(w.shape[1])):
if np.isinf(d[x, y]):
q.put((d[x, y], (x, y)))
while not q.empty():
_, p = q.get()
if v[p]:
continue
neighbourhood = []
if p[0] - 1 >= 0:
neighbourhood.append((p[0] - 1, p[1]))
if p[0] + 1 <= w.shape[0] - 1:
neighbourhood.append((p[0] + 1, p[1]))
if p[1] - 1 >= 0:
neighbourhood.append((p[0], p[1] - 1))
if p[1] + 1 < w.shape[1]:
neighbourhood.append((p[0], p[1] + 1))
for x, y in neighbourhood:
# тут вычисляется расстояние
d_tmp = d[p] + np.abs(w[x, y] - w[p])
if d[x, y] > d_tmp:
d[x, y] = d_tmp
q.put((d[x, y], (x, y)))
v[p] = True
return d
# вызываем алгоритм для двух масок
d_fg = dijkstra(xy_fg, likelihood_fg)
d_bg = dijkstra(xy_bg, 1 - likelihood_fg)
plt.matshow(d_fg, cmap=cm.bwr)
plt.show()
plt.matshow(d_bg, cmap=cm.bwr)
plt.show()
А теперь относим к объекту все те точки, от которых расстояние до объекта меньше чем расстояния до фона (можно добавить некоторый зазор).
margin = 0.0
mask = (d_fg < (d_bg + margin)).astype(np.uint8)
plt.matshow(mask)
plt.show()
Можно отправить себя в космос.
img_fg = img_input/255.0
img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
x = int(img_bg.shape[0] - img_fg.shape[0])
y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
mask_3d = np.dstack([mask, mask, mask])
img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
plt.imshow(img_bg)
Вы наверняка заметили, что маска слегка рваная на краях. Но это легко исправить методами математической морфологии.
Допустим, у нас есть структурный элемент (СЭ) типа "диск" — бинарная маска диска.
Мы воспользуемся размыканием, что сначала удалит "волосатость" по краям, а потом вернет первоначальный размер (объект "похудеет" после эрозии).
mask = morphology.opening(mask, morphology.disk(11))
plt.imshow(mask)
После применения такой маски результат станет поприятнее:
img_fg = img_input/255.0
img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
x = int(img_bg.shape[0] - img_fg.shape[0])
y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
mask_3d = np.dstack([mask, mask, mask])
img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = \
mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
plt.imshow(img_bg)
Возьмем случайную фотку из интернетов для эксперимента по переносу лица.
img_target = np.array(Image.open('./../data/target.jpg'))
img_target = (transform.rescale(img_target, scale=0.5, mode='constant')*255).astype(np.uint8)
print(img_target.shape)
plt.imshow(img_target)
Найдем на подопытном все 62 колючевые точки лица, напомню, что они будут в том же порядке как и на любом другом лице.
img_gray = cv2.cvtColor(img_target, cv2.COLOR_BGR2GRAY)
rects_target = detector(img_gray, 0)
shape_target = predictor(img_gray, rects_target[0])
shape_target = face_utils.shape_to_np(shape_target)
Чтобы перенести одно лицо на другое, нужно первое лицо отмасштабировать под новое, повернуть и передвинуть, т.е. применить некоторое аффинное преобразование к первому лицу. Оказывается, что и аффинное преобразование не некоторое, а вполне конкретное. Оно должно быть таким, которое переводит 62 точки первого лица в 62 точки второго лица. Получается, что для получения оператора аффинного преобразования нам необходимо решить задачу линейной регрессии.
Данное уравнение легко решается с помощью псевдообратной матрицы:
Так и сделаем:
# добавим справа к матрицам колонку единиц,
# иначе будет только масштабирование и поворот, без переноса
X = np.hstack((shape, np.ones(shape.shape[0])[:, np.newaxis]))
Y = np.hstack((shape_target, np.ones(shape_target.shape[0])[:, np.newaxis]))
# учим оператор
A = np.dot(np.dot(np.linalg.inv(np.dot(X.T, X)), X.T), Y)
# выбираем точки лица по искомой маске
X = np.array([(y, x, 1) for (x, y) in
it.product(range(mask.shape[0]),
range(mask.shape[1])) if mask[x, y] == 1.0])
# вычисляем новые координаты маски на целевом изображении
Y = np.dot(X, A).astype(np.int)
img_tmp = img_target.copy()
for y, x, _ in Y:
if x < 0 or x >= img_target.shape[0] or y < 0 or y >= img_target.shape[1]:
continue
img_tmp[x, y, :] = np.array([0, 0, 0])
plt.imshow(img_tmp)
img_trans = img_target.copy().astype(np.uint8)
points_face = {}
for ix in range(X.shape[0]):
y1, x1, _ = X[ix, :]
y2, x2, _ = Y[ix, :]
if x2 < 0 or x2 >= img_target.shape[0] or
y2 < 0 or y2 >= img_target.shape[1]:
continue
points_face[(x2, y2)] = img_input[x1, y1, :]
for (x, y), c in points_face.items():
img_trans[x, y, :] = c
plt.imshow(img_trans)
В качестве домашней работы вы можоте самостоятельно сделать следующие улучшения:
Ноутбук с исходниками находится тут. Приятного времяпрепровождения.
Как обычно спс bauchgefuehl за редактуру.
|
Сегментация лица на селфи без нейросетей |
Приветствую вас, коллеги. Оказывается, не все компьютерное зрение сегодня делается с использованием нейронных сетей. Хотя многие стартапы и заявляют, что у них дип лернинг везде, спешу вас разочаровать, они просто хотят хайпануть немножечко. Рассмотрим, например, задачу сегментации. В нашем слаке развернулась целая драма. Одна богатая и высокотехнологичная селфи-компания собрала датасет для сегментации селфи с помощью нейросетей (а это непростое и недешевое занятие). А другая, более бедная и не очень развитая решила, что можно подкупить людей, размечающих фотки, и
спполучить базу. В общем, страсти в этих ваших Интернетах еще те. Недавно я наткнулся на статью, где без всяких нейросетей на устройстве делают очень даже хорошую сегментацию. Для сегментации от пользователя требуется дать алгоритму несколько подсказок, но с помощью dlib и opencv такие подсказки легко автоматизируются. В качестве бонуса мы так же сгладим вырезанное лицо и перенесем на какого-нибудь рандомного человека, тем самым поймем, как работают маски во всех этих снапчятах и маскарадах. В общем, классика еще жива, и если вы хотите немного окунуться в классическое компьютерное зрение на питоне, то добро пожаловать под кат.
Кратко опишем алгоритм, а затем перейдем к его реализации по шагам. Допустим, у нас есть некоторое изображение, мы просим пользователя нарисовать на изображении две кривых. Первая (синий цвет) должна полностью принадлежать объекту интереса. Вторая (зеленый цвет) должна касаться только фона изображения.
Далее делаем следующие шаги:
Дальнейший материал будет разбавляться вставками кода на питоне, если вы планируете выполнять его по мере чтения поста, то вам понадобятся следующие импорты:
%matplotlib inline
import matplotlib
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style("dark")
plt.rcParams['figure.figsize'] = 16, 12
import pandas as pd
from PIL import Image
from tqdm import tqdm_notebook
from skimage import transform
import itertools as it
from sklearn.neighbors.kde import KernelDensity
import matplotlib.cm as cm
import queue
from skimage import morphology
import dlib
import cv2
from imutils import face_utils
from scipy.spatial import Delaunay
Идея того, как автоматизировать штрихи была навеяна приложением FaceApp, которое якобы использует нейросети для трансформации. Как мне кажется, они если и используют сети где то, то только в детектировании особых точек на лице. Взгляните на скриншот справа, они предлагают выровнять свое лицо по контуру. Вероятно, алгоритм детекции обучен примерно на таком масштабе. Как только лицо попадает в контур, сама рамка контура исчезает, значит особые точки вычислились. Позвольте вам представить сегодняшнего подопытного, а так-же напомнить, что из себя представляют эти самые особые точки на лице.
img_input = np.array(Image.open('./../data/input2.jpg'))[:500, 400:, :]
print(img_input.shape)
plt.imshow(img_input)
Теперь воспользуемся возможностями бесплатного программного обеспечения с открытым исходным кодом и найдем рамку вокруг лица и особые точки на лице, всего их 62.
# инстанцируем класс для детекции лиц (рамка)
detector = dlib.get_frontal_face_detector()
# инстанцируем класс для детекции ключевых точек
predictor = dlib.shape_predictor('./../data/shape_predictor_68_face_landmarks.dat')
# конвертируем изображение в много оттенков серого
img_gray = cv2.cvtColor(img_input, cv2.COLOR_BGR2GRAY)
# вычисляем список рамок на каждое найденное лицо
rects = detector(img_gray, 0)
# вычисляем ключевые точки
shape = predictor(img_gray, rects[0])
shape = face_utils.shape_to_np(shape)
img_tmp = img_input.copy()
for x, y in shape:
cv2.circle(img_tmp, (x, y), 1, (0, 0, 255), -1)
plt.imshow(img_tmp)
Оригинальная рамка на лице слишком мелкая (зеленый цвет), нам понадобится рамка, которая полностью содержит в себе лицо с некоторым зазором (красный цвет). Коэффициенты расширения рамки получены эмпирическим путем с помощью анализа нескольких десятков селфи разного масштаба и разных людей.
# оригинальная рамка
face_origin = sorted([(t.width()*t.height(),
(t.left(), t.top(), t.width(), t.height()))
for t in rects],
key=lambda t: t[0], reverse=True)[0][1]
# коэффициенты расширения рамки
rescale = (1.3, 2.2, 1.3, 1.3)
# расширение рамки, так чтобы она не вылезла за края
(x, y, w, h) = face_origin
cx = x + w/2
cy = y + h/2
w = min(img_input.shape[1] - x, int(w/2 + rescale[2]*w/2))
h = min(img_input.shape[0] - y, int(h/2 + rescale[3]*h/2))
fx = max(0, int(x + w/2*(1 - rescale[0])))
fy = max(0, int(y + h/2*(1 - rescale[1])))
fw = min(img_input.shape[1] - fx, int(w - w/2*(1 - rescale[0])))
fh = min(img_input.shape[0] - fy, int(h - h/2*(1 - rescale[1])))
face = (fx, fy, fw, fh)
img_tmp = cv2.rectangle(img_input.copy(), (face[0], face[1]),
(face[0] + face[2], face[1] + face[3]),
(255, 0, 0), thickness=3, lineType=8, shift=0)
img_tmp = cv2.rectangle(img_tmp, (face_origin[0], face_origin[1]),
(face_origin[0] + face_origin[2], face_origin[1] + face_origin[3]),
(0, 255, 0), thickness=3, lineType=8, shift=0)
plt.imshow(img_tmp)
Теперь у нас имеется область, которая точно не относится к лицу — всё, что вне красной рамки. Выберем оттуда некоторое количество случайных точек и будем считать их штрихами фона. Также у нас имеются 62 точки, которые точно расположены на лице. Для упрощения задачи я выберу 5 из них: по одной на уровне глаз на краю лица, по одной на уровне рта на краю лица и одну внизу посередине подбородка. Все точки внутри этого пятиугольника будут принадлежать только лицу. Опять же для простоты будем считать, что лицо вертикально расположено на изображении и потому мы можем отразить полученный пятиугольник по оси , тем самым получив восьмиугольник. Все, что внутри восьмиугольника будем считать штрихом объекта.
# выбираем вышеописанные пять точек
points = [shape[0].tolist(), shape[16].tolist()]
for ix in [4, 12, 8]:
x, y = shape[ix].tolist()
points.append((x, y))
points.append((x, points[0][1] + points[0][1] - y))
# я не особо в прототипе запариваюсь над производительностью
# так что вызываю триангуляцию Делоне,
# чтобы использовать ее как тест на то, что точка внутри полигона
# все это можно делать быстрее, т.к. точный тест не нужен
# для прототипа :good-enough:
hull = Delaunay(points)
xy_fg = []
for x, y in it.product(range(img_input.shape[0]), range(img_input.shape[1])):
if hull.find_simplex([y, x]) >= 0:
xy_fg.append((x, y))
print('xy_fg%:', len(xy_fg)/np.prod(img_input.shape))
# вычисляем количество точек для фона
# примерно равно что бы было тому, что на лице
r = face[1]*face[3]/np.prod(img_input.shape[:2])
print(r)
k = 0.1
xy_bg_n = int(k*np.prod(img_input.shape[:2]))
print(xy_bg_n)
# накидываем случайные точки
xy_bg = zip(np.random.uniform(0, img_input.shape[0], size=xy_bg_n).astype(np.int),
np.random.uniform(0, img_input.shape[1], size=xy_bg_n).astype(np.int))
xy_bg = list(xy_bg)
xy_bg = [(x, y) for (x, y) in xy_bg
if y < face[0] or y > face[0] + face[2] or x < face[1] or x > face[1] + face[3]]
print(len(xy_bg)/np.prod(img_input.shape[:2]))
img_tmp = img_input/255
for x, y in xy_fg:
img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([1, 0, 0]) * 0.5
for x, y in xy_bg:
img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([0, 0, 1]) * 0.5
plt.imshow(img_tmp)
Теперь у нас есть два набора данных: точки объекта и точки фона .
points_fg = np.array([img_input[x, y, :] for (x, y) in xy_fg])
points_bg = np.array([img_input[x, y, :] for (x, y) in xy_bg])
Посмотрим на распределение цветов по RGB каналам в каждом из множеств. Первая гистограмма — для объекта, вторая — для фона.
fig, axes = plt.subplots(nrows=2, ncols=1)
sns.distplot(points_fg[:, 0], ax=axes[0], color='r')
sns.distplot(points_fg[:, 1], ax=axes[0], color='g')
sns.distplot(points_fg[:, 2], ax=axes[0], color='b')
sns.distplot(points_bg[:, 0], ax=axes[1], color='r')
sns.distplot(points_bg[:, 1], ax=axes[1], color='g')
sns.distplot(points_bg[:, 2], ax=axes[1], color='b')
Радует, что распределения отличаются. Это значит, что если мы сможем получить функции, оценивающие вероятность принадлежности точки к нужному распределению, то мы получим нечеткие маски. И оказывается такой способ есть — kernel density estimation. Для заданного набора точек, можно построить функцию оценки плотности для новой точки следующим образом (для простоты пример для одномерного распределения):
где:
Мы для простоты будем использовать Гауссово ядро:
Хотя для скорости Гауссово ядро не лучший выбор и если взять ядро Епанечникова, то все будет считаться быстрее. Так же я буду использовать KernelDensity из sklearn, что в итоге выльется в 5 минут скоринга. Авторы этой статьи утверждают, что замена KDE на оптимальную реализацию сокращает расчеты на устройстве до одной секунды.
# инстанцируем классы KDE для объекта и фона
kde_fg = KernelDensity(kernel='gaussian',
bandwidth=1,
algorithm='kd_tree',
leaf_size=100).fit(points_fg)
kde_bg = KernelDensity(kernel='gaussian',
bandwidth=1,
algorithm='kd_tree',
leaf_size=100).fit(points_bg)
# инициализируем и вычисляем маски
score_kde_fg = np.zeros(img_input.shape[:2])
score_kde_bg = np.zeros(img_input.shape[:2])
likelihood_fg = np.zeros(img_input.shape[:2])
coodinates = it.product(range(score_kde_fg.shape[0]),
range(score_kde_fg.shape[1]))
for x, y in tqdm_notebook(coodinates,
total=np.prod(score_kde_fg.shape)):
score_kde_fg[x, y] = np.exp(kde_fg.score(img_input[x, y, :].reshape(1, -1)))
score_kde_bg[x, y] = np.exp(kde_bg.score(img_input[x, y, :].reshape(1, -1)))
n = score_kde_fg[x, y] + score_kde_bg[x, y]
if n == 0:
n = 1
likelihood_fg[x, y] = score_kde_fg[x, y]/n
В итоге у нас есть несколько масок:
score_kde_fg
— оценка вероятности быть точкой объекта score_kde_bg
— оценка вероятности быть точкой объекта likelihood_fg
— нормализированная вероятность быть точкой объекта 1 - likelihood_fg
нормализированная вероятность быть точкой фона Посмотрим на следующие распределения.
sns.distplot(score_kde_fg.flatten())
plt.show()
sns.distplot(score_kde_bg.flatten())
plt.show()
Распределение значений likelihood_fg:
sns.distplot(likelihood_fg.flatten())
plt.show()
Вселяет надежду то, что на есть два пика, и количество точек, принадлежащих лицу, явно не меньше, чем фоновых точек. Нарисуем полученные маски.
plt.matshow(score_kde_fg, cmap=cm.bwr)
plt.show()
plt.matshow(score_kde_bg, cmap=cm.bwr)
plt.show()
plt.matshow(likelihood_fg, cmap=cm.bwr)
plt.show()
plt.matshow(1 - likelihood_fg, cmap=cm.bwr)
plt.show()
К сожалению, часть косяка двери получилась частью лица. Хорошо, что косяк далеко от лица. Этим-то свойством мы и воспользуемся в следущей части.
Представим изображение как граф, узлами которого являются пиксели, а ребрами соединены точки сверху и снизу от текущей точки, а так же справа и слева от неё. Весами ребер будем считать абсолютное значение разницы вероятностей принадлежности точек к объекту или к фону:
Соответственно, чем вероятности ближе к друг другу, тем меньше вес ребра между точками. Воспользуемся алгоритмом Дейкстры для поиска наикратчайших путей и их расстояний от точки до всех остальных. Алгоритм мы вызовем два раза, подав на вход все вероятности принадлежности в объекту и затем вероятности принадлежности точек к фону. Понятие расстояния зашьем сразу в алгоритм, а расстояние между точками, принадлежащими одной группе (объекту или фону), будет равно нулю. В рамках алгоритма Дейкстры мы можем поместить все эти точки в группу посещенных вершин.
def dijkstra(start_points, w):
d = np.zeros(w.shape) + np.infty
v = np.zeros(w.shape, dtype=np.bool)
q = queue.PriorityQueue()
for x, y in start_points:
d[x, y] = 0
q.put((d[x, y], (x, y)))
for x, y in it.product(range(w.shape[0]), range(w.shape[1])):
if np.isinf(d[x, y]):
q.put((d[x, y], (x, y)))
while not q.empty():
_, p = q.get()
if v[p]:
continue
neighbourhood = []
if p[0] - 1 >= 0:
neighbourhood.append((p[0] - 1, p[1]))
if p[0] + 1 <= w.shape[0] - 1:
neighbourhood.append((p[0] + 1, p[1]))
if p[1] - 1 >= 0:
neighbourhood.append((p[0], p[1] - 1))
if p[1] + 1 < w.shape[1]:
neighbourhood.append((p[0], p[1] + 1))
for x, y in neighbourhood:
# тут вычисляется расстояние
d_tmp = d[p] + np.abs(w[x, y] - w[p])
if d[x, y] > d_tmp:
d[x, y] = d_tmp
q.put((d[x, y], (x, y)))
v[p] = True
return d
# вызываем алгоритм для двух масок
d_fg = dijkstra(xy_fg, likelihood_fg)
d_bg = dijkstra(xy_bg, 1 - likelihood_fg)
plt.matshow(d_fg, cmap=cm.bwr)
plt.show()
plt.matshow(d_bg, cmap=cm.bwr)
plt.show()
А теперь относим к объекту все те точки, от которых расстояние до объекта меньше чем расстояния до фона (можно добавить некоторый зазор).
margin = 0.0
mask = (d_fg < (d_bg + margin)).astype(np.uint8)
plt.matshow(mask)
plt.show()
Можно отправить себя в космос.
img_fg = img_input/255.0
img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
x = int(img_bg.shape[0] - img_fg.shape[0])
y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
mask_3d = np.dstack([mask, mask, mask])
img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
plt.imshow(img_bg)
Вы наверняка заметили, что маска слегка рваная на краях. Но это легко исправить методами математической морфологии.
Допустим, у нас есть структурный элемент (СЭ) типа "диск" — бинарная маска диска.
Мы воспользуемся размыканием, что сначала удалит "волосатость" по краям, а потом вернет первоначальный размер (объект "похудеет" после эрозии).
mask = morphology.opening(mask, morphology.disk(11))
plt.imshow(mask)
После применения такой маски результат станет поприятнее:
img_fg = img_input/255.0
img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
x = int(img_bg.shape[0] - img_fg.shape[0])
y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
mask_3d = np.dstack([mask, mask, mask])
img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = \
mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
plt.imshow(img_bg)
Возьмем случайную фотку из интернетов для эксперимента по переносу лица.
img_target = np.array(Image.open('./../data/target.jpg'))
img_target = (transform.rescale(img_target, scale=0.5, mode='constant')*255).astype(np.uint8)
print(img_target.shape)
plt.imshow(img_target)
Найдем на подопытном все 62 колючевые точки лица, напомню, что они будут в том же порядке как и на любом другом лице.
img_gray = cv2.cvtColor(img_target, cv2.COLOR_BGR2GRAY)
rects_target = detector(img_gray, 0)
shape_target = predictor(img_gray, rects_target[0])
shape_target = face_utils.shape_to_np(shape_target)
Чтобы перенести одно лицо на другое, нужно первое лицо отмасштабировать под новое, повернуть и передвинуть, т.е. применить некоторое аффинное преобразование к первому лицу. Оказывается, что и аффинное преобразование не некоторое, а вполне конкретное. Оно должно быть таким, которое переводит 62 точки первого лица в 62 точки второго лица. Получается, что для получения оператора аффинного преобразования нам необходимо решить задачу линейной регрессии.
Данное уравнение легко решается с помощью псевдообратной матрицы:
Так и сделаем:
# добавим справа к матрицам колонку единиц,
# иначе будет только масштабирование и поворот, без переноса
X = np.hstack((shape, np.ones(shape.shape[0])[:, np.newaxis]))
Y = np.hstack((shape_target, np.ones(shape_target.shape[0])[:, np.newaxis]))
# учим оператор
A = np.dot(np.dot(np.linalg.inv(np.dot(X.T, X)), X.T), Y)
# выбираем точки лица по искомой маске
X = np.array([(y, x, 1) for (x, y) in
it.product(range(mask.shape[0]),
range(mask.shape[1])) if mask[x, y] == 1.0])
# вычисляем новые координаты маски на целевом изображении
Y = np.dot(X, A).astype(np.int)
img_tmp = img_target.copy()
for y, x, _ in Y:
if x < 0 or x >= img_target.shape[0] or y < 0 or y >= img_target.shape[1]:
continue
img_tmp[x, y, :] = np.array([0, 0, 0])
plt.imshow(img_tmp)
img_trans = img_target.copy().astype(np.uint8)
points_face = {}
for ix in range(X.shape[0]):
y1, x1, _ = X[ix, :]
y2, x2, _ = Y[ix, :]
if x2 < 0 or x2 >= img_target.shape[0] or
y2 < 0 or y2 >= img_target.shape[1]:
continue
points_face[(x2, y2)] = img_input[x1, y1, :]
for (x, y), c in points_face.items():
img_trans[x, y, :] = c
plt.imshow(img_trans)
В качестве домашней работы вы можоте самостоятельно сделать следующие улучшения:
Ноутбук с исходниками находится тут. Приятного времяпрепровождения.
Как обычно спс bauchgefuehl за редактуру.
|