Info Guide #10
30 апреля 2007

Форматы - Подробно о декодере jpeg.

<b>Форматы</b> - Подробно о декодере jpeg.

 

   Всем  привет! Помните  меня? :) Поскольку  тема данной статьи
интересует многих, я, не долго думая, решил нацарапать статейку. 
Несмотря  на  всю кажущуюся сложность, постараюсь изложить всё в 
простой, понятной  форме. Хочу  сразу предупредить: всё, о чем я 
буду  писать, есть  результат  моих собственных экспериментов, а 
посему не является истиной в последней инстанции. Это всего лишь 
мои  мысли. Таким  образом, если  что не так, я не виноват :). В 
статье  буду  использовать фрагменты из документации по жпегу by 
Ceryx, а также оптимизированные и страшно изуродованные:)) куски 
кода  из  пасовского исходника жпег декодера by Алексей Абрамов. 
Там, правда, мало  что от него осталось, но в любом случае его я 
использовал в качестве базы. Данный материал не рассчитан на но─ 
вичков - как минимум, требуются знания языка Паскаль. Вступление 
сказано, теперь переходим непосредственно к делу. 

   Декодирование жпега можно разделить на две стадии.
   1-я - Сканирование  жпега, обработка маркеров и загрузка таб─
лиц. Это  стадия  начальной инициализации, на ней осуществляется 
настройка  декодера  на  соответствующий жпег, всё это я опишу в 
первую очередь. 
   2-я - Непосредственно  сам процесс декодирования, это найдёте
дальше по тексту. Для лучшего понимания алгоритмов, кое-где буду 
приводить куски пасовского исходника. 
   Немного  теории. JPEG  представляет  собой  упакованный  кадр
фотореалистичного изображения, то есть расчитан он в основном на
сжатие  цветных фотографий с глубиной цвета 24 бита (до цветовых
преобразований  подразумевается по 8 бит на каждую цветовую ком─
поненту RGB). Чтобы было понятно, как декодировать жпег, вкратце
опишу процесс сжатия кадра.
   Кадр разбивается на блоки 8x8. Над каждым блоком производится
ДКП (Дискретное Косинусное Преобразование), тем самым происходит
трансформация яркостных данных из временной области в частотную.
Затем полученная частотная матрица квантизируется, при этом про─
исходит оптимизация частот. Собственно, на данном этапе и проис─
ходит  сжатие, за  счёт  отбрасывания  излишней  высокочастотной
информации. Далее  все члены матрицы вытягиваются в одну цепочку
зигзагом и кодируются по RLC (Zero Run Length Coding). Финальный
этап - кодирование по Хаффману, в результате которого из полного
блока 8x8 остаётся лишь упакованная горстка битов. Процесс деко─
дирования выполняется в обратном кодированию порядке. Конечно, я
описал  лишь  общую  схему процесса сжатия, но думаю, пока этого
вполне достаточно.
   Нам понадобится ещё несколько понятий. Цвета в жпеге хранятся
не  как  RGB, а в формате YCbCr: Y - компонента яркости; Сb/Cr -
цветоразностные  компоненты, приблизительно  показывают, сколько
голубой  и красной составляющей в цвете. Таким образом, если нас
не интересуют цвета, можно извлечь только Y компоненту. Также по
тексту  будут  фигурировать обозначения DC/AC. В полученном нами
векторе из 64 элементов, необходимых для последующего преобразо─
вания по ДКП, первый элемент со смещением 0 называется DC - это,
так сказать, нулевая частота,то есть фоновая яркость; все после─
дующие 63 элемента - AC. Это необходимо потому, что разные коэф─
фициенты кодируются по разному. 0-я частота, как правило, меняе─
тся слабо, поэтому кодируется не сам коэффициент, а разность ме─
жду этим и предыдущим DC коэффициентом. AC приходится кодировать
как  есть, там  уже  частоты меняются существенно, на протяжении
всего кадра.
   Жпег представляет собой файл, поделенный на части - сегменты.
Вот что из себя представляет сегмент:
 

   - заголовок (4 байта):
        $ff     идентификатор сегмента
         n      тип сегмента (1 байт)
        sh, sl  размер сегмента, включая эти 2 байта,
                но не включая $FF и тип сегмента.
                Не в Intel'овском, а в Motorol'овском порядке:
                старший байт первый, младший последний!
  - содержимое сегмента, макс. 65533 байта.

   В  начале  сегмента стоит маркер - определённая метка: первый
байт всегда FF, следующий - тип сегмента. Формат JPEG использует
мотороловской  формат  для слов, то есть старший байт слова идёт
первым, младший вторым.
   Приведу основные маркеры, которые нам понадобятся:
 

   D8 - SOI  Start Of Image
   C0 - SOF0 Start Of frame (baseline)
   C2 - SOF2 Start Of frame (progressive)
   C4 - DHT  Define Huffman table
   DB - DQT  Define Quantization table
   DD - DRI  Define Restart Interval
   DA - SOS  Start Of Scan
  D9 - EOI  End Of Image

    Немного подробнее опишу маркеры:
    D8,D9 = начало, конец файла;
   C0,C2 = определить основные параметры кадра (разрешение, цве─
 тность, таблицы); 
   C4 = таблицы  Хаффмана (необходимы для декодирования битового
 потока); 
    DB = таблицы квантизации (нужны для процесса деквантизации);
   DD = определить  интервал  перезапуска (редко  используется в
 декодере); 
   DA = начало сканирования (с этого маркера начинаются непосре─
дственно упакованные данные самого жпега). 

   Дабы  не  захламлять ваши головы, уважаемые читатели, не буду
сейчас  углубляться в алгоритмы паковки жпега, сделаю это позже.
Скажу лишь, что  всё, что  нам необходимо вначале сделать, - это
просканировать  файл  от  начала, от маркера SOI до маркера SOS,
попутно инициализируя соответствующие переменные и таблицы. Мар─
кер  SOS  определяет  начало пакованных данных жпега, а всё, что
идёт  после  него, относится  уже  к процессу декодирования, это
рассмотрим дальше.
   Процесс  сканирования  жпега начинается с чтения маркера SOI.
Если  в  начале файла его нет, то это не жпег и можно смело пре─
кращать чтение.Сразу за маркером следуют 2 байта длины сегмента,
исключение составляют SOI и EOI, у них сегмент отсутствует.
   Вот как выглядит основной цикл сканирования:

   ...
   Repeat
     BlockRead(PictureFile,v,2);
     if Lo(v)<>$FF then begin WriteLn('Invalid file format');
                              Close(PictureFile); Halt end;
     b:=Hi(v); Marker[b]:=True;
     if (b<>$D8) and (b<>$D9) then begin
       BlockRead(PictureFile,v,2); FilePtr:=Swap(v)-2;
       Case b of
         $C0,$C2: ...               { Main Image Parameters }
         $C4:     ...       { Read & compile Huffman Tables }
         $DA:     ...                       { Start Of Scan }
         $DB:     ...          { Define Quantization Tables }
         $DD:     ...             { Define Restart Interval }
       End;
       while FilePtr<>0 do begin
                             BlockRead(PictureFile,v,1);
                             dec(FilePtr)
                           end;
       if IOResult<>0 then begin
                             WriteLn('I/O error !');
                             Close(PictureFile);
                             Halt
                           end
     end
  Until (b=$D9) or (b=$DA);
  ...
 

   BlockRead - читает из файла заданное количество байт
   Lo/Hi     - выделяет младший/старший байт
  Swap      - меняет старший и младший байт местами

   Все  остальные маркеры и их сегменты, соответственно, пропус─
каются. Сканирование  маркеров  выполняется  до тех пор, пока не
встретится SOS. Это говорит о том, что все подготовительные опе─
рации  выполнены  и  далее  следует  битовый поток данных самого
жпега. Теперь рассмотрим подробнее обработку самих маркеров. Из─
лагать  буду  в  такой последовательности: вначале полный формат
соответствующего сегмента,далее фрагмент кода,затем комментарий.
Пока описание буду давать краткое, более детально всё рассмотрим
далее. Поэтому, если вдруг вам что-то будет неясно, советую пока
пропустить это место и читать дальше.

SOF0,SOF2: Начало кадра: 
 ~~~~~~~~~~~~~~~~~~~~~~~~ 
   - $ff, $c0 (SOF0)
   - длина (старший, младший байт), 8+кол.компонент*3
   - точность данных (1 байт) в формате бит/элемент, обычно 8
   - высота жпега (2 байта, Ст-Мл)
   - ширина жпега (2 байта, Ст-Мл)
   - кол.компонент (1 байт): обычно 1=чёрно-белое;3=цветное YCbCr
   - для каждого компонента: 3 байта
      - идентификатор компонента (1=Y, 2=Cb, 3=Cr)
      - сэмплинг фактор (бит 0-3 верт., 4-7 гор.)
     - номер таблицы квантизации

   ...
   $C0,$C2: begin
           vv  :=ReadByte;              { Main Image Parameters }
         Height:=ReadWord;
         Width :=ReadWord;
         planes:=ReadByte;
         if (vv<>8) or ((planes<>1) and (planes<>3))
         then begin
           WriteLn('Only 8-bit Grayscale/RGB images supported');
           Close(PictureFile);
           Halt
         end;
         For hh:=0 to planes-1 do begin
           CmpID[hh].C:=ReadByte; vv:=ReadByte;
           CmpID[hh].H:=Hi4(vv);
           CmpID[hh].V:=Lo4(vv);
           CmpID[hh].T:=ReadByte
         end;
        method:=b end;
  ...
 

   ReadByte/ReadWord - чтение байта/слова из файла
  Lo4/Hi4 - выделяет младшую/старшую часть байта

   Вначале следуют: разрядность  данных (обычно 8 бит, остальные
значения  можно не обрабатывать); высота, ширина картинки в пик─
селах; количество  компонент (определяет тип изображения: 1=чёр─
но-белое, 3=цветное). Далее для каждой компоненты следуют 3 бай─
та: тип  компоненты (1=Y,2=Cb,3=Cr); сэмплинг фактор; номер таб─
лицы квантизации. Все эти параметры необходимо сохранить в соот─
ветствующих переменных и массивах, они нам понадобятся позже.

DHT: Определить таблицу Хаффмана (ТХ): 
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
   - $ff, $c4 (DHT)
   - длина (старший, младший байт)
   - информационный байт ТХ:
      бит 0..3: номер ТХ (0..3, иначе ошибка)
      бит 4   : тип ТХ, 0=DC таблица, 1=AC таблица
      бит 5..7: не используется=0
   - 16 байтов: количество символов с кодами длиной 1..16, сумма
     этих байтов есть общее количество кодов, должно быть <= 256
   - n байтов: таблица, содержащая символы в порядке увеличения
    длины кода (n = общее число кодов)
    Комментарий:
   - один DHT сегмент может содержать несколько таблиц,
    каждая со своим информационным байтом.

   ...
   $C4:begin
         Repeat                 { Read & compile Huffman Tables }
           hh:=ReadByte;
           For vv:=0 to 15 do HT.L[vv]:=ReadByte;
           aa:=0;
           For vv:=0 to 15 do
             For m:=1 to HT.L[vv] do begin
               HT.V[aa]:=ReadByte;inc(aa);
             end;
           c:=0;aa:=0;
           For vv:=0 to 15 do begin
             if HT.L[vv]>0 then begin
               HT.H2[Hi4(hh),Lo4(hh),vv].SV:=aa-c;
               HT.H2[Hi4(hh),Lo4(hh),vv].EV:=aa+HT.L[vv];
             end;
             For m:=1 to HT.L[vv] do begin
               HT.H1[Hi4(hh),Lo4(hh),aa].V:=HT.V[aa];
               if vv<8 then begin
                 HT.H1[Hi4(hh),Lo4(hh),c].L:=vv+1;
                 HT.H1[Hi4(hh),Lo4(hh),c].LV:=HT.V[aa];
               end;
               inc(aa);inc(c)
             end;
             c:=c shl 1;
           end;
         Until FilePtr=0;
      end;
  ...

   Здесь несколько сложнее. Дело в том,что мало просто загрузить
эти таблицы. Необходимо также преобразовать их в удобный для ра─
боты  декодера  формат. Поэтому остановимся на этом поподробней.
Для  начала  немного теории. Хаффман относится к статистическому
кодированию, то  есть символам с большим числом вхождений в файл
присваивается код с меньшей разрядностью, с меньшим числом - бо─
льшая разрядность. Таким образом образуется символьный алфавит с
непропорциональными длинами присвоенных ему кодов. За счет этого
достигается  сжатие групп символов. В результате чего образуется
выходной битовый поток данных. Для успешного декодирования бито─
вого  потока необходимо иметь таблицы соответствия символов и их
кодов соответствующей длины. Нас будут интересовать коды длин от
0 до 15, то есть 16 бит максимум.
   Вернёмся к нашему фрагменту кода. В начале стоит информацион─
ный байт, в нём: биты 0..3 - номер таблицы Хаффмана; бит 4 - тип
таблицы  0=DC/1=AC. За  ним  следует  16 байт, которые описывают
количество символов с длиной кодов от 1 до 16, сумма этих байтов
есть  общее  количество  кодов  и не должна превышать 256. Потом
идут  символы  в порядке увеличения длин кодов. Внимание!!! Идут
символы, но не их коды. То есть коды нам ещё придётся им присво─
ить. Один DHT сегмент может иметь в себе несколько таблиц,каждую
со своим информационным байтом. Всего таких таблиц может быть 8:
4 для DC и 4 для AC. Мы имеем таблицу символов и их длин. Теперь
нам  необходимо  определить  коды  Хаффмана для каждого символа.
Делается  это  по  следующему  алгоритму: вначале  стартовый код
c = 0;  по порядку проходим все символы нашей таблицы от длины 1
до 16;  на каждой итерации увеличиваем значение кода на единицу;
при  изменении  длины  кода  умножаем  код на 2, что равносильно
сдвигу кода на один разряд влево. В результате имеем полную таб─
лицу  всех  символов  и соответствующих им кодов Хаффмана. Что с
ней  делать  дальше, станет  понятно  позже. Пока нам необходимо
просто сохранить все эти данные.

DQT: Определить таблицу квантизации (ТК): 
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
   - $ff, $db (DQT)
   - длина (старший, младший байт)
   - информационный байт ТК:
      бит 0..3: номер ТК (0..3, иначе ошибка)
      бит 4..7: точность ТК, 0 = 8 бит, иначе 16 бит
  - n байт ТК, n = 64*(точность+1)
    Комментарий:
   - один DQT сегмент может содержать несколько таблиц,
     каждая со своим информационным байтом.
   - для точности 1 (16 бит) порядок следования - старший, потом
    младший (для каждого из 64 слов).

   ...
   $DB: begin
         Repeat                    { Define Quantization Tables }
           hh:=ReadByte;
           For vv:=0 to $3F do
             if Hi4(hh)=0 then qtmp[vv]:=ReadByte;
           for m:=0 to 63 do Quant[Lo(hh),m]:=qtmp[z2t[m]];
           for v:=0 to 7 do
             for w:=0 to 7 do begin
               if w=0
               then cw:=frac
               else cw:=round(frac*cos((w*PI)/16)*sqrt(2));
               if v=0
               then cv:=frac
               else cv:=round(frac*cos((v*PI)/16)*sqrt(2));
               cw:=(cw*cv) shr prec;
               Quant[Lo(hh),v*8+w]:=mul1(Quant[Lo(hh),v*8+w],cw);
             end;
         Until FilePtr=0;
       end;
  ...

   Таблицы  квантизации  необходимы для восстановления частотной
матрицы и имеют размерность 8x8, то есть всего таких коэффициен─
тов  в одной  матрице будет 64. На листинге: вначале считывается
информационный байт, в нём биты 0..3 - номер таблицы квантизации
от  0 до 3; биты 4..7 - разрядность  элементов матрицы (0=8 бит,
иначе 16 бит). Далее выполняется чтение и масштабирование элеме─
нтов. Один  DQT сегмент может содержать несколько таблиц кванти─
зации, каждую со своим информационным байтом. Большинство жпегов
рассчитаны на 8-битные таблицы.

DRI: Определить интервал перезапуска: 
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
   - $ff, $dd (DRI)
   - длина (старший, младший байт) = 4
   - рестарт интервал (старший, младший байт) в единицах
     MCU блоков - это значит, что через каждые n MCU блоков может
     быть найден маркер RSTn. Первый маркер должет быть RST0,
    затем RST1, и так далее, до RST7, затем снова RST0.

   ...
  $DD: begin RestartInterval:=ReadWord end
  ...

   Всё, что необходимо сделать,- это сохранить его в переменной.
Отмечу, что встречаются они крайне редко.

SOS: Начало сканирования: 
 ~~~~~~~~~~~~~~~~~~~~~~~~~ 
   - $ff, $da (SOS)
   - длина (старший, младший байт),
     6+2*(кол.компонент сканирования)
   - количество компонент сканирования (1 байт),
     должно быть >= 1 и <= 4 (иначе ошибка), обычно 1 либо 3
   - для каждого компонента: 2 байта
      - идентификатор компонента (1=Y, 2=Cb, 3=Cr), смотреть SOF0
      - используемая таблица Хаффмана:
         - бит 0..3: AC таблица (0..3)
         - бит 4..7: DC таблица (0..3)
  - 3 байта, должны быть пропущены (???)
    Комментарий:
  - Данные жпега следуют сразу за SOS сегментом.
 

   $DA: begin
         m:=ReadByte;                           { Start Of Scan }
         For hh:=0 to m-1 do begin
           Scan.Cmp[hh]:=ReadByte;
           vv:=ReadByte;
           Scan.TD[hh]:=Hi4(vv); Scan.TA[hh]:=Lo4(vv)
         end;
         Scan.Ss:=ReadByte;
         Scan.Se:=ReadByte;
         vv:=ReadByte;
         Scan.Ah:=Hi4(vv); Scan.Al:=Lo4(vv)
       end;

   За  ним  следует  количество сканируемых компонент - обычно 1
либо 3. Далее для каждой компоненты следуют 2 байта: 1-й - иден─
тификатор компоненты (1=Y, 2=Cb, 3=Cr), 2-й - Номер используемой
таблицы Хаффмана, здесь биты 0..3 = AC таблица (0..3), 4..7 = DC
таблица  (0..3). Затем  идут  3 байта которые необходимо пропус─
тить. Как  было уже сказано раньше, этот маркер является послед─
ним, за ним непосредственно следуют сжатые данные жпега.



   Итак, все приготовления сделаны, теперь необходимо переходить
непосредственно к самому процессу декодирования жпега.Для начала
объясню, как  расположены сжатые данные. Информация в жпеге хра─
нится  блоками  8x8, то  есть по 64 байта на каждую из компонент
Y/Cb/Cr. Хотя это частный случай,когда сэмплинг фактор 1:1:1, но
об этом позже. Сразу за Y компонентой следуют Cb и Cr, таким об─
разом, мы  имеем всего 3*64 байта на блок 8x8 изображения. Блоки
начинаются  с  левого  верхнего  угла  изображения  и идут слева
направо  и сверху вниз. То есть мы постепенно спускаемся вниз. В
конце  этого битстрима стоит маркер конца жпега EOI, который нам
не обязательно отслеживать, ведь мы и так уже знаем сколько пик─
селей, а соответственно, и  блоков  в нашем  жпеге. Все байтовые
выравнивания  маркеров осуществляются заполнением оставшихся би─
тов  единицами, поэтому, если  в потоке встречается байт FF, его
необходимо пропускать.
   Общий список стадий декодирования выглядит следующим образом:
 

   1) Хаффман декодер (декодирование DC/AC коэффициентов)
   2) Деквантизация вектора из 64 элементов
   3) Зиг-Заг сортировка и восстановление блока 8x8
  4) Применение к блоку ОДКП

   Повторить первые 4 стадии для каждого блока 8x8, каждого ком─
понента изображения Y/Cb/Cr.
 

   5) Масштабирование Cb/Cr
   6) Преобразование уровня
  7) Преобразование YCbCr->RGB

   Все эти стадии описывают лишь декодирование одного блока пик─
селей MCU. Для остальных необходимо повторить эти стадии,попутно
считывая данные из файла и копируя их в соответствующее место на
экране или в буфере. Рассмотрим подробно каждую стадию.

                       1. Хаффман декодер

   Все  данные закодированы Хаффманом, этим достигается конечное
сжатие жпега. Что представляет собой этот вид кодирования? Как я
уже   писал,  каждому  кодируемому  символу  сопоставляется  код
Хаффмана  в зависимости  от  частоты появления символа в потоке.
Чем  меньше вероятность появления символа, тем большей длины код
ему назначается, и наоборот. Тем самым происходит так называемое
непропорциональное кодирование. За счёт этого производится опти─
мизация  избыточности. В результате мы имеем битовый поток (бит─
стрим). Так  как  данные  имеют битовую структуру, а текущий код
имеет  неизвестно какую длину, наш дисковый драйвер должен уметь
читать данные последовательно бит за битом. Далее на каждой ите─
рации необходимо добавлять бит к уже имеющимся и проверять соот─
ветствие по таблицам Хаффмана.Если код найден,то раскодированное
значение  сохраняется, иначе  продолжается декодирование. Встаёт
вопрос, как можно быстро найти текущий код в таблице?
   Вначале приведу процедуру чтения битового потока:

procedure NextBit(var V:byte); begin V:=(V shl 1)+Read1bit end; 

function Read1bit:byte; { Take one bit from Current Byte } 
 begin 
   if Current_bit=0 then begin ReadByte;
     if Current_byte=$FF then begin
       Repeat ReadByte Until Current_byte<$FF;
       if (Current_byte>=$D0) and (Current_byte<=$D7) then begin
         FillChar(DC,SizeOf(DC),0);ReadByte;
       end;
       if Current_byte=0 then Current_byte:=$FF;
     end
   end;
   Read1bit:=(Current_byte shr 7) and 1;
   Current_byte:=Current_byte shl 1;
  inc(Current_bit); if Current_bit=8 then Current_bit:=0
end; 

   Как видно, процедура NextBit просто добавляет следующий бит к
переменной V. Функция Read1bit возвращает следующий считаный бит
из  потока. Она  также  пропускает  байт FF и инициализирует все
DC коэффициенты, в  случае, если  встречается  маркер  RST0-RST7
(D0-D7). Теперь перейдем к сути, декодеру:

function hd(T,C:byte):byte; 
{ Decoding Huffman Code from bitstream } 
var v,code:byte; 
 begin 
  v:=0;
 { L - HuffCode len; L=0 - no code; L=len+1 (1..8) lookup } 
   NextBit(v);
   if HT.H1[T,C,v].L=1 then begin hd:=HT.H1[T,C,v].LV;exit end;
   NextBit(v);
   if HT.H1[T,C,v].L=2 then begin hd:=HT.H1[T,C,v].LV;exit end;
   NextBit(v);
   if HT.H1[T,C,v].L=3 then begin hd:=HT.H1[T,C,v].LV;exit end;
   NextBit(v);
   if HT.H1[T,C,v].L=4 then begin hd:=HT.H1[T,C,v].LV;exit end;
   NextBit(v);
   if HT.H1[T,C,v].L=5 then begin hd:=HT.H1[T,C,v].LV;exit end;
   NextBit(v);
   if HT.H1[T,C,v].L=6 then begin hd:=HT.H1[T,C,v].LV;exit end;
   NextBit(v);
   if HT.H1[T,C,v].L=7 then begin hd:=HT.H1[T,C,v].LV;exit end;
   NextBit(v);
  if HT.H1[T,C,v].L=8 then begin hd:=HT.H1[T,C,v].LV;exit end;
 { SV - Start Value (aa-w); EV - Next Code Len Value } 
   NextBit(v);code:=v+HT.H2[T,C, 8].SV;
   if code<HT.H2[T,C, 8].EV
   then begin hd:=HT.H1[T,C,code].V;exit end;
   NextBit(v);code:=v+HT.H2[T,C, 9].SV;
   if code<HT.H2[T,C, 9].EV
   then begin hd:=HT.H1[T,C,code].V;exit end;
   NextBit(v);code:=v+HT.H2[T,C,10].SV;
   if code<HT.H2[T,C,10].EV
   then begin hd:=HT.H1[T,C,code].V;exit end;
   NextBit(v);code:=v+HT.H2[T,C,11].SV;
   if code<HT.H2[T,C,11].EV
   then begin hd:=HT.H1[T,C,code].V;exit end;
   NextBit(v);code:=v+HT.H2[T,C,12].SV;
   if code<HT.H2[T,C,12].EV
   then begin hd:=HT.H1[T,C,code].V;exit end;
   NextBit(v);code:=v+HT.H2[T,C,13].SV;
   if code<HT.H2[T,C,13].EV
   then begin hd:=HT.H1[T,C,code].V;exit end;
   NextBit(v);code:=v+HT.H2[T,C,14].SV;
   if code<HT.H2[T,C,14].EV
   then begin hd:=HT.H1[T,C,code].V;exit end;
   NextBit(v);code:=v+HT.H2[T,C,15].SV;
   if code<HT.H2[T,C,15].EV
   then begin hd:=HT.H1[T,C,code].V;exit end;
  hd:=$ff;
end; 

   Хочу  привести  вам  результаты  своих экспериментов в данной
области. Еще раз предупреждаю, что практически всё, о чем я буду
говорить и говорил раньше, есть результат моих собственных домы─
слов, поэтому  может отличаться от общепринятых методов и форму─
лировок. Как выяснилось, большая часть кодов не превышает 8 бит,
таким  образом, можно создать 256-байтную таблицу перекодировки.
В этом случае декодирование происходит экстремально быстро: всё,
что  нам нужно - просто взять из таблицы уже готовое значение. В
случае, если  код  >8  бит, тут немного сложнее. Нам нужно знать
все  начальные  позиции  SV и конечные позиции EV для длин кодов
8..16. То  есть  надо  создать  табличку значений, а вернее, три
таблички.
   Первая  будет содержать последовательно все наши закодирован─
ные  символы, назовём  её таблицей V. Расскажу, как сформировать
две другие. Для каждой длины кода от 8..16 нужно задать  началь─
ную  позицию  SV = смещению  первого  кода в таблице V минус сам
первый код. Например, у нас есть код %110 = 6, идёт он под номе─
ром  5  в таблице, тогда  SV = 5 - 6 = -1. Третья таблица должна
содержать  конечную  позицию  EV для текущей длины кода. Как и в
предыдущем  случае, для  всех  длин  кодов от 8..16 нужно задать
EV = смещению  первого  кода  в таблице  V плюс количество кодов
этой  длины. По предыдущему примеру, если количество кодов теку─
щей длины L = 4, то EV = 5 + 4 = 9. Всё это было приведено рань─
ше в куске кода обработки маркера DHT.
   Теперь объясню,для чего это всё нужно.В соответствии с нашими
таблицами,как показано во фрагменте кода выше,поиск значения ко─
да выполняется следующим образом.Для соответствующей длины кода:
складываем  текущий  код и SV, code = v + SV; если code < EV, то
извлекаем  из таблицы V декодированный символ, идёт он по смеще─
нию code, или, что равнозначно, hd = V[code].
 

   Ред.: Поскольку  биты из потока приходится читать в любом ме─
тоде декодирования Хаффмана, то проще (и,как ни странно,быстрее, 
если вести разговор о процессоре Z80) декодировать коды непосре─ 
дственно  по  дереву, бит  за битом, не используя дополнительных 
таблиц. Соответствующие  процедуры  вы  можете позаимствовать из 
распаковщика smallunr.H (см.в приложении). В ZXUnRar декодирова─ 
ние  тоже идёт побитно, но для этого предварительно генерируется 
процедура разбора кодов Хаффмана на основе текущего дерева, поэ─ 
тому получается ещё более высокая скорость декодирования. 

   Если  вы  думаете, что процесс декодирования коэффициентов на
этом  заканчивается, то вы ошибаетесь - он только начинается :).
Пока мы имеем только раскодированные байты,из которых необходимо
получить коэффициенты DC/AC. Плюс ко всему для увеличения эффек─
тивности  сжатия  было  добавлено  RLC сжатие последовательности
нулей. Посмотрим, как раскодировать эти коэффициенты.

   Декодирование  DC коэффициента производится по следующему ал─
горитму:
 

    В начале DC = 0
  а) извлечение соответствующего кода Хаффмана (проверяем в
 таблице Хаффмана для DC) 
   б) смотрим, к какой категории этот код принадлежит
  в) читаем N = биты категории, и определяем значение Diff =
 = (категория, N битов) 
   г) DC = DC + Diff
  д) пишем DC в 64-элементный вектор: vector[0] = DC

   Декодирование  AC  коэффициентов  производится  по следующему
алгоритму:
 

   Для  каждого  AC  коэффициента,  пока  не  встретился  EOB  и
 AC_counter <= 64 
  а) извлечение соответствующего кода Хаффмана (проверяем
 в таблице Хаффмана для AC) 
  б) декодируем код Хаффмана в соответствии с (кол_пред_0,
 категория) 
  в) читаем N=биты категории, и определяем значение AC =
 = (категория, N битов) 
  г) пишем в 64х элементный вектор последовательность нулей =
 = кол_пред_0 
   д) увеличиваем AC_counter на кол_пред_0
  е) пишем AC в 64-элементный вектор: vector[AC_counter] = AC

   Фрагмент кода для чтения коэффициентов DC/AC выглядит следую─
щим образом:
 

   hb:=HD(0,Scan.TD[b]);
   vec[0]:=DC[b]+Bits2Integer(Lo4(hb),ReadBits(Lo4(hb)));
   DC[b]:=vec[0];xx:=1;
   if method<>$C2 then Repeat
     hb:=HD(1,Scan.TA[b]);
     if hb=0 then Repeat vec[xx]:=0; inc(xx) Until xx>=64
     else begin yy:=Hi4(hb);
       for m:=1 to yy do begin vec[xx]:=0; inc(xx) end;
       vec[xx]:=Bits2Integer(Lo4(hb),ReadBits(Lo4(hb))); inc(xx)
     end
  Until xx>=64;

   Объясню  подробнее. Сначала  определяем  DC. Для  этого нужно
декодировать Diff. Кодируется он двумя элементами (кат, Nбит). В
начале  идут  4  бита  (тетрада) категории, представляющие собой
длину считываемого кода, которая и кодируется Хаффманом. То есть
сначала декодируем её, а затем, уже зная длину кода Diff, читаем
N бит. Далее  идёт  преобразование  N битов  в знаковое слово по
следующим правилам:
 

           Значение        Категория            Биты
               0               0                 -
             -1,1              1                0,1
          -3,-2,2,3            2            00,01,10,11
    -7,-6,-5,-4,4,5,6,7        3  000,001,010,011,100,101,110,111
        -15..-8,8..15          4       0000..0111,1000..1111
       -31..-16,16..31         5     00000..01111,10000..11111
       -63..-32,32..63         6                 .
      -127..-64,64..127        7                 .
     -255..-128,128..255       8                 .
     -511..-256,256..511       9                 .
    -1023..-512,512..1023     10                 .
   -2047..-1024,1024..2047    11                 .
   -4095..-2048,2048..4095    12                 .
   -8191..-4096,4096..8191    13                 .
 -16383..-8192,8192..16383   14                 .
-32767..-16384,16384..32767  15                 . 

   Преобразованием занимается следующий код:
 

   function Bits2Integer(bits:byte; value:word):integer;
   begin
     if (value and (1 shl (bits-1))>0) then
       Bits2Integer:=value
     else
       Bits2Integer:=-(value xor (1 shl bits-1));
  end;

   В конце  определяем  значение  DC  как сумму предыдущего DC и
найденного  Diff. Итоговое значение сохраняется в векторе по ну─
левому смещению.
   Теперь о том, как определить коэффициенты AC. Здесь сложнее -
их может быть  несколько. Кроме того, дополнительно используется
кодирование последовательности нулей (RLC). Для каждого элемента
от 2 до 64  необходимо  декодировать байт, содержащий в тетрадах
данные  (кол_пред_0, категория),  где  кол_пред_0  =  количество
предшествующих нулей. Далее от текущей позиции необходимо запол─
нить  вектор нулями в количестве кол_пред_0. При этом, если байт
равен (0,0), то  это  признак  конца  блока  EOB, в этом  случае
оставшиеся элементы вектора заполняются нулями, и на этом запол─
нение вектора заканчивается. Если этого не произошло,выполняется
чтение группы из Nбит бит и преобразование значения AC коэффици─
ента, как и в предыдущем случае.
   Декодирование DC/AC коэффициентов необходимо выполнять по со─
ответствующим таблицам Хаффмана. Ещё один момент. Существует два
формата  следования  данных  в жпеге. Первый называется baseline
(маркер С0), о нём я и писал, в нём  все 64 коэффициента вектора
идут  подряд. Жпеги этого типа открываются за один проход сверху
вниз. Существует  ещё  один  формат - progressive (маркер C2). В
нём  за  один  кадр считывается только один коэффициент, сначала
DC, далее  последовательно все AC. Таким образом один общий скан
разбивается на несколько последовательно идущих сканов. Количес─
тво коэффициентов зависит от качества сжатия жпега. Для открытия
жпега этого типа необходим кадровый буфер для хранения коэффици─
ентов  DC/AC. Преимуществом  данного  типа  является возможность
увидеть  кадр изображения, не дожидаясь конца файла. Чтение сле─
дующей порции коэффициентов будет лишь улучшать качество картин─
ки, кадр будет как бы фокусироваться. Ввиду сложности реализации
прогрессивной  развертки, я  не  стал поддерживать её полностью,
сделав лишь чтение первого скана, содержащего DC коэффициенты.

           2. Деквантизация вектора из 64х элементов

   На  этой  стадии  выполняется восстановление оптимизированных
коэффициентов  вектора. Выполняется  это  следующим  образом. На
этапе подготовки было выполнено чтение всех необходимых нам таб─
лиц квантизации. Всё, что нам теперь нужно,- просто умножить все
элементы нашего вектора на соответствующие элементы таблицы ква─
нтизации. Можно  объединить эту стадию со стадией ОДКП (обратное
ДКП), как поступил я сам.

        3. Зиг-Заг сортировка и восстановление блока 8x8

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

    0  1  5  6 14 15 27 28
    2  4  7 13 16 26 29 42
    3  8 12 17 25 30 41 43
    9 11 18 24 31 40 44 53
   10 19 23 32 39 45 52 54
   20 22 33 38 46 51 55 60
   21 34 37 47 50 56 59 61
  35 36 48 49 57 58 62 63

   То  есть элементы вектора необходимо записывать в ячейки мат─
рицы, в  соответствии с их порядковыми номерами. В результате мы
имеем полностью восстановленную матрицу для последующей,пожалуй,
самой важной из всех, стадии.

                   4. Применение к блоку ОДКП

   Это самая интересная часть декодирования. ОДКП (Обратное Дис─
кретное  Косинусное Преобразование) относится к семейству преоб─
разований  Фурье  и выполняет преобразование данных из частотной
области  во временную. То есть на входе мы имеем матрицу частот,
после  применения  ОДКП  будет  матрица дискретных значений, или
яркости, пикселей. Главная  трудность этой стадии состоит в том,
что  на самом деле преобразования Фурье выполняются слишком мед─
ленно. Не  стану здесь расписывать всякие классические математи─
ческие  формулы  и  выкладки, на  эту  тему можно написать целую
книгу, приведу  лишь  самое, на мой взгляд, оптимальное решение.
Существует  семейство  быстрых алгоритмов преобразования Фурье -
ОБПФ (Обратное Быстрое Преобразование Фурье). Из  множества дан─
ных методов я выбрал схему AA&N, как самую быструю. Единственный
минус данного метода - небольшая потеря точности, хотя на глаз я
её не заметил.
   Приведу фрагмент кода, считающий матрицу 1x8:

   ...
   { Even part }
   t0:=tout[i*8+0];t1:=tout[i*8+2];
  t2:=tout[i*8+4];t3:=tout[i*8+6];
 

   t10:=t0+t2;t11:=t0-t2;t13:=t1+t3;
   t12:=(t1-t3)*(2*c4)-t13;
   t0:=t10+t13;t3:=t10-t13;
  t1:=t11+t12;t2:=t11-t12;
 

   { Odd part }
   t4:=tout[i*8+1];t5:=tout[i*8+3];
  t6:=tout[i*8+5];t7:=tout[i*8+7];
 

   z13:=t6+t5;z10:=t6-t5;
   z11:=t4+t7;z12:=t4-t7;
   t7:=z11+z13;
   t11:=(z11-z13)*(2*c4);
   z5:=(z10+z12)*(2*c2);
   t10:=(2*(c2-c6))*z12-z5;
   t12:=(-2*(c2+c6))*z10+z5;
  t6:=t12-t7;t5:=t11-t6;t4:=t10+t5;
 

   tout[i*8+0]:=t0+t7;tout[i*8+7]:=t0-t7;
   tout[i*8+1]:=t1+t6;tout[i*8+6]:=t1-t6;
   tout[i*8+2]:=t2+t5;tout[i*8+5]:=t2-t5;
  tout[i*8+4]:=t3+t4;tout[i*8+3]:=t3-t4;
  ...

    Здесь:
     tout[] - рабочая матрица 1x8
    i - номер текущей строки матрицы
    Константы:
     c2 = cos(2*PI/16);
     c4 = cos(4*PI/16);
    c6 = cos(6*PI/16);

   Данным кодом  необходимо пройтись вначале по всем строкам на─
шей 8x8 матрицы, а затем по всем столбцам. Получается  16 итера─
ций: 8 на строки + 8 на столбцы. При  обработке  столбцов  перед
финальной  записью  результата следует разделить его на 8, этого
требует  специфика  метода. Есть  ещё одна тонкость, без которой
алгоритм  не  будет работать. Перед обработкой начальную матрицу
необходимо умножить на константу. Вот как это будет выглядеть:

   ...
   for j:=0 to 7 do
     for i:=0 to 7 do begin
       if i=0 then ci:=1 else ci:=cos((i*PI)/16)*sqrt(2);
       if j=0 then cj:=1 else cj:=cos((j*PI)/16)*sqrt(2);
       tout[j*8+i]:=tin[j*8+i]*ci*cj;
    end;
  ...

   Как видно, если номер элемента не нулевой,нужно умножить этот
элемент на cos((i*PI)/16)*sqrt(2), иначе на единицу, то же самое
и по j. Эти ухищрения делаются для уменьшения количества умноже─
ний  в цикле  обработки. Если  предварительно перемножить данные
константы  с таблицами квантизации и объединить стадии 2 и 4, то
есть включить в ОБПФ деквантизацию,можно выиграть немного скоро─
сти. Это и было проделано раньше при обработке маркера DQT, смо─
треть фрагмент кода.

 ──────────────────────────────────────────────────────────────── 
   Все  описанные выше этапы позволяют получить коэффициенты то─
лько  одной  компоненты  (Y/Cb/Cr). Поэтому четыре первые стадии 
необходимо  повторить для каждой компоненты, если, конечно, жпег 
полноцветный. Далее  следует описание стадий уже после декодиро─ 
вания всех 3 компонент. 
──────────────────────────────────────────────────────────────── 

                    5. Масштабирование Cb/Cr

   В результате  предыдущих  стадий была получена информация о 3
компонентах  Y/Cb/Cr. То  есть  3 блока 8x8, описывающие пиксели
изображения. На  самом  деле это является частным случаем, когда
масштаб (сэмплинг фактор) компонент Y/Cb/Cr=1:1:1, но так бывает
не всегда. Часто  масштаб компонент принимается 2:1:1, что озна─
чает, что на 2 элемента яркости Y приходится по 1 элементу цвет─
ности  Cb/Cr. Тоже  самое  происходит и по другой координате, то
есть и по X, и по Y. Эти данные загружались раньше,при обработке
маркера SOS. Существует понятие минимального кодированного блока
- MCU (Minimum Coded Unit), которое  описывает блок изображения.
При  сэмплинг факторе 1:1:1  MCU равен 8x8. При 2:1:1  MCU равен
16x16. Во  втором случае получается, что данных Y компоненты в 4
раза  больше, чем  для  Cb/Cr. Если  представить блок 8x8 как DU
(DataUnit), то последний случай запишется в виде: YDU, YDU, YDU,
YDU, CbDU, CrDU. На  4 блока  данных для яркости Y приходится по
одному блоку цветности Cb/Cr. Такое допущение позволяет получить
ещё большее сжатие при практически незаметном ухудшении качества
картинки. С  учётом  сказанного для каждой компоненты необходимо
также  учитывать  масштаб  и  выполнять  полностью загрузку MCU.
Блоки  данных из 64 элементов распологаются в MCU слева направо,
сверху  вниз. После того, как будет загружена полностью информа─
ция о всех компонентах, необходимо выполнить ресэмплинг, то есть
отмасштабировать,если необходимо, компоненты Cb/Cr. При сэмплинг
факторе 2:1:1  в результате получим 3 матрицы элементов 16x16. В
случае 1:1:1 все компоненты идут один к одному,и масштабирование
выполнять  не  нужно, MCU  будет равен 8x8. В принципе, бывают и
другие  вариации, например, Cb/Cr  по  X  может  быть на 2 юнита
(1:1:1), а по Y  на 1 (2:1:1). Но  такие  случаи  бывают  крайне
редко, я не стал морочить ими голову и поддержал только два пер─
вых.

                    6. Преобразование уровня

   Необходимо преобразовать значения наших компонент из знаковой
в беззнаковую форму. Сделать это очень просто - всё, что нужно,-
это прибавить 128 ко всем 8-битным знаковым значениям наших ком─
понент. На данном этапе также выполняются регулировки яркостного
и цветового баланса. Если  в таблицах яркости и цветности учесть
сразу и значение уровня, то данное преобразование будет выполня─
ться автоматически.

                  7. Преобразование YCbCr->RGB

   Это преобразование является заключительным этапом декодирова─
ния  и  осуществляет  преобразование  из  цветового пространства
YCbCr  в формат экранных пикселей RGB. Делается это по стандарт─
ным формулам:
 

   R = Y                    + 1.402  *(Cr-128)
   G = Y - 0.34414*(Cb-128) - 0.71414*(Cr-128)
  B = Y + 1.772  *(Cb-128)

   Все значения YCbCr - 8 битные беззнаковые. В результате имеем
декодированные  пиксели  изображения  в формате true color (по 8
бит на компоненту).

 ──────────────────────────────────────────────────────────────── 
   Вот,собственно,и всё,о чём я хотел вам рассказать.Изначально,
правда, задумывалось написать статью в формате спековского асма, 
но, учитывая  неподъёмность  исходников, пришлось  отказаться от 
этой  затеи  и  расписать на примере пасовских фрагментов. Можно 
было  бы  ещё написать про масштабирование полученного изображе─ 
ния, про  его обработку выходными фильтрами, но это тема отдель─ 
ной  статьи. Да  и  без этого, думаю, информации для размышления 
подкинул  достаточно. Так что, господа кодеры, изучайте, думайте 
 и пишите качественные декодеры жпега для нашего любимого спекки. 
   Ред.: В приложении  лежит  просмотрщик  xjpeg  by scor^3umf и
исходники этого просмотрщика (публикация исходников не означает, 
что  автор  забросил  этот  проект - перед  внесением каких-либо 
изменений свяжитесь с автором по адресу scorpzx@mail.ru ). 



Другие статьи номера:

Ликбез - Аккумуляторы. Практика использования.

Ликбез - Аккумуляторы. Результаты опытов с различными аккумуляторами.

Возможности Спектрума - Описание формата ani-файлов на ZX.

Inferno - Авторы журнала.

Возможности Спектрума - Способы воспроизведения многоканальной музыки на бипере.

Возможности Спектрума - О поддержке формата DVD на ZX.

Gameland - О конкурсе нелепых (или корявых) игр для ZX Spectrum - Crap Games Competition.

Графика - Как быстро рисовать цветные картинки.

Inferno - Вступление от редактора.

Inferno - Ошибки в предыдущих номерах.

For Coderz - Код Грея и оптимизация программ.

For Coderz - Построение графического пользовательского интерфейса.

Форматы - Подробно о декодере jpeg.

Железо - Описание микросхемы К561ПУ4.

Inferno - Письма в редакцию.

Форматы - Формат пакованного файла MegaLZ.

Scorpion ZS - Структура разметки винчестера на компьютере Scorpion.

ZX Клоны - Мультиплатформенность на ZX Spectrum. Компьютеры SAM Coupe и MSX.

Реклама - Реклама NedoPC.

Inferno - Об оболочке.

Активный отдых - Команда "Спектрум" на соревновании по ночному ориентированию Окинчица 2004.

Sofтинка - Сравнительная таблица результатов упаковки кодовых файлов различными упаковщиками.

Реклама - Реклама King of Evil.

Sofтинка - Программы для печати в приложении к журналу.

Sofтинка - Музыкальный редактор Pro Tracker v3.71. История изменений.

Реклама - Реклама от В. Богдановича.

Железо - О некоторых RND-генераторах.

Возможности Спектрума - Аппаратный скроллинг на ZX Spectrum.

Pentagon - Синхроселектор видеосигнала на Pentagon. Проблемы и схема.

DIY - Универсальный TAPE интерфейс. Схема загрузки и записи с ленты.

Звук - Особенности звукового устройства TurboSound FM.

DIY - Схема анализатора состояния TTL вывода.

Будущее Спектрума - Видеоконтроллеры V9990. Расширение графических возможностей ZX Spectrum.

Sofтинка - Обновления в просмотрщиках картинок: ANSI viewer, MCX viewer.

Интервью - Интервью с музыкантом X-Raizor из Omega Hackers Group.


Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Похожие статьи:
Просто Мария - 2836-я серия, бразильско-мексиканского-пуэрториканского телесериала.
Железяка - Схема супер замедлителя компьютера.
Вступление - содержание номера.
Форум - вопросы и ответы по играм: Jungle Warrior, Buratino, Streaker, Paris Dakar, Bloodwych, Hero Quest, Apollo, Cliff Hanger, Cyber Hordes, Paradise Valley, Star Drive, Laser Platoon, Movie, Where Time Stood Still, The Sceptre, Carrier Command и т.д.
Новости - вести с полей: CPU, EТERNIТY INDUSТRY.

В этот день...   20 апреля