[Из песочницы] Детектирование движения в видеопотоке на FPGA

9628aaaec3174d568350ebabfe265fe3.jpg

Предисловие


Меня давно интересовала тема обработки видео, вот только на отладочных платках 7-х и 9-х ARM-ов это получалось очень медленно и от этого становилось не интересно.

В настоящее время полным-полно мощного многоядерного железа и создано много библиотек для работы с видео, но мой выбор пал на ПЛИС.

Данный проект берёт своё начало 5 или 6 лет назад, во времена, когда не было Aliexpress-а и подобных ему магазинов, где за смешные деньги можно приобрести модуль цифровой камеры или отладочную плату с FPGA. Первая версия проекта была начата с использованием камеры HV7131GP от мобильника на самодельной плате, дисплея от Siemens S65 и отладочной платы Terasic DE2. Потом лет 5 проект пылился на полке и на диске.

Выглядело это так:

Впоследствии была приобретена плата с FPGA Altera Cyclone II EP2C8F256 и модуль камеры OV7670 специально для данного проекта. После покупки платы оказалось, что документации на неё нет и продавец на запрос ничего не ответил. Путём долгого копания в сети я нашел проект, сделанный на этой плате и позаимствовал из него assignments.

5e288190b50e4e7dae5eb68304204534.png
4fae0341d4454b1399694941cb9816a3.png

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

Хочу сразу заметить, что программирование ПЛИС не является моей основной специализацией, а, больше, хобби в свободное время. Поэтому я могу ошибаться в сделанных выводах и мои решения могут быть далеко не оптимальны. В погоне за Fmax многие участки кода были написаны так, что могут показаться излишними, странными, бессмысленными и неоптимальными.

Инструментарий


В качестве основной среды разработки я выбрал HDL Designer от Mentor Graphics. В нем сделаны все графические блоки и связки между ними. Для синтеза и трассировки используется среда Quartus II от Altera.

Структура проекта


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

f44f5b9577424db7b9ed65cf85e28fe8.png

В редакторе HDL Designer она выглядет так:

416abfd6389047ab8086484d57fe3bf9.png

На схеме отображены не все блоки проекта т.к. они находятся на уровне выше.

946ca8d8ec8a4745a1993e6f8ea11d32.png

Модуль захвата


417d9205d7494fb797f5832a9fc00858.png Модуль захвата видеопотока принимает на вход данные от камеры 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


Модуль преобразования формата


c9a9b151cc8e4dfaa147971a5ffb2552.png Формат 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;


Модуль преобразования цветового пространства


ede83a5f8f2a47bfa97b42a9d0cac5c0.png В конечном итоге мы всегда работаем с данными в формате 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


2f8838a46b6b4369b397a8ff7125766a.png Этот модуль делает нам из 24-х битного RGB формата 16-ти битный. Нам это удобно т.к. Занимает меньше места в памяти, уменьшает bitrate, имеет приемлемую для наших целей цветопередачу и, самое главное, укладывается в одно слово данных SDRAM, что существенно облегчает работу. Сигнал строба данных просто передаётся из предыдущего модуля.

Код модуля очень простой:

assign oRGB = {iR[7:3], iG[7:2], iB[7:3]};
assign ostrb = istrb;

Rescaler


b4f70ed9be44493e8389bb239311feef.png Этот модуль пришел в проект с самого начала. Его цель — преобразовать входной поток 640×480 точек в поток 320×240, 160×120, 128×120, 80×60 и 320×480. Эти форматы нужны были для работы с 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


635835ef0adb454c99aac6346b00d1e9.png Это двухклоковое FIFO dcfifo, мегафункция Altera 256×17. Шестнадцатый бит — сигнал frame_start добавлен для удобства индикации начала нового фрейма после rescaler-а.

Клок записи — 50 МГц, клок чтения — 100 Мгц, он же клок SDRAM контроллера.

Контроллер записи-чтения


34aa12f1925d420b8fbbc0994ff75e55.png Этот громоздкий модуль являет собой одного писателя, который забирает данные из модуля FIFO IN и пишет их в SDRAM попеременно в разные области памяти для чётных и нечётных фреймов и двух читателей, которые читают данные из SDRAM, каждый из своей области памяти и записывают их в выходные FIFO. Приоритет отдан читателям, так как они работают на HDMI контроллер с частотой 25 МГц (640×480), а он промедлений не терпит, в 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


a761e778fc904daab5404381897fc5aa.png За основу был взят модуль с сайта 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


e773dfacf6af42eb900db2977d7febb2.png Как и в случае с FIFO IN эти FIFO являются двухклоковыми мегафункциями 1024×16 dcfifo.

Клок записи равен 100 МГц, клок чтения 25 Мгц.

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


fa4e53ac086843508235d0553a8cbd18.png Вот и добрались до модуля, который и есть соль земли этого проекта. Как видно, на него приходят данные и управляющие сигналы с обоих выходных 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] — референсный кадр (bacground).

Референсный кадр или bacground обычно делается когда нет никакого движения. Например, если мы хотим детектировать движение в одном углу комнаты, то перед этим мы должны сделать и запомнить снимок этого угла, когда там нет никакого движения, а потом из всех последующих снимков попиксельно вычитать этот самый background. Всё очень просто. Однако, из-за шумов в изображении, автоматического баланса белого в камере и других факторов нам необходимо применить порог срабатывания детектора. Этот порог применяется к разности кадров. Если разность больше порога, то движение есть, иначе — нет.

P[F (t)] > Threshold

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

  • Зависимость от освещённости
  • Зависимость от смещения камеры
  • Зависимость от погодных условий
  • Влияние автоматического баланса белого

Любое изменение внешних факторов приведёт к обнаружению движения и ложному срабатыванию детектора.

Образно, схема детектора выглядит так:

55b561e76dc349feaabfa733e0dd8675.png

Способ второй. Frame difference


Этот способ по реализации мало чем отличается от предыдущего. Все отличия заключаются в том, что вместо bacground-а из текущего фрейма вычитается предыдущий и разность сравнивается с порогом 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 контроллер


1bb3afa5f17443f9b02c19e3b0476259.png
c67de73edeee486880702ef28f6e051f.png

Частично этот модуль был взят с того же сайта fpga4fun.com и переделан согласно статье с сайта marsohod.org. Вместо использования диф. пары LVDS я использовал мегафункцию DDIO. Для чего это сделано можно ознакомиться прочитав статью по ссылке выше.

Клоки


9926754700a54cf29050df606c5da8e5.png
2033b0f8ea9d41ed8695dfd359d857de.png

В качестве системного взят клок 50 МГц с генератора на плате. Из него сделаны клоки для SDRAM контроллера и SDRAM чипа. Эти клоки имеют одну и ту же частоту 100 МГц, но сдвинуты по фазе на 90 градусов. Для этого используется мегафункция PLL.

Клок 125 МГц (clk_TMDS2) используется для DDIO, после которых он превращается в 250 МГц. Такая вот хитрость.

Клок видеоданных pixel_clock равен 25 МГц, делается методом деления на 2 системного клока 50 Мгц.

Настройка камеры OV7670


45614fd0ae724fb19f8bdd55024fdb68.png Для настройки камеры используется сторонний модуль SCCB интерфейса. Он немного переделан под нужды проекта и способен на-лету записывать значения регистров камеры по команде от интерфейса UART.

UART


4a8aa93728964144a452ca58cd5f8992.png Модуль состоит из приёмника и передатчика UART и модуля io_controller

Код модулей приёмника и передатчика был взят с просторов интернета. Модули работают на скорости 115200 бод с настройками 8N1.

781c0750468c44c6a3497ec6f8120d7d.png
Этот модуль (io_controller) является связующим звеном между приёмо-передатчиком UART и внешними модулями проекта. Он осуществляет вывод статистики в UART, приём и обработку команд. С помощью него можно осуществить смену разрешения дисплея, изменить формат вывода данных с камеры (YCbCr или RGB), записать любой её регистр и вывести любую запрошенную статистику.

Видео с демонстрацией результата


Качество видео
Приношу извинения за качество видео, такой уж у меня телефон.

Видео 1. Frame Difference

В левой части экрана выведено изображение с камеры в формате 320×240, а на правой — пороговая разница кадров. Левое изображение подкрашено красным в местах, где мы задетектировали движение.

На видео видно, что при остановке объекта движение не детектируется, а при снижении скорости объекта детектируется заметно хуже.

Видео 2. Background Subtraction


Можно заметить, что при приближении объекта к камере, меняется баланс белого и мы получаем ложное срабатывание детектора. Такие явления можно фильтровать или компенсировать. Одним из методов компенсации является обучение с усреднением референсного изображения (Approximate Median Filter).

Выводы


Данную разработку можно и нужно усовершенствовать путём усложнения алгоритмов детектирования. Также было бы неплохо реализовать трекинг движущихся объектов методом отрисовки прямоугольной рамки вокруг объекта.

На видео заметны горизонтальные прямоугольники. Это явление связано с багом чтения из SDRAM контроллера, который полностью побороть мне пока не удалось.

Материалы по теме


→ Статья про детектор движения на OpenCV
→ Yet another детектор на OpenCV
→ Background subtraction
→ Методы усовершенствования детектирования

Комментарии (1)

  • 5 марта 2017 в 19:14

    +1

    Проблема баланса белого решается переводом камеры в прибитый гвоздями ручной режим.

© Habrahabr.ru