nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Программа захвата на QuatCore / видеопроцессоре

Очередной день пребываю в ступоре: громко думаю, как лучше всего соединить между собой процессор QuatCore и "видеопроцессор" - этот относительно небольшой (106 ЛЭ) модуль, который "перемалывает" пиксели по одному за такт, но весьма ограничен в своём интеллекте.

Наверное, всё же соединю "стандартно" через шину данных, и выделю адреса на чтение и на запись, а чтобы успешно проходить через "бутылочные горлышки", введу буфера FIFO (First In - First Out) как на входе, так и на выходе. Как ни странно, они запросто могут сожрать больше ресурсов, чем процессор и видеопроцессор вместе взятые! Есть кое-какие идейки, как можно размер этих буферов снизить до "теоретического минимума", но это потом (до "целиком параметризуемого модуля" с возможностью одновременного чтения и записи я эти идеи пока не довёл). Для начала хочется увидеть всё это хозяйство в работе!

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


Начнём с более простого варианта, когда источником изображения является аналоговая камера высокой чёткости. Он более прост, поскольку можно проводить "подготовительные работы" для следующей строки во время обратного хода. В сигналах 720p длительность обратного хода как минимум 10,3 мкс (сигнал CVI), а на самом деле времени на обработку ещё чуточку больше, поскольку нам удобно будет чуть обрезать изображение с краёв, ограничившись 1024 точками по горизонтали вместо 1280. При тактовой частоте 25 МГц, на считывание этих 1024 точек уходит 40,96 мкс, поэтому на "обратный ход" остаётся 12,34 мкс, или 308 тактов процессора. За это время надо успеть подготовить хотя бы первый отрезок для сканирования следующей строки.

Более сложной будет работа с 1205ХВ014: там "два сканирующих луча", один по верхнему полукадру, второй по нижнему, и время обратного хода всего 8 тактов! Только чтобы оба "луча" обработать, нам на самом деле понадобится ДВА видеопроцессора, и в центральной зоне нужно будет придумывать "склейку", если одно пятно попадает в оба полукадра. Поэтому нужно держать в голове и этот вариант, стремиться, чтобы программа и весь интерфейс взаимодействия могли расширяться на этот случай, но сразу с него начинать - башка раскалывается...

В любом случае, общая структура процедуры для нахождения ярких точек будет примерно такой:

	.rodata
		VSync		dw	0x8000	;ожидание кадрового синхроимпульса, значение для регистра произвольное
		Hsync		dw	0x4000	;ожидание строчного синхроимпульса, а затем ещё интервал front porch (между импульсом и началом картинки)
		WholeRow	dw	1023	;для обработки всей строки целиком					
	.code
		ProcessFrame proc
					X		Vsync
					i		0
						
					;ждём кадрового синхроимпульса
					ACQ		[X+2i]		
					;пропускаем "пустые строки" вверху изображения (нужно только для аналоговой камеры, в цифровой такой фигни нет вроде бы...)
					j		10
			@@topRows:	ACQ		[X+2i+1]
					jLOOP		@@topRows
					
					;и теперь наконец-то выходим на информативную часть строки!
			@@NewRow:	X		WholeRow
					ACQ		[X+2i]		;попросили прямо всю строку обработать
					
					;--------------------
					;ДОВОЛЬНО ХИТРЫЙ КОД
					;--------------------
								
		ProcessFrame endp


Смысл в том, что сначала мы отдаём видеопроцессору "задание" с режимом синхронизации по кадровому импульсу, и он "застрянет", пока его не получит. Затем - пропускаем несколько строк, так часто бывает нужно в аналоговом видеосигнале, где верхние и нижние строчки "выходят за границы экрана" и не содержат ничего полезного. Точное значение строк потом настрою.

Но в целом, пока код выглядит почти что информативным. Немножко раздражает, что нельзя любой "литерал" задать прямо в коде, только числа от -64 до +63, а другие значения нужно брать из памяти. Но даже адрес в памяти в виде "литерала" задать нельзя. Можно будет немножко "поёрзать", так подобрать порядок бит в шине данных для видеопроцессора, чтобы все самые информативные попали в интервал для "литералов", но это всё детали...

Что приятно: нам с точки зрения программирования не так уж важно, "застрянет" ли выполнение на одной из команд ACQ, отвечающих за кадровые и строчные синхроимпульсы. Пока можно ожидать, что у нас будет буфер FIFO размером 8..16 ячеек, поэтому кадровый синхроимпульс мы "проскочим", затем набьём буфер строчными синхроимпульсами (пропуск верхних, неинформативных строк), и когда он заполнится до отказа - мы так и должны застрять в цикле @@topRows, пока буфер немного не освободится.

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

И вот наконец мы даём задание найти самый яркий пиксель в первой информативной строке.

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

Так что, когда после команды на "чтение всей строки" (то есть, нахождения самого яркого пикселя на строке и его координаты) наконец-то поступит команда наподобие

C   GPU

(получить данные ОТ видеопроцессора и поместить их в регистр C),

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

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

А интересно оно тем, как именно надо этот процесс распределить по времени, увидеть "установившийся режим".

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

Но как ни странно, в коде они должны "поменяться местами". В этом циклическом процессе удобнее будет прочитывать результаты строки и ТУТ ЖЕ использовать их для выдачи заданий на следующую строку. Хотя надо быть очень осторожными, и не забыть, сколько посылок и каких мы ожидали на этой строке, не "затереть" их показаний к следующей.

Нам понадобится массив "активных ярких точек", т.е тех точек, которые мы уже обнаружили, и которые мы ожидаем увидеть на текущей строке. Для каждой точки нужно указать координаты X,Y, причём в этом массиве они всегда будут отсортированы слева направо. И также нужна яркость точки, Lum (Luminance). И мы должны помнить, сколько их в данный момент, с помощью переменной PointsCount. Скорее всего это будет регистр, но у нас ещё есть "локальные переменные" [SP] и [SP+1], что в сочетании с регистрами X,Y,Z, i,j,k, C, Acc даёт довольно приличное пространство для манёвра.

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

Начинается всё с PointsCount=0 (точек не найдено). Цикл по всем этим точкам у нас не выполняется ни разу, и остаётся только обработать "одним махом" все оставшиеся точки, т.е от PixelsProcessed=0 до Width-1. (Width-это ширина строки, в тестовой картинке Width=32, когда будет всё "по-взрослому", Width=1024.) Мы запрашиваем два числа: максимальная яркость Lum и координата точки X, где эта яркость была достигнута. И далее возможны два варианта:

- сколько-нибудь ярких точек обнаружено не было, т.е максимальная яркость ниже порога. В таком случае мы посылаем ровно одно задание на обработку, от PixelsProcessed=0 до Width-1=1023. Иными словами, и следующую строку мы возьмём "оптом".
- яркая точка обнаружена. Мы добавляем её в список ярких точек, и даём ТРИ задания на обработку, первое от PixelsProcessed до X-R-1 (проверить первый "пустой регион"), второе от X-R до X+R (уточнить координаты УЖЕ НАЙДЕННОЙ яркой точки), третье: от X+R+1 до Width-1 (проверить второй "пустой регион").

И наконец, отправляем запрос на обработку со строчным синхроимпульсом, ну чтобы два раза не ходить. Потом, когда будем данные получать долго и мучительно, по крайней мере будем уверены - видеопроцессор "без работы не сидит" - он ждёт синхроимпульса! Собственно, "режима ожидания" как такового скорее всего и не будет у видеопроцессора - он всё время будет чем-то занят, но иногда это что-то нам будет без надобности.

И на этом обработку строки считаем завершённой.

Пока ярких точек не обнаружено, мы так и будем обрабатывать строку за строкой "в один присест". Но пусть на 4-й строке мы наконец-то нашли яркую точку, с координатой X=14 и яркостью Lum=437 (см. тестовая картинка для видеоизмерителя), поэтому для следующей строки "заказали" отрезки [0;10], [11;17] и [17;31]. И теперь PointsCount=1, ActivePointX[0]=14, ActivePointY[0]=4, ActivePointLum[0]=437.

И начинается обработка очередной строки. Снова выставляем PixelsProcessed=0 и заходим в цикл по "активным точкам". Теперь уже там есть одна точка с координатами (14;4) и яркостью 437.

Внутри цикла мы понимаем, что первым должен быть интервал [0;10], на котором ищем НОВУЮ яркую точку. Поэтому запрашиваем два числа от видеопроцессора: макс. яркость на этом интервале и X-координата самой яркой точки. И снова возможны два варианта:
- сколько-нибудь яркой точки обнаружено не было, т.е яркость оказалась ниже пороговой. НЕ СПЕШИМ запрашивать отрезок такой же длины на следующую строку! Ведь если УЖЕ ОБНАРУЖЕННАЯ яркая точка, та, что была (14;4) передвинется влево или вправо, то длина этого отрезка изменится. Пока мы её не знаем, поэтому не спешим... PixelsProcessed остаётся на том же месте, в данном случае на нуле.
- яркая точка обнаружена. Тогда наш ОДИНОЧНЫЙ отрезок распадается на 3. Запросы на 2 из 3 мы посылаем сразу, добавляем новую точку в наш массив, но переменную цикла так модифицируем, чтобы не приняться повторно за ту же самую точку, т.е был цикл от i=1 до 1, а будет - от 2 до 2, грубо говоря. А третий отрезок опять же не спешим "заказывать", т.к он ещё до конца не определён. PixelsProcessed сдвигается вправо и указывает на начало третьего отрезка. Т.е мы знаем, что с этой точки мы ещё "не трогали".

По результатам данной обработки, у нас может измениться PixelsProcessed. Теперь принимаемся за саму яркую точку, (14;4). Раз мы уже послали запрос на её обработку, то обязаны сейчас принять результаты, иначе у нас всё собьётся! Как обычно: максимальная яркость и координата точки, где эта яркость была достигнута. Сравниваем максимальную яркость с той, что для этой точки была записана. И как обычно, возможно два варианта:
- яркость на этой строке оказалась выше. Значит, мы подбираемся ближе к середине пятна, и координаты точки нужно обновить. Как только мы это сделаем, можно отправить два запроса для следующей строки: [PixelsProcessed;X-R-1] (поиск НОВОЙ точки) и [X-R;X+R]. Но и тут возможна подлянка: если выйдет X-R-1 меньше, чем PixelsProcessed, то либо точка совсем "прижалась к краю фотоприёмной матрицы", тогда мы первый из отрезков попросту не заказываем. На следующей строке мы сможем сообразить не снимать соответствующие показания, поскольку и там этот интервал будет посчитан, и окажется пустым. Либо, если это не край экрана, пустой интервал будет означать: ДВЕ НЕЗАВИСИМО НАЙДЕННЫЕ ТОЧКИ СЛИЛИСЬ В ОДНО ПЯТНО! Это показатель, что мы видим что-то кардинально неправильное, вместо пятен ожидаемого радиуса R (а на самом деле и того меньше) мы обнаружили большую засвеченную область. Ещё предстоит придумать, как на это реагировать - просто "выкинуть" эти два пятна из массива нельзя - следующей же строкой опять на него наткнёмся, притом возможно несколько раз, так что под самое окончание пятна какая-то координата всё равно "всплывёт". Наверное, надо оставить ОДНУ точку (их было две, но они слились в одну), и ей приписать какой-нибудь флажок "большое пятно". В общем, вопрос пока открытый... Но в любом случае, обновляем PixelsProcessed до X+R, чтобы знать, откуда начать дальше.
- яркость на этой строке оказалась ниже, т.е самая яркая точка скорее всего была выше. Тогда надо проверить: а к следующей строке эту точку вообще надо проверять? Т.е попадает ли следующая строка в пятно радиусом R от текущих координат этой точки? Если в течение нескольких строк подряд её Y-координата осталась на том же месте, к примеру, сейчас мы уже на 8-й строке, а координаты остались (14;4) то нужно будет её попросту "выкинуть" из нашего массива "активных точек". А точнее, не выкинуть, а переместить в массив "обработанных" точек. Опять, нужно внимательно проследить за выполнением нашего цикла по "активным точкам", чтобы мы не перескочили случайно через одну, из-за удаления оттуда текущей точки. А вот посылать запросов на обработку отрезков тогда не надо пока что: у нас там сидит необработанный участок от PixelsProcessed до текущих координат, а вот как далеко вправо продлится этот отрезок - мы пока не знаем...

Если же точку удалять пока рано, то посылаем видеопроцессору запрос на обработку двух отрезков, [PixelsProcessed;X-R-1] (поиск НОВОЙ точки) и [X-R;X+R], здесь X - старая координата, из массива активных точек. А третий отрезок, от X+R+1 и дальше - пока что оставляем, т.к не знаем его правой границы.

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

Вот, собственно, и всё!
Если так подумать, алгоритм не шибко сложный. Муторный - это да. Если пока что ограничиться "квадратными" пятнами, то из всей математики нам понадобится лишь сложение и вычитание, ну и сравнение как результат вычитания. Это хорошие операции, довольно быстрые на данном "железе". Кажется, что 308 тактов вполне "за глаза".

Осталось его реализовать на ассемблере, и потом проверить на симуляции...


Продолжение следует...
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

  • Нахождение двух самых отдалённых точек

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

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…

  • Балансируем конвейер QuatCore

    В пятницу у нас всё замечательно сработало на симуляции, первые 16 миллисекунд полёт нормальный. А вот прошить весь проект на ПЛИС и попробовать "в…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments