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码的程序分别称为decoded和decodea。它们返回一个数字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
- begin
- decodea
- if val > 0 then
- begin
- if nz > 0 then
- for i = 1 to nz do
- begin
- l = l + 1
- w[l] = 0
- end
- begin
- num
- l = l + 1
- w[l] = m
- end
- begin
- if (nz = 15) and (val = 0) then
- for i = 1 to 16 do
- begin
- l = l + 1
- w[l] = 0
- end
- begin
- for i = 1 to 16 do
- if (nz = 0) and (val = 0) then
- while l < 64 do
- begin
- l = l + 1
- w[l] = 0
- end
- begin
- while l < 64 do
- 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
- begin
- 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
- begin
- for i = 0 to 7 do
- i0 = i0 + 1
- if i0 = wid8 then
- begin
- i0 = 0
- j0 = j0 + 1
- end
- begin
- end
- begin
- 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]。decoded和decodea程序都包含读取下一个位的程序nbit。decoded程序的代码可以如下所示:
- c = 0
- j = 0
- while c > maxcoded[j] do
- begin
- nbit
- c = 2 * c + bit
- j = j + 1
- end
- begin
- val = huffvald[valptrd[j] + c - mincoded[j]]
decodea程序的代码类似,只是数字val(字节)现在被分成两个半字节:nz = val div 16和val = val - nz * 16 - 第一个半字节nz表示零的个数。
decoded和decodea程序返回的数字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
- begin
- m = m + bit * q
- end
- begin
- if bit1 = 0 then
- m = -(q0 - 1 - m)
- end
- begin
现在来编写nbit程序,它返回位流中的下一个位,称为bit,并用于decoded、decodea和num程序。下一个位来自一个数组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
- begin
- end
- begin
- c = digit(b)
- s = 0
- end
- begin
- s = s + 1
- bit = c[s]
- if s = 8 then
- end
- begin
最后,编写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
- begin