跳转至内容

JPEG - 想法与实践/灰度图片绘制程序

来自维基教科书,开放的书籍,开放的世界

现在来编写一个程序,可以读取灰度JPEG文件并绘制图片。各个段落不需要按照特定顺序排列(除了APP0必须紧接在SOI之后),因此读取文件的程序需要寻找标记,当找到标记(与SOI和EOI不同)时,程序需要读取接下来的两个字节,它们表示段落的长度。在读取过程中,需要不断地统计读取的字节数,将数字r从0开始加1。当所有段落都读取完成(并且信息被整理到我们使用的数组中)后,跳转到位置r = rhead,数据从这里开始(紧接在SOS段落之后 - rhead是SOS的最后一个字节的数字)。

编码数据按位读取,但是它们在文件中以字节形式存储,因为每个8位块在写入文件时都被转换为一个字节。因此,我们需要一个程序来提供下一个位,并在每使用8位后读取下一个字节。我们将此程序称为nbit,该程序的代码将在此部分的末尾展示。

程序的结构是,每次读取到足够的字节形成一个64元素数组w[l](l = 1, ..., 64)时,绘制一个8x8的正方形(通过“setpixel”程序)。读取过程由数字l控制,每插入一个数字到w[l]中,l就加1。当l = 64时,w通过Z字形函数转换为一个8x8矩阵,然后将这个8x8矩阵(g(u, v))进行反量化和反余弦变换,得到一个8x8矩阵f[i, j](i, j = 0, ..., 7),包含颜色值(带符号字节,通过加128转换为无符号字节)。如果这个8x8正方形的坐标为(i0, j0)(i0 = 0, ..., wid8-1, j0 = 0, ..., hei8-1),那么需要用值f[i, j]着色的点的坐标为(i0*8 + i, j0*8 + j)。当绘制完8x8正方形后,将l重置为1,并将8x8正方形的坐标(i0, j0)改为下一个正方形的坐标,即如果i0 < wid8,则i0 = i0 + 1;如果i0 = wid8,则i0 = 0,同时 j0 = j0 + 1。

解码DC和AC码的程序分别称为decodeddecodea。它们返回一个数字val,供num程序用来计算一个数字m。这些程序的代码将在主程序之后展示。

对于l = 1,使用decoded。它返回一个数字val,表示接下来需要读取的位数,这些位构成数字m的位表达式,m加上之前的DC值(存储在变量dc0中)就是w的DC项:dc = m + dc0,w[1] = dc。

对于l > 1,使用decodea。它返回两个半字节nz和val。第一个半字节nz表示零的个数,第二个半字节val表示如果val > 0,接下来需要读取的位数。在这种情况下(val > 0),将l增加nz次(如果nz > 0),并将这nz个l对应的w[l]设置为0。然后将l再次增加1,接下来的val位构成数字m的位表达式,m就是w[l]的值。如果val = 0,那么nz的值要么是15,要么是0。如果nz = 15,则将l增加16次,并将这16个l对应的w[l]设置为0。如果nz = 0,则表示所有接下来的AC项都为零,即将l增加1,直到l = 64,并将这所有的l对应的w[l]设置为0。

当l = 64时,数组w[l]就完成了,我们可以绘制8x8正方形。为了更快地绘制图片,我们将反余弦变换中的计算(对于每个(i, j))限制在u, v = 0, ..., 5,这样我们只需要使用64项中的前36项。由于计算的不确定性,颜色值(加128后)可能小于0或大于255,因此可能需要进行钳位。

读取文件的数据部分和绘制每个8x8正方形的操作都在一个循环(drawloop)中进行,当到达文件末尾时,循环结束。全局变量r从r = rhead(头部的最后一个字节)开始,每读取一个字节就加1。

r = rhead
i0 = 0
j0 = 0
l = 1
s = 8
b = 0
dc = 0
dc0 = 0

drawloop

if l = 1 then
begin
dc0 = dc
decoded
num
dc = m + dc0
w[1] =dc
end
decodea
if val > 0 then
begin
if nz > 0 then
for i = 1 to nz do
begin
l = l + 1
w[l] = 0
end
num
l = l + 1
w[l] = m
end
if (nz = 15) and (val = 0) then
for i = 1 to 16 do
begin
l = l + 1
w[l] = 0
end
if (nz = 0) and (val = 0) then
while l < 64 do
begin
l = l + 1
w[l] = 0
end
if l = 64 then
begin
l = 1
for j = 0 to 7 do
for i = 0 to 7 do
begin
t = w[1] * cq[0, 0] / sqrt(2)
for v = 1 to 5 do
t = t + cs[j, v] * cq[0, v] * w[iz(0, v)]
s = t / sqrt(2)
for u = 1 to 5 do
begin
cq[u, 0] * w[iz(u, 0)] / sqrt(2)
for v = 1 to 5 do
t = t + cs[j, v] * cq[u, v] * w[iz(u, v)]
s = s + cs[i, u] * t
end
k = round(s + 128)
if k < 0 then
k = 0
if k > 255 then
k = 255
setpixel(i0 * 8 + i, j0 * 8 + j, k, k, k)
end
i0 = i0 + 1
if i0 = wid8 then
begin
i0 = 0
j0 = j0 + 1
end
end
goto drawloop

decoded程序解码DC值(l = 1)的霍夫曼码,decodea程序解码AC值(l > 1)的霍夫曼码。它们使用从霍夫曼表中构造的数组mincode[k], maxcode[k], valptr[k]和huffval[k]。用于解码DC值的霍夫曼表的数组称为mincoded[k], maxcoded[k], valptrd[k]和huffvald[k],用于解码AC值的霍夫曼表的数组称为mincodea[k], maxcodea[k], valptra[k]和huffvala[k]。decodeddecodea程序都包含读取下一个位的程序nbitdecoded程序的代码可以如下所示:

c = 0
j = 0
while c > maxcoded[j] do
begin
nbit
c = 2 * c + bit
j = j + 1
end
val = huffvald[valptrd[j] + c - mincoded[j]]

decodea程序的代码类似,只是数字val(字节)现在被分成两个半字节:nz = val div 16和val = val - nz * 16 - 第一个半字节nz表示零的个数。

decodeddecodea程序返回的数字val表示接下来需要读取的位数,这些位构成数字m的位表达式。m由num程序计算,该程序也使用读取下一个位的程序nbit。但是,如果读取的第一个位是0,则表示数字m是负数,它的数值就是计算得到的m的二进制补码,即m = -(q0-1 - m),其中q0 = 2val(第一个位的读取bit1由数字z控制)。

procedure num
begin
q0 = round(exp(val * ln(2)))
q = q0
z = 0
m = 0
while q > 1 do
begin
q = q div 2
nbit
if z = 0 then
begin
bit1 = bit
z = 1
end
m = m + bit * q
end
if bit1 = 0 then
m = -(q0 - 1 - m)
end

现在来编写nbit程序,它返回位流中的下一个位,称为bit,并用于decodeddecodeanum程序。下一个位来自一个数组c[i](从1到8),该数组在每使用8位后更新:读取一个新的字节b,并用b的位表达式更新c:c = digit(b) - digit程序的代码如下所示。位读取过程由一个全局变量s控制,它从0开始,每次调用nbit程序就加1,当s = 8时重置为0(我们需要从s = 8开始,以便可以读取第一个字节)。但是,由于在写入文件时,我们在每个值为255的字节后都写了一个零字节,因此在读取时,如果遇到一个字节为255,则需要跳过下一个字节。唯一的例外是当255后面的字节为217时,因为此时我们遇到了(255, 217)对,这是EOI标记(图像末尾),这时需要关闭文件并将绘制程序设置为停止(将变量z从0改为1,并跳转到mainloop,即窗口的“getmessage”循环)。nbit程序的代码可以如下所示:

procedure nbit;
begin
if s = 8 then
begin
r = r + 1
read(fu, b)
if b = 255 then
begin
r = r + 1
read(fu, b1)
if b1 = 217 then
begin
close(fu)
z = 1
goto mainloop
end
end
c = digit(b)
s = 0
end
s = s + 1
bit = c[s]
end

最后,编写digit(b)函数的代码,该函数返回字节b的位表达式。此函数与写入程序中同名的函数相同,只是它现在只适用于字节,并且它的位数组从1到8,以便可以从零开始。

q = 128
i = 0
while i < 8 do
begin

i = i + 1
j = b div q
b = b - j * q
q = q div 2
digit[i] = j
end
华夏公益教科书