Pull to refresh

Детектирование движения в видеопотоке на FPGA

Reading time 13 min
Views 25K

Предисловие


Меня давно интересовала тема обработки видео, вот только на отладочных платках 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:

Capture
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 это выглядит вот так:

YCbCr 4:2:2 => 4:4:4
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 выглядит так:

YCbCr => RGB
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. Другими словами это наследие других проектов. В данном проекте имеется возможность изменять разрешение экрана на-лету, и этот модуль играет здесь первую скрипку. Модуль также формирует статистику количества данных за кадр для отладки. Создавался модуль давно и его код надлежит санированию, но пока работает, мы его трогать не будем.

Код модуля довольно ёмкий и в этой статье я приведу только основную его часть:

Rescaler
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
Методы усовершенствования детектирования

UPD


Как и обещал, публикую проект. Доступен на яндекс диске. Это копия проекта сделанная в Квартусе, в HDL Designer пока выкладывать не стану, вряд ли он запустится у кого если даже и выложу.
ссылка на проект
Tags:
Hubs:
+77
Comments 25
Comments Comments 25

Articles