Предисловие
Меня давно интересовала тема обработки видео, вот только на отладочных платках 7-х и 9-х ARM-ов это получалось очень медленно и от этого становилось не интересно.
В настоящее время полным-полно мощного многоядерного железа и создано много библиотек для работы с видео, но мой выбор пал на ПЛИС.
Данный проект берёт своё начало 5 или 6 лет назад, во времена, когда не было Aliexpress-а и подобных ему магазинов, где за смешные деньги можно приобрести модуль цифровой камеры или отладочную плату с FPGA. Первая версия проекта была начата с использованием камеры HV7131GP от мобильника на самодельной плате, дисплея от Siemens S65 и отладочной платы Terasic DE2. Потом лет 5 проект пылился на полке и на диске.
Выглядело это так:
Впоследствии была приобретена плата с FPGA Altera Cyclone II EP2C8F256 и модуль камеры OV7670 специально для данного проекта. После покупки платы оказалось, что документации на неё нет и продавец на запрос ничего не ответил. Путём долгого копания в сети я нашел проект, сделанный на этой плате и позаимствовал из него assignments.
В этой статье я хочу познакомить читателя с методами захвата изображения с камеры, преобразования цветового пространства, изменения масштаба изображения, вывода изображения на дисплей через интерфейс HDMI и детектирования движения объектов в видеопотоке используя ПЛИС фирмы Altera.
Хочу сразу заметить, что программирование ПЛИС не является моей основной специализацией, а, больше, хобби в свободное время. Поэтому я могу ошибаться в сделанных выводах и мои решения могут быть далеко не оптимальны. В погоне за Fmax многие участки кода были написаны так, что могут показаться излишними, странными, бессмысленными и неоптимальными.
Инструментарий
В качестве основной среды разработки я выбрал HDL Designer от Mentor Graphics. В нем сделаны все графические блоки и связки между ними. Для синтеза и трассировки используется среда Quartus II от Altera.
Структура проекта
Структурная схема проекта приведена на рисунке ниже. Она отражает только основные функциональные узлы, которые будут детально рассмотрены ниже по тексту.
В редакторе HDL Designer она выглядет так:
На схеме отображены не все блоки проекта т.к. они находятся на уровне выше.
Модуль захвата
Модуль захвата видеопотока принимает на вход данные от камеры pixel_data в формате YCbCr 4:2:2 или RGB:565 и управляющие сигналы синхронизации кадровой и строчной развёртки hsync, vsync, переводит их в клоковый домен clk (50 МГц), формирует управляющий сигнал out_pixel_valid и out_vclk и передаёт их в модуль преобразования формата данных. Также этот модуль формирует статистику out_stat о количестве принятых данных за 1 кадр. Статистика может быть считана через UART. Модуль управляется внешним сигналом разрешения захвата данных capt_en. Этот сигнал выставляет модуль настройки камеры по завершении настройки. Код на Verilog:
always @(posedge clk) begin
hs_sync_1 <= hsync;hs_sync_2 <= hs_sync_1;
vs_sync_1 <= vsync;vs_sync_2 <= vs_sync_1;
vclk_sync_1 <= pclk;vclk_sync_2 <= vclk_sync_1;
pixdata_sync_1 <= pixel_data;pixdata_sync_2 <= pixdata_sync_1;
end
reg vclk_old;
always @(posedge clk)vclk_old <= vclk_sync_2;
wire vclk_posedge = (vclk_old == 1'b0) && (vclk_sync_2 == 1'b1);
reg sample_new,sample_hsync,sample_vsync;
reg [7:0] sample_pixel;
always @(posedge clk) begin
sample_new <= vclk_posedge;
if (vclk_posedge) begin
sample_hsync <= hs_sync_2;
sample_vsync <= vs_sync_2;
sample_pixel <= pixdata_sync_2;
end
End
reg last_vsync_sample,P2_vsync_triggered,P2_vsync_end_triggered;
reg P2_sample_vsync,P2_sample_new,P2_sample_hsync;
reg [7:0] P2_sample_pixel;
reg P2_new_frame,capt_done,capt_enable;
always @(posedge clk) begin
if (capt_en == 1'b1 || P2_vsync_triggered == 1'b1) capt_enable <= 1'b1;
else capt_enable <= 1'b0;
end
always @(posedge clk)
if (!nRst) begin
last_vsync_sample <= 1'b0,P2_vsync_triggered <= 1'b0;
P2_vsync_end_triggered <= 1'b0,P2_new_frame <= 1'b0;
capt_done <= 1'b0;
end else begin
if (capt_enable) begin
if (sample_new) begin
last_vsync_sample <= (sample_vsync/* && capt_en*/);
P2_sample_pixel <= sample_pixel;
P2_sample_hsync <= sample_hsync;
P2_sample_vsync <= sample_vsync;
end
// Pipeline Step
P2_sample_new <= sample_new;
if (!P2_vsync_end_triggered) begin
if ((last_vsync_sample == 1'b1) && (sample_vsync == 1'b0)) begin
P2_vsync_triggered <= 1'b1; P2_new_frame <= 1'b1;
end
if (P2_vsync_triggered && sample_vsync) begin
P2_vsync_end_triggered <= 1'b1; P2_vsync_triggered <= 1'b0;
capt_done <= ~capt_done;
end
end else begin
P2_vsync_end_triggered <= 1'b0; P2_vsync_triggered <= 1'b0;
end
if (P2_new_frame) P2_new_frame <= 1'b0;
end else begin
last_vsync_sample <= 1'b0;P2_vsync_triggered <= 1'b0;
P2_vsync_end_triggered <= 1'b0;P2_new_frame <= 1'b0;capt_done <= 1'b0;
end
end
Модуль преобразования формата
Формат YCbCr 4:2:2 не очень удобен для дальнейшей работы т.к. данные следуют вот в такой последовательности: Y0 Cb0 Y1 Cr1 Y2 Cb2 Y3 Cr3… По этому мы преобразуем его в формат YCbCr 4:4:4. По сути, всё преобразование сводится к выдачи данных Y Cb Cr за 1 такт сигнала data_strob. На языке Verilog это выглядит вот так:
always @(posedge clk)
if (!nRst) pix_ctr <= 2'b0;
else begin
if (pixel_valid) begin
if (vclk) pix_ctr <= pix_ctr + 1'b1;
end else pix_ctr <= 2'd0;
end
always @(posedge clk)
case (pix_ctr)
2'd0:begin YYY <= pixel_data; CCr <= Crr; CCb <= Cbb; Ypix_clock <= 1'b1;end
2'd1:begin Cbb <= pixel_data; YY <= YYY; end
2'd2:begin YYY <= pixel_data; CCr <= Crr; CCb <= Cbb; Ypix_clock <= 1'b1;end
2'd3:begin Crr <= pixel_data; YY <= YYY; end
endcase
assign data_strob = Ypix_clock;
assign Y = YY;
assign Cb = CCb;
assign Cr = CCr;
Модуль преобразования цветового пространства
В конечном итоге мы всегда работаем с данными в формате RGB, поэтому нам необходимо их получить из YCbCr. Делается это по формуле из даташита на камеру:
R = Y + 1.402(Cr — 128)
G = Y — 0.714(Cr — 128) — 0.344(Cb — 128)
B = Y + 1.772(Cb — 128)
На языке Verilog выглядит так:
parameter PRECISION = 11;
parameter OUTPUT = 8;
parameter INPUT = 8;
parameter OUT_SIZE = PRECISION + OUTPUT;
parameter BUS_MSB = OUT_SIZE + 2;
always @ (posedge clk)
if (!nRst) begin
R_int <= 22'd0; G_int <= 22'd0; B_int <= 22'd0;
end else begin
if (istrb) begin
//R = Y + 1.371(Cr - 128)
R_int <= (Y_reg << PRECISION)+(C1*(Cr_reg-8'd128));
//G = Y - 0.698(Cr-128)-0.336(Cb-128)
G_int <= (Y_reg << PRECISION)-(C2*(Cr_reg-8'd128))-(C3*(Cb_reg-8'd128));
//B = Y + 1.732(Cb-128)
B_int <= (Y_reg << PRECISION)+(C4*(Cb_reg-8'd128));
end
end
assign R = (R_int[BUS_MSB]) ? 8'd16 : (R_int[OUT_SIZE+1:OUT_SIZE] == 2'b00) ? R_int[OUT_SIZE-1:PRECISION] : 8'd240;
assign G = (G_int[BUS_MSB]) ? 8'd16 : (G_int[OUT_SIZE+1:OUT_SIZE] == 2'b00) ? G_int[OUT_SIZE-1:PRECISION] : 8'd240;
assign B = (B_int[BUS_MSB]) ? 8'd16 : (B_int[OUT_SIZE+1:OUT_SIZE] == 2'b00) ? B_int[OUT_SIZE-1:PRECISION] : 8'd240;
Модуль преобразования формата RGB:24 в RGB:565
Этот модуль делает нам из 24-х битного RGB формата 16-ти битный. Нам это удобно т.к. Занимает меньше места в памяти, уменьшает bitrate, имеет приемлемую для наших целей цветопередачу и, самое главное, укладывается в одно слово данных SDRAM, что существенно облегчает работу. Сигнал строба данных просто передаётся из предыдущего модуля.
Код модуля очень простой:
assign oRGB = {iR[7:3], iG[7:2], iB[7:3]};
assign ostrb = istrb;
Rescaler
Этот модуль пришел в проект с самого начала. Его цель — преобразовать входной поток 640x480 точек в поток 320x240, 160x120, 128x120, 80x60 и 320x480. Эти форматы нужны были для работы с LCD дисплеем от Siemens S65, TFT дисплеем для платы Arduino и реализации вращения изображения в блочной памяти FPGA и SDRAM используя алгоритм CORDIC. Другими словами это наследие других проектов. В данном проекте имеется возможность изменять разрешение экрана на-лету, и этот модуль играет здесь первую скрипку. Модуль также формирует статистику количества данных за кадр для отладки. Создавался модуль давно и его код надлежит санированию, но пока работает, мы его трогать не будем.
Код модуля довольно ёмкий и в этой статье я приведу только основную его часть:
always @(posedge clk)
if (!nRst) begin
w_ctr <= 16'd0;h_ctr <= 16'd0;frame_start <= 1'b0;
rsmp_w <= 8'd0;rsmp_h <= 8'd0;
end else begin
if (resampler_init) begin
w_ctr <= 16'd0;h_ctr <= 16'd0;frame_start <= 1'b0;
rsmp_w <= 8'd0;rsmp_h <= 8'd0;
end else begin
/* This case works ONLY if the input strobe is valid */
if (istrb) begin
if (w_ctr == I_WIDTH-1'b1) begin
w_ctr <= 16'd0;
if (h_ctr == I_HEIGHT-1'b1) begin
h_ctr <= 16'd0;
frame_start <= 1'b1;
end else begin
h_ctr <= h_ctr + 1'b1;frame_start <= 1'b0;
end
if (rsmp_h == H_FACT-1'b1) begin
rsmp_h <= 8'd0;
end else begin
rsmp_h <= rsmp_h + 1'b1;
end
end else begin
w_ctr <= w_ctr + 1'b1; frame_start <= 1'b0;
end
if (rsmp_w == W_FACT-1'b1) begin
rsmp_w <= 8'd0;
end else begin
rsmp_w <= rsmp_w + 1'b1;
end
end
end
end
reg pix_valid;
always @(rsmp_w or rsmp_h or wh_multiply or H_FACT) begin
if (wh_multiply == 1'b1) begin
pix_valid = ((rsmp_w == 8'd0) && (rsmp_h == 8'd0))?1'b1:1'b0;
end else begin
pix_valid = ((rsmp_w == 8'd0) && (rsmp_h != 8'd0 ))?1'b1:1'b0;
end
end
assign pixel_valid = pix_valid;
always @(posedge clk)
if (!nRst) begin
frame_enable <= 1'b0;
end else begin
if (resampler_init) begin
frame_enable <= 1'b0;
end else begin
if (frame_start) begin
if (!lcd_busy)
frame_enable <= 1'b1;
else
frame_enable <= 1'b0;
end
end
end
reg local_frame_start = 1'b0;
always @(posedge clk)
if (!nRst) begin
ostrb_port <= 1'b0;
dout_port <= 17'd0;
local_frame_start <= 1'b0;
end else begin
local_frame_start <= frame_start ? 1'b1: local_frame_start;
if (istrb && !resampler_init && !lcd_busy) begin
if (pixel_valid) begin
// if our column and our row
if (frame_enable && !dout_dis) begin
dout_port[16:0] <= {local_frame_start, din[15:0]};
ostrb_port <= 1'b1;
local_frame_start <= 1'b0;
end else begin
ostrb_port <= 1'b0;
end
end else
ostrb_port <= 1'b0;
end else
ostrb_port <= 1'b0;
end
FIFO IN
Это двухклоковое FIFO dcfifo, мегафункция Altera 256x17. Шестнадцатый бит — сигнал frame_start добавлен для удобства индикации начала нового фрейма после rescaler-а.
Клок записи — 50 МГц, клок чтения — 100 Мгц, он же клок SDRAM контроллера.
Контроллер записи-чтения
Этот громоздкий модуль являет собой одного писателя, который забирает данные из модуля FIFO IN и пишет их в SDRAM попеременно в разные области памяти для чётных и нечётных фреймов и двух читателей, которые читают данные из SDRAM, каждый из своей области памяти и записывают их в выходные FIFO. Приоритет отдан читателям, так как они работают на HDMI контроллер с частотой 25 МГц (640x480), а он промедлений не терпит, в FIFO всегда должны быть данные для обработки и вывода на экран. Оставшееся от заполнения выходных FIFO время, это время неактивной области экрана плюс время опустошения FIFO, работает писатель.
При разработке данного модуля я столкнулся с проблемой: если использовать сигналы FIFO full и empty, то FIFO начинает сбоить и ломать данные. Это не происходит для FIFO IN т.к. частота клока записи в него существенно ниже частоты чтения из него. Этот баг проявляется на выходных FIFO. Клок записи 100 МГц в 4 раза выше клока чтения 25 МГц, что, по моим догадкам, приводит к тому, что указатель записи догоняет и перегоняет указатель чтения. В сети нашел упоминания о неком баге альтеровских FIFO, не знаю, связан ли он с моей проблемой или нет. Саму проблему решить удалось не используя сигнала wr_full и rd_empty, а используя сигналы wrusedw и rdusedw. Я сделал контроллер состояний FIFO по цепям fifo_almost_full и fifo_almost_empty. Выглядит это так:
// FIFO 1
wire out_fifo_almost_full = &fifo_wr_used[9:4];
wire out_fifo_almost_empty = !(|fifo_wr_used[10:8]);
// FIFO 2
wire out_fifo_almost_full_2 = &fifo_wr_used_2[9:4];
wire out_fifo_almost_empty_2 = !(|fifo_wr_used_2[10:8]);
Также, в модуле реализована смена режимов работы: Background Subtraction или Frame Difference. Это достигается сигналом learning, который подключен к тактовой кнопке на плате.
Весь код модуля я приводить не буду, его довольно много и никакого ноу-хау там нет. Данный модуль работает на частоте SDRAM 100 МГц.
Контроллер SDRAM
За основу был взят модуль с сайта fpga4fun.com и немного переделан под наш тип микросхемы SDRAM K4S561632 с добавлением инициализации чипа и дополнительных задержек для соблюдения времянки:
Row active to row active delay: tRRD 15 n sec и
Row precharge time: tRP 20 n sec
Код модуля можно скачать с сайта по ссылке выше. Основной проблемой стало написание констрейнов в TimeQuest для правильной работы нашей SDRAM и подбор сдвига фазы клока на пин SDRAM_CLK с PLL. В остальном всё заработало сразу. Запись и чтение производится бёрстами, используется только один активный банк на 4 мегаслова, рефреши не используются.
FIFO OUT
Как и в случае с FIFO IN эти FIFO являются двухклоковыми мегафункциями 1024x16 dcfifo.
Клок записи равен 100 МГц, клок чтения 25 Мгц.
Детектор движения
Вот и добрались до модуля, который и есть соль земли этого проекта. Как видно, на него приходят данные и управляющие сигналы с обоих выходных FIFO, клок контроллера HDMI 25 МГц pixel_clock, счетчики пикселей counter_x, counter_y и сигнал активной области дисплея blank. Выходят с него сигналы R G B, готовые для отображения на дисплее.
В нем также реализованы цепи заполненности FIFO:
// FIFO 1
wire in_fifo_data_avail = |fifo_rd_used[10:4];
wire in_fifo_almost_empty = !(|fifo_rd_used[10:4]);
// FIFO 2
wire in_fifo_data_avail_2 = |fifo_rd_used_2[10:4];
wire in_fifo_almost_empty_2 = !(|fifo_rd_used_2[10:4]);
wire fifos_available = in_fifo_data_avail & in_fifo_data_avail_2;
wire fifos_almost_empty = in_fifo_almost_empty | in_fifo_almost_empty_2;
Нам необходимо контролировать область экрана, в которую мы выводим картинку с камеры:
wire in_frame = ((counter_x < RES_X) && (counter_y < RES_Y))?1'b1:1'b0;
wire frame_start = ((counter_x == 0) && (counter_y == 0))?1'b1:1'b0;
Читаются оба FIFO одновременно по флагу наличия данных в них обоих:
// Reader FIFO 1 & 2
always @(posedge pix_clk or negedge nRst)
if (!nRst) begin
fifo_rd_req <= 1'b0;
fifo_rd_req_2 <= 1'b0;
pixel_data <= 16'h0000;
worker_state <= 2'h1;
end else begin
case (worker_state)
2'h0: begin
if (in_frame) begin
if (fifos_almost_empty) begin
//worker_state <= 2'h1;
fifo_rd_req <= 1'b0;
fifo_rd_req_2 <= 1'b0;
end else begin
pixel_data <= fifo_data;
pixel_data_2 <= fifo_data_2;
fifo_rd_req <= 1'b1;
fifo_rd_req_2 <= 1'b1;
end
end else begin
fifo_rd_req <= 1'b0;
fifo_rd_req_2 <= 1'b0;
end
end
2'h1: begin
if (blank) begin
worker_state <= 2'h2;
end
end
2'h2: begin
// start reading if more than 16 words are already in the fifo
if (fifos_available && frame_start) begin
fifo_rd_req <= 1'b1;
fifo_rd_req_2 <= 1'b1;
worker_state <= 2'h0;
еnd
end
endcase
end
Считанные из FIFO данные имеют формат RGB:565, для наших целей его надо преобразовать в черно-белое представление. Делается это так:
// Convert to grayscale frame 1
wire [7:0] R1 = {pixel_data[15 : 11], pixel_data[15 : 13]};
wire [7:0] G1 = {pixel_data[10 : 5], pixel_data[10 : 9]};
wire [7:0] B1 = {pixel_data[4 : 0], pixel_data[4 : 2]};
wire [7:0] GS1 = (R1 >> 2)+(R1 >> 5)+(G1 >> 1)+(G1 >> 4)+(B1 >> 4)+(B1 >> 5);
// Convert to grayscale frame 2
wire [7:0] R2 = {pixel_data_2[15 : 11], pixel_data_2[15 : 13]};
wire [7:0] G2 = {pixel_data_2[10 : 5], pixel_data_2[10 : 9]};
wire [7:0] B2 = {pixel_data_2[4 : 0], pixel_data_2[4 : 2]};
wire [7:0] GS2 = (R2 >> 2)+(R2 >> 5)+(G2 >> 1)+(G2 >> 4)+(B2 >> 4)+(B2 >> 5);
Сигналы GS1 и GS2 и есть наше черно-белое представление.
Теперь немного об алгоритмах. Есть много способов детектирования движения. В данной статье я рассмотрю только два из них, на мой взгляд, самые простые и легко реализуемые в рамках этого проекта.
Способ первый. Background subtraction.
Идея заключается в том, что для нахождения движения или объекта в потоке видеоданных используется вычитание:
P[F(t)] = P[I(t)] — P[B]
P[F(t)] — результирующая разность,
P[I(t)] — текущий кадр с камеры,
P[B] — референсный кадр или background
Референсный кадр или background обычно делается когда нет никакого движения. Например, если мы хотим детектировать движение в одном углу комнаты, то перед этим мы должны сделать и запомнить снимок этого угла, когда там нет никакого движения, а потом из всех последующих снимков попиксельно вычитать этот самый background. Всё очень просто. Однако, из-за шумов в изображении, автоматического баланса белого в камере и других факторов нам необходимо применить порог срабатывания детектора. Этот порог применяется к разности кадров. Если разность больше порога, то движение есть, иначе — нет.
P[F(t)] > Threshold
Недостатков у этого метода больше чем достоинств, однако его применяют для детектирования движения из-за простоты реализации. Недостатками являются:
- Зависимость от освещённости
- Зависимость от смещения камеры
- Зависимость от погодных условий
- Влияние автоматического баланса белого
Любое изменение внешних факторов приведёт к обнаружению движения и ложному срабатыванию детектора.
Образно, схема детектора выглядит так:
Способ второй. Frame difference
Этот способ по реализации мало чем отличается от предыдущего. Все отличия заключаются в том, что вместо background-а из текущего фрейма вычитается предыдущий и разность сравнивается с порогом Threshold.
Математическое представление выглядит так:
P[F(t)] = P[I(t)] — P[I(t — 1)] > Threshold
Достоинством данного метода является относительная устойчивость к внешним факторам. Даже при изменении положения камеры или освещённости это не вызовет долговременного ложного срабатывания, а только кратковременное в пределах двух последовательных кадров.
Недостатками являются:
- Зависимость от частоты кадров
- Невозможность детектирования недвижимых объектов
- Слабое детектирование объектов, имеющих малую скорость
Из-за вышеперечисленных недостатков данный метод не нашёл широкого применения в чистом виде.
Реализация на языке Verilog.
В нашем случае неважно какой кадр из какого мы вычитаем, нам важна абсолютная разница между ними.
reg [7:0] difference = 0;
wire [7:0] max_val = (GS1 > GS2) ? GS1 : GS2;
wire [7:0] min_val = (GS1 < GS2) ? GS1 : GS2;
always @(posedge pix_clk) begin
if (in_frame) begin
difference <= max_val - min_val;
end else
difference <= 8'h00;
end
wire [15:0] out_val = in_frame ? (difference > `BS_THRESHOLD) ? 16'hF1_00 : pixel_data_2 : in_frame2 ? pixel_data_diff : 16'h00_00;
Как видно из кода, мы заменяем пиксель на красный цвет (16'hF1_00 ), если разность больше порога BS_THRESHOLD.
Для вывода на экран нам надо преобразовать данные из формата RGB:565 в формат RGB:24
// VGA 24 bit
assign R = {out_val[15 : 11], out_val[15 : 13]};
assign G = {out_val[10 : 5], out_val[10 : 9]};
assign B = {out_val[4 : 0], out_val[4 : 2]};
HDMI контроллер
Частично этот модуль был взят с того же сайта fpga4fun.com и переделан согласно статье с сайта marsohod.org. Вместо использования диф. пары LVDS я использовал мегафункцию DDIO. Для чего это сделано можно ознакомиться прочитав статью по ссылке выше.
Клоки
В качестве системного взят клок 50 МГц с генератора на плате. Из него сделаны клоки для SDRAM контроллера и SDRAM чипа. Эти клоки имеют одну и ту же частоту 100 МГц, но сдвинуты по фазе на 90 градусов. Для этого используется мегафункция PLL.
Клок 125 МГц (clk_TMDS2) используется для DDIO, после которых он превращается в 250 МГц. Такая вот хитрость.
Клок видеоданных pixel_clock равен 25 МГц, делается методом деления на 2 системного клока 50 Мгц.
Настройка камеры OV7670
Для настройки камеры используется сторонний модуль SCCB интерфейса. Он немного переделан под нужды проекта и способен на-лету записывать значения регистров камеры по команде от интерфейса UART.
UART
Модуль состоит из приёмника и передатчика UART и модуля io_controller
Код модулей приёмника и передатчика был взят с просторов интернета. Модули работают на скорости 115200 бод с настройками 8N1.
Этот модуль (io_controller) является связующим звеном между приёмо-передатчиком UART и внешними модулями проекта. Он осуществляет вывод статистики в UART, приём и обработку команд. С помощью него можно осуществить смену разрешения дисплея, изменить формат вывода данных с камеры (YCbCr или RGB), записать любой её регистр и вывести любую запрошенную статистику.
Видео с демонстрацией результата
Видео 1. Frame Difference
В левой части экрана выведено изображение с камеры в формате 320x240, а на правой — пороговая разница кадров. Левое изображение подкрашено красным в местах, где мы задетектировали движение.
На видео видно, что при остановке объекта движение не детектируется, а при снижении скорости объекта детектируется заметно хуже.
Видео 2. Background Subtraction
Можно заметить, что при приближении объекта к камере, меняется баланс белого и мы получаем ложное срабатывание детектора. Такие явления можно фильтровать или компенсировать. Одним из методов компенсации является обучение с усреднением референсного изображения (Approximate Median Filter).
Выводы
Данную разработку можно и нужно усовершенствовать путём усложнения алгоритмов детектирования. Также было бы неплохо реализовать трекинг движущихся объектов методом отрисовки прямоугольной рамки вокруг объекта.
На видео заметны горизонтальные прямоугольники. Это явление связано с багом чтения из SDRAM контроллера, который полностью побороть мне пока не удалось.
Материалы по теме
> Статья про детектор движения на OpenCV
> Yet another детектор на OpenCV
> Background subtraction
> Методы усовершенствования детектирования
Комментарии (20)
GREGOR_812
05.03.2017 23:14+1Шикарная статья, хочу повторить) А у Вас есть гитхаб проекта или другой доступ к нему?
ubobrov
05.03.2017 23:24+2В гитхаб выложу, но чуть позже. Надо код причесать и оптимизировать. Там баг с чтением из SDRAM есть, вот исправлю его и опубликую.
Fogger
06.03.2017 00:28Интересно почему отдано предпочтение среде HDL Designer от Mentor's? Там тоже свой VIP или они пользуются альтеровским, судя по рисункам? было упоминание ранних опытов про частичную обработку фреймов на A9, данные капчились из сквозного потока между камерой и монитором в память hps, или или рип из фреймбуфера как конечный блок потока, dma при заполнении кадра ?
ubobrov
06.03.2017 00:42HDL Designer использую потому, что осваивать ПЛИС начал на FPGA Advantage под Windows, затем переехал на Linux и FA в полном наборе не нашел, а к HDS привык. На А9, да, данные брал из фреймбуфера. Первые опыты были на SAM9G20 лет 5 назад. Результаты меня не впечатлили. Сейчас в ящике лежит платка на Allwinner H3, вот на ней стоит попробовать, но это потом.
Andruwkoo
06.03.2017 23:50HDL Designer хорош даже в отрыве от любых библиотек. Он может работать и с Альтерой и Ксайлинксом. В графическом виде HDL код намного понятнее и нагляднее. а если не хватает графики, то можно вставить embedded block и будут кусок необходимого кода прям на графике.
Ivan_83
06.03.2017 01:30+3С поиском движения не всё так просто и однозначно.
Нужно понять для чего это.
Если это поиск движения где то со стационарной камеры — то всё относительно просто: запоминаем 10 кадров, дальше сравниваем текущий кадр и тот кадр который был 10 кадров назад.
На практике где то от 4 до 8 кадров хорошо работает.
Это нужно чтобы лучше видеть медленно двигающиеся объекты. Но если кадров много или скорость большая то начнётся двоение.
А вот если у нас камера двигается то всё становится сильно сложнее, ибо нам нужно в начале совместить текущий кадр с кадром который был 40-10 кадров назад, потом только вычесть одно из другого.
В целом это называется стабилизацией изображения и это отдельная большая и не простая тема.
Вернее так: стабилизировать когда нет вращения — тоже относительно просто и быстро: строится градиентный спуск (оригинал масштабируется до мизерных размеров, последовательно уменьшаясь в два раза скажем до 5-15 пикселей) далее на самом мелком ищется скажем кусок из центра другого изображеня обычным вычитанием и где сумма после вычитания меньше — то скорее и есть, дальше оно поднимается по пирамиде +-1 пиксель «двигаем» и сравниваем на каждом уровне для уровне…
Более внятно/подробно описано тут: https://habrahabr.ru/post/219815/
Стабилизацию с вращением я делал и видел только на OpenCV — есть готовый пример прямо в ихнем репозитории.
Очень жручая штука.
Ну и последнее это как после вычитания изображений понять где движение а где шум.
Тут я пробовал игратся с текущим результатом вычитания изображения и предыдущим, либо игрался с разницей между текущим и -10 и -9 кадрами, накладывая их по разному друг на дружку чтобы шумы подавились а движение осталось… до конца не доигрался и переключился на другую задачу.
В зависимости от настроек стабилизации (насколько глубокий поиск и пр) и разрешения изображения — частенько бывало что core i7-47** не хватало для 25 фпс.
Но самая жопа это конечно подбор всех уровней и прочего, тут приходит понимание что универсальных констант нет и нужно адаптивщину с эвристикой.
В общем я к чем.
1. Нужно бы поставить более чётко цель: в каких условиях ищем движение
2. Лучше всего откатать в начале алгоритмы на компе, в том же OpenCV это делается легко, без него придётся чуть больше кодить всякие примитивы для обработки изображений.
2 rPman
Решается в OpenCV трешхолдом: всё что до порога становится чёрным, всё что выше белым. Дальше можно небольшое размытие сделать или что то ещё, чтобы убрать точки внутри контуров букв.chersanya
06.03.2017 07:07Нет, задача выравнивания неравномерной яркости пороговой обработкой не решается — спокойно может быть так, что чёрная буква на яркой части изображения ярче, чем белая бумага на тёмной части. Поэтому действительно, как писал rPman, стандартный и часто используемый для этого способ — поделить изображение на сильно размытую свою копию.
Ivan_83
07.03.2017 09:59Может и так, это былая сырая идея на попробовать :)
2 kolpeex
Нет.
Даже в случае дрожания камеры акселерометры мало помогут:
— нужно как то совмещать кадры и данные акселерометров
— у камеры может быть разное фокусное расстояние: грубо говоря если она как телескоп смотрит сильно в даль то гироскоп должен быть сильно чувствительным, а если смотрит близко то опять же имеем сильно другие коэффициенты поправок
Опять же, вычислительно не сильно затратно стабилизировать дрожание без вращения.
kolpeex
06.03.2017 14:05Наверно, было бы проще делать стабилизацию изображения, если бы с видеопотоком были бы данные акселерометров камеры.
Meklon
Проблема баланса белого решается переводом камеры в прибитый гвоздями ручной режим.
datacompboy
кроме баланса белого есть еще набигание облаков и вылезание лун
Meklon
Это да. Просто обычно проще, когда баланс и экспозиция жестко заданы. Меньше неожиданностей от картинки.
datacompboy
меня особенно уставали переключения в ночной режим и обратно, да…
rPman
Я, будучи полным нубом в работе с фото, буквально вчера взялся за решение простой задачи — есть фото текстовых документов (черное на белом, рукописный текст), сделанных под углом, с неравномерным освещением, — нужно подготовить на основе этого печать копии этих документов (достаточных для чтения).
Исправить наклон бумаги дело пары десятков секунд, да и по теме статьи совершенно не требуется, а вот проблему с неравномерным освещением (в т.ч. во времени, в моем случае разные фото имели разную яркость)
И я нашел, мне кажется, просто гениальное решение (использовал GIMP), берем копию изображения, сильно размываем ее (чтобы исчезли значимые элементы, используется простое гауссовое размытие), затем вычитаем (лучше деление и нормализуем) результат из оригинала, получаем яркое изображение, которому абсолютно пофиг на плавные разводы освещения.
Для объектов с четким границами мне кажется метод идеальный, не требует заметных вычислений.
ProLimit
Поздравляю, вы изобрели High-pass фильтрацию :) Неужели в GIMP ее нет в стандартных инструментах?
rPman
да, кажется есть, но меня интересовала грубо говоря формула, хотя бы просто идея.