// а теперь нам осталось вычислить поле cpCurrent для прочитанного
// поведения
Dec(Frame[I].cpRepeat);
case Frame[I].cpCode of
01: Frame[I].cpCurrent := Frame[I].cpCurrent + Frame[I].cpExtent;
10: Frame[I].cpCurrent := Frame[I].cpCurrent - Frame[I].cpExtent;
end;
end else begin
// этот фрагмент приведен ниже и описывает, что происходит,
// когда поле cpIndex содержит индекс обслуживаемого набора поведений
end;
Теперь пришло время объяснить, почему поля cpCurrent и cpExtent заданы типами с плавающей точкой. Дело в том, что в поле cpExtent элемента массива опорного кадра хранится величина изменения цветовой плоскости за один кадр повтора, а не за весь его период. Когда величина изменения за весь период делится на количество повторов, остаются дробные части. Отбрасывать дробные части нельзя, потому что динамика изменения цветовой плоскости может быть разная. Из кадра в кадр поле cpCurrent изменяется на величину поля cpExtent. Представьте, что за 100 кадров цветовая плоскость пикселя изменилась всего на 1 процент. Разделив 1 на 100 кадров и округлив результат, мы получим 0. В результате точка на протяжении 100 кадров не изменится на 1 процент, ведь мы будем добавлять к полю cpCurrent нулевое значение поля cpExtent. А вот если мы будем добавлять с дробными частями, точка за 100 кадров плавно достигнет изменения в 1 процент. Округление до целого числа выполняется только в момент вывода цветовых плоскостей пикселя в реальный кадр на экране, но внутри опорного кадра все вычисления делаются исключительно с дробными частями.
И снова возвратимся к программному коду декодера. Нам осталось рассмотреть часть кода, когда в поле cpRepeat не осталось повторов, а в поле cpIndex указан индекс обслуживаемого набора поведений цветовой плоскости. В этом случае нужно взять следующее поведение из обслуживаемого набора и занести его данные в текущий элемент массива опорного кадра. Если же в наборе пройдены все поведения (выполненное поведение было последним в наборе), тогда нужно взять следующее поведение прямо из видеопотока и уже его данные занести в текущий элемент массива опорного кадра. Еще раз вспомним, что при чтении из видеопотока декодер должен уметь обслуживать специальный код внедрения других данных в видеопоток.
// вычисляем индекс следующего поведения в наборе
Inc(Frame[I].cpBehaviour);
// если в наборе еще не закончились поведения, тогда
// читаем следующее поведение из набора, иначе
// сбрасываем cpIndex в 0FFFFh и читаем поведение из общего видеопотока
if (Frame[I].cpBehaviour <> 0) and
(Frame[I].cpBehaviour <= Behaviours.Behaviours[Frame[I].cpIndex].Count) then begin
// читаем следующее поведение набора (индекс следующего
// поведения уже находится в Frame[I].cpBehaviour),
// а результат чтения возвращается в переменные C, R и E
GetBehaviour(Frame[I].cpIndex, Frame[I].cpBehaviour, C, R, E);
// следующим циклом обеспечиваем поддержку безвозратной вложенности
// наборов поведений
while C = 11 do begin
Frame[I].cpIndex := R;
Frame[I].cpBehaviour := 0;
// читаем поведение с индексом Frame[I].cpBehaviour = 0
// из набора поведений с индексом Frame[I].cpIndex,
// а результат чтения возвращается в переменные C, R и E
GetBehaviour(Frame[I].cpIndex, Frame[I].cpBehaviour, C, R, E);
end;
// Заносим данные поведения, прочитанного из набора, в текущий элемент
// массива опорного кадра. В поле cpExtent заносится величина
// изменения цветовой плоскости за один кадр повтора,
// а не за весь его период
Frame[I].cpCode := C;
Frame[I].cpRepeat := R;
Frame[I].cpExtent := E / R;
// а теперь нам осталось вычислить поле cpCurrent для прочитанного
// поведения
Dec(Frame[I].cpRepeat);
case Frame[I].cpCode of
01: Frame[I].cpCurrent := Frame[I].cpCurrent + Frame[I].cpExtent;
10: Frame[I].cpCurrent := Frame[I].cpCurrent - Frame[I].cpExtent;
end;
end else begin
// сбрасываем cpIndex в 0FFFFh, так как мы уже читаем поведение
// из видеопотока, а не из массива поведений
Frame[I].cpIndex := $FFFF;
Frame[I].cpBehaviour := 0;
// А теперь читаем поведение из видеопотока. Следующий фрагмент
// кода один в один похож на участок рассмотренного выше кода,
// поэтому здесь я его не комментирую
ReadBehaviour(C, R, E);
while (C = 00) and (R = 8192) do begin
// здесь обрабатывается вложенный блок данных
ReadBehaviour(C, R, E);
end;
while C = 11 do begin
Frame[I].cpIndex := R;
Frame[I].cpBehaviour := 0;
GetBehaviour(Frame[I].cpIndex, Frame[I].cpBehaviour, C, R, E);
end;
Frame[I].cpCode := C;
Frame[I].cpRepeat := R;
Frame[I].cpExtent := E / R;
Dec(Frame[I].cpRepeat);
case Frame[I].cpCode of
01: Frame[I].cpCurrent := Frame[I].cpCurrent + Frame[I].cpExtent;
10: Frame[I].cpCurrent := Frame[I].cpCurrent - Frame[I].cpExtent;
end;
end;
После цикла по всем элементам массива опорного кадра в полях cpCurrent элементов массива находятся значения, которые должны быть занесены в реальный кадр на экране. Собственно говоря, картинка реального кадра уже получена в опорном кадре, но данные картинки еще нужно перевести из процентных отношений в прямые значения, если они не были такими (установлен бит RatioType поля Properties заголовка видеопотока). Затем полученные прямые значения нужно перевести из YCbCr в RGB, если установлен бит ColorSpace поля Properties. Но нужно обратить внимание, что содержимое полей cpCurrent элементов массива опорного кадра ни в коем случае нельзя изменять, так как нам нужны непереведенные значения для декодирования следующего кадра. Для перевода из YCbCr в RGB необходимо извлекать из массива опорного кадра сразу по три цветовых плоскости пикселя, потому что формулам преобразования YCbCr в RGB нужны значения трех плоскостей. Поэтому процедура отображения кадра на экране будет иметь два цикла - по высоте и по ширине кадра.
// глобальные переменные
var
Header: PBCvideoHeader; // заголовок видеопотока
Frame: array of ColorPlane; // опорный кадр
FrameNum: DWord; // номер текущего кадра
procedure ShowFrame;
var
W: Word; // ширина
H: Word; // высота
I: DWord; // индекс плоскости в массиве опорного кадра
Y: Double; // значение плоскости Y
Cb: Double; // значение плоскости Cb
Cr: Double; // значение плоскости Cr
R: Byte; // значение красного цвета
G: Byte; // значение зеленого цвета
B: Byte; // значение синего цвета
begin
// в цикле рисуем все пиксели на реальном экране
// (допустим его имя VideoScreen)
for H := 1 to Header.Height do begin
for W := 1 to Header.Width do begin
I := (H-1) * Header.Width * 3 + (W-1) * 3;
// читаем значения трех цветовых плоскостей пикселя
// из соответствующих элементов массива опорного кадра
Cr := Frame[I].cpCurrent;
Y := Frame[I+1].cpCurrent;
Cb := Frame[I+2].cpCurrent;
// если значения заданы с помощью процентных
// отношений (установлен бит RatioType поля Properties),
// тогда перевести их в прямые значения
if Header.Properties and 2 <> 0 then begin
Cr := Cr * (256 / 100);
Y := Y * (256 / 100);
Cb := Cb * (256 / 100);
end;
// если прямые значения необходимо перевести
// из YCbCr в RGB (установлен бит ColorSpace поля Properties),
// тогда перевести их в RGB, иначе они уже заданы в виде RGB
if Header.Properties and 1 <> 0 then begin
R := Trunc(Y + 1.402 * (Cr - 128));
G := Trunc(Y - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128));
B := Trunc(Y + 1.772 * (Cb - 128));
end else begin
R := Trunc(Cr);
G := Trunc(Y);
B := Trunc(Cb);
end;
// а теперь осталось занести цвет пикселя (он находится
// в переменных R, G и B) в реальный кадр на экране, где
// координаты пикселя заданы переменными W и H
// (в Canvas цвет хранится как BGR, а не как RGB)
VideoScreen.Canvas.Pixels[W-1, H-1] := (B * $10000) or (G * $100) or R;
end;
end;
// +1 кадр видеофильма показали на экране,
// и если видеопоток закончился (счетчик кадров
// достиг количества кадров в видео), тогда остановить программу
Inc(FrameNum);
if FrameNum >= Header.FrameCount then Halt;
end;
Если из приведенного кода убрать все комментарии, можно заметить, что программный код декодера небольшой. К тому же алгоритм декодирования оказывается простым и быстрым. Кстати, программный код декодера можно оптимизировать еще в более компактный, так как перестановкой проверки поля cpIndex сначала на реальный индекс (а не на 0FFFFh) можно добиться исключения одинаковых фрагментов кода, читающих поведение из общего видеопотока или набора поведений из массива поведений. Но это уже вы сами решайте, как вам будет удобнее.
За счет чего происходит сжатие
В качестве примера я взял из фильма "Шестой день" поведение красной цветовой плоскости произвольного пикселя в центральной части кадра. Всего взято 100 кадров, что равно 4 секундам фильма при скорости 25 кадров в секунду. Специально выбрал фрагмент фильма, где развивается динамичное действие. Через кадр быстро проезжает машина, а за ней появляется новая сцена с вертолетом. А теперь посмотрим, как вела себя цветовая плоскость R на протяжении 100 кадров.
185, 193, 194, 194, 192, 197, 197, 207, 207, 204, 204, 201, 201, 197, 200, 197, 208, 210, 208, 206, 209, 209, 216, 216, 216, 215, 214, 214, 214, 214, 214, 214, 217, 217, 216, 216, 215, 216, 079, 030, 028, 009, 047, 021, 008, 017, 060, 025, 024, 018, 013, 015, 015, 017, 017, 017, 017, 017, 017, 016, 016, 016, 012, 011, 015, 015, 015, 014, 005, 006, 008, 008, 008, 008, 008, 004, 011, 012, 012, 012, 012, 012, 012, 012, 012, 012, 012, 012, 012, 012, 007, 007, 006, 053, 054, 058, 053, 052, 050, 047
Здесь указаны непосредственные значения красной цветовой плоскости пикселя для каждого из 100 извлеченных кадров. Значение цветовой плоскости всегда лежит в пределах одного байта, а значит, изменяется только от 0 до 255. Как вы можете заметить, в малых значениях добавлены ведущие нули. Я сделал это для того, чтобы выровнять значения для их удобного просмотра при любом размере текстового окна.
Можно сразу заметить, что в наборе байт встречается очень мало смежных повторений одного и того же состояния цветовой плоскости, чтобы кодировать этот фрагмент любым аналогом RLE-алгоритма. Но мы-то рассматриваем фрагмент 100 представленных байт как окончательный и готовый к сжатию, поэтому не находим, что этот набор байт пригоден для эффективного RLE-сжатия. Кодировщику же не интересен сам набор байт, поскольку ему важна только разница между каждыми смежными байтами. И теперь посмотрите, как с помощью разниц набор байт начинает превращаться в эффективный для RLE-алгоритма блок. Нужно отметить, что кодировщик всегда берет разницу по модулю между каждыми смежными байтами.
000, 008, 001, 000, 002, 005, 000, 010, 000, 003, 000, 003, 000, 004, 003, 003, 011, 002, 002, 002, 003, 000, 007, 000, 000, 001, 001, 000, 000, 000, 000, 000, 003, 000, 001, 000, 001, 001, 137, 049, 002, 019, 038, 026, 013, 009, 043, 035, 001, 006, 005, 002, 000, 002, 000, 000, 000, 000, 000, 001, 000, 000, 004, 001, 004, 000, 000, 001, 009, 001, 002, 000, 000, 000, 000, 004, 007, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 005, 000, 001, 047, 001, 004, 005, 001, 002, 003
Получившийся набор состоит из 100 разниц и выглядит уже легче для визуального восприятия. Хотя еще ничего особого не произошло. Тот же самый набор байт, но выраженный через разницы между смежными байтами. Заметьте, в нем стало еще меньше повторяющихся смежных и одинаковых по значению байт. Поэтому для RLE-алгоритма новый набор байт еще не эффективен, разве что содержимое его байт занимает в битовом представлении намного меньше места.