Предисловие


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

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


  1. Meklon
    05.03.2017 19:14
    +3

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


    1. datacompboy
      05.03.2017 21:23

      кроме баланса белого есть еще набигание облаков и вылезание лун


      1. Meklon
        05.03.2017 21:45

        Это да. Просто обычно проще, когда баланс и экспозиция жестко заданы. Меньше неожиданностей от картинки.


        1. datacompboy
          05.03.2017 22:22

          меня особенно уставали переключения в ночной режим и обратно, да…


    1. rPman
      06.03.2017 00:45

      Я, будучи полным нубом в работе с фото, буквально вчера взялся за решение простой задачи — есть фото текстовых документов (черное на белом, рукописный текст), сделанных под углом, с неравномерным освещением, — нужно подготовить на основе этого печать копии этих документов (достаточных для чтения).

      Исправить наклон бумаги дело пары десятков секунд, да и по теме статьи совершенно не требуется, а вот проблему с неравномерным освещением (в т.ч. во времени, в моем случае разные фото имели разную яркость)

      И я нашел, мне кажется, просто гениальное решение (использовал GIMP), берем копию изображения, сильно размываем ее (чтобы исчезли значимые элементы, используется простое гауссовое размытие), затем вычитаем (лучше деление и нормализуем) результат из оригинала, получаем яркое изображение, которому абсолютно пофиг на плавные разводы освещения.

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


      1. ProLimit
        06.03.2017 13:46
        +1

        Поздравляю, вы изобрели High-pass фильтрацию :) Неужели в GIMP ее нет в стандартных инструментах?


        1. rPman
          07.03.2017 01:33

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


  1. GREGOR_812
    05.03.2017 23:14
    +1

    Шикарная статья, хочу повторить) А у Вас есть гитхаб проекта или другой доступ к нему?


    1. ubobrov
      05.03.2017 23:24
      +2

      В гитхаб выложу, но чуть позже. Надо код причесать и оптимизировать. Там баг с чтением из SDRAM есть, вот исправлю его и опубликую.


  1. Fogger
    06.03.2017 00:28

    Интересно почему отдано предпочтение среде HDL Designer от Mentor's? Там тоже свой VIP или они пользуются альтеровским, судя по рисункам? было упоминание ранних опытов про частичную обработку фреймов на A9, данные капчились из сквозного потока между камерой и монитором в память hps, или или рип из фреймбуфера как конечный блок потока, dma при заполнении кадра ?


    1. ubobrov
      06.03.2017 00:42

      HDL Designer использую потому, что осваивать ПЛИС начал на FPGA Advantage под Windows, затем переехал на Linux и FA в полном наборе не нашел, а к HDS привык. На А9, да, данные брал из фреймбуфера. Первые опыты были на SAM9G20 лет 5 назад. Результаты меня не впечатлили. Сейчас в ящике лежит платка на Allwinner H3, вот на ней стоит попробовать, но это потом.


      1. ser-mk
        06.03.2017 15:51

        У вас весь workflow для FPGA на Linux?


        1. ubobrov
          06.03.2017 16:14

          MATLAB и Simulink? Нет. Мне оно пока ненужно.


    1. Andruwkoo
      06.03.2017 23:50

      HDL Designer хорош даже в отрыве от любых библиотек. Он может работать и с Альтерой и Ксайлинксом. В графическом виде HDL код намного понятнее и нагляднее. а если не хватает графики, то можно вставить embedded block и будут кусок необходимого кода прям на графике.


  1. 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 трешхолдом: всё что до порога становится чёрным, всё что выше белым. Дальше можно небольшое размытие сделать или что то ещё, чтобы убрать точки внутри контуров букв.


    1. chersanya
      06.03.2017 07:07

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


      1. Ivan_83
        07.03.2017 09:59

        Может и так, это былая сырая идея на попробовать :)

        2 kolpeex
        Нет.
        Даже в случае дрожания камеры акселерометры мало помогут:
        — нужно как то совмещать кадры и данные акселерометров
        — у камеры может быть разное фокусное расстояние: грубо говоря если она как телескоп смотрит сильно в даль то гироскоп должен быть сильно чувствительным, а если смотрит близко то опять же имеем сильно другие коэффициенты поправок
        Опять же, вычислительно не сильно затратно стабилизировать дрожание без вращения.


    1. kolpeex
      06.03.2017 14:05

      Наверно, было бы проще делать стабилизацию изображения, если бы с видеопотоком были бы данные акселерометров камеры.


  1. Daffodil
    06.03.2017 11:43

    Классная статья. Можете теперь попробовать копать в сторону optical flow и сжатия видео. Ну а дальше можно превращать хобби в профессию.


    1. ubobrov
      06.03.2017 11:55

      Идеи кое-какие есть, но в профессию пока рано превращать, я ещё не настоящий сварщик ).