И вот, перед самыми майскими праздниками пришел ответ с тестовым заданием. Задание казалось простым, но я решил отказаться от дальнейшего общения вообще, так как почему-то за две недели порыв поиска новой работы прошел, да и праздники впереди. Однако в тот же день я заболел. Вполне себе серьезным насморком со всеми вытекающими. И на следующий день я решил попробовать забороть это тестовое задание и посмотреть, что из этого выйдет. И об этом мой рассказ.
Ниже текст того самого задания:
Необходимо создать небольшое веб приложение, используя любой веб фреймворк (желательно тот, который Вы лучше всего знаете). Верстка не важна, уделяйте основное внимание бэкенду, оформлению кода, мелочам.
Фактически приложение должно состоять из одной страницы, но по желанию можно разделить на несколько (отдельно форму загрузки).
Необходимые элементы — форма загрузки фотографии и таблица со списком загруженных фото. Авторизация не обязательна.
Форма загрузки:
Текстовое поле для ввода названия фото
Выбор файла
Таблица:
Превью фото (необходимо сделать уменьшенную копию фото (миниатюру); также данное превью должно являться ссылкой на оригинальное/полное изображение, которое открывается по клику на превью)
Название фото (которое пользователь указывает при загрузке)
Производитель и модель камеры (из EXIF, если присутствует)
Размер файла
Дата создания фото (из EXIF)
Дата загрузки фото
Кнопка удаления
Требования:
Не сохранять уже существующие фото. Проверять наличие дубликата файла и выдавать ошибку в случае обнаружения.
Проверять, является ли загружаемый файл изображением, если нет — выдавать ошибку. (Не использовать проверку наличия EXIF данных в качестве валидации)
Не позволять сохранять фото, созданные более года назад (проверять дату создания фото из EXIF).
Если отсутствует дата создания фото в EXIF, тогда следует выдать ошибку и не добавлять файл.
Итак, в качестве веб-фреймворка для Python был выбран Tornado, я с ним давно знаком. Мы будем поднимать несколько backend серверов, поэтому нам понадобится балансер и Supervisor. Изначально я думал о HAProxy в качестве балансера, но тут меня осенило, что картинки может хорошо раздавать NGINX. В итоге в начале архитектура мне показалась такой: NGINX балансирует соединения и раздает статику с диска, 4 сервера Tornado обрабатывают запросы, Redis синхронизирует backend.
На Tornado упала ноша анализа поступающих картинок и создание миниатюр. В задании не сказано, какие форматы необходимо поддерживать, поэтому я поискал описание EXIF в википедии, где упоминаются форматы TIFF и JPEG. Если это все, то дела не так уж плохи, библиотека Pillow для Python поддерживает оба формата, а также EXIF метаданные. Но есть нюанс — TIFF изображения не открываются браузером. Это делает невозможным открытие оригинального файла в браузере, поэтому я решил перекодировать эти изображения в JPEG и дополнительно сохранить вместе с полученным файлом EXIF данные, из которых можно было бы восстановить всю необходимую информацию для отображения в таблице.
Саму таблицу мы сохраним в Redis. И хотя Redis полностью загружается в память и при аварийной остановке шансов восстановить последние изменения базы данных не велики, я считаю, что он способен вместить в себя очень большое количество описаний картинок и его хватит надолго. А в аварийных случаях недостающую информацию можно восстановить из метаданных JPEG файлов.
Решение с метаданными в JPEG мне показалось красивым, и, хотя Pillow вполне умеет сохранять EXIF в JPEG, сами метаданные при этом должны быть уже в бинарном формате. То есть, Pillow выдает метаданные в виде словаря, но вот из словаря в метаданные никак не умеет. Была найдена библиотека Gexiv2, так же работающая с метаданными, но ее установка потребовала сноровки.
Попытка собрать Gexiv2 из исходников много раз приводила к ошибкам об отсутствующих библиотеках. В поиске очередной такой библиотеки я наткнулся на установочный пакет этой библиотеки для Ubuntu. Но и тут возникла проблема. Python на систему я установил через pyenv, и запускать скрипты собирался из virtualenv, но в таком случае установленный в систему Gexiv2 оказывается недоступен. Есть определенные танцы с бубном на эту тему, но уже потратив час на Gexiv2, я решил отказаться от virtualenv и использовать системный Python 2.7.6.
Gexiv2 успешно редактирует EXIF в файлах, но с данными в памяти у него туго. А я принципиально не хотел дважды обращаться к файлу: один раз на запись JPEG, второй раз на запись в этот же файл метаданных. Я еще не знал, что меня ждет. А ждало меня следующее — в документации Gexiv2 были перечислены поддерживаемые форматы, такие как EXV, CR2, CRW и многие другие. Таким образом Pillow уже не справлялось с задачей чтения загружаемых изображений. Так я нашел ImageMagick, и соответствующий адаптер под Python — Wand.
Wand выглядел многообещающе — поддержка множество форматов, чтение EXIF, относительно простая установка. Но чтобы сохранять JPEG со своими метаданными мне все равно нужен Pillow. Потратив некоторое время мне повезло найти библиотеку piexif, которая помогала редактировать метаданные в Pillow, и одной проблемой стало меньше. Потратив несколько часов можно сесть и программировать.
Алгоритм был простой, Wand загружает картинку из памяти, выдает EXIF данные, потом Wand отдает буфер RGB, считаем его md5 хеш чтобы проверить на дубликаты, конвертируем буфер в JPEG и сохраняем со своими метаданными, плюс сохраняем миниатюру. Конечно же соответствующе обновляем данные в Redis. Осталось проверить. Однако найти в интернете картинки с метаданными, да еще и свежими — проблема. И я потратил еще немало времени на поиск программы, которая бы хорошо редактировала EXIF данные.
И вот, первый JPEG семпл готов, загружаем — работает! А вот второй семпл, CR2 файл размером 7MB выдал несколько сюрпризов. Первый — Wand не смог его прочитать из буфера, ему потребовалась подсказка формата в виде расширения исходного файла. Но и тут проблема, библиотека стала писать, что не находит какой-то временный файл. Опять поиски, оказалось нужно установить утилиту ufraw, и файл прочитался. За 11 секунд. А потом в JPEG вывалилось нечто больше похожее на шум чем на исходную картинку.
Изначально я грешил на Wand, мне казалось, что он криво конвертирует картинку в RGB буфер, однако, запустив калькулятор, я обнаружил что буфер ровно в 2 раза больше чем необходимо — то есть на канал приходится не 8, а 16 бит. Ура, одна строчка и все работает. Но что делать с долгой загрузкой файла? Даже если серверов будет четыре, такое же количество больших CR2 файлов просто сделают сервис недоступным.
В течение первого дня я потратил немалую часть времени на установку системы, на поиск и установку различных библиотек, на исследование предметной области, но в итоге написал всего лишь 150 строк кода. И в итоге тестовое задание было провалено — результат работы приложения был неприемлем. А больной организм просился отдыхать.
На второй день было решено отказаться от обработки изображения во время запроса, загрузку изображения возложить на NGINX, оставить в backend только один сервер, а также запустить три скрипта, которые бы обрабатывали изображения.
Начал я со сборки Upload модуля NGINX, и, конечно же, безуспешно. Провозившись некоторое время, я понял, что автор его забросил, и на последней версии этот модуль не заработает. Ну и ладно — пусть Tornado сервер сохраняет входящие файлы на диск. Далее фактически копипаста в новые скрипты обработки изображения. Конфигурируем Supervisor и NGINX, допиливаем скрипт установки. И оно работает.
Из минусов: после загрузки изображения пользователю открывается страница со статусом загрузки, прежде чем изображение появится в таблице проходит некоторое время, пришлось отказаться от хеширования чисто RGB данных, теперь хешируется весь файл для проверки на дубликаты. В случае ошибки результат загрузки хранится один день.
В итоге я считаю, что при относительно низкой нагрузке данное приложение будет работать стабильно, а на мощном сервере с множеством ядер можно говорить о хорошей производительности. К сожалению, данное тестовое задание было наполнено по большей части поиском библиотек и администрированием, в нем очень мало программирования. Что хотел выяснить работодатель этим заданием мне не очень понятно. Может требовалось написать свою библиотеку для получения EXIF данных? И нельзя назвать такое тестовое задание небольшим — по времени вышло более 8 часов. Сильно бы упростило задачу внесение конкретики по поддерживаемым форматам изображений, более развернутое объяснение целевого использования приложения.
Исходники можно посмотреть на github. А я дальше болеть.
Комментарии (35)
frol
13.05.2015 12:30-1Честно говоря, как мне кажется, тестовое задание как явление себя изжило в некотором роде. Лучше всего навыки демонстрирует активность на github/bitbucket/etc, при этом не обязательно иметь свои проекты — форки и даже оформленные отчёты об ошибках очень красочно иллюстрируют способности человека.
Как мне кажется, тестовое задание стоит давать только для людей «без опыта работы», но и то тестовое задание не должно быть всеобъемлющим. Я бы предложил для тестового задания просто любую задачу с ProjectEuler.semmaxim
13.05.2015 15:56+3А если у человека нет аккаунта и/или совсем нет активности на github/bitbucket? Он вообще программировать не умеет?
Ну нет у меня времени влезать в opensource-разработки, нету. А проблемы или баги, которые встречаются, обычно уже описаны и зафиксированы.frol
13.05.2015 16:18+1Тогда, скорее всего, вы не сможете показать никакой готовый код из-за SLA, так что придётся оценивать как «без видимого результата работы», что для работодателя аналогично случаю «без опыта работы». Любой человек может научиться писать код не работая нигде, не у каждого будет проект, который стоит показать, так что остаётся для этого случая и остаются тестовые задания, но, как я и сказал раньше — тестовое задание не должно быть тестовым проектом, так как это слишком.
frol
13.05.2015 12:32+5Кстати, к решению вашего тестового задания можно было и существенно проще подходить, но тогда действительно грош цена такому решению будет и оно не покажет всего потенциала.
coocheenin
13.05.2015 15:43Скорее вакансия кодера/технолога, а не программиста. Решили не конкретизировать и написать «разработчик»…
un1t
13.05.2015 16:01+11Мы будем поднимать несколько backend серверов, поэтому нам понадобится балансер и Supervisor. Изначально я думал о HAProxy в качестве балансера, но тут меня осенило, что картинки может хорошо раздавать NGINX. В итоге в начале архитектура мне показалась такой: NGINX балансирует соединения и раздает статику с диска, 4 сервера Tornado обрабатывают запросы, Redis синхронизирует backend.
Зачем все это нужно было делать, в задании про это ни слова.
dzigoro
13.05.2015 16:06+1Сильно бы упростило задачу внесение конкретики по поддерживаемым форматам изображений, более развернутое объяснение целевого использования приложения.
Тестовое задание — это не только проверка ваших навыков кодера, но и навыков разработчика: умение обдумать, сформулировать и задать вопросы по заданию. Отсутствие вопросов обычно сильно портит впечатление.
А так, Вы молодец, конечно.nxsofsys Автор
13.05.2015 16:26Вопросы конечно возникли. На вскидку это были: конкретизация форматов изображений и планируемая нагрузка на сервис(загрузок в секунду). Спортивный интерес состоял также в том, чтобы понять на практике, имеют ли они значение, или это некие придирки, которые похожи на торговлю о сложности задания.
flint
13.05.2015 16:46+4Вы могли бы потратить несколько минут на уточнение задания и тем самым сэкономить несколько часов, не реализуя того, что никому не нужно. Одно из наших тестовых заданий примерно такое же по сложности, и здесь суть в том, чтобы можно было увидеть общий подход к решению задач: простоту, понятность, вопросы, удобочитаемость кода, комментарии, тесты, логирование, использование линтеров, легкость установки и тп. Небольшое приложение, которое в кратчайшие сроки можно оформить как следует. Как правило, после просмотра кода у кандидата всегда спрашивают, какие он сам видит недостатки у своего решения, и что бы он поменял перед выпуском в продакшн.
К сожалению, данное тестовое задание было наполнено по большей части поиском библиотек и администрированием, в нем очень мало программирования. Что хотел выяснить работодатель этим заданием мне не очень понятно.
А это самое первое, что нужно выяснять, когда берешься за любую задачу. На основе простого тестового задания, вы придумали какое-то свое, особенное, и делали уже его.nxsofsys Автор
13.05.2015 16:58Я думаю этот комментарий отвечает на Ваши замечания. В общем случае, все, что Вы говорите, абсолютно верно.
KeFA
13.05.2015 16:06+3Что хотел выяснить работодатель этим заданием мне не очень понятно. Может требовалось написать свою библиотеку для получения EXIF данных?
А может нужно было позадавать уточняющих вопросов? Когда разработчик уточняет детали задачи перед ее выполнением, то это его также характеризует помимо самого тестового задания.
Лично мне не понятно зачем Вам было погружаться впервую очередь в вопросы масштабирования и производительности, когда в требованиях об этом ни слова.nxsofsys Автор
13.05.2015 16:32Я попытался сделать решение, которое бы устроило конкретно меня, если бы я выдал кому-либо такое задание. Сразу после получения задания я отписал работодателю, что более не имею интереса к его вакансии, таким образом смысла себя как то себя характеризовать перед работодателем не было.
JC_Piligrim
14.05.2015 16:19+3Выполнять это задание таким образом как повод разобраться с технологиями, которые до этого не трогали — это ок.
Но когда разработчик на полном серьёзе вместо того, чтобы делать то, что нужно бизнесу пишет свою универсальную операционную систему, которая поможет решить задачу через год — то работодателю становится всё ясно. Тут правильно выше написали, что такие задания при приеме на работу очень хорошо характеризуют соискателя.
iroln
13.05.2015 16:11Раз уж тема про тестовые задания, то хотелось бы узнать, что уважаемое сообщество думает на счёт вот такого тестового задания? Своё мнение я тоже могу высказать, если оно будет кому-то интересно.
frol
13.05.2015 16:24+4ИМХО:
Я, конечно, не совсем в теме распознаваний и технологий с этим связанными, но неделю на тестовое задание — это по-моему перебор. Кроме того, задание такого объёма, что мне кажется оно займёт неделю фултайм работы (если не в режиме наколеночного скрипта это делать). Авторы тестового задания должно быть предлагают очень хорошие условия после устройства к ним, иначе я бы не стал тратить время на такое тестовое задание.RPG18
13.05.2015 16:44Там да же подсказка есть
поощряется использование OpenCV
Распознавание дорожных знаков (OpenCV)
Как понимаю все сводится к копипасту примеров по OpenCV.iroln
13.05.2015 16:57Скажем так, описанный в статье по ссылке метод очень ненадёжен и не может использоваться в реальных условиях для роботизированных автомобилей. К тому же там ничего не написано про фильтр для классификации групп знаков, а показан только результат его работы.
параметры для этого фильтра подбирались опытным путем
На одной картинке сработало, а на остальных другие параметры. Пороговая обработка и Canny — это всё, что там применили. Никакой адаптивности, никакой классификации (остальное не написано), подобным подходом не удастся получить устойчивое надёжное решение этой задачи. Поэтому копипаста простейших примеров использования примитивов машинного зрения из OpenCV не прокатит.iroln
13.05.2015 17:07Там в конце ещё написано про нейросеть для классификации контуров, чтобы определить форму знаков, как я понимаю (так называемый Structural Analysis и Shape Descriptors). Так вот, в OpenCV нет никаких нейронных сетей, а есть только примитивы (кирпичики) и классификатор придётся придумывать, тестировать и писать самостоятельно, используя только лишь примитивы из OpenCV, и никакая копипаста тут не поможет.
BelBES
13.05.2015 17:14+1Для первого задания этот пример уже не подходит, т.к. знаки могут иметь различную форму, цвет, не иметь белой рамки по контуру и т.д. и т.п. Лучшего качества наверно можно было бы добиться на SVM+HoG, какойнибудь каскадный детктор или Deep Learning, но сомневаюсь, что к заданию приложили достаточное для обучения количество позитивов/негативов. Как простое решение можно было бы попробовать простой feature matching или контурный анализ попробовать, надеясь что знаки достаточно фичастые. Вообщем уже в первом задании можно потратить немало времени на проверку различных гипотез.
Во втором задании да, можно наверно было-бы применить наработки из предложенной статьи, но вопрос качества остается открытым.
Третье задание целиком копипастится из OpenCV.
А с учетом того, что ко всем заданиям необходимо еще написать сопроводительные документы с анализом качества реализации и т.п., то как правильно сказал frol, тут действительно для качественного выполнения задания стоит тратить неделю фулл-тайма.
iroln а откуда такое задание на собеседование и что за условия там предлагают, если хоть кто-то берется за его выполнение?:)iroln
13.05.2015 17:35… каскадный детктор или Deep Learning, но сомневаюсь, что к заданию приложили достаточное для обучения количество позитивов/негативов
Тестовые данные, которые были приложены к заданию, не позволяют использовать никакие обучающиеся алгоритмы (никаких позитивов/негативов). На счёт сроков и объёма задания, если делать всё по уму, при этом начиная с нуля (не имея работающего прототипа/модели), и недели фулл-тайма не хватит. Как вы и сказали:уже в первом задании можно потратить немало времени на проверку различных гипотез
На счёт «откуда» и условий ответил в личку, думаю, так будет правильно.BelBES
13.05.2015 18:35На счёт сроков и объёма задания, если делать всё по уму, при этом начиная с нуля (не имея работающего прототипа/модели), и недели фулл-тайма не хватит.
Скорей всего там расчет и сделан на тех, кто придет, например, с результатами своей Ph.D. работы связанной с ADAS.
kmike
13.05.2015 17:35А зачем было EXIF записывать к превьюшке? Я так понял, на это ушло 90% времени, и про это ничего в ТЗ не было.
nxsofsys Автор
13.05.2015 17:46EXIF я пишу к отконвертированному в JPEG изображению из эстетических соображений: я посчитал удобным хранить все необходимые для сервиса данные прямо в картинке, потому что для восстановления сервиса будет необходимо хранить только эти картинки. И эта задача помогла мне обнаружить дополнительные форматы изображений, которые я решил поддержать в своем решении. В целом, я бы не сказал, что реализация именно этой задачи была долгой, просто при работе над ней вскрылись нюансы, которые я пожелал покрыть.
VBart
13.05.2015 18:36Начал я со сборки Upload модуля NGINX, и, конечно же, безуспешно. Провозившись некоторое время, я понял, что автор его забросил, и на последней версии этот модуль не заработает. Ну и ладно — пусть Tornado сервер сохраняет входящие файлы на диск.
Открою страшную тайну: NGINX умеет это делать без модуля из коробки.
SelenIT2
13.05.2015 18:53… (Не использовать проверку наличия EXIF данных в качестве валидации)
Не позволять сохранять фото, созданные более года назад (проверять дату создания фото из EXIF).
Если отсутствует дата создания фото в EXIF, тогда следует выдать ошибку и не добавлять файл.
Так всё-таки?Pe4enie
13.05.2015 20:42Вы из контекста вырвали, вчитайтесь:
Проверять, является ли загружаемый файл изображением, если нет — выдавать ошибку. (Не использовать проверку наличия EXIF данных в качестве валидации)
Просят не принимать наличие в файле EXIF данных в качестве доказательства, что файл является изображениемnxsofsys Автор
13.05.2015 20:47EXIF может служить метаданными не только для изображений, но и для звуковых файлов. Видимо, поэтому было поставлено такое условие.
SelenIT2
13.05.2015 21:49Это логично. Но формулировка расплывчатая, и это открывает веселые перспективы. Вот так сдашь задание с выполненным последним пунктом, а его зарубят: «Сказано же — EXIF для валидации не использовать!» :)
aml
Ни хрена себе тестовое задание. А зарплату за него заплатят?
RPG18
А что тут собственно такого? В задание не сказано использовать NGINX, а простой рабочий прототип можно сварганить за пару часов. Окценты были сразу расставлены: