Страница Ивана Рощина > Статьи >

© Иван Рощин, Москва

ZXNet: 500:95/462.53
E-mail: bestview@mtu-net.ru
WWW: http://www.ivr.da.ru

Вывод чёрно-белых изображений с градациями яркости

Радиомир. Ваш компьютер» 8—10/2002)
Дата последнего редактирования: 24.03.2003.

Как вы, наверно, уже догадались :), речь пойдёт о различных способах вывода на экран ZX Spectrum чёрно-белых изображений с градациями яркости. Для определённости положим, что размер изображения — 256x192, что соответствует разрешению экрана ZX Spectrum, а количество градаций яркости — 256.

Понятно, что о реальных 256 градациях яркости на ZX Spectrum можно только мечтать, так что вывести мы сможем лишь что-то похожее на исходное изображение.

А как определить, какой способ вывода лучше? Можно, конечно, сравнивать исходное и получаемое изображения «на глаз». Но гораздо удобнее иметь объективный критерий — вычисляемую по некоторой формуле величину (обозначим её R), отражающую различие между этими изображениями: от 0 (изображения совпадают) до 1 (максимальное различие).

О том, как именно я определил эту величину, вы можете прочесть в Приложении 1. Далее рядом с каждым изображением, полученным при использовании того или иного способа вывода, я буду приводить значение R.

Да, а какое же изображение возьмём в качестве исходного? Вот оно:

Рис. 1

Итак, первый способ вывода. Все пикселы с яркостью меньше 128 выводим чёрными, а с яркостью 128 и больше — белыми:

Рис. 2

R = 0,0906

Следующий способ — с использованием так называемых «чанков». В каждом квадрате 4x4 выводим одну из 17 текстур с наиболее близким уровнем яркости (в первой текстуре все 16 пикселов чёрные, во второй — 15 чёрных и 1 белый, …, в последней — все 16 пикселов белые). Таким образом, получается 17 псевдоградаций яркости.

Рис. 3

R = 0,0708

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

Ещё один способ вывода. Для каждого пиксела (а не для участка 4x4, как в предыдущем способе) определяем наиболее близкую по яркости текстуру и выводим на экран пиксел из этой текстуры. Координаты пиксела, который берётся из текстуры, определяются как остатки от деления координат пиксела в изображении на размер текстуры.

На рис. 4 а приведено изображение, получающееся при использовании 17 текстур 4x4, а на рис. 4 б — при использовании 256 текстур 16x16 (вообще-то их 257, а не 256, но, так как в исходном изображении лишь 256 градаций яркости, одна из текстур не используется).

Рис. 4
а)

R = 0,0683
б)

R = 0,0680

Это было, так сказать, вступление. :) Рассматривались уже известные способы вывода, которые, как вы могли заметить, не отличаются достаточным качеством. А вот теперь пойдёт речь о том, как выжать из ZX Spectrum всё, на что он способен. Я расскажу о способах, позволяющих повысить качество выводимого изображения на порядок!

Смотрите: на ZX Spectrum нам доступны 8 цветов с двумя градациями яркости (bright=0 и bright=1). Так как чёрный цвет при bright=1 остаётся чёрным, всего получается не 16 цветов, а 15. При чёрно-белом режиме отображения эти 15 цветов превращаются в 15 градаций яркости. За счёт их использования и можно повысить качество изображения!

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

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

Кстати, как мне кажется (на практике не проверял), чтобы получить чёрно-белое изображение на цветном мониторе, достаточно соорудить простейший переходник: три сигнала R, G, B, идущие от компьютера, преобразуем в один (чёрно-белый) и подаём его на входы R, G, B монитора, вот и всё. При этом из деталей понадобятся лишь несколько сопротивлений.

Итак, как выводить изображение? Очевидно, сначала надо определить яркость каждой из имеющихся 15 градаций — число в диапазоне 0—255. Рассказ о том, как это сделать, получился довольно объёмным, и я решил вынести его в Приложение 2. А сейчас будем считать, что яркости вычислены и мы получили значения a0a7 для цветов 0—7 при bright=0 и значения b0b7 для тех же цветов при bright=1.

Так как атрибуты задаются для знакоместа (8x8 пикселов), изображение будем выводить познакоместно. Рассмотрим процесс вывода одного знакоместа.

Пусть MIN_PIX и MAX_PIX — соответственно минимальный и максимальный уровни яркости в текущем знакоместе исходного изображения. Пусть MIN и MAX — соответственно яркости paper и ink (при выбранном значении bright) в текущем знакоместе выводимого изображения. Зная MIN_PIX и MAX_PIX, надо определить атрибуты знакоместа (ink, paper и bright) так, чтобы выполнялись условия: MIN <= MIN_PIX, MAX >= MAX_PIX. Очевидно, в большинстве случаев это можно сделать не единственным способом. Тогда из всех возможных пар (MIN, MAX) выбираем такую, чтобы разность между MAX и MIN была наименьшей.

Чтобы не перебирать все 128 возможных сочетаний ink, paper и bright, удобно поступить следующим образом. Сначала проверяем, выполняется ли условие MAX_PIX > a7? Если да, то bright не может равняться 0, т.е. bright=1. Среди b0b7 отыскиваем максимальное bi, при котором выполняется условие bi <= MIN_PIX, и минимальное bj, при котором выполняется условие bj >= MAX_PIX. Тогда paper=i, а ink=j.

Если же MAX_PIX <= a7, придётся рассмотреть два варианта: bright=0 и bright=1. Находим bi и bj, как было описано выше. Затем среди a0a7 отыскиваем максимальное ak, при котором выполняется условие ak <= MIN_PIX, и минимальное al, при котором выполняется условие al >= MAX_PIX. Теперь надо выбрать из двух пар (bi,bj), (ak,al) такую, где разница между значениями меньше. Если это первая пара, то bright=1, ink=j, paper=i. Если вторая пара, то bright=0, ink=l, paper=k.

После того, как атрибуты знакоместа определены, остаётся узнать, какие пикселы в нём будут цвета ink, а какие — paper. В предыдущем способе вывода мы получали 257 псевдоградаций яркости между чёрным и белым с помощью 257 текстур 16x16. Ну а в этом способе мы можем получить с помощью тех же текстур 257 псевдоградаций между MIN и MAX. Для каждого пиксела определяем наиболее близкую по яркости текстуру и выводим на экран пиксел из этой текстуры.

Вот что получается при таком способе вывода. Не правда ли, качество изображения значительно повысилось?

Рис. 5

R = 0,0106

Процедура, выводящая изображение этим способом, приведена в Приложении 3. Там же вы найдёте процедуры для способов, описанных далее.

Для дальнейшего повышения качества изображения можно использовать так называемый multicolor-эффект. Сущность его в следующем: если изменить атрибут знакоместа, когда луч уже прорисовал его часть на экране, то оставшаяся часть знакоместа будет прорисована уже с новым значением атрибута. Так можно увеличить атрибутное разрешение по вертикали.

Если надо увеличить разрешение вдвое (т.е. задать свой атрибут для каждой половины знакоместа — области размером 8x4 пиксела), удобнее не перезаписывать значения атрибутов при прорисовке изображения, а сделать так: построить верхние половины знакомест со своими атрибутами на одном экране, нижние половины со своими атрибутами — на другом, а затем переключать экраны каждый раз, когда прорисуются очередные 4 строки пикселов (т.е. верхняя или нижняя половина строки знакомест).

Вот что получается при таком способе вывода:

Рис. 6

R = 0,0090

Очевидно, максимум возможного — свой атрибут для каждой линии знакоместа (8 пикселов). Вот получаемое при этом изображение:

Рис. 7

R = 0,0071

При практической реализации максимального атрибутного разрешения, однако, возникает проблема. Дело в том, что за 224 такта (время прорисовки одной строки) нужно обновить 32 байта атрибутов строки, а сделать это не так-то просто: обычные способы пересылки данных требуют гораздо больше времени. В процедуре, приведённой в Приложении 3, я решил эту проблему так: выводится не всё изображение, а окно шириной 11 знакомест, которое можно перемещать вправо-влево. Вы можете попробовать увеличить ширину выводимой области изображения, используя способы быстрой пересылки данных — например, описанные в [5].

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

Пусть атрибуты обоих изображений одинаковы. Если раньше яркость пиксела в знакоместе могла быть одной из двух: MIN или MAX, то при быстром чередовании двух изображений она может быть уже одной из трёх: MIN, MAX или (MIN+MAX)/2 (когда на одном изображении яркость пиксела равна MIN, а на другом — MAX). За счёт этого и повышается качество. Если раньше мы получали 257 псевдоградаций яркости от MIN до MAX с помощью 257 различных текстур 16x16, то теперь получим 257 псевдоградаций от MIN до (MIN+MAX)/2 и ещё 257 — от (MIN+MAX)/2 до MAX, а всего, таким образом, будет 513 различных псевдоградаций от MIN до MAX.

При формировании таких двух изображений их атрибуты (они одинаковы) определяются так же, как и раньше. Для каждого пиксела определяем, какая из 513 псевдоградаций (от 0 до 512) наиболее близка к нему по яркости. Если номер псевдоградации меньше 256, то на первом изображении соответствующий пиксел выключаем, а на втором — берём из текстуры с номером, равным номеру псевдоградации. Если же номер псевдоградации больше или равен 256, то на первом изображении пиксел включаем, а на втором — берём из текстуры с номером, равным номеру псевдоградации, уменьшенному на 256.

Используя этот способ для вывода исходного изображения, получим такие два изображения:

Рис. 8
а)

б)

При их быстром чередовании мы увидим среднее изображение — вот оно:

Рис. 9

R = 0,0026

Однако, рассматривая его, вы будете разочарованы из-за довольно заметного мерцания с частотой 25 Гц. Можно ли его уменьшить, и если да, то как? Попробую объяснить на примере. Пусть надо получить в некоторой области экрана серый цвет за счёт быстрой смены белого и чёрного. Можно сделать так: в одном кадре показывать всю эту область белой, а в другом — чёрной (рис. 10 а). Мерцание при этом будет очень заметно. А можно в одном кадре выводить в этой области «шахматную» текстуру, а в другом кадре — такую же текстуру, но в которой вместо белых пикселов — чёрные и наоборот (рис. 10 б). Тогда мерцание будет гораздо меньше, а издалека — вообще не будет заметно.

Рис. 10
а)

б)

Почему так получается? При рассматривании изображения издалека яркость соседних пикселов усредняется, а так как во втором случае соседние пикселы мерцают в противофазе, то их средняя яркость и в одном, и в другом кадре будет одинаковой, и в результате изображение не будет мерцать.

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

В нашем случае (рис. 8) изображения сильно отличаются друг от друга, и поэтому мерцание при их смене очень заметно. Чтобы изображения стали более похожими друг на друга, сделаем следующее: просматривая пикселы (для определённости, слева направо и сверху вниз), определим мерцающие (т.е. различающиеся на первом и втором изображениях); первый такой пиксел сделаем тёмным на первом изображении и светлым на втором, следующий — наоборот, светлым на первом изображении и тёмным на втором (т.е. он будет в противофазе с предыдущим), и так далее.

Вот во что превращаются приведённые на рис. 8 изображения после такой обработки:

Рис. 11
а)

б)

Сразу видно, что теперь они гораздо более похожи друг на друга. Мерцание при их быстрой смене становится значительно меньше, а издалека — вообще не заметно. Результирующее изображение будет, разумеется, тем же самым (рис. 9): ведь от перемены мест слагаемых сумма не меняется.

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

Быстрое чередование двух изображений можно совместить с multicolor’ом, что ещё повысит качество. Вот что получится, когда для каждой половины знакоместа задаётся свой атрибут:

Рис. 12

R = 0,0022

А вот — когда свой атрибут задаётся для каждой линии знакоместа:

Рис. 13

R = 0,0017

Описанные выше способы иллюстрировались на примере одного исходного изображения. Ниже приведены примеры для ещё нескольких изображений (слева — исходное, справа — получающееся при выводе). Использовался способ с быстрым чередованием двух изображений и multicolor’ом, атрибуты задавались для каждой линии знакоместа.

(Кстати, сканирование фотографий, послуживших исходными изображениями, выполнил по моей просьбе Алексей Летаев — спасибо!)

Рис. 14


R = 0,0020
а) б)


R = 0,0021
в) г)


R = 0,0029
д) е)


R = 0,0023
ж) з)

Чтобы увидеть на изображении ещё больше подробностей, можно, во-первых, использовать вывод с увеличением, и, во-вторых, повысить контрастность выводимого участка изображения.

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

Рис. 15
а)

б)

Приведённые в Приложении 3 процедуры вывода могут выводить указанную область изображения с увеличением в 2, 4, 8… раз. Думаю, вам не составит труда догадаться, как увеличивать изображение в количество раз, не являющееся степенью двойки.

Теперь о повышении контрастности. Если в выводимом изображении (или в выводимой части изображения) самая тёмная точка светлее 0 и/или самая светлая точка темнее 255, то можно «растянуть» диапазон яркости до [0;255] по следующей формуле:

A' = 255  AAmin ,

AmaxAmin
где  A — исходная яркость,
A' — новая яркость,
Amin — яркость самой тёмной точки,
Amax — яркость самой светлой точки.

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

Рис. 16

Гамма-коррекция

Градации яркости в исходном изображении обозначены числами от 0 до 255. Эти числа мы, собственно, и считали яркостью пикселов (имея в виду определение яркости, данное в Приложении 2). То есть мы считали, что яркость пиксела линейно зависит от числа, соответствующего этому пикселу в изображении: если, например, одному пикселу соответствует число 10, а другому — 20, то второй пиксел в два раза ярче первого.

Однако в общем случае это не так. Зависимость здесь не линейная, а степенная (показатель степени называют «гамма»). Линейная зависимость — лишь частный случай, когда гамма равна 1. На рис. 17 приведены графики для трёх возможных случаев (по оси x откладывается значение, соответствующее пикселу в изображении, а по оси y — фактическая яркость пиксела).

Рис. 17
а) гамма=1

б) гамма>1

в) гамма<1

Как видим, если гамма больше 1, то в изображении с большей точностью представлена информация о тёмных областях и с меньшей точностью — о светлых, а если гамма меньше 1 — наоборот.

Чтобы правильно выводить изображения с гаммой, отличной от единицы, и нужна гамма-коррекция. Из значения пиксела (обозначим его x) мы должны получить его яркость (обозначим её y). Если и значение, и яркость — числа от 0 до 255, то формула такова:

y = 255 ( x ) Gamma .

255

Удобно сначала вычислить y для всех x от 0 до 255, помещая значения в таблицу, а при выводе просто брать из таблицы уже готовое значение.

Приведённые в Приложении 3 процедуры вывода могут выполнять гамма-коррекцию, если установить в 1 флаг условной компиляции GAMMA_CORR. При этом значением гаммы по умолчанию считается 1,5. Для другого значения надо будет пересчитать таблицу коррекции с помощью программы на Бейсике, приведённой там же. Кстати, эту программу можно запустить и просто, чтобы посмотреть, какие графики получаются при разных значениях гаммы.

А как узнать, какое значение гаммы у выводимого изображения? Если вы берёте это изображение из файла в формате png (или в каком-либо другом формате, где значение гаммы указывается в самом файле), то сложностей не возникает. А если вы (как и я) будете брать изображение из pcx-файла, где таких сведений не содержится? Тогда придётся подбирать этот коэффициент опытным путем. Попробуйте сначала вывести изображение без гамма-коррекции. Если оно выглядит естественно, то больше ничего и не надо. Если оно слишком тёмное — значит, гамма больше 1; если слишком светлое — гамма меньше 1.

Кстати, изображения с гаммой больше 1 будут выводиться более точно за счёт того, что они содержат больше информации о тёмных областях, а распределение спектрумовских градаций яркости (см. рис. 19 в Приложении 2) как раз таково (по крайней мере, у меня), что тёмные области воспроизводятся гораздо точнее светлых.

Работа с pcx-файлами

Исходные изображения надо откуда-то брать. Я считывал их из файлов в формате pcx. В Приложении 4 приведены две процедуры для работы с такими файлами. Первая процедура читает данные из pcx-файла и формирует в памяти массив данных о пикселах. Вторая процедура выполняет обратную задачу — записывает pcx-файл по находящимся в памяти данным о пикселах (именно с её помощью я подготовил все pcx-файлы с рисунками для этой статьи).

Ещё одна приведённая там процедура непосредственно с pcx-файлами не работает, но окажется полезной, если вам нужно преобразовать изображение в спектрумовском формате (6912 байтов) в чёрно-белый pcx-файл. Она формирует в памяти массив данных о пикселах такого изображения по заданным значениям градаций яркости, а на основе этих данных уже можно создать pcx-файл.

И ещё одно небольшое дополнение

Если исходное изображение — цветное, и вы хотите вывести его в чёрно-белом виде, то для получения яркости пиксела по известным значениям компонент R, G, B воспользуйтесь следующей формулой:

Y = 0,299R + 0,587G + 0,114B.

Так как вычислять яркость нужно для каждого пиксела, а даже изображение размером со спектрумовский экран — 256x192 — содержит 49152 пиксела, то понятно, что это вычисление должно быть как можно более быстрым. Удобно применить табличный способ.

Пусть имеется таблица длиной 768 (т.е. 3*256) байтов, начинающаяся с адреса, кратного 256 (обозначим этот адрес TAB_RGB). Пусть в первых 256 байтах этой таблицы содержатся значения функции y=0,299x для x от 0 до 255, округлённые до целых (для практического применения такая точность вполне достаточна), во вторых 256 байтах — значения функции y=0,587x, а в третьих 256 байтах — значения функции y=0,114x (также для x от 0 до 255).

Вот 32-байтная процедура построения такой таблицы:

MK_T_RGB   LD   BC,TAB_RGB+#2FF ;Адрес последнего байта таблицы.
           LD   H,29            ;0,114*256
           CALL MK_T_RGB_1

           LD   H,150           ;0,587*256
           CALL MK_T_RGB_1

           LD   H,77            ;0,299*256

MK_T_RGB_1 XOR  A
           LD   L,A
           LD   D,A
           LD   E,H

MK_T_RGB_2 SBC  HL,DE
           RET  C

;Запись округлённого результата:

           LD   A,H
           BIT  7,L
           JR   Z,MK_T_RGB_3
           INC  A
MK_T_RGB_3 LD   (BC),A

           DEC  BC
           JR   MK_T_RGB_2

С помощью этой таблицы можно очень быстро вычислить яркость пиксела — например, используя следующий фрагмент программы:

;Вход:  значения компонент R,G,B - в регистрах L,D,E
;       соответственно.
;Выход: вычисленное значение Y - в аккумуляторе.
;Длина: 9 байтов.
;Время выполнения: 44 такта.

           LD   H,TAB_RGB/256
           LD   A,(HL)        ;A=0,299R
           INC  H
           LD   L,D
           ADD  A,(HL)        ;A=0,299R+0,587G
           INC  H
           LD   L,E
           ADD  A,(HL)        ;A=0,299R+0,587G+0,114B

Если надо обработать сразу группу пикселов, выгоднее будет не загружать каждый раз регистр H, а загрузить его только один раз, а затем только изменять командами INC и DEC (обрабатывая по два пиксела в цикле), как в нижеприведённой процедуре:

;Вход: IX - адрес начала исходных данных (R1,G1,B1,R2,G2,B2...);
;      DE - куда помещать результат (Y1,Y2...);
;      BC - количество обрабатываемых пикселов (должно быть
;           чётным), делённое на 2.
;Длина: 49 байтов.

CALC_Y     EXX
           LD   DE,6
           EXX
           LD   H,TAB_RGB/256

CALC_Y_1   LD   L,(IX)        ;L=R1
           LD   A,(HL)        ;A=0,299R1

           INC  H
           LD   L,(IX+1)      ;L=G1
           ADD  A,(HL)        ;A=0,299R1+0,587G1

           INC  H
           LD   L,(IX+2)      ;L=B1
           ADD  A,(HL)        ;A=0,299R1+0,587G1+0,114B1

           LD   (DE),A
           INC  DE

           LD   L,(IX+5)      ;L=B2
           LD   A,(HL)        ;A=0,114B2

           DEC  H
           LD   L,(IX+4)      ;L=G2
           ADD  A,(HL)        ;A=0,114B2+0,587G2

           DEC  H
           LD   L,(IX+3)      ;L=R2
           ADD  A,(HL)        ;A=0,114B2+0,587G2+0,299R2

           LD   (DE),A
           INC  DE

           EXX
           ADD  IX,DE         ;IX:=IX+6
           EXX

           DEC  BC
           LD   A,B
           OR   C
           JR   NZ,CALC_Y_1

           RET

Если заранее известно, что DE на входе будет чётным (или нечётным), то первую (или, соответственно, вторую) команду INC DE можно заменить на более быструю INC E.

Если обрабатывается не более 512 пикселов, то в качестве счётчика можно использовать не регистровую пару BC, а только один регистр B, используя для организации цикла команду DJNZ.

Приложения

Приложение 1. Определение различия между изображениями.
Приложение 2. Определение яркостей цветов.
Приложение 3. Процедуры вывода изображений.
Приложение 4. Работа с pcx-файлами.

Литература

  1. А.Бордачев. «О формате PCX». Библиотека информационной технологии, вып. 8. Москва, «Инфоарт», 1993.
  2. Т.Кенцл. «Форматы файлов Internet». СПб, «Питер», 1997.
  3. К.Бочков. «Сканирование — это так просто…». «Мир ПК» 11/2000.
  4. С.Кащавцев. «Волшебная сила кривых — II». «Компьютерра» 49—50/1998.
  5. Е.Зарецкий. «Быстрый вывод графики». «Радиолюбитель. Ваш компьютер» 5, 6/2001.
Страница Ивана Рощина > Статьи >