Разработка приложения с использованием метода Лукаса-Канаде

Особенности и методы построения приложений с технологией оптического потока. Простые функции для преобразования изображений. Примитивные типы данных в OpenCV. Выбор языка программирования. Разработка приложения с использованием алгоритма Лукаса-Канаде.

Рубрика Программирование, компьютеры и кибернетика
Вид дипломная работа
Язык русский
Дата добавления 07.08.2018
Размер файла 1,1 M

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

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

Размещено на http://www.allbest.ru/

Федеральное агентство связи

Федеральное государственное образовательное бюджетное учреждение высшего профессионального образования

«Поволжский государственный университет телекоммуникаций и информатики»

Факультет Заочного обучения

Направление (специальность) Информационные системы и технологии

Кафедра Информационных систем и технологий

Выпускная квалификационная работа (бакалаврская работа)

Разработка приложения с использованием метода Лукаса-Канаде

Разработал 35И В.Л. Петров

Руководитель ассистент А.В. Виноградов

Н. контролер доцент к.т.н., с.н.с. О.Л. Куляс

Самара 2017

Введение

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

Все выше сказанное определило актуальность темы работы - Разработка приложения с использованием метода Лукаса-Канаде.

Целью дипломной работы является разработка приложения для работы с оптическим потоком, используя метод Лукаса-Канаде

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

• Рассмотреть понятие оптический поток;

• Подготовить среду разработки языка С++;

• Разработать приложение;

• Провести анализ разработанного приложения.

Объектом исследования является работа с изображениями при помощи оптического потока.

Предметом исследования является реализация алгоритма Лукаса-Канаде.

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

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

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

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

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

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

1. Библиотека OpenCV

оптический поток алгоритм изображение

1.1 Простейшие функции

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

В Visual Studio необходимо в свойствах проекта указать библиотеки (для debug, необходимо использовать библиотеки с d на конце) highgui.lib, cxcore.lib, ml.lib, cv.lib и путь к заголовочным файлам OpenCV .../opencv/* /include. Какправило, "include" - эточто-тотипаC:/program files/opencv/cv/include, .../opencv/cxcore/include,.../opencv/ml/include и .../opencv/otherlibs/highgui.

После выполнения этих настроек,всеготово для написания первой программы[1].

Некоторые заголовочные файлы могут существенно облегчить вашу жизнь. Множество полезных макросов можно найти в .../opencv/cxcore/include/cxtypes.h и cxmisc.h. При помощи них возможно делать такие вещи, как: инициализацию структур и массивов, сортировку списка и т.д. Наиболее важные заголовочные файлы: .../cv/include/cv.h и .../cxcore/include/cxcore.h для компьютерного зрения, .../otherlibs/highgui/highgui.h дляввода/вывода, .../ml/include/ml.h для машинного обучения

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

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

IplImage* img = cvLoadImage(argv[1] );

Эта строка загружает изображение. Функция cvLoadImage() является высокоуровневой: определяет формат файла по его имени; автоматически выделяет память, необходимую для изображения. Обратите внимание, функция cvLoadImage() поддерживает широкий спектр типов изображений: BMP, DIB, JPEG, JPE, PNG, PBM, PGM, PPM, SR, RAS и TIFF. Функция возвращает указатель типа IplImage. Это наиболее часто используемый тип данных в OpenCV. Используется для обработки всех видов изображений: одноканальные, многоканальные, целочисленные, вещественные и т.д. В дальнейшем манипуляция изображением производиться через созданный указатель IplImage. cvNamedWindow( "DisplayPicture", CV_WINDOW_AUTOSIZE );

Еще одна высокоуровневая функция cvNamedWindow(), открывает окно для размещения и отображения изображения. Функция из библиотеки HighGUI, присваивает имя окну (в нашем случае "DispalyPicture"). Последующие вызовы HighGUI будут взаимодействовать с созданным окном по его имени.

Второй аргумент функции cvNameWindow() определяет свойства окна. Может быть установлен как 0 (значение по умолчанию) или как CV_WINDOW_AUTOSIZE. В первом случае, размер окна не будет зависеть от размера загружаемого изображения, изображение будет подстраиваться под размеры окна. Во втором случае, окно будет подстраиваться под размеры загружаемого изображения.

cvShowImage( "DisplayPicture", img );

Всякий раз, когда есть изображение в виде указателя IplImage, его можно отобразить в существующем окне с помощью функции cvShowImage(). Функция требует, чтобы имя окна уже существовало (создано с помощью cvNamedWindow()). Вызов cvShowImage() перерисовывает окно, с соответствующим изображением в нем, и изменяет его размеры, если при создании был указан флаг CV_WINDOW_AUTOSIZE.

cvWaitKey(0);

Функция cvWaitKey() заставляет программу ожидать нажатие любой клавиши. Если аргумент - положительное число, программа будет ожидать заданное количество миллисекунд, а затем продолжит выполнение программы, даже если ничего не будет нажато. Если параметр - 0 или отрицательное число, программа будет бесконечно ожидать нажатие клавиши[2].

cvReleaseImage(&img );

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

cvDestroyWindow( "DisplayPicture" );

В завершении, нужно уничтожить окно. Функция cvDestroyWindow() закрывает окно и высвобождает любую, связанную с этим окном, память (внутренний буфер для изображения, который содержит копию информации о пикселях из img). Для столь простой программы можно было и не использовать cvDestroyWindow() или cvReleaseImage(), т.к. все ресурсы и окна автоматически уничтожаются операционный системой при выходе, однако использование этих функций хорошая привычка.

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

1.2 Работа с видеопотоком

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

Простая программа для воспроизведения видеофайла с диска при помощи OpenCV

cvReleaseCapture( &capture ); // Закрытие файла cvDestroyWindow( "PlayVideo" ); // Уничтожение окна

}

Функция main() начинается с создания окна, в примере имя окна "PlayVideo". После начинается самое интересное.

CvCapture* capture = cvCreateFileCapture( argv[1] );

Функция cvCreateFileCapture() принимает в качестве аргумента имя видеофайла, а возвращает указатель на структуру типа CvCapture. Эта структура содержит всю информацию из видеофайла, включая информацию о состоянии[3].

frame = cvQueryFrame( capture );

После, в цикле while(1), происходит чтение видеофайла кадр за кадром. Функция cvQueryFrame() принимает в качестве аргумента указатель на структуру CvCapture и помещает последующий кадр в память (которая на самом деле часть структуры CvCapture). Возвращает указатель полученного кадра. В отличии от cvLoadImage(), которая выделяет память под изображение, cvQueryFrame() использует выделенную память в структуре CvCapture. Поэтому не требуется использовать cvReleaseImage() для указателя на "кадр". Вместо этого память из-под кадра будет освобождена, когда произойдет уничтожение структуры CvCapture.

c = cvWaitKey(33);

if( c == 27 ) { break;

}

После отображения кадра, программа будет ожидать нажатие клавиши в течении 33 мс. Если пользователь нажмет клавишу, то будет получен ASCII код этой клавиши; иначе будет получена -1. Если пользователь нажмет Esc (ASCII 27), то чтение кадров прекратиться. Если в течении 33 мс пользователь не нажмет клавишу, то произойдет чтение последующего кадра.

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

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

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

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

Давайте взглянем на детали.

Int g_slider_position = 0;

CvCapture* g_capture = NULL;

Во-первых, нужно определить глобальную переменную для позиции ползунка.

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

void onTrackbarSlide(int pos) {

cvSetCaptureProperty(

g_capture

,CV_CAP_PROP_POS_FRAMES

,pos

);

}

Функция-обработчик вызывается каждый раз, когда пользователь изменяет положение ползунка. Эта функция принимает 32-разрядное целое число, которое устанавливает положение ползунка.

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

CV_CAP_PROP_POS_FRAMES указывает на то, что устанавливается позиция для чтения кадров в единицах (также можно использовать AVI_RATIO вместо FRAMES, если потребуется установить позицию в процентах). Как результат - новое положение позиции ползунка. Библиотека HighGUI довольно-таки умна и потому такие ситуации, как запрос к не ключевому кадру будут обрабатываться автоматически; произойдет откат назад к ближайшему ключевому кадру и уже от него произойдет перемотка вперед до нужного кадра.

int frames = (int) cvGetCaptureProperty(

g_capture

,CV_CAP_PROP_FRAME_COUNT

);

Как было отмечено ранее, для получения данных из структуры CvCapture используется функция cvGetCaptureProperty(). В нашем случае, требуется узнать количество кадров в видео для откалиброки ползунка.

Для создания ползунка используется функция cvCreateTrackbar(), которая принимает в качестве аргументов: имя объекта ползунка, имя окна, переменную положения ползунка, максимальное значение ползунка и функцию-обработчик (или NULL).

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

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

И в заключении, не был рассмотрен кусок кода, необходимый для отображения авто перемещения ползунка поверх видео. Эта задача остается в качестве упражнения[4].

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

Одна из самых простых операций это сглаживание изображения, которая эффективно снижает информативность изображения, сворачивая его гауссовой или другой аналогичной функцией ядра. Сделать нечто подобное довольно таки легко. Начнем с создания нового окна "Transform-out", где будем отображать результат преобразований. После отображения захваченного исходного кадра (функция cvShowImage()), произведем сглаживание изображения из кадра и отобразим в окне результата[5].

Ранее, новый кадр создавался при содействии функции cvCreateFileCapture().

Происходил покадровый перебор всех кадров из структуры CvCapture, один за другим, с размещением их в одном и том же указателе. В нашем случае использование данного указателя не приемлемо и потому создается собственная структура под результат сглаживания. Для этого используется новая функция cvCreateImage(). Аргументы функции cvCreateImage( CvSize size, int depth, int channels ):

size - размер существующей структуры изображения (удобно получать при помощи функции cvGetSize())

depth - тип данных для каждого канала channels - количество каналов

В примере 2-3 создается 8-ми битное 3-х канальное изображение того же размера, что и исходное изображение.

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

Не забывайте освобождать ресурсы, выделенные под результирующее изображение (используйте cvReleaseImage()).

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

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

OpenCV это достигается при помощи функции cvPyrDown(), которая выполняет гауссово сглаживание, а затем удаляет каждую вторую строку из изображения. Это полезно в самых разнообразных и важных алгоритмах компьютерного зрения

IplImage* doPyrDown( IplImage* in, int filter = IPL_GAUSSIAN_5x5 ) {

// Проверка деления исходного изображения на 2

//

assert( 0 == in->width%2 && 0 == in->height%2 );

// Создание контейнера с размером вдвое меньшим, чем исходное изображение.

// В остальном параметры те же

//

IplImage* out = cvCreateImage(

cvSize( in->width/2, in->height/2 ) ,in->depth

,in->nChannels

);

// Сжатие исходного изображения

cvPyrDown( in, out );

return( out );

};

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

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

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

Например, требуется уменьшить изображение в два раза, а затем найти контур в уменьшенном еще раз вдвое изображении.

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

Механизм "само очистки" будет более аккуратным, если использовать эту функцию, однако существует еще одна проблема: что, если нам потребуется что-то сделать с промежуточным изображением? Ничего может не получиться из-за возможного отсутствия доступа к этому изображению[6].

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

указатель IplImage, возвращаемый cvCreateFileCapture(), указывает на структуру типа

CvCapture и инициализируется только в момент загрузки видеофайла. Освобождение памяти, вызовом функции cvRealeaseImage(), приведет к некоторым неожиданным проблемам. Мораль сей басни такова: очистка мусора важна, но только мусора, которой создавали сами!

2. Захват видео с камеры

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

OpenCV - а точнее ее часть, HighGUI - помогает с легкостью справиться с подобного рода ситуациями. Подход схож с тем, как происходило чтение AVI файла. Вместо вызова cvCreateFileCapture(), используется cvCreateCameraCapture(). В качестве аргумента используется не имя файла, как ранее, а идентификационный номер камеры, и то имеет смысл его использовать, когда доступно сразу несколько камер.

Значение по умолчанию равно -1, что означает "просто выбрать одну"; естественно, это работает, когда доступна только одна камера (см. Главу 4 для получения более подробной информации).

Функция cvCreateCameraCapture() возвращает указатель на структуру CvCapture, которая обрабатывается так же, как и в примере с обработкой видео файла с диска.

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

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

Во многих приложениях требуется записывать потоковое видео в файл и OpenCVпредоставляет простые в использовании средства для этого. Просто нужно создать устройство захвата, прочитать из него кадры один за одним и при помощи созданного устройства записи поместить кадры один за другим в видео файл. Устройство записи создается при помощи функции cvCreateVideoWriter()[8].

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

#include <cv.h>

#include <highgui.h>

int main( int argc, char* argv[] )

CvCapture* capture = 0; // Переменнаязахвата

capture = cvCreateFileCapture( argv[1] ); // Чтениевидеофайласдиска

// Не удалось захватить видео поток с диска

if( !capture ) { return -1;

}

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

//

IplImage *bgr_frame=cvQueryFrame( capture );

// Частотакадров

//

double fps = cvGetCaptureProperty( capture, CV_CAP_PROP_FPS );

// Размеркадра

//

CvSize size = cvSize(

(int)cvGetCaptureProperty( capture, CV_CAP_PROP_FRAME_WIDTH ) ,(int)cvGetCaptureProperty( capture, CV_CAP_PROP_FRAME_HEIGHT )

);

Все начинается с открытия видео файла с диска; затем берется первый кадр при помощи функции cvQueryFrame() для определения необходимых свойств при помощи функции cvGetCaptureProperty(). Потом создается устройство для записи и покадрово записываются преобразованные кадры в формате log-polar в новый видео файл. Взаключении происходит освобождение занимаемой памяти.

Вызов функции cvCreateVideoWriter() сопровождается передачей в нее нескольких параматров:

cvCreateVideoWriter(const char* filename, int fourcc, double fps, CvSize frame_size, int

fileName - имя нового видео файла

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

frame_size - размер кадра

is_color - 1: кодирование цветных кадров; 0: кодирование кадров в оттенках серого

Есть бесчисленное множество кодеков; прежде чем выбрать тот или иной кодек,убедитесь, что он доступен на вашей машине (кодеки установливаются отдельно отOpenCV). В примере выбран довольно популярный кодек MJPG; на это указывает макрос CV_FOURCC(), который принимает четыре символа в качестве аргументов.

Эти символы составляют "четырехзначный код" кодека, каждый кодек имеет такой код.

Четырехзначный код движущейся jpg-картинки и есть MJPG (motion jpeg), потому и указано CV_FOURCC('M','J','P','G').

Список кодеков, включённых в OpenCV:

'X', 'V', 'I', 'D' - кодек XviD 'P', 'I', 'M', '1' - MPEG-1 'M', 'J', 'P', 'G' - motion-jpeg 'M', 'P', '4', '2' - MPEG-4.2 'D', 'I', 'V', '3' - MPEG-4.3 'D', 'I', 'V', 'X' - MPEG-4 'U', '2', '6', '3' - H263

'I', '2', '6', '3' - H263I 'F', 'L', 'V', '1' - FLV1

Прежде чем перейти к следующей главе, подведем итоги. Уже известно, как API OpenCV предоставляет множество простых в использовании инструментов для загрузки изображений из файлов, чтения видео файлов с жёсткого диска и захвата видео с камер. Так же известно, что библиотека предоставляет множество простых функций для работы с этими изображениями. Однако, всё ещё не были представлены более мощные инструменты OpenCV, которые позволяют производить более сложные манипуляции с абстрактными типами данных[9].

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

3. Простые функции для преобразования изображений

3.1 Примитивные типы данных вOpenCV

OpenCV включает в себя множество примитивных типов данных. Эти данные не являются примитивными с точки зрения C, но являются самыми элементарными с точки зрения OpenCV. Посмотреть, как устроены все эти структуры, можно в файле cxtypes.h в директории .../OpenCV/cxcore/include.

Самый простейший тип - CvPoint. Это простая структура состоит только из двух полей x и y типа int. CvPoint2D32f содержит два поля x и y типа float. CvPoint3D32f содержит три поля x, y и z типа float.

CvSize содержит два поля width и height типа int. CvSize2D32f содержит два поля widthи height типа float.

CvRect содержит четыре поля x, y, width и height типа int.

CvScalar содержит четыре переменные типа double. На самом деле CvScalar включает в себя одно поле val, которое является указателем на массив, содержащий четыре числа типа double.

Все эти типы данных имеют конструкторы с именами похожими на cvSize (обычно именуются так же, как и структуры, только первая буква - строчная). Помните, что этоC, а не C++, и все эти "конструкторы" всего лишь inline функции, принимающие список аргументов и возвращающие желаемую структуру со значениями установленными соответствующим образом[10].

Конструкторы для типов данных из таблицы 3-1 - cvPointXXX(), cvSize(), cvRect(), cvScalar() - чрезвычайно полезны, потому что упрощают ваш код. Например, чтобы нарисовать белый прямоугольник с координатами углов (5, 10) и (20, 30), достаточно написать следующий код:

cvRectangle(

myImg // Изображение

,cvPoint(5,10) // Верхний левый угол

,cvPoint(20,30) // Нижний правый угол

,cvScalar(255,255,255) // Цвет

);

cvScalar в отличии от всех остальных структур содержит три конструктора. Первый может принимать один, два, три или четыре аргумента и присваивать их соответствующим элементам val[]. Второй конструктор cvRealScalar() принимает один аргумент и устанавливает соответствующее значение val[0], остальные элементы устанавливаются в 0. Третий конструктор cvScalarAll() так же принимает один аргумент и инициализирует все элементы val[] этим значением.

При использовании OpenCV, неоднократно будет использоваться типIplImage. IplImage это базовая структура, используемая для кодирования того, что мы называем "изображение". Эти изображения могут быть чёрно-белыми, цветными, 4-хканальными (RGB+Alpha), и каждый канал может содержать либо целые, либо вещественные значения. Следовательно, этот тип является более общим, чем вездесущие 3-х канальные 8-битные изображения, которые сразу приходят на ум.

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

Несмотря на то, что OpenCV написана на C, структуры, используемые вOpenCV, объектно-ориентированные; в действительности, IplImage происходит отCvMat, которая является производной от CvArr.

Прежде, чем перейти к деталям, необходимо взглянуть на другой тип данных: CvMat,структура для матриц. Хотя OpenCV полностью написана на C, относительная взаимосвязь между CvMat и IplImage похожа на наследование в C++. IplImage можно рассматривать как производную от CvMat. Класс CvArr можно рассматривать как абстрактный базовый класс. В прототипах функций зачастую будет указано CvArr(точнее указатель CvArr). В таких случаях вместо CvArr можно использовать и указатель CvMat и указатель IplImage.Learning OpenCV (перевод на русский)

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

cvMat* cvCreateMat ( int rows, int cols, int type );

Здесь type может быть любым из длинного списка предопределенных типов; тип задаётся так: CV_<глубина в битах>(S|U|F)C<число каналов>. Например, матрица может состоять из 32-битных чисел с плавающей точкой(CV_32FC1), 8-битных без знаковых триплетов(CV_8UC3) и из множества других. Элементами CvMat могут быть не только числа. Возможность представить один элемент составным значением позволяет делать такие вещи, как представление нескольких цветовых каналов в изображении RGB. С простыми изображениями, содержащие красную, зелёную и синюю составляющую, большинство операторов будут работать с каждым каналом по отдельности (если не указано обратное).

Внутренняя структура CvMat является довольно таки простой.

Матрицы имеют ширину, высоту, тип, шаг (длина строки в байтах, а не в int или float) иуказатель на массив данных (и еще несколько вещей, о которых пока не будет рассказано). Получить доступ к элементам матрицы можно непосредственно через указатель на CvMat или через специальные функции. Например, для получения размера матрицы можно либо вызвать функцию cvGetSize(CvMat*), либо непосредственно обратиться к соответствующим полям через указатель как matrix->height и matrix->width.

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

Матрицы могут быть созданы несколькими способами. Наиболее распространенным является подход с использованием cvCreateMat(), которая по существу использует более элементарные функции cvCreateMatHeader() и cvCreateData(). cvCreateMatHeader() создает структуру CvMat, без выделения памяти под данные, в то время, как cvCreateData() выделяет память под данные. По тем или иным причинам,порой, требуется создание лишь заголовка матрицы. Ещё один метод заключается в вызове функции cvCloneMat(CvMat*), которая создает новую матрицу на основе существующей. Когда матрица больше не нужна, память из под неё может быть освобождена вызовом функции cvReleaseMat(CvMat**).

По аналогии с другими структурами OpenCV для CvMat также есть конструктор cvMat(). Конструктор не выделяет память под данные, а лишь создаёт заголовок (по аналогии с cvInitMatHeader()). Хороший способ создать матрицу из уже имеющихся данных показан в примере 3-3.

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

cvGetElemType( const CvArr* arr )

cvGetDims( const CvArr* arr, int* sizes = NULL ) cvGetDimSize( const CvArr* arr, int index )

Первая функция возвращает тип элемента матрицы (например, CV_8UC1, CV_64_FC4и т.д.). Вторая принимает указатель на массив и дополнительный указатель на целое число, а возвращает количество измерений (в примере матрица 2х2). Если указатель на число не NULL, то будет возвращена размерность принимаемого массива.

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

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

Простой способ

Самый простой способ получить данные матриц это воспользоваться макросомCV_MAT_ELEM(). Этот макрос принимает в качестве аргументов указатель на матрицу, тип элементов, сроку и столбец, а возвращает запрашиваемый элемент.

Доступ к данным матрицы через макрос CV_MAT_ELEM

CvMat* mat = cvCreateMat( 5, 5, CV_32FC1 ); // Создание матрицы

float element_3_2 = CV_MAT_ELEM( *mat, float, 3, 2 );

"Под капотом" этот макрос просто вызывает другой макрос CV_MAT_ELEM_PTR().

Этот макрос (пример 3-5) принимает в качестве аргументов указатель на матрицу,номер строки и столбца запрашиваемого элемента и возвращает указатель на нужный элемент. Одно важное отличие CV_MAT_ELEM() от CV_MAT_ELEM_PTR() в том, чтоCV_MAT_ELEM() преобразует указатель в соответствии с типом. Если требуется задать значение элементу матрицы, то нужно непосредственно вызватьCV_MAT_ELEM_PTR(); при этом, однако, необходимо сделать приведение типов в явном виде.

CvMat* mat = cvCreateMat( 5, 5, CV_32FC1 ); // Создание матрицы

float element_3_2 = 7.7; // Значение элемента в строке

*( (float*)CV_MAT_ELEM_PTR( *mat, 3, 2 ) ) = element_3_2;

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

Сложный способ

Два макроса, которые были рассмотрены в простом способе, могут работать только с одно- и двумерными матрицами (одномерные массивы или вектора, на самом деле,просто 1xn матрица). OpenCV предоставляет механизмы для обработки многомерных массивов. Фактически OpenCV позволяет обрабатывать n-мерные матрицы "условно любого" размера.

Для получения доступа к элементам матрицы используется семейство функций cvPtr*D и cvGet*D описанные в примере 3-6 и 3-7. Семейство cvPtr*D содержит функции cvPtr1D(), cvPtr2D(), cvPtr3D() и cvPtrND(). Каждая из первых трех принимает указатель на матрицу CvArr, соответствующий количеству целых индексов, инеобязательный параметр, указывающий на тип выходного параметра. Все эти процедуры возвращают указатель на необходимый элемент. cvPtrND() вторым аргументом принимает указатель на массив, содержащий соответствующее количество индексов.

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

double cvGetReal1D( const CvArr* arr, int idx0 ); // Дляодно-

double cvGetReal2D( const CvArr* arr, int idx0, int idx1 ); // двух-

double cvGetReal3D( const CvArr* arr, int idx0, int idx1, int idx2 ); // трёх-

double cvGetRealND( const CvArr* arr, int* idx ); // N-канальных

CvScalar cvGet1D( const CvArr* arr, int idx0 );

CvScalar cvGet2D( const CvArr* arr, int idx0, int idx1 );

CvScalar cvGet3D( const CvArr* arr, int idx0, int idx1, int idx2 );

CvScalar cvGetND( const CvArr* arr, int* idx );

Тип возвращаемого значения для первых четырех функций - число типа double, для других четырех CvScalar. Это означает, что имеет место значительное увеличение ненужных расходов при использовании этих функций. Они должны быть использованы только там, где это уместно и эффективно; иначе лучше использовать cvPtr*D.

Причин, по которой лучше использовать cvPtr*D() заключается в том, что эти функции возвращают указатель на необходимый элемент, что в свою очередь позволяет использовать арифметику указателей для перемещения по матрице. Важно помнить, что каналы в многоканальной матрице являются смежными. Например, трехканальная двумерная матрица, представляющая байты для красного, зеленого, синего цветов (RGB), хранит данные как: rgbrgbrgb .... Поэтому для перемещения указателя на следующий канал, необходимо увеличить указатель на единицу. Если потребуется перейти к следующему "пикселю" или набору элементов, увеличьте указатель на число равное числу каналов (в приведенном примере на 3).

Иной способ узнать шаг элемента матрицы заключается в получении длины строки матрицы в байтах. В структуре CvMat, колонок или ширины не достаточно для перемещения между строками матрицы, т.к. эффективное выделение памяти под матрицы или изображения выполняется до ближайшей границы в четыре байта. Таким образом под матрицы шириной в три байта будет выделено четыре байта и последний байт не будет использован. По этой причине, после получения указателя на элемент, необходимо увеличить его на соответствующий шаг. Если имеется матрица типа int или float и соответствующий указатель на элемент, то шаг для получения следующей строки: для int - step/4, для double - step/8 (при этом C будет автоматически умножать смещение в соответствии с типом данных).

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

double cvmGet( const CvMat* mat, int row, int col )

void cvmSet( CvMat* mat, int row, int col, double value )

Так, вызов функции cvmSet()

cvmSet( mat, 2, 2, 0.5000 );

эквивалентен вызову функции cvSetReal2D

cvSetReal2D( mat, 2, 2, 0.5000 );

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

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

floatsum( constCvMat* mat ) { floats = 0.0f;

// Переборстрокматрицы

for(int row=0; row<mat->rows; row++ ) {

// Указатель на начало соответствующей строки

const float* ptr = (const float*)(mat->data.ptr + row * mat->step);

// Переборэлементовстроки

for( col=0; col<mat->cols; col++ ) { s += *ptr++;

}

}

return( s );

}

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

Хотя структура CvMat и содержит поля width и height, для совместимости со старой структурой IplImage, предпочтительней использовать поля rows и cols. В заключении, обратите внимание на то, что ptr пересчитывается для каждой строки, а не просто берется с самого начала и затем смещается. Это может показаться лишним, но, указывая на ROI внутри большого массива, нет никакой гарантии, что данные будут непрерывны по строкам.

Один вопрос который может часто возникать - и который важно понимать - сводится к разнице между многомерным массивом (или матрицей) из многомерных элементов, и многомерным массивом с одномерными элементами. Предположим есть n точек в трехмерном измерении, которые необходимо передать некоторой функции OpenCV в виде CvMat* (скорее CvArr*). Есть четыре очевидных способа сделать это, при этом не все эквиваленты друг другу. Первый способ: использовать двумерный массив типа CV32FC1 из n строк и трех столбцов (nx3). Второй способ: использовать двумерный массив типа CV32FC1 с 3 строками и n столбцов (3xn). Третий и четвертый способы: использовать массив типа CV32FC3 с n строк и одного столбца (nx1) или массив с одной строкой и n столбцов (1xn). В некоторых случаях эти способы взаимозаменяемы.

В заключении пару слов о типах CvPoint2D и CvPoint2D32f. Эти типы данных определены как структуры C и, следовательно, имеют строго определенную компоновку памяти. В частности, числа типа int или float, которые содержат эти структуры образуют последовательный "канал". Как результат одномерный массив в стиле C этих объектов имеет такое же распределение памяти, как и n-by-1 или 1-by-n массивы типа CV32FC2. Тоже самое справедливо и для массивов структур типа CvPoint3D32f.

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

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

Однако, даже при наличии в OpenCV хорошо оптимизированных процедур, выполняющих большой спектр задач, всегда найдутся такие задачи, с которыми эти процедуры не справятся. Рассмотрим случай трехканального HSV изображения, в котором будем изменять насыщенность и значение цвета от 0 до 255 (максимальное значение для 8-битного изображения), оставляя оттенок неизменным. Лучше всего сделать это с использованием указателя, также как это было сделано с матрицами в примере 3-9. Тем не менее есть незначительные отличия, которые связаны с различиями в устройстве структур IplImage и CvMat. Пример 3-11 отражает наиболее быстрый способ обработки трехканального HSV изображения.

. Установка "S" и "V" составляющих HSV изображения в 255:

void saturate_sv( IplImage* img ) { for( int y=0; y<img->height; y++ ) {

uchar* ptr = (uchar*) (

img->imageData + y * img->widthStep

);

for( int x=0; x<img->width; x++ ) { ptr[3*x+1] = 255;

ptr[3*x+2] = 255;

}

}

}

В начале происходит вычисление указателя ptr, который указывает на начало соответствующей строки y. Затем значение насыщенности устанавливается в 255. Так, как это трехканальное изображение, расположение канала c в колонке x вычисляется как 3x+c*.

Одно важное отличие IplImage от CvMat заключается в поведении imageData. Данные

CvMat являются объединением, поэтому существует возможность задать тип указателя. Указатель imageData задается жестко как uchar*. Уже известно, что указатель данных не обязательно может быть типа uchar, в случае же с изображением арифметика над указателем сводится к добавлению к указателю значения widthStep, который так же в байтах, и потому уже не нужно беспокоиться о приведении типов, после получения результата. При работе с матрицами, необходимо уменьшать смещение, так как указатель данных не всегда может быть типа byte, в то время, как указатель данных изображения всегда типа byte, смещение можно использовать "как есть".

ROI и widthStep имеют большое практическое значение, так как во многих ситуациях ускоряют операции компьютерного зрения, за счет обработки части изображения. Все функции OpenCV поддерживают ROI и widthStep. Для включения или выключения поддержки ROI, используются функции cvSetImageROI() и cvResetImageROI(). Т.к. выделение части изображения имеет вид прямоугольника, нужно использовать структуру CvRect, которую можно передать в функцию cvSetImageROI() для "включения ROI"; для "выключения ROI" необходимо передать указатель на изображение в функцию cvResetImageROI().

void cvSetImageROI( IplImage* image, CvRect rect );

void cvResetImageROI( IplImage* image );

Чтобы увидеть, как используется ROI, рассмотрим пример загрузки и выделения некоторой области изображения. Код из примера 3-12 загружает изображение,устанавливает x, y, width, height предполагаемого ROI и объявляет целое число, для изменнеия области ROI. Затем задается область ROI, при помощи конструктора cvRect(). Важно выключить область ROI с помощью функции cvResetImageROI перед выводом изображения на экран, иначе в вывод попадет часть (область ROI)изображения:

#include <cv.h>

#include <highgui.h>

int main( int argc, char** argv ) { IplImage* src;

// argv[1] - путьдоизображения

if( argc == 7 && ((src=cvLoadImage(argv[1],1)) != 0 )) {

int x = atoi(argv[2]); // Отступ от левого верхнего угла изображения по осиint y = atoi(argv[3]); // Отступ от левого верхнего угла изображения по оси

int width = atoi(argv[4]); // Ширина ROI

int height = atoi(argv[5]); // Высота ROI

int add = atoi(argv[6]); // Значение увеличения

// Захват области изображения. Установка ROI

cvSetImageROI( src, cvRect(x,y,width,height) );

// Сложение скаляра с изображением

cvAddS(src, cvScalar(add),src );

// Сброс ROI

cvResetImageROI( src );

cvNamedWindow( "Roi_Add", 1 ); // Создание окна cvShowImage( "Roi_Add", src ); // Вывод результата cvWaitKey();

}

return 0;

}

4. Разработка приложения

4.1 Выбор языка программирования

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

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

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

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

Для этого Javaнаделена некоторыми дополнительными возможностями. Например, в отличие от C++ (или C), программист не должен в явном виде "освобождать" (возвращать) выделенную память операционной системе. Освобождение неиспользуемой памяти (сборка "мусора") автоматически обеспечивается средой выполнения Java в ущерб производительности и эффективности использования памяти (см. далее). Это освобождает программиста от утомительной задачи по слежению за освобождением памяти - главного источника ошибок в приложениях. Одна эта возможность языка должна значительно увеличить продуктивность программирования в сравнении с C++ (или C).

Однако проведенное исследование показывает, что на практике сборка "мусора" и другие возможности Java не оказывают большого влияния на продуктивность программирования. Одна из классических моделей оценки программного обеспечения CoCoMo, предложенная BarryBoehm, предопределяет стоимость и сроки разработки программного продукта на основе стоимостных коэффициентов, которые учитывают такие факторы, как суммарный опыт программирования разработчика, опыт программирования на заданном языке, желаемая надежность программы и т.д. Boehm пишет, чтонезависимо от уровня используемого языка, начальные трудозатраты всегда высокие.Подобная методика подсчета использовалась в другом исследовании, проведенном C.E.Walston и C.P.Felix, IBM,Метод измерения и оценки программирования (Amethodofprogrammingmeasurementandesti-mation).

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

Существует более позднее исследование, которое явно включает Java и которое подтверждает эту гипотезу. В Эмпирическом сравнении C, C++, Java, Perl, Python, Rexx и Tcl (AnempiricalcomparisonofC, C++, Java, Perl, Python, Rexx, andTcl) LutzPrechelt из университета Karlsruhe описывает проведенный им эксперимент, в котором студентам информатики поручили выполнить определенный проект и выбрать для его реализации, руководствуясь личными предпочтениями, один из языков программирования:C, C++ или Java (остальные языки были рассмотрены в другой части исследования). Собранные данные показали почти одинаковые результаты для C++ и Java (C был на третьем месте по многим параметрам). Эти результаты подтверждаются нашим собственным опытом: если программисты вольны в самостоятельном выборе языка программирования (чаще руководствуясь при этом своим опытом), программисты с равным опытом работы (например, измеренным в годах) достигают одной и той же продуктивности. Второй интересный аспект, который бы мы хотели отметить (но который не имеет формального экспериментального подтверждения), заключается в том, что менее опытные разработчики достигают лучших результатов с Java, разработчики со средним опытом разработки достигают одинаковых результатов с обоими языками программирования, опытные разработчики достигают лучших результатов с C++. Эти наблюдения могут быть объяснены тем, что для C++ доступны более совершенные средства разработки; и этот факт тоже должен быть принят во внимание.

Интересный способ определения продуктивности программирования предлагает метод функциональных единиц (FunctionPoint), разработанный CapersJones. Функциональная единица - это метрика программного обеспечения, которая зависит лишь от его функциональности, а не от конкретной реализации. Эта метрика позволяет использовать в качестве критерия оценки продуктивности программирования число строк кода, необходимых для обеспечения одной функциональной единицы, в свою очередь, уровень языка определяется числом функциональных единиц, которые могут быть созданы за определенное время. Интересно, что обе величины: число строк кода на единицу функциональности и уровень языка одинаковы для обоих языков (уровень языка:C++ и Java - 6, C - 3.5, Tcl - 5; число строк кода на единицу функциональности: C++ и Java - 53, C - 91, Tcl - 64).

Подводя итог: оба исследования и практика опровергают утверждение, что Java обеспечивает программистам лучшую продуктивность программирования, нежели C++.

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

Объем предлагаемой информации о сравнении языков огромен, но в конечном итоге он приходит к заключению, что "Java-программы выполняются по крайней мере в 1.22 раза медленнее C/C++ программ". Заметьте, что сказанопо крайней мере; средняя же скорость работы Java-программ гораздо меньше. Наш собственный опыт показывает, что Java-программы выполняются приблизительно в 2-3 раза медленнее своих C/C++ аналогов. На задачах, ориентированных на интенсивное использование процессора, Java-программы проигрывают еще сильнее.

В случае программ с пользовательским графическим интерфейсом увеличение времени отклика интерфейса является более критичным, чем низкая производительность программы. Проведенные исследования показывают, что пользователи более терпимы к задачам, выполняющимся в течение двух или трех минут, чем к программам, которые не реагируют мгновенно на их воздействия, например, на нажатия кнопок. Эти исследования показывают, что если время отклика программы больше, чем 0,7 секунды, пользователи считают ее медленной. Мы вернемся к этой проблеме, когда будем сравнивать пользовательский графический интерфейс в программах Java и C++.

Объяснение того, почему Java-программы медленнее C++ программ, заключается в следующем. C++ программы компилируются компилятором C++ в двоичный формат, который затем исполняется непосредственно процессором; таким образом, выполнение программы осуществляется аппаратными средствами. (Это несколько упрощенно, так как большинство современных процессоров выполняют микрокод, но это не принципиально при обсуждении данного вопроса.) С другой стороны, компилятор Java компилирует исходный код в "байт-код", который непосредственно исполняется не процессором, а с помощью другого программного обеспечения, виртуальной машины Java (JavaVirtualMachine, JVM). В свою очередь, JVM исполняется процессором. Таким образом, выполнение байт-кода Java-программ осуществляется не быстрыми аппаратными средствами, а с помощью более медленной программной эмуляции.

...

Подобные документы

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.