PBRT阅读:第八章 胶片和图象管线 第8.1-8.3节

来源:互联网 发布:2017传智播客java视频 编辑:程序博客网 时间:2024/04/26 12:02

转载:http://www.opengpu.org/forum.php?mod=viewthread&tid=5220&fromuid=10107

第八章 胶片和图象管线

相机中的胶片类型对入射光转换为图像颜色的方式有很大的影响。在pbrt中,我们用Film类描述对模拟相机中的感光设备。在求得每条相机光线的辐射亮度以后,Film类的实现确定采样对附近像素的贡献值,并更新图像表示。当程序退出主渲染循环时,Film通常还要把最终的图像写入磁盘文件。

本章值描述一个Film类的实现。它使用像素重构公式来计算最终的像素值,并把图像的浮点颜色值写入磁盘。对于一个基于物理的渲染器而言,用浮点格式来创建图像要比其它用8位无符号整数的图像格式要灵活得多,因为浮点数格式可以避免在图像量化过程中丢失重要信息。

然而,为了在现代显示设备上显示这样的图像,必须把这些浮点像素值映射到显示器上的离散值。

因此,本章还描述了一个图像处理管线,对图像进行一系列的变换,来应付显示设备的局限性。例如,计算机显示器通常需要像素的颜色由RGB三元色来表示,而不需要一个任意的光谱功率分布的表示。所以,用基函数系数表示的光谱值要被转换为RGB值才能被显示在显示器上。在第8.4节中我们要讨论一个相关的问题,即如何将图像显示在可显示的辐射亮度范围(跟真实世界相比较)非常有限的显示器上。所以,我们在像素值的映射过程中要使得所显示的图像和跟在理想显示设备上所显示的图像尽可能地接近。

8.1 Film接口

Film基类定义了Film实现的抽象接口:

<Film Declarations> =
    class Film {
    public:
        <Film Interface>
        <Film Public Data>
    }

Film的构造器需要整个图像在x,y方向上的分辨率,并将它们分别存放在共用成员变量Film::xResolution和Film::yResolution中。在第6章所介绍的Camera类在进行某些相机变换时(例如,光栅到相机空间的变换)将要用对到这些值。

<Film Interface>
    Film(int xres, int yres)
        : xResolution(xres), yResolution(yres) {
    }

<Film Public Data>
    const int xResolution, yResolution;

Film类的第一个函数是Film::AddSample(),它以一个采样及其对应的相机光线,辐射亮度值和alpha值做为参数来更新图像。

<Film Interface> +=
    virtual void AddSample(const Sample &sample, const Ray &ray,
            const Spectrum &L, float alpha) = 0;

当退出渲染主循环时,Scene::Render()调用Film::WriteImage(),该函数可以允许Film对图像任意的处理工作,然后再显示或存盘。

<Film Interface> +=
    virtual void WriteImage() = 0;

Film的最后的一项责任是负责决定采样器进行采样所需要的整数像素值的范围。虽然对于简单的Film实现而言,像素范围是从(0, 0)到(xResolution - 1, yResolution - 1),但是由于像素重构滤波器的范围的有限,通常需要在图像边界稍微靠外的地方采样。

<Film Interface> +=
    virtual void GetSampleExtent(int *xstart , int *xend,
                    int *ystart , int *yend) const = 0;

8.2 图像胶片

在pbrt中,我们只提供一个Film类的实现:ImageFilm。这个类用给定的重构滤波器对图像采样值进行滤波,并把结果图像写入磁盘。

<ImageFilm Declarations> =
    class ImageFilm : public Film{
    public:
        < ImageFilm public Method>
    private:
        <ImageFilm Private Data>
    }

除了整个图像的分辨率以外,ImageFilm构造器的参数还有一个滤波器函数,一个裁剪窗口(即一个位于[0,1]x[0,1]区域内的矩形),输出图像文件名,一个是否将像素颜色乘以alpha值的布尔参数,以及将部分图像写入磁盘的频率。

<ImageFilm Method Definitions> =
    ImageFilm::ImageFilm(int xres, int yres,
            Filter *filt , const float crop[4],
            const string &fn, bool premult, int wf)
    : Film(xres, yres) {
        filter = filt;
        memcpy(cropWindow, crop, 4 * sizeof(float));
        filename = fn;
        premultiplyAlpha = premult;
        writeFrequency = sampleCount = wf;
        <Compute film image extent>
        <Allocate film image storage>
        <Precompute filter weight table>
    }

<ImageFilm Private Data> =
    Filter *filter;
    int writeFrequency, sampleCount;
    string filename;
    bool premultiplyAlpha;
    float cropWindow[4];

裁剪窗口和总体的图像分辨率给出了实际被存储或写入磁盘的像素范围。裁剪窗口对查找错误很有用,也有利于将大的图像分成若干份,并在不同的计算机上渲染再整合。裁剪窗口是用NDC空间来定义的,坐标范围是从0到1。ImageFilm::xPixelStart和ImageFilm::yPixelStart存放裁剪窗口的左上角的坐标位置,ImageFilm::xPixelCount和ImageFilm::yPixelCount分别给出了在各个方向的像素总数。它们的计算如下:

<Compute film image extent> =
    xPixelStart = Ceil2Int (xResolution * cropWindow[0]);
    xPixelCount = Ceil2Int (xResolution * cropWindow[1]) - xPixelStart;
    yPixelStart = Ceil2Int (yResolution * cropWindow[2]);
    yPixelCount = Ceil2Int (yResolution * cropWindow[3]) - yPixelStart;

<ImageFilm Private Data> +=
    int  xPixelStart, xPixelCount, yPixelStart, yPixelCount;

有了(可能被裁剪过的)图像的像素分辨率,构造器就申请一个Pixel结构数组,每个数组元素放一个像素。像素的辐射亮度值被存放在Pixel::L里,alpha值被存放在Pixel::alpha,而Pixel::weightSum存放采样对像素的贡献值的滤波权值之和。这个值被用于像素滤波公式(7.3)。因为每个图像采样要存取一个小区域中的像素信息,所以ImageFilm用BlockedArray来存放像素,这样可以降低缓存访问失败次数。

<Allocate film image storage> =
    pixels = new BlockedArray<Pixel>(xPixelCount, yPixelCount);

<ImageFilm Private Data> +=
    struct Pixel {
        Pixel () : L(0.f) {
        alpha = 0.f;
        weightSum =0.f;
    }

    Spectrum L;
    float alpha, weightSum;
};

BlockedArray<Pixel> *pixels;

根据pbrt对像素滤波器的缺省设置,每个图像采样大约对16个像素有贡献值。特别是对于简单的场景,虽然花在光线求交测试和着色计算上的时间很少,但花在为每个采样更新图像的时间却很长。因此,ImageFilm预先计算好一个滤波器值的表,这样Film::AddSample()函数就可以避免对Filter::Evalulate()的虚函数调用以及滤波器求值的开销,而是直接使用表里的值进行滤波。这里没有使用精确的采样位置来滤波,但是所带来的误差在实际应用中几乎可以忽略不计。

这里的实现合理地假设滤波器可以满足f(x , y) = f(|x|, |y|),这样表格只需存放滤波器偏置值的正值部分(即第一象限)。这个假设对pbrt中所介绍的所有滤波器都是成立的,这样只需要四分之一的表格大小,从而提高了内存存取的一致性和缓存效率。

<Precompute filter weight table> =
    #define FILTER_TABLE_SIZE 16
    filterTable = new float[FILTER_TABLE_SIZE * FILTER_TABLE_SIZE];
    float *ftp = filterTable;
    for ( int y = 0; y < FILTER_TABLE_SIZE; ++y) {
        float fy = ( (float ) y + .5f) *
                filter->yWidth / FILTER_TABLE_SIZE;

        for (int x = 0; x < FILTER_TABLE_SIZE; ++x) {
            float fx = ( ( float ) x + .5f) *
                filter->xWidth / FILTER_TABLE_SIZE;
            *ftp++ = filter->Evaluate(fx,fy);
        }
    }

(ImageFilm Private Data) +=
    float *filterTable;
为了理解ImageFilm::AddSample(),我们回忆一下像素滤波公式:

pbrt-081-01.jpg 

该公式用附近采样的辐射亮度值的加权和来计算每个像素值I(x, y),其中利用了滤波函数f来计算权值。因为pbrt用到的所有滤波器具有有限的范围,所以该函数先要计算有哪些像素会被当前采样所影响。然后,利用像素滤波公式为每个像素计算两个累加和:第一个累加和是对公式的分子部分的累加计算,另一个是对公式的分母部分的累加计算,最后像素值是两个累加和相除所得到的结果。

<ImageFilm Method Definitions> +=
    void ImageFilm::AddSample(const Sample &sample, const Ray &ray,
            const Spectrum &L, float alpha) {
        <Compute sample's raster extent>
        <Loop over filter support and add sample to pixel arrays>
        <Possibly write out in-progress image>
    }

为了找出那些被采样所影响的像素,Film::AddSample()将采样的连续坐标转换为离散坐标,即把x,y坐标分别减去0.5。然后,将两个坐标值分别在x,y方向上偏移,偏移量为滤波器的宽度,然后对最小的坐标值取Ceiling值,对最大的坐标值取floor值,因为超出滤波器范围的像素肯定不会受当前采样的影响。如图:

pbrt-081-02.jpg 

<Compute sample's raster extent> =
    float dImageX = sample.imageX - 0.5f;
    float dImageY = sample.imageY - 0.5f;
    int x0 = Ceil2Int (dImageX - filter->xWidth);
    int x1 = Floor2Int(dImageX + filter->xWidth);
    int y0 = Ceil2Int (dImageY - filter->yWidth);
    int y1 = Floor2Int(dImageY + filter->yWidth);
    x0 = max(x0, xPixelStart);
    x1 = min(x1, xPixelStart + xPixelCount - 1);
    y0 = max(y0, yPixelStart);
    y1 = min(y1, yPixelStart + yPixelCount - 1);

有了该采样的像素范围(x0,y0)到(x1,y1),该函数对所有这些像素进行循环,然后对采样值进行相应的滤波:
<Loop over filter support and add sample to pixel arrays> =
    <Precompute x and y filter table offsets>
    for (int y = y0; y <= y1; ++y)
        for(int x = 0; x <= x1; ++x) {
            <Evaluate filter value at (x, y) pixel>
            <Update pixel values with filtered sample contribution>
        }

每个像素(x,y)使用以其为中心的滤波函数。为了计算某个采样的过滤器权值,需要得到像素到采样位置的整数偏移值,然后求滤波器的值。如果我们直接求滤波器的值,需要做下面的计算:

    fliterWt = filter->Evaluate(x - dImageX, y - dimageY);

实际上,我们是从一个预先计算好的表格里取值的。

给定了采样位置(x,y)和像素位置(x',y'),该例程计算偏置量(x'-x, y'-y),并把它转换为在滤波权值表里的相应位置,以便在表中取值。我们可以直接这样做:将采样偏置值除以其所在方向上的滤波器宽度,从而得到一个0到1之间的值,然后在乘以表格的大小。如果注意到对于x方向上的每行像素y的偏置量是常量(相似地,y方向上x偏置量也是常量),就可以做进一步的优化。因此,我们在做循环之前,预先计算好这些索引值,省去了循环中的重复工作。

<Precompute x and y filter table offsets> =
    int *ifx = (int *)alloca((x1-x0+1) * sizeof(int));
    for (int x = x0; x <= x1 ; ++x) {
        float fx = fabsf((x - dImageX) *
        filter->invXWidth * FILTER_TABLE_SIZE);
        ifx[x-x0] = min(Floor2Int(fx), FILTER_TABLE_SIZE-1);
    }

    int *ify = (int *)alloca((y1-y0+1) * sizeof(int));
    for (int y = y0; y <= y1 ; ++y) {
        float fy = fabsf((y - dImageY) *
        filter->invYWidth * FILTER_TABLE_SIZE);
        ifx[y-y0] = min(Floor2Int(fy), FILTER_TABLE_SIZE-1);
    }

这样对于每个像素而言,它的关于滤波器权值表的x 和y偏置值可以通过查表求得,同时也就知道了它对于滤波器权值表的偏置值以及滤波器权值:

<Evaluate filter value at (x, y) pixel> =
    int offset = ify[y - y0] *FILTER_TABLE_SIZE + ifx[x-x0];
    folat filterWt = filterTable[offset];

<Update pixel values with filtered sample contribution> =
    Pixel &pixel = (*pixels)(x - xPixelStart, y - yPixelStart);
    pixel.L.AddWeighted(filterWt, L);
    pixel.alpha += alpha * filterWt;
    pixel.weightSum += filterWt;

因为像素重构滤波器跨越多个像素,所以Sampler必须生成实际像素范围之外的采样。这样图像边界上的像素更其它的像素相比,在每个方向上都有相同的采样密度。这对于利用裁剪窗口来渲染子图像的方法同样重要,因为这可以避免在子图像边缘产生的人为缺陷。

<ImageFilm Method Definitions> +=
    void ImageFilm::GetSampleExtent(int *xstart, int *xend,
                int *ystart, int *yend) const {
        *xstart = Floor2Int(xPixelStart - filter->xWidth);
        *xend   = Ceil2Int (xPixelStart + xPixelCount +
              filter->xWidth);
        *ystart = Floor2Int(yPixelStart - filter->yWidth);
        *yend   = Ceil2Int (yPixelStart + yPixelCount +
              filter->yWidth);

有些图像需要很长的渲染时间,如果渲染器可以定期地输出图像,就对用户有所助益。如果ImageFilm:writeFrequency被设为非零值,则要重复地调用Imagefilm:WriteImage():

<Possibly write out in-progress image> =
    if( --sampleCount == 0) {
        WriteImage();
        sampleCount = writeFrequency;
    }

8.2.1 图像输出

主渲染循环结束后,Scene::Render()调用Film::WriteImage()来将最终的图像结果写入一个文件。如前所述,ImageFilm类也用它定期地将部分图像结果写盘。

<ImageFilm Method Definitions> +=
    void ImageFilm::WriteImage() {
        <Convert image to RGB and compute final pixel values>
        <Write RGBA image>
        <Release temporary image memory>
    }

首先,该函数将像素值做一个备份,使得胶片上的图像值不再受滤波器的影响。这样在渲染过程中,该函数可以被重复调用,将部分图像写盘。

<Convert image to RGB and compute final pixel values> =
    int nPix = xPixelCount * yPixelCount;
    float *rgb = new float[ 3 * nPix] , *alpha = new float[nPix];
    int offset = 0;
    for (int y = 0; y < yPixelCount; ++y) {
        for (int x = 0; x < xPixelCount; ++x) {
            <Convert pixel spectral radiance to RGB>
            alpha[offset] = (*pixels)(x,y).aphla;
            <Normalize pixel with weight sum>
            <Compute premultiplied alpha color>
            ++offset;
        }
    }

如果有了显示设备的反应特征信息,就可以将像素值转换为设备相关的RGB值。首先,先将它们转换为设备无关的XYZ三刺激值,然后在转换为RGB值。这相当于又换了一次光谱基函数,新的基函数是有显示设备的RGB光谱反应曲线来决定的。这里,我们将XYZ值转换为基于HDTV标准的设备RGB值。这对于大多数的现代显示设备而言都是很合适的。

<Convert pixel spectral radiance to RGB> =
    float xyz[3];
    (*pixels)(x, y).L.XVZ(xyz);
    const float rWeight[3] = { 3.240479f, -1.537150f, -0.498535f};
    const float gWeight[3] = {-0.969256f, 1.875991f,  0.041556f };
    const float bWeight[3] = { 0.055648f, -0.204043f, 1.05731tf };
    rgb[3*offset]    = rWelght[0]*xyz[0] +
            = rWeight[1]*xyz[1] +
              rWeight[2]*xyz[2];
    rgb[3*offset+1]    = gWelght[0]*xyz[0] +
            = gWeight[1]*xyz[1] +
              gWeight[2]*xyz[2];
    rgb[3*offset+2]    = bWelght[0]*xyz[0] +
            = bWeight[1]*xyz[1] +
              bWeight[2]*xyz[2];

在初始化像素值的时候,由像素滤波函数所算出的最终值是将每个像素采样值除以Pixel::weightSum来得到的。由于重构滤波函数存在负值区域,故像素值可能出现负值,这时将需要负值改为0.

<Normalize pixel with weight sum> =
    float weightSum = (*pixels) (x, y).weightSum;
    if(weightSum != 0.f) {
        float invWt = 1.f / weightSum;
        rgb[3 * offset] = Clamp(rgb[3*offset] * invWt, 0.f, INFINITY);
        rgb[3 * offset+1] = Clamp(rgb[3*offset+1] * invWt, 0.f, INFINITY);
        rgb[3 * offset+2] = Clamp(rgb[3*offset+2] * invWt, 0.f, INFINITY);
        alpha[offset] = Clamp(alpha[offset] * invWt, 0.f, INFINITY);

作为一种选择,每个像素值可以乘上其alpha值,这个步骤被称为预乘alpha(又称关联alpha).

<Compute premutiplied alpha color> =
    if( premultiplyAlpha) {
        rgb[3 * offset] *= alpha[offset];
        rgb[3 * offset+1] *= alpha[offset];
        rgb[3 * offset+2] *= alpha[offset];

WriteRGBAImage()函数处理将图像写盘的细节:
<WriteRGBAImage> =
    WriteRGBAImage(filename, rgb, alpha, xPixelCount, yPixelCount,
                xResolution, yResolution, xPixelStart, yPixelStart);

存盘之后,就可以释放工作内存空间了:

<Release temporary image memory> =
    delete [] alpha;
    delete[] rgb;

8.3 图像管线

把通用的浮点数图像转换为适合于显示的格式需要一系列的图像变换。这些变换是在函数ApplyImagingPipeLine()里实现的。有些颇为棘手的问题需要仔细地加以对待,例如显示器局限问题,人类视觉系统的行为,等等。pbrt本身(包括插件)并不使用这个函数,而是用于外围的程序,如toos/exrtotiff.cpp程序。该函数还可以用于另一种Film的实现,该实现直接用整数像素格式来创建图像。传给该函数的参数被用来指导这个转换过程,其意义在本章会陆续被介绍。

图像管线有4个阶段。首先,选择性地使用色调重现算法将像素辐射亮度值范围映射到有局限的便于显示的范围中。再者,使用gamma校正,负责调整被显示的颜色值和其在显示器上的亮度值之间的非线性关系。然后,将像素值进行比例变换来覆盖显示器所需的输入值范围。最后,使用抖动(dithering)对像素值加上少许的随机噪声,这样可以打断不同区域中出现的不同颜色的过渡。

<Image Pipeline Function Definitions> =
    void ApplyImagingPipeline(float *rgb, int xResolution,
                int yResolution, float *yWeight,
                float bloomRadius, float bloomWeight,
                const char *toneMapName, const ParamSet *toneMapParams,
                float gamma, float dither, int maxDisplayValue) {
        int nPix = xResolution * yResolution;
        <Possibly apply bloom effect to image>
        <Apply tone reproduction to image>
        <Handle out-of-gamut RGB values>
        <Apply gamma correction to image>
        <Map image to display range>
        <Dither image>
    }

原创粉丝点击