Статьи

Обнаружение лиц с помощью Ruby: FFI в двух словах

Пазл человек женщина лицо люди проблема В последние пару лет я полюбил Ruby. Это было так здорово — просто делать вещи, не слишком заботясь о типах и управлении памятью. Великолепная выразительность языка, простота использования всей системы Gem и приятная интерактивная оболочка для игры по-настоящему заставляют вас чувствовать себя как дома. И поскольку вы читаете RubySource.com , вполне вероятно, что вы уже чувствуете то же самое.

Но есть немало доменов, в которых Ruby отстает. Rails дал Ruby много положительной рекламы (и некоторую отрицательную ), но также сформировал экосистему Ruby, что сильно подтолкнуло ее к веб-разработке. Это можно рассматривать как благословение или проклятие.
С одной стороны, Ruby действительно хорош в веб-домене. С другой стороны, он не является серьезным конкурентом таким языкам, как Python, в таких областях, как научное программирование или разработка игр. Хотя я вполне уверен, что это будет медленно меняться, но есть несколько вариантов, которые вы можете выбрать прямо сейчас:

  • Swig, чтобы обернуть интерфейсы C / C ++
  • RubyInline для встраивания стороннего кода в ваш Ruby
  • Расширение C / C ++ для расширения Ruby
  • Spawn to call директория программ
  • FFI для загрузки библиотек и связывания в Ruby. Это то, что я расскажу в этой статье.

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

Почему вы должны рассмотреть возможность использования FFI

FFI имеет некоторые преимущества по сравнению с написанием расширений на C / C ++ или вызовом программ в подоболочке:

  • С FFI вам часто даже не приходится писать код на C / C ++. Вызов методов, которые уже доступны в хорошо поддерживаемой библиотеке, может сэкономить время.
  • FFI практически одинаков в любой среде Ruby, может быть, это MRI , Rubinius или даже JRuby без изменений.
  • Это проще для ваших конечных пользователей. Им не понадобятся заголовки разработки или компилятор C / C ++.

Когда FFI не может быть правильным инструментом

  • Вы разрабатываете только небольшой скрипт, который должен отображать некоторые выходные данные программы в вашей системе. Во что бы то ни стало, просто создайте подпроцесс и вызовите эту программу напрямую, как в командной оболочке, а затем сгладьте вывод. Конечно, это немного глупо, но когда это экономит ваше время и не должно распространяться среди тысяч пользователей, почему бы и нет? Будьте очень осторожны с пользовательскими параметрами!
  • Библиотека, которую вы пытаетесь обернуть, использует множество функций времени компиляции или препроцессора. Возможно, вы сможете реализовать некоторые из них в Ruby, но иногда это противоречит первоначальной цели.
  • Если вам нужно написать много пользовательского кода на C / C ++ и считать каждую миллисекунду, вы, возможно, захотите пройти лишнюю милю и написать расширение C / C ++, хотя создание собственной библиотеки lib и взаимодействие с ней может быть лучшим вариантом. Я покажу вам, как это сделать позже.
  • У вас есть проблемы с использованием FFI с тяжелым интерфейсом обратного вызова. Обратные вызовы поддерживаются FFI, но их не всегда легко реализовать.
  • Большинство систем FFI не могут видеть константы, созданные с помощью #define . Вы можете переопределить их в Ruby довольно легко, но это может быть утомительной работой, и если они меняются в более новой версии библиотеки, вам также придется изменить свой код.

Основной пример

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

Может быть, мы хотели бы использовать функцию из LIBC . Можно хотеть получить идентификатор процесса текущего процесса.
Теперь, когда вы знакомы с Ruby, вы знаете, что есть удобная глобальная переменная $$ которая дает именно это. Ради этого простого примера давайте пойдем в обход и сделаем это через функцию getpid в FFI и LIBC :

 require 'ffi' module MyLibcWrapper extend FFI::Library ffi_lib FFI::Library::LIBC attach_function :getpid, [], :int end puts MyLibcWrapper.getpid puts $$ 

Прежде всего, мы должны требовать FFI Gem (и, возможно, rubygems прежде, если вы используете старую версию Ruby). Затем мы создаем модуль, который расширяет модуль FFI::Library . Теперь мы можем использовать методы этого модуля для привязки к библиотеке LIBC и, наконец, присоединить функцию getpid к нашему модулю. Обратите внимание, что аргументы для attach_function являются символом (представляющим имя функции внутри attach_function библиотеки), а затем массивом типов аргументов для этой функции (getpid
не требует аргументов) и, наконец, тип возврата этой функции (int).
Теперь эта функция доступна как статическая функция нашего модуля. Как вы надеетесь увидеть (не забудьте gem install ffi ), когда вы запускаете этот код, оба метода возвращают один и тот же идентификатор процесса.

Расширенный пример

Но как насчет более сложного примера? Есть хороший шанс, что вам придется работать с какой-то структурой и передавать указатели на адреса памяти, ведь здесь мы говорим о C / C ++. Возможно, вам нужно выяснить, на какой системе работает ваша программа. К счастью, есть еще одна функция LIBC «uname», которая предоставляет такую ​​информацию, поэтому давайте обернем ее:

 require 'ffi' module MyLibcWrapper extend FFI::Library ffi_lib FFI::Library::LIBC # define a FFI Struct to hold the data that we can retrieve class UTSName < FFI::Struct layout :sysname , [:char, 65], :nodename , [:char, 65], :release , [:char, 65], :version , [:char, 65], :machine , [:char, 65], :domainname, [:char, 65] def to_hash Hash[members.zip values.map(&:to_s)] end end # takes a pointer, returns an integer attach_function :uname, [:pointer], :int def self.uname_info uts = UTSName.new # create a place in memory to hold the data raise 'uname info unavailable' if uname(uts) != 0 # retrieve data uts.to_hash end end puts MyLibcWrapper.uname_info # => {:sysname=>"Linux", :nodename=>"picard", :release=>"3.0.0-32-generic", :version=>"#50-Ubuntu SMP Thu Feb 28 22:32:30 UTC 2013", :machine=>"x86_64", :domainname=>"(none)"} 

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

Простой пользовательский пример

Теперь вы, возможно, помните, что одна классная программа на C / C ++, которую вы написали давным-давно, выполняет эту удивительную вещь за доли секунды, и вы действительно хотите использовать ее в своем новейшем веб-сервисе Ruby для создания следующего разрушительного приложения-убийцы!

Позвольте мне рассказать вам о процессе превращения вашей собственной программы на C / C ++ в общую библиотеку.

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

 extern "C" unsigned long factorial(int n); //offer a C-compatible interface unsigned long factorial(int n){ unsigned long f = 1; for (int c=1; c<=n; c++) f *= c; return f; } 

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

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

 g++ -shared -fPIC -Wall -o libfactorial.so factorial.c 

Однако вы можете добавить метод main и скомпилировать его дважды: один раз с флагом и один раз без флага -shared. Это простой способ проверить вашу библиотеку без необходимости встраивать ее куда-либо.

Теперь для Ruby. Мы уже знаем шаблон:

 require 'ffi' module MyAwesomeLib extend FFI::Library ffi_lib './libfactorial.so' # load library from the same folder # this time we take an integer and return an unsigned integer attach_function :factorial, [:int], :uint end puts MyAwesomeLib.factorial(6) # => 720 

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

Распознавание лиц в миллисекундах

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

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

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

Вам нужно будет установить OpenCV, хотя есть пакеты и инструкции почти для каждой платформы. Кроме того, вам понадобится файл lbpcascade_frontalface.xml . Этот файл является частью пакета opencv-doc и может быть найден в сжатой форме в /usr/share/doc/opencv-doc/examples/lbpcascades/lbpcascade_frontalface.xml.gz (в Ubuntu) или в Google .

После того, как вы приобрели все необходимое, вы можете создать свой исходный файл faces.cpp :

 #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/objdetect/objdetect.hpp> #include <opencv2/highgui/highgui.hpp> #include <iostream> #include <stdio.h> #include <unistd.h> using namespace std; using namespace cv; char buf[4096]; extern "C" char* detect_faces(char* input_file, char* output_file); int main(int argc, char** argv) { if(argc<2){ fprintf(stderr, "usage:n%s <image>n%s <image> <outimg>n", argv[0], argv[0]); exit(-1); } printf("%s", detect_faces(argv[1], argc<3 ? NULL : argv[2])); exit(0); } char* detect_faces(char* input_file, char* output_file) { CascadeClassifier cascade; if(!cascade.load("lbpcascade_frontalface.xml")) exit(-2); //load classifier cascade Mat imgbw, image = imread((string)input_file); //read image if(image.empty()) exit(-3); cvtColor(image, imgbw, CV_BGR2GRAY); //create a grayscale copy equalizeHist(imgbw, imgbw); //apply histogram equalization vector<Rect> faces; cascade.detectMultiScale(imgbw, faces, 1.2, 2); //detect faces for(unsigned int i = 0; i < faces.size(); i++){ Rect f = faces[i]; //draw rectangles on the image where faces were detected rectangle(image, Point(fx, fy), Point(fx + f.width, fy + f.height), Scalar(255, 0, 0), 4, 8); //fill buffer with easy to parse face representation sprintf(buf + strlen(buf), "%i;%i;%i;%in", fx, fy, f.width, f.height); } if(output_file) imwrite((string)output_file, image); //write output image return buf; } 

На этот раз я предоставил основной метод, чтобы вы могли играть с программой напрямую, без FFI. Я решил использовать простой буфер символов и заполнить его строками, представляющими обнаруженные лица. Каждая строка представляет одно обнаруженное лицо в виде «x; y; ширина; высота». На мой взгляд, этот метод очень универсален и позволяет вам возвращать почти все, что вы хотите, не беспокоясь об удовольствиях динамических размеров многомерных массивов из функций C / C ++.

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

Но сейчас давайте скомпилируем! Возможно, вы захотите создать небольшой make-файл или скрипт сборки:

 LIBS="-lopencv_imgproc -lopencv_highgui -lopencv_core -lopencv_objdetect" g++ -I/usr/local/include/opencv -I/usr/local/include/opencv2 -L/usr/lib -L/usr/local/lib -fpic -Wall -c "faces.cpp" $LIBS //create shared library g++ -shared -I/usr/local/include/opencv -I/usr/local/include/opencv2 -o libfaces.so faces.o -L/usr/local/lib $LIBS //create executable (in case you want to play with it directly) g++ -I/usr/local/include/opencv -I/usr/local/include/opencv2 -o faces faces.o -L/usr/local/lib $LIBS 

После компиляции мы можем запустить его ./faces image_with_faces.jpg detected_faces.jpg и, надеюсь, он обнаружит некоторые лица для нас.

Наконец, последний шаг — завернуть его в некоторый код Ruby:

 require 'ffi' module Faces extend FFI::Library ffi_lib File.join(File.expand_path(File.join(File.dirname(__FILE__))), 'libfaces.so') attach_function :detect_faces, [:string, :string], :string def self.faces_in(image) keys = [😡,:y,:width,:height] detect_faces(image, nil).split("n").map do |e| vals = e.split(';').map(&:to_i) Hash[ keys.zip(vals) ] end end end p Faces.faces_in('test.jpg') 

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

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

Я искренне надеюсь, что смогу дать вам некоторое представление и вдохновить вас выйти за пределы Руби в одиночку для вашего следующего крутого проекта! Если вам понравилась эта статья, не стесняйтесь рассказать своим друзьям, блог, чирикать и распространять слово 🙂