Skip to content

第13章:图像处理基础

13.1 图像表示

13.1.1 数字图像的概念

数字图像是由离散的像素点组成的二维数组,每个像素存储颜色或灰度值。

数字图像的结构

图像可以看作一个二维矩阵 I(x, y):

        x(列)
    0   1   2   3   4   ...  width-1
  ┌───┬───┬───┬───┬───┬─────────────┐
0 │ p │ p │ p │ p │ p │     ...     │
  ├───┼───┼───┼───┼───┼─────────────┤
1 │ p │ p │ p │ p │ p │     ...     │
  ├───┼───┼───┼───┼───┼─────────────┤
y │ p │ p │ p │ p │ p │     ...     │
  ├───┼───┼───┼───┼───┼─────────────┤
  │ . │ . │ . │ . │ . │             │
  │ . │ . │ . │ . │ . │             │
  └───┴───┴───┴───┴───┴─────────────┘
height-1


像素格式:

1. 灰度图(Grayscale)
   每个像素一个值:0-255
   0 = 黑,255 = 白

2. RGB 彩色图
   每个像素三个值:R, G, B
   每个通道 0-255

3. RGBA
   RGB + Alpha(透明度)
   4 个通道

4. 其他格式
   - HSV/HSL(色相、饱和度、亮度)
   - YUV/YCbCr(亮度 + 色度)
   - CMYK(印刷用)

13.1.2 图像存储

javascript
/**
 * 图像类
 */
class Image {
    constructor(width, height, channels = 4) {
        this.width = width;
        this.height = height;
        this.channels = channels;
        this.data = new Uint8ClampedArray(width * height * channels);
    }
    
    /**
     * 获取像素
     */
    getPixel(x, y) {
        if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
            return null;
        }
        
        const i = (y * this.width + x) * this.channels;
        
        if (this.channels === 1) {
            return this.data[i];
        } else if (this.channels === 3) {
            return {
                r: this.data[i],
                g: this.data[i + 1],
                b: this.data[i + 2]
            };
        } else {
            return {
                r: this.data[i],
                g: this.data[i + 1],
                b: this.data[i + 2],
                a: this.data[i + 3]
            };
        }
    }
    
    /**
     * 设置像素
     */
    setPixel(x, y, color) {
        if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
            return;
        }
        
        const i = (y * this.width + x) * this.channels;
        
        if (this.channels === 1) {
            this.data[i] = color;
        } else if (this.channels === 3) {
            this.data[i] = color.r;
            this.data[i + 1] = color.g;
            this.data[i + 2] = color.b;
        } else {
            this.data[i] = color.r;
            this.data[i + 1] = color.g;
            this.data[i + 2] = color.b;
            this.data[i + 3] = color.a ?? 255;
        }
    }
    
    /**
     * 从 Canvas 创建
     */
    static fromCanvas(canvas) {
        const ctx = canvas.getContext('2d');
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        
        const img = new Image(canvas.width, canvas.height, 4);
        img.data.set(imageData.data);
        return img;
    }
    
    /**
     * 导出为 ImageData
     */
    toImageData() {
        if (this.channels === 4) {
            return new ImageData(
                new Uint8ClampedArray(this.data),
                this.width,
                this.height
            );
        }
        
        // 转换为 RGBA
        const rgba = new Uint8ClampedArray(this.width * this.height * 4);
        
        for (let i = 0; i < this.width * this.height; i++) {
            if (this.channels === 1) {
                const v = this.data[i];
                rgba[i * 4] = v;
                rgba[i * 4 + 1] = v;
                rgba[i * 4 + 2] = v;
                rgba[i * 4 + 3] = 255;
            } else if (this.channels === 3) {
                rgba[i * 4] = this.data[i * 3];
                rgba[i * 4 + 1] = this.data[i * 3 + 1];
                rgba[i * 4 + 2] = this.data[i * 3 + 2];
                rgba[i * 4 + 3] = 255;
            }
        }
        
        return new ImageData(rgba, this.width, this.height);
    }
    
    /**
     * 克隆图像
     */
    clone() {
        const img = new Image(this.width, this.height, this.channels);
        img.data.set(this.data);
        return img;
    }
}

13.2 点操作

13.2.1 点操作的概念

点操作(Point Operations)是指只根据当前像素的值来计算新值,不考虑邻近像素。

点操作特点

输入像素 → 变换函数 → 输出像素

常见点操作:
- 亮度调整
- 对比度调整
- 反相
- 阈值化
- 伽马校正


图示:

输入图像              输出图像
┌─────────┐           ┌─────────┐
│ A B C D │  f(x)     │f(A)f(B)...│
│ E F G H │ ──────►   │f(E)f(F)...│
│ I J K L │           │f(I)f(J)...│
└─────────┘           └─────────┘

每个像素独立处理

13.2.2 点操作实现

javascript
/**
 * 点操作集合
 */
class PointOperations {
    /**
     * 亮度调整
     * @param offset 亮度偏移 (-255 到 255)
     */
    static brightness(image, offset) {
        const result = image.clone();
        
        for (let i = 0; i < result.data.length; i++) {
            // 跳过 alpha 通道
            if (result.channels === 4 && (i + 1) % 4 === 0) continue;
            
            result.data[i] = Math.max(0, Math.min(255, result.data[i] + offset));
        }
        
        return result;
    }
    
    /**
     * 对比度调整
     * @param factor 对比度因子 (0 到 2+,1 为原始)
     */
    static contrast(image, factor) {
        const result = image.clone();
        
        for (let i = 0; i < result.data.length; i++) {
            if (result.channels === 4 && (i + 1) % 4 === 0) continue;
            
            // (pixel - 128) * factor + 128
            const value = (result.data[i] - 128) * factor + 128;
            result.data[i] = Math.max(0, Math.min(255, value));
        }
        
        return result;
    }
    
    /**
     * 反相
     */
    static invert(image) {
        const result = image.clone();
        
        for (let i = 0; i < result.data.length; i++) {
            if (result.channels === 4 && (i + 1) % 4 === 0) continue;
            
            result.data[i] = 255 - result.data[i];
        }
        
        return result;
    }
    
    /**
     * 阈值化
     * @param threshold 阈值 (0-255)
     */
    static threshold(image, threshold) {
        const result = new Image(image.width, image.height, 1);
        
        for (let y = 0; y < image.height; y++) {
            for (let x = 0; x < image.width; x++) {
                const pixel = image.getPixel(x, y);
                
                // 计算亮度
                let brightness;
                if (typeof pixel === 'number') {
                    brightness = pixel;
                } else {
                    brightness = 0.299 * pixel.r + 0.587 * pixel.g + 0.114 * pixel.b;
                }
                
                result.setPixel(x, y, brightness > threshold ? 255 : 0);
            }
        }
        
        return result;
    }
    
    /**
     * 伽马校正
     * @param gamma 伽马值 (通常 0.4 到 2.5)
     */
    static gammaCorrection(image, gamma) {
        const result = image.clone();
        
        // 预计算查找表
        const lut = new Uint8Array(256);
        const invGamma = 1 / gamma;
        
        for (let i = 0; i < 256; i++) {
            lut[i] = Math.round(255 * Math.pow(i / 255, invGamma));
        }
        
        for (let i = 0; i < result.data.length; i++) {
            if (result.channels === 4 && (i + 1) % 4 === 0) continue;
            
            result.data[i] = lut[result.data[i]];
        }
        
        return result;
    }
    
    /**
     * 直方图均衡化
     */
    static histogramEqualization(image) {
        const result = image.clone();
        const totalPixels = image.width * image.height;
        
        // 计算直方图(对灰度或亮度)
        const histogram = new Array(256).fill(0);
        
        for (let y = 0; y < image.height; y++) {
            for (let x = 0; x < image.width; x++) {
                const pixel = image.getPixel(x, y);
                let brightness;
                
                if (typeof pixel === 'number') {
                    brightness = pixel;
                } else {
                    brightness = Math.round(0.299 * pixel.r + 0.587 * pixel.g + 0.114 * pixel.b);
                }
                
                histogram[brightness]++;
            }
        }
        
        // 计算累积分布函数 (CDF)
        const cdf = new Array(256).fill(0);
        cdf[0] = histogram[0];
        
        for (let i = 1; i < 256; i++) {
            cdf[i] = cdf[i - 1] + histogram[i];
        }
        
        // 找到 CDF 最小非零值
        let cdfMin = 0;
        for (let i = 0; i < 256; i++) {
            if (cdf[i] > 0) {
                cdfMin = cdf[i];
                break;
            }
        }
        
        // 创建映射
        const lut = new Uint8Array(256);
        for (let i = 0; i < 256; i++) {
            lut[i] = Math.round((cdf[i] - cdfMin) / (totalPixels - cdfMin) * 255);
        }
        
        // 应用映射
        for (let y = 0; y < result.height; y++) {
            for (let x = 0; x < result.width; x++) {
                const pixel = result.getPixel(x, y);
                
                if (typeof pixel === 'number') {
                    result.setPixel(x, y, lut[pixel]);
                } else {
                    result.setPixel(x, y, {
                        r: lut[pixel.r],
                        g: lut[pixel.g],
                        b: lut[pixel.b],
                        a: pixel.a
                    });
                }
            }
        }
        
        return result;
    }
}

13.3 卷积操作

13.3.1 卷积的概念

卷积(Convolution)是图像处理中最基本的邻域操作,用一个核(Kernel)在图像上滑动,计算加权和。

卷积运算

核(Kernel)/ 卷积矩阵:
┌───┬───┬───┐
│ a │ b │ c │
├───┼───┼───┤
│ d │ e │ f │   3×3 核
├───┼───┼───┤
│ g │ h │ i │
└───┴───┴───┘


卷积过程:

图像区域:              核:
┌───┬───┬───┐         ┌───┬───┬───┐
│ p1│ p2│ p3│         │ k1│ k2│ k3│
├───┼───┼───┤    *    ├───┼───┼───┤
│ p4│ p5│ p6│         │ k4│ k5│ k6│
├───┼───┼───┤         ├───┼───┼───┤
│ p7│ p8│ p9│         │ k7│ k8│ k9│
└───┴───┴───┘         └───┴───┴───┘

结果 = p1×k1 + p2×k2 + p3×k3 + 
       p4×k4 + p5×k5 + p6×k6 + 
       p7×k7 + p8×k8 + p9×k9


边界处理:
1. 零填充(Zero Padding)
2. 边界复制(Replicate)
3. 镜像(Mirror)
4. 环绕(Wrap)

13.3.2 卷积实现

javascript
/**
 * 卷积操作
 */
class Convolution {
    /**
     * 执行卷积
     * @param image 输入图像
     * @param kernel 卷积核(一维数组,按行存储)
     * @param kernelSize 核大小
     */
    static convolve(image, kernel, kernelSize) {
        const result = new Image(image.width, image.height, image.channels);
        const half = Math.floor(kernelSize / 2);
        
        for (let y = 0; y < image.height; y++) {
            for (let x = 0; x < image.width; x++) {
                const sum = image.channels === 1 ? 0 : { r: 0, g: 0, b: 0 };
                
                // 遍历核
                for (let ky = 0; ky < kernelSize; ky++) {
                    for (let kx = 0; kx < kernelSize; kx++) {
                        const px = x + kx - half;
                        const py = y + ky - half;
                        
                        // 边界处理:复制边界
                        const clampedX = Math.max(0, Math.min(image.width - 1, px));
                        const clampedY = Math.max(0, Math.min(image.height - 1, py));
                        
                        const pixel = image.getPixel(clampedX, clampedY);
                        const weight = kernel[ky * kernelSize + kx];
                        
                        if (typeof pixel === 'number') {
                            sum += pixel * weight;
                        } else {
                            sum.r += pixel.r * weight;
                            sum.g += pixel.g * weight;
                            sum.b += pixel.b * weight;
                        }
                    }
                }
                
                // 设置结果
                if (typeof sum === 'number') {
                    result.setPixel(x, y, Math.max(0, Math.min(255, Math.round(sum))));
                } else {
                    result.setPixel(x, y, {
                        r: Math.max(0, Math.min(255, Math.round(sum.r))),
                        g: Math.max(0, Math.min(255, Math.round(sum.g))),
                        b: Math.max(0, Math.min(255, Math.round(sum.b))),
                        a: 255
                    });
                }
            }
        }
        
        return result;
    }
    
    /**
     * 可分离卷积(更高效)
     */
    static separableConvolve(image, kernelX, kernelY) {
        // 先水平卷积
        let temp = this.convolve1D(image, kernelX, true);
        // 再垂直卷积
        return this.convolve1D(temp, kernelY, false);
    }
    
    static convolve1D(image, kernel, horizontal) {
        const result = new Image(image.width, image.height, image.channels);
        const half = Math.floor(kernel.length / 2);
        
        for (let y = 0; y < image.height; y++) {
            for (let x = 0; x < image.width; x++) {
                const sum = image.channels === 1 ? 0 : { r: 0, g: 0, b: 0 };
                
                for (let k = 0; k < kernel.length; k++) {
                    let px, py;
                    
                    if (horizontal) {
                        px = x + k - half;
                        py = y;
                    } else {
                        px = x;
                        py = y + k - half;
                    }
                    
                    px = Math.max(0, Math.min(image.width - 1, px));
                    py = Math.max(0, Math.min(image.height - 1, py));
                    
                    const pixel = image.getPixel(px, py);
                    const weight = kernel[k];
                    
                    if (typeof pixel === 'number') {
                        sum += pixel * weight;
                    } else {
                        sum.r += pixel.r * weight;
                        sum.g += pixel.g * weight;
                        sum.b += pixel.b * weight;
                    }
                }
                
                if (typeof sum === 'number') {
                    result.setPixel(x, y, Math.max(0, Math.min(255, Math.round(sum))));
                } else {
                    result.setPixel(x, y, {
                        r: Math.max(0, Math.min(255, Math.round(sum.r))),
                        g: Math.max(0, Math.min(255, Math.round(sum.g))),
                        b: Math.max(0, Math.min(255, Math.round(sum.b))),
                        a: 255
                    });
                }
            }
        }
        
        return result;
    }
}

13.4 滤波

13.4.1 常用滤波器

常用滤波核

1. 均值滤波(Box Blur)
   ┌───┬───┬───┐
   │1/9│1/9│1/9│
   ├───┼───┼───┤
   │1/9│1/9│1/9│
   ├───┼───┼───┤
   │1/9│1/9│1/9│
   └───┴───┴───┘
   效果:模糊,去噪

2. 高斯滤波
   ┌────┬────┬────┐
   │1/16│2/16│1/16│
   ├────┼────┼────┤
   │2/16│4/16│2/16│    (近似)
   ├────┼────┼────┤
   │1/16│2/16│1/16│
   └────┴────┴────┘
   效果:平滑,保持边缘

3. 锐化
   ┌───┬───┬───┐
   │ 0 │-1 │ 0 │
   ├───┼───┼───┤
   │-1 │ 5 │-1 │
   ├───┼───┼───┤
   │ 0 │-1 │ 0 │
   └───┴───┴───┘
   效果:增强边缘

4. Sobel 边缘检测(水平)
   ┌───┬───┬───┐
   │-1 │ 0 │ 1 │
   ├───┼───┼───┤
   │-2 │ 0 │ 2 │
   ├───┼───┼───┤
   │-1 │ 0 │ 1 │
   └───┴───┴───┘
   效果:检测垂直边缘

13.4.2 滤波器实现

javascript
/**
 * 图像滤波器
 */
class ImageFilters {
    /**
     * 均值模糊
     */
    static boxBlur(image, size = 3) {
        const kernel = new Array(size * size).fill(1 / (size * size));
        return Convolution.convolve(image, kernel, size);
    }
    
    /**
     * 高斯模糊
     */
    static gaussianBlur(image, radius = 1, sigma = null) {
        if (sigma === null) sigma = radius / 2;
        
        const size = radius * 2 + 1;
        const kernel = [];
        let sum = 0;
        
        // 生成高斯核
        for (let y = -radius; y <= radius; y++) {
            for (let x = -radius; x <= radius; x++) {
                const value = Math.exp(-(x * x + y * y) / (2 * sigma * sigma));
                kernel.push(value);
                sum += value;
            }
        }
        
        // 归一化
        for (let i = 0; i < kernel.length; i++) {
            kernel[i] /= sum;
        }
        
        return Convolution.convolve(image, kernel, size);
    }
    
    /**
     * 高斯模糊(可分离版本,更快)
     */
    static gaussianBlurFast(image, radius = 1, sigma = null) {
        if (sigma === null) sigma = radius / 2;
        
        const size = radius * 2 + 1;
        const kernel1D = [];
        let sum = 0;
        
        for (let i = -radius; i <= radius; i++) {
            const value = Math.exp(-(i * i) / (2 * sigma * sigma));
            kernel1D.push(value);
            sum += value;
        }
        
        for (let i = 0; i < kernel1D.length; i++) {
            kernel1D[i] /= sum;
        }
        
        return Convolution.separableConvolve(image, kernel1D, kernel1D);
    }
    
    /**
     * 锐化
     */
    static sharpen(image, amount = 1) {
        const kernel = [
            0, -amount, 0,
            -amount, 1 + 4 * amount, -amount,
            0, -amount, 0
        ];
        return Convolution.convolve(image, kernel, 3);
    }
    
    /**
     * Sobel 边缘检测
     */
    static sobel(image) {
        // 转为灰度
        const gray = this.grayscale(image);
        
        // Sobel X
        const kernelX = [
            -1, 0, 1,
            -2, 0, 2,
            -1, 0, 1
        ];
        
        // Sobel Y
        const kernelY = [
            -1, -2, -1,
            0, 0, 0,
            1, 2, 1
        ];
        
        const gx = Convolution.convolve(gray, kernelX, 3);
        const gy = Convolution.convolve(gray, kernelY, 3);
        
        // 计算梯度幅值
        const result = new Image(image.width, image.height, 1);
        
        for (let y = 0; y < image.height; y++) {
            for (let x = 0; x < image.width; x++) {
                const px = gx.getPixel(x, y);
                const py = gy.getPixel(x, y);
                const magnitude = Math.sqrt(px * px + py * py);
                result.setPixel(x, y, Math.min(255, magnitude));
            }
        }
        
        return result;
    }
    
    /**
     * 拉普拉斯边缘检测
     */
    static laplacian(image) {
        const gray = this.grayscale(image);
        
        const kernel = [
            0, 1, 0,
            1, -4, 1,
            0, 1, 0
        ];
        
        return Convolution.convolve(gray, kernel, 3);
    }
    
    /**
     * 转为灰度
     */
    static grayscale(image) {
        const result = new Image(image.width, image.height, 1);
        
        for (let y = 0; y < image.height; y++) {
            for (let x = 0; x < image.width; x++) {
                const pixel = image.getPixel(x, y);
                
                if (typeof pixel === 'number') {
                    result.setPixel(x, y, pixel);
                } else {
                    // 加权平均
                    const gray = Math.round(
                        0.299 * pixel.r + 0.587 * pixel.g + 0.114 * pixel.b
                    );
                    result.setPixel(x, y, gray);
                }
            }
        }
        
        return result;
    }
    
    /**
     * 中值滤波(去噪)
     */
    static median(image, size = 3) {
        const result = new Image(image.width, image.height, image.channels);
        const half = Math.floor(size / 2);
        
        for (let y = 0; y < image.height; y++) {
            for (let x = 0; x < image.width; x++) {
                const values = image.channels === 1 ? [] : { r: [], g: [], b: [] };
                
                // 收集邻域像素
                for (let ky = -half; ky <= half; ky++) {
                    for (let kx = -half; kx <= half; kx++) {
                        const px = Math.max(0, Math.min(image.width - 1, x + kx));
                        const py = Math.max(0, Math.min(image.height - 1, y + ky));
                        
                        const pixel = image.getPixel(px, py);
                        
                        if (typeof pixel === 'number') {
                            values.push(pixel);
                        } else {
                            values.r.push(pixel.r);
                            values.g.push(pixel.g);
                            values.b.push(pixel.b);
                        }
                    }
                }
                
                // 取中值
                if (Array.isArray(values)) {
                    values.sort((a, b) => a - b);
                    result.setPixel(x, y, values[Math.floor(values.length / 2)]);
                } else {
                    values.r.sort((a, b) => a - b);
                    values.g.sort((a, b) => a - b);
                    values.b.sort((a, b) => a - b);
                    
                    const mid = Math.floor(values.r.length / 2);
                    result.setPixel(x, y, {
                        r: values.r[mid],
                        g: values.g[mid],
                        b: values.b[mid],
                        a: 255
                    });
                }
            }
        }
        
        return result;
    }
}

13.5 形态学操作

13.5.1 形态学基础

形态学操作

形态学操作主要用于二值图像和灰度图像的形状处理。

基本操作:

1. 腐蚀(Erosion)
   - 缩小白色区域
   - 去除小的白色噪点
   
   原图:              腐蚀后:
   ┌─────────────┐     ┌─────────────┐
   │   ██████    │     │    ████     │
   │  █████████  │  →  │   ███████   │
   │   ██████    │     │    ████     │
   └─────────────┘     └─────────────┘

2. 膨胀(Dilation)
   - 扩大白色区域
   - 填充小的黑色空洞
   
   原图:              膨胀后:
   ┌─────────────┐     ┌─────────────┐
   │    ████     │     │   ██████    │
   │   ███████   │  →  │  █████████  │
   │    ████     │     │   ██████    │
   └─────────────┘     └─────────────┘

3. 开运算(Opening)= 腐蚀 + 膨胀
   - 去除噪点,保持形状

4. 闭运算(Closing)= 膨胀 + 腐蚀
   - 填充孔洞,保持形状

13.5.2 形态学实现

javascript
/**
 * 形态学操作
 */
class Morphology {
    /**
     * 创建结构元素
     */
    static createStructuringElement(shape, size) {
        const element = [];
        const half = Math.floor(size / 2);
        
        for (let y = -half; y <= half; y++) {
            for (let x = -half; x <= half; x++) {
                if (shape === 'rect') {
                    element.push({ x, y });
                } else if (shape === 'cross') {
                    if (x === 0 || y === 0) {
                        element.push({ x, y });
                    }
                } else if (shape === 'ellipse') {
                    if (x * x + y * y <= half * half) {
                        element.push({ x, y });
                    }
                }
            }
        }
        
        return element;
    }
    
    /**
     * 腐蚀
     */
    static erode(image, structElement) {
        const result = new Image(image.width, image.height, 1);
        
        for (let y = 0; y < image.height; y++) {
            for (let x = 0; x < image.width; x++) {
                let minValue = 255;
                
                for (const offset of structElement) {
                    const px = x + offset.x;
                    const py = y + offset.y;
                    
                    if (px >= 0 && px < image.width && py >= 0 && py < image.height) {
                        const pixel = image.getPixel(px, py);
                        const value = typeof pixel === 'number' ? pixel : pixel.r;
                        minValue = Math.min(minValue, value);
                    } else {
                        minValue = 0;
                    }
                }
                
                result.setPixel(x, y, minValue);
            }
        }
        
        return result;
    }
    
    /**
     * 膨胀
     */
    static dilate(image, structElement) {
        const result = new Image(image.width, image.height, 1);
        
        for (let y = 0; y < image.height; y++) {
            for (let x = 0; x < image.width; x++) {
                let maxValue = 0;
                
                for (const offset of structElement) {
                    const px = x + offset.x;
                    const py = y + offset.y;
                    
                    if (px >= 0 && px < image.width && py >= 0 && py < image.height) {
                        const pixel = image.getPixel(px, py);
                        const value = typeof pixel === 'number' ? pixel : pixel.r;
                        maxValue = Math.max(maxValue, value);
                    }
                }
                
                result.setPixel(x, y, maxValue);
            }
        }
        
        return result;
    }
    
    /**
     * 开运算 = 腐蚀 + 膨胀
     */
    static opening(image, structElement) {
        const eroded = this.erode(image, structElement);
        return this.dilate(eroded, structElement);
    }
    
    /**
     * 闭运算 = 膨胀 + 腐蚀
     */
    static closing(image, structElement) {
        const dilated = this.dilate(image, structElement);
        return this.erode(dilated, structElement);
    }
    
    /**
     * 形态学梯度 = 膨胀 - 腐蚀
     */
    static gradient(image, structElement) {
        const dilated = this.dilate(image, structElement);
        const eroded = this.erode(image, structElement);
        
        const result = new Image(image.width, image.height, 1);
        
        for (let y = 0; y < image.height; y++) {
            for (let x = 0; x < image.width; x++) {
                const d = dilated.getPixel(x, y);
                const e = eroded.getPixel(x, y);
                result.setPixel(x, y, Math.max(0, d - e));
            }
        }
        
        return result;
    }
}

13.6 图像缩放

13.6.1 缩放算法

图像缩放算法

1. 最近邻插值
   - 选择最近的像素
   - 快速但有锯齿

2. 双线性插值
   - 4 个相邻像素的加权平均
   - 平滑但可能模糊

3. 双三次插值
   - 16 个相邻像素
   - 更锐利,计算量大

4. Lanczos
   - 使用 sinc 函数
   - 高质量,计算量最大

13.6.2 缩放实现

javascript
/**
 * 图像缩放
 */
class ImageResize {
    /**
     * 最近邻插值
     */
    static nearest(image, newWidth, newHeight) {
        const result = new Image(newWidth, newHeight, image.channels);
        
        const scaleX = image.width / newWidth;
        const scaleY = image.height / newHeight;
        
        for (let y = 0; y < newHeight; y++) {
            for (let x = 0; x < newWidth; x++) {
                const srcX = Math.floor(x * scaleX);
                const srcY = Math.floor(y * scaleY);
                
                const pixel = image.getPixel(srcX, srcY);
                result.setPixel(x, y, pixel);
            }
        }
        
        return result;
    }
    
    /**
     * 双线性插值
     */
    static bilinear(image, newWidth, newHeight) {
        const result = new Image(newWidth, newHeight, image.channels);
        
        const scaleX = (image.width - 1) / (newWidth - 1);
        const scaleY = (image.height - 1) / (newHeight - 1);
        
        for (let y = 0; y < newHeight; y++) {
            for (let x = 0; x < newWidth; x++) {
                const srcX = x * scaleX;
                const srcY = y * scaleY;
                
                const x0 = Math.floor(srcX);
                const y0 = Math.floor(srcY);
                const x1 = Math.min(x0 + 1, image.width - 1);
                const y1 = Math.min(y0 + 1, image.height - 1);
                
                const tx = srcX - x0;
                const ty = srcY - y0;
                
                const c00 = image.getPixel(x0, y0);
                const c10 = image.getPixel(x1, y0);
                const c01 = image.getPixel(x0, y1);
                const c11 = image.getPixel(x1, y1);
                
                if (typeof c00 === 'number') {
                    const c0 = c00 * (1 - tx) + c10 * tx;
                    const c1 = c01 * (1 - tx) + c11 * tx;
                    result.setPixel(x, y, Math.round(c0 * (1 - ty) + c1 * ty));
                } else {
                    const lerp = (a, b, t) => a * (1 - t) + b * t;
                    
                    result.setPixel(x, y, {
                        r: Math.round(lerp(lerp(c00.r, c10.r, tx), lerp(c01.r, c11.r, tx), ty)),
                        g: Math.round(lerp(lerp(c00.g, c10.g, tx), lerp(c01.g, c11.g, tx), ty)),
                        b: Math.round(lerp(lerp(c00.b, c10.b, tx), lerp(c01.b, c11.b, tx), ty)),
                        a: Math.round(lerp(lerp(c00.a, c10.a, tx), lerp(c01.a, c11.a, tx), ty))
                    });
                }
            }
        }
        
        return result;
    }
}

13.7 本章小结

图像处理操作分类

类型操作特点
点操作亮度、对比度、伽马独立处理每个像素
邻域操作卷积、滤波使用相邻像素
形态学腐蚀、膨胀形状处理
几何操作缩放、旋转坐标变换

常用滤波核

滤波器效果应用
均值模糊去噪
高斯平滑预处理
锐化增强边缘细节增强
Sobel检测边缘边缘提取
中值去噪椒盐噪声

关键要点

  1. 点操作只涉及单个像素,速度快
  2. 卷积是图像处理的基础操作
  3. 可分离卷积可以大幅提高效率
  4. 形态学操作用于二值图像的形状处理
  5. 图像缩放需要插值算法保证质量

下一章预告:在第14章中,我们将学习颜色科学,包括颜色空间、颜色转换和色彩管理。


文档版本:v1.0
字数统计:约 10,000 字

如有转载或 CV 的请标注本站原文地址