© Иван Рощин, Москва
| ZXNet | : 500:95/462.53 |
| : bestview@mtu-net.ru | |
| WWW | : http://www.ivr.da.ru |
(«Радиомир. Ваш
компьютер» 810/2002)
Дата последнего редактирования: 24.03.2003.
Как вы, наверно, уже догадались :), речь пойдёт о различных способах
вывода на экран ZX Spectrum
Понятно, что о реальных 256 градациях яркости на ZX Spectrum можно только
мечтать, так что вывести мы сможем лишь
А как определить, какой способ вывода лучше? Можно, конечно, сравнивать исходное и получаемое изображения «на глаз». Но гораздо удобнее иметь объективный критерий вычисляемую по некоторой формуле величину (обозначим её R), отражающую различие между этими изображениями: от 0 (изображения совпадают) до 1 (максимальное различие).
О том, как именно я определил эту величину, вы можете прочесть в Приложении 1. Далее рядом с каждым изображением, полученным при использовании того или иного способа вывода, я буду приводить значение R.
Да, а какое же изображение возьмём в качестве исходного? Вот оно:
![]() |
Итак, первый способ вывода. Все пикселы с яркостью меньше 128 выводим чёрными, а с яркостью 128 и больше белыми:
![]() |
| R = 0,0906 |
Следующий способ с использованием так называемых «чанков». В каждом квадрате 4x4 выводим одну из 17 текстур с наиболее близким уровнем яркости (в первой текстуре все 16 пикселов чёрные, во второй 15 чёрных и 1 белый, , в последней все 16 пикселов белые). Таким образом, получается 17 псевдоградаций яркости.
![]() |
| R = 0,0708 |
Рассматривать такое изображение (как и изображения, получаемые с использованием далее описанных способов) желательно не с близкого расстояния, когда оно воспринимается как мешанина пикселов, а издалека, когда яркость соседних пикселов усредняется. Если невозможно отойти от монитора на достаточное расстояние, или с такого расстояния изображение кажется очень маленьким, то можно добиться того же эффекта, «размыв» изображение для этого достаточно установить перед экраном фильтр из полупрозрачной полиэтиленовой плёнки.
Ещё один способ вывода. Для каждого пиксела (а не для участка 4x4, как в предыдущем способе) определяем наиболее близкую по яркости текстуру и выводим на экран пиксел из этой текстуры. Координаты пиксела, который берётся из текстуры, определяются как остатки от деления координат пиксела в изображении на размер текстуры.
На
|
|
Это было, так сказать, вступление. :) Рассматривались уже известные способы вывода, которые, как вы могли заметить, не отличаются достаточным качеством. А вот теперь пойдёт речь о том, как выжать из ZX Spectrum всё, на что он способен. Я расскажу о способах, позволяющих повысить качество выводимого изображения на порядок!
Смотрите: на ZX Spectrum нам доступны 8 цветов с двумя
градациями яркости (bright=0 и bright=1). Так как чёрный цвет
при bright=1 остаётся чёрным, всего получается не 16 цветов, а 15. При
Естественно, для этого нужно, чтобы ZX Spectrum был подключён к
Сразу же отмечу, что основанный на этом эффекте способ вывода
Кстати, как мне кажется (на практике не проверял), чтобы получить
Итак, как выводить изображение? Очевидно, сначала надо определить яркость каждой из имеющихся 15 градаций число в диапазоне 0255. Рассказ о том, как это сделать, получился довольно объёмным, и я решил вынести его в Приложение 2. А сейчас будем считать, что яркости вычислены и мы получили значения a0a7 для цветов 07 при 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. Для каждого пиксела определяем наиболее близкую по яркости текстуру и выводим на экран пиксел из этой текстуры.
Вот что получается при таком способе вывода. Не правда ли, качество изображения значительно повысилось?
![]() |
| R = 0,0106 |
Процедура, выводящая изображение этим способом, приведена в Приложении 3. Там же вы найдёте процедуры для способов, описанных далее.
Для дальнейшего повышения качества изображения можно
использовать так называемый
Если надо увеличить разрешение вдвое (т.е. задать свой атрибут для каждой половины знакоместа области размером 8x4 пиксела), удобнее не перезаписывать значения атрибутов при прорисовке изображения, а сделать так: построить верхние половины знакомест со своими атрибутами на одном экране, нижние половины со своими атрибутами на другом, а затем переключать экраны каждый раз, когда прорисуются очередные 4 строки пикселов (т.е. верхняя или нижняя половина строки знакомест).
Вот что получается при таком способе вывода:
![]() |
| R = 0,0090 |
Очевидно, максимум возможного свой атрибут для каждой линии знакоместа (8 пикселов). Вот получаемое при этом изображение:
![]() |
| R = 0,0071 |
При практической реализации максимального атрибутного разрешения, однако,
возникает проблема. Дело в том, что за 224 такта (время прорисовки одной
строки) нужно обновить 32 байта атрибутов строки, а сделать это не
Есть ещё один способ улучшения качества. Можно сформировать такие два изображения, чтобы среднее между ними было наиболее похоже на оригинал, а затем быстро чередовать их при этом зритель как раз и увидит среднее изображение.
Пусть атрибуты обоих изображений одинаковы. Если раньше яркость пиксела в знакоместе могла быть одной из двух: 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.
Используя этот способ для вывода исходного изображения, получим такие два изображения:
|
|
При их быстром чередовании мы увидим среднее изображение вот оно:
![]() |
| R = 0,0026 |
Однако, рассматривая его, вы будете разочарованы
|
|
Почему так получается? При рассматривании изображения издалека яркость соседних пикселов усредняется, а так как во втором случае соседние пикселы мерцают в противофазе, то их средняя яркость и в одном, и в другом кадре будет одинаковой, и в результате изображение не будет мерцать.
Иначе говоря, средняя яркость достаточно малых участков на первом и втором изображениях должна быть примерно одинаковой, а достигнуть этого можно, если соседние мерцающие пикселы будут в противофазе.
В нашем случае (рис. 8) изображения сильно отличаются друг от друга, и поэтому мерцание при их смене очень заметно. Чтобы изображения стали более похожими друг на друга, сделаем следующее: просматривая пикселы (для определённости, слева направо и сверху вниз), определим мерцающие (т.е. различающиеся на первом и втором изображениях); первый такой пиксел сделаем тёмным на первом изображении и светлым на втором, следующий наоборот, светлым на первом изображении и тёмным на втором (т.е. он будет в противофазе с предыдущим), и так далее.
Вот во что превращаются приведённые на рис. 8 изображения после такой обработки:
|
|
Сразу видно, что теперь они гораздо более похожи друг на друга. Мерцание при их быстрой смене становится значительно меньше, а издалека вообще не заметно. Результирующее изображение будет, разумеется, тем же самым (рис. 9): ведь от перемены мест слагаемых сумма не меняется.
Кстати, вот совет, как не видеть мерцания даже с близкого расстояния. Возьмите лист бумаги, проткните в нём дырку и смотрите на экран сквозь эту дырку. Количество света, попадающего в глаз, будет меньше, и мерцание сильно уменьшится.
Быстрое чередование двух изображений можно совместить с multicolorом, что ещё повысит качество. Вот что получится, когда для каждой половины знакоместа задаётся свой атрибут:
![]() |
| R = 0,0022 |
А вот когда свой атрибут задаётся для каждой линии знакоместа:
![]() |
| R = 0,0017 |
Описанные выше способы иллюстрировались на примере одного исходного изображения. Ниже приведены примеры для ещё нескольких изображений (слева исходное, справа получающееся при выводе). Использовался способ с быстрым чередованием двух изображений и multicolorом, атрибуты задавались для каждой линии знакоместа.
(Кстати, сканирование фотографий, послуживших исходными изображениями, выполнил по моей просьбе Алексей Летаев спасибо!)
![]() |
![]() |
| R = 0,0020 | |
| а) | б) |
![]() |
![]() |
| R = 0,0021 | |
| в) | г) |
![]() |
![]() |
| R = 0,0029 | |
| д) | е) |
![]() |
![]() |
| R = 0,0023 | |
| ж) | з) |
Чтобы увидеть на изображении ещё больше подробностей, можно,
При выводе с увеличением мы не сможем увидеть сразу всю картинку, но за
счёт этого на видимом фрагменте сможем различить больше деталей. Вот пример:
на
|
|
Приведённые в Приложении 3 процедуры вывода могут выводить указанную область изображения с увеличением в 2, 4, 8 раз. Думаю, вам не составит труда догадаться, как увеличивать изображение в количество раз, не являющееся степенью двойки.
Теперь о повышении контрастности. Если в выводимом изображении (или в выводимой части изображения) самая тёмная точка светлее 0 и/или самая светлая точка темнее 255, то можно «растянуть» диапазон яркости до [0;255] по следующей формуле:
| A' = 255 | AAmin | , |
| AmaxAmin |
| где | A исходная яркость, |
| A' новая яркость, | |
| Amin яркость самой тёмной точки, | |
| Amax яркость самой светлой точки. |
После такого преобразования мы увидим тёмные участки изображения темнее,
чем надо, а светлые светлее, но при этом можно будет различить
больше подробностей. На рис. 16 вы можете увидеть изображение,
полученное, как и
![]() |
Градации яркости в исходном изображении обозначены числами от 0 до 255. Эти числа мы, собственно, и считали яркостью пикселов (имея в виду определение яркости, данное в Приложении 2). То есть мы считали, что яркость пиксела линейно зависит от числа, соответствующего этому пикселу в изображении: если, например, одному пикселу соответствует число 10, а другому 20, то второй пиксел в два раза ярче первого.
Однако в общем случае это не так. Зависимость здесь не линейная, а
степенная (показатель степени называют «гамма»). Линейная
зависимость лишь частный случай, когда гамма равна 1. На
рис. 17 приведены графики для трёх возможных случаев (по оси x
откладывается значение, соответствующее пикселу в изображении, а по оси
|
|
|
Как видим, если гамма больше 1, то в изображении с большей точностью представлена информация о тёмных областях и с меньшей точностью о светлых, а если гамма меньше 1 наоборот.
Чтобы правильно выводить изображения с гаммой, отличной от единицы, и нужна
| y = 255 ( | x | ) | Gamma | . |
| 255 |
Удобно сначала вычислить y для всех x от 0 до 255, помещая значения в таблицу, а при выводе просто брать из таблицы уже готовое значение.
Приведённые в Приложении 3 процедуры вывода
могут выполнять
А как узнать, какое значение гаммы у выводимого изображения? Если вы берёте
это изображение из файла в формате png (или в
Кстати, изображения с гаммой больше 1 будут выводиться более точно за счёт того, что они содержат больше информации о тёмных областях, а распределение спектрумовских градаций яркости (см. рис. 19 в Приложении 2) как раз таково (по крайней мере, у меня), что тёмные области воспроизводятся гораздо точнее светлых.
Исходные изображения надо
Ещё одна приведённая там процедура непосредственно с
Если исходное изображение цветное, и вы хотите вывести его в
| 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).
Вот
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.