Предисловие
Меня давно интересовала тема обработки видео, вот только на отладочных платках 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] — референсный кадр (bacground).
Референсный кадр или bacground обычно делается когда нет никакого движения. Например, если мы хотим детектировать движение в одном углу комнаты, то перед этим мы должны сделать и запомнить снимок этого угла, когда там нет никакого движения, а потом из всех последующих снимков попиксельно вычитать этот самый background. Всё очень просто. Однако, из-за шумов в изображении, автоматического баланса белого в камере и других факторов нам необходимо применить порог срабатывания детектора. Этот порог применяется к разности кадров. Если разность больше порога, то движение есть, иначе — нет.
P[F(t)] > Threshold
Недостатков у этого метода больше чем достоинств, однако его применяют для детектирования движения из-за простоты реализации. Недостатками являются:
- Зависимость от освещённости
- Зависимость от смещения камеры
- Зависимость от погодных условий
- Влияние автоматического баланса белого
Любое изменение внешних факторов приведёт к обнаружению движения и ложному срабатыванию детектора.
Образно, схема детектора выглядит так:
Способ второй. 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 контроллер
Частично этот модуль был взят с того же сайта 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
→ Методы усовершенствования детектирования
Автор: ubobrov