C++大白话系列-高级专题篇-34-图片存储原理:从像素到文件

图片存储原理:从像素到文件

> 简单的零和一怎么就变成了多姿多彩的图像世界?

引子

神奇的图片

大家会不会很好奇:

  • 计算机是怎么用0和1存储和显示图片的呢?
  • 简单的0和1怎么就变成了多姿多彩的图像世界?

像素点的秘密

放大显示器

我们先来看这么一张图片:

用手机拍摄的显示器的一小部分:

  • 上面显示的是电脑桌面上的一个文件
  • 可以看到这个文件的图标边缘是白色的
  • 文件名称也是白色的

目前我们把它放大到必定程度看一下:

  • 哎,有没有发现
  • 显示器上竟然出现了一个个小点
  • 没错,这就是我们所说的像素点!

像素的概念

分辨率示例:

  • 我的屏幕分辨率是 3840 × 2160
  • 所以这个屏幕上就有 3840 × 2160 = 8,294,400 个小点
  • 这就是所谓的800万像素

进一步发现

大家有没有进一步的发现?

放大之后:

  • 隐约看到原来白色的文件名
  • 出现了一些彩色的东西

这张图片已经放大到极限了:

  • 我们换一张用手机超级微距拍摄的图片
  • 我们也把这张图片放大到必定的程度

哎,看到了没有?

  • 原来白色的地方出现了几种颜色,对不对?

学生质疑: “肯定是你做的手脚,好端端白色地方怎么会变成彩色呢?暗箱操作,真不要脸。”

验证实验

两个验证方法

老师: “早就知道有人会怀疑我,所以目前你可以做两个事情验证一下。”

验证1:拉远距离

  1. 先暂停文章,把手机放下
  2. 然后慢慢地拉大你和手机之间的距离
  3. 大约5到10米的样子
  4. 再看一下,你看到的是白色的字还是彩色的字?

验证2:近距离拍照

  1. 用你的手机近距离拍摄电脑屏幕上白色地方的照片
  2. 然后在手机上放大查看
  3. 有没有看到彩色的色块?

结果:

  • 然后你就会发现:”老师诚不欺我,对不对。”

三原色原理

颜色的秘密

那这是为什么呢?

实则就是三原色混合导致的结果:

  • 我们都知道我们所看到的各种颜色
  • 是可以由红、绿、蓝三种颜色混合而成的

显示原理

我们的屏幕显示就是利用了这个原理:

在每一个像素点上:

  • 都安装上了红、绿、蓝三种颜色的控制小灯
  • 通过控制每个小灯的明暗程度
  • 就可以得到我们想要的颜色了

再次验证

好的,目前你可以:

  1. 重新拉远你和手机之间的距离
  2. 看看亮不同灯的区块都是什么颜色
  3. 是不是感觉很神奇?

原理:

  • 就是由于我们从远处看时,看到的是混合光
  • 所以就得到了这些颜色
  • 而我们屏幕显示器的这些小灯是超级超级小的
  • 所以就相当于你在二三十厘米处看到的就是混合光

计算机存储图片

数据表明

然后我们知道了显示的原理:

  • 那计算机是怎么存储图片数据给屏幕进行显示的呢?

如果说:

  • 我们用三个数字来表明一个像素点
  • 红、绿、蓝三个灯的明暗程度
  • 是可以得到不同的混合光数据呢?

唉,没错,计算机也是这样子存储的!

存储示例

100×100的图片:

  • 里面就是有 100 × 100 = 10,000 个像素点
  • 每个像素点都有三个灯的分量

如果我们用十进制的两位数代表一个分量:

  • 我们就可以混合得到 100 × 100 × 100 = 1,000,000 种颜色
  • 100万种颜色!

二进制表明

但是在计算机其中是用二进制的:

在计算机中:

  • 一般是使用 8位二进制数 代表一个分量的
  • 它的取值范围等价于十进制的 0 到 255

三种分量混合得到的:

  • 256 × 256 × 256 = 16,777,216 种颜色
  • 1600多万种颜色!

所以我们就可以:

  • 24个二进制数 代表一个像素颜色
  • 8位红色 + 8位绿色 + 8位蓝色 = 24位

纯色图片示例

纯红色:

RGB(255, 0, 0)

这样的数代表的是红色:

  • 重复10,000次
  • 就是一张 100×100 的纯红色图片了

而丰富多彩的图片:

  • 就是每个像素点数据不同

代码验证

学生质疑

学生: “老师,我感觉你在一本正经的胡说八道,除非你能证明给我看。”

老师: “好的,满足你,我们就用代码生成一张BMP图片试试。”

生成BMP图片

BMP图片格式:

  • 是一种未压缩的位图格式
  • 直接存储像素数据

代码实现:

#include <iostream>
#include <fstream>

#pragma pack(push, 1)
// BMP文件头
struct BMPFileHeader {
    uint16_t bfType = 0x4D42;      // "BM"
    uint32_t bfSize;               // 文件大小
    uint16_t bfReserved1 = 0;
    uint16_t bfReserved2 = 0;
    uint32_t bfOffBits = 54;       // 数据偏移
};

// BMP信息头
struct BMPInfoHeader {
    uint32_t biSize = 40;          // 信息头大小
    int32_t biWidth;               // 图片宽度
    int32_t biHeight;              // 图片高度
    uint16_t biPlanes = 1;
    uint16_t biBitCount = 24;      // 24位真彩色
    uint32_t biCompression = 0;    // 不压缩
    uint32_t biSizeImage;          // 图像大小
    int32_t biXPelsPerMeter = 0;
    int32_t biYPelsPerMeter = 0;
    uint32_t biClrUsed = 0;
    uint32_t biClrImportant = 0;
};
#pragma pack(pop)

void CreateRedBMP(const char* filename, int width, int height) {
    // 计算每行字节数(必须是4的倍数)
    int rowSize = ((width * 3 + 3) / 4) * 4;
    int imageSize = rowSize * height;

    // 设置文件头
    BMPFileHeader fileHeader;
    fileHeader.bfSize = 54 + imageSize;

    // 设置信息头
    BMPInfoHeader infoHeader;
    infoHeader.biWidth = width;
    infoHeader.biHeight = height;
    infoHeader.biSizeImage = imageSize;

    // 创建文件
    std::ofstream file(filename, std::ios::binary);

    // 写入文件头
    file.write((char*)&fileHeader, sizeof(fileHeader));
    file.write((char*)&infoHeader, sizeof(infoHeader));

    // 写入像素数据(BMP是BGR格式,从下到上)
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            // BGR顺序:蓝、绿、红
            uint8_t blue = 0;
            uint8_t green = 0;
            uint8_t red = 255;  // 红色分量为255

            file.write((char*)&blue, 1);
            file.write((char*)&green, 1);
            file.write((char*)&red, 1);
        }

        // 填充到4字节对齐
        for (int i = 0; i < rowSize - width * 3; i++) {
            uint8_t padding = 0;
            file.write((char*)&padding, 1);
        }
    }

    file.close();
    std::cout << "成功生成 " << filename << std::endl;
}

int main() {
    // 生成100×100的纯红色图片
    CreateRedBMP("red_100x100.bmp", 100, 100);

    // 生成1920×1080的纯红色图片
    CreateRedBMP("red_1920x1080.bmp", 1920, 1080);

    return 0;
}

运行结果

我们运行完程序就可以得到这么一张图片了:

来,我们试一下老师会不会翻车呢?

  • 双击一下
  • 哎,成功了!
  • 老师并没有一本正经的胡说八道!

所以大家清楚了没有?

  • 计算机就是这样子用0和1表明图片数据用来显示的

Nice!

图片压缩

原始数据的问题

那大家会不会觉得:

  • 存储在计算机中的图片都是这样子的原始数据呢?

那当然不是的!

如果都是原始数据的话:

  • 只要是尺寸一样的图片,它们的大小都是一样的
  • 而且它们的数据量会超级大

数据量计算

我们用刚刚的代码生成一张 1920×1080 的红色图片:

查看属性可以知道:

  • 它的大小达到了今年的 5.9MB

但是平时我们用了不少这样尺寸的图片:

  • 为什么就没有那么大呢?

压缩算法

那是由于:

  • 一般图片都是经过一些算法压缩过的
  • 由巨量的原始图像数据
  • 经过无损压缩或者有损压缩的算法
  • 变成了体积更小的文件

两种压缩方式:

1. 无损压缩

  • 可以完整的还原成原始数据
  • 不丢失任何信息

2. 有损压缩

  • 有必定程度的损失原始数据
  • 但文件更小

常见图片格式

️ JPG和PNG

比较有代表性的:

  • JPG文件:有损压缩
  • PNG文件:无损压缩

主要区别:

格式

压缩方式

透明通道

文件大小

适用场景

JPG

有损压缩

❌ 不支持

较小

照片、复杂图像

PNG

无损压缩

✅ 支持

较大

图标、截图、需要透明

透明通道

PNG支持透明通道:

  • 这也是为什么大家有时候看到
  • 有些图片有些地方是透明的缘由了

透明通道(Alpha通道):

  • 除了RGB三个分量
  • 还有一个A(Alpha)分量
  • 表明透明度:0(完全透明)到255(完全不透明)

压缩效果对比

我们再把这张5.9MB的纯红色图片:

  • 转换成JPG格式
  • 转换成PNG格式

看一下:

  • 发现它们的体积都特别小,对吧?

示例对比:

  • 原始BMP:5.9MB
  • 转换为JPG:约20KB
  • 转换为PNG:约10KB

为什么纯色图片压缩率这么高?

  • 由于纯色图片有大量重复的数据
  • 压缩算法可以高效地压缩这些重复数据

完整示例

生成彩色图片

生成渐变色图片:

#include <iostream>
#include <fstream>
#include <cmath>

void CreateGradientBMP(const char* filename, int width, int height) {
    int rowSize = ((width * 3 + 3) / 4) * 4;
    int imageSize = rowSize * height;

    BMPFileHeader fileHeader;
    fileHeader.bfSize = 54 + imageSize;

    BMPInfoHeader infoHeader;
    infoHeader.biWidth = width;
    infoHeader.biHeight = height;
    infoHeader.biSizeImage = imageSize;

    std::ofstream file(filename, std::ios::binary);
    file.write((char*)&fileHeader, sizeof(fileHeader));
    file.write((char*)&infoHeader, sizeof(infoHeader));

    // 生成渐变色
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            // 水平渐变
            uint8_t red = (uint8_t)(255.0 * x / width);
            uint8_t green = (uint8_t)(255.0 * y / height);
            uint8_t blue = 128;

            // BGR顺序
            file.write((char*)&blue, 1);
            file.write((char*)&green, 1);
            file.write((char*)&red, 1);
        }

        // 填充
        for (int i = 0; i < rowSize - width * 3; i++) {
            uint8_t padding = 0;
            file.write((char*)&padding, 1);
        }
    }

    file.close();
    std::cout << "成功生成渐变图片 " << filename << std::endl;
}

int main() {
    CreateGradientBMP("gradient.bmp", 800, 600);
    return 0;
}

生成圆形图案

void CreateCircleBMP(const char* filename, int width, int height) {
    int rowSize = ((width * 3 + 3) / 4) * 4;
    int imageSize = rowSize * height;

    BMPFileHeader fileHeader;
    fileHeader.bfSize = 54 + imageSize;

    BMPInfoHeader infoHeader;
    infoHeader.biWidth = width;
    infoHeader.biHeight = height;
    infoHeader.biSizeImage = imageSize;

    std::ofstream file(filename, std::ios::binary);
    file.write((char*)&fileHeader, sizeof(fileHeader));
    file.write((char*)&infoHeader, sizeof(infoHeader));

    int centerX = width / 2;
    int centerY = height / 2;
    int radius = std::min(width, height) / 3;

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            // 计算到中心的距离
            int dx = x - centerX;
            int dy = y - centerY;
            double distance = std::sqrt(dx * dx + dy * dy);

            uint8_t red, green, blue;
            if (distance < radius) {
                // 圆内:红色
                red = 255;
                green = 0;
                blue = 0;
            } else {
                // 圆外:白色
                red = 255;
                green = 255;
                blue = 255;
            }

            // BGR顺序
            file.write((char*)&blue, 1);
            file.write((char*)&green, 1);
            file.write((char*)&red, 1);
        }

        // 填充
        for (int i = 0; i < rowSize - width * 3; i++) {
            uint8_t padding = 0;
            file.write((char*)&padding, 1);
        }
    }

    file.close();
    std::cout << "成功生成圆形图案 " << filename << std::endl;
}

本文要点回顾

  • 像素点:屏幕上的最小显示单元
  • 分辨率:像素点的数量(如3840×2160)
  • 三原色:红、绿、蓝(RGB)
  • 颜色表明:每个分量8位(0-255)
  • 24位真彩色:1600万种颜色
  • BMP格式:未压缩的位图
  • 压缩算法:无损压缩、有损压缩
  • JPG vs PNG:有损vs无损,透明通道

记忆口诀

> 屏幕像素小灯组,红绿蓝光混合出。
>
> 每个分量八位数,千万颜色任你选。
>
> 原始数据体积大,压缩算法来帮忙。
>
> JPG有损PNG无损,透明通道PNG有。

实战练习

练习1:生成彩虹图

创建一个水平彩虹渐变图片

点击查看提示

  • 宽度分成7段
  • 每段一种彩虹色
  • 红、橙、黄、绿、青、蓝、紫

练习2:棋盘图案

生成一个8×8的黑白棋盘图案

点击查看答案

void CreateChessBoardBMP(const char* filename, int cellSize) {
    int width = cellSize * 8;
    int height = cellSize * 8;
    int rowSize = ((width * 3 + 3) / 4) * 4;
    int imageSize = rowSize * height;

    BMPFileHeader fileHeader;
    fileHeader.bfSize = 54 + imageSize;

    BMPInfoHeader infoHeader;
    infoHeader.biWidth = width;
    infoHeader.biHeight = height;
    infoHeader.biSizeImage = imageSize;

    std::ofstream file(filename, std::ios::binary);
    file.write((char*)&fileHeader, sizeof(fileHeader));
    file.write((char*)&infoHeader, sizeof(infoHeader));

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int cellX = x / cellSize;
            int cellY = y / cellSize;

            uint8_t color;
            if ((cellX + cellY) % 2 == 0) {
                color = 255;  // 白色
            } else {
                color = 0;    // 黑色
            }

            // BGR
            file.write((char*)&color, 1);
            file.write((char*)&color, 1);
            file.write((char*)&color, 1);
        }

        // 填充
        for (int i = 0; i < rowSize - width * 3; i++) {
            uint8_t padding = 0;
            file.write((char*)&padding, 1);
        }
    }

    file.close();
}

练习3:计算图片文件大小

计算不同尺寸BMP图片的文件大小

点击查看答案

int CalculateBMPSize(int width, int height) {
    // 每行字节数(4字节对齐)
    int rowSize = ((width * 3 + 3) / 4) * 4;

    // 图像数据大小
    int imageSize = rowSize * height;

    // 文件头 + 信息头 + 图像数据
    int totalSize = 54 + imageSize;

    return totalSize;
}

int main() {
    std::cout << "100×100: " << CalculateBMPSize(100, 100) << " 字节" << std::endl;
    std::cout << "1920×1080: " << CalculateBMPSize(1920, 1080) << " 字节" << std::endl;
    std::cout << "3840×2160: " << CalculateBMPSize(3840, 2160) << " 字节" << std::endl;
    return 0;
}

输出:

100×100: 30054 字节 (约29KB)
1920×1080: 6220854 字节 (约5.9MB)
3840×2160: 24883254 字节 (约23.7MB)

互动时间

思考题:

  1. 为什么BMP文件需要4字节对齐?
  2. JPG和PNG分别适合什么场景?
  3. 如何表明半透明的颜色?

如果本文对你有协助,欢迎:

  • 点赞支持
  • 关注不迷路
  • 评论区分享你对图片的理解
  • ⭐ 收藏慢慢看

—本文为”C++ 大白话”系列第 34 篇

常见问题

Q1:为什么BMP使用BGR而不是RGB顺序?

A:

  • 历史缘由,早期Windows采用BGR
  • 与硬件相关的设计决策
  • 目前大多数格式使用RGB
  • 但BMP为了兼容性保持BGR

Q2:什么是4字节对齐?

A:

  • BMP每行的字节数必须是4的倍数
  • 如果不够,需要填充0
  • 这是为了提高内存访问效率
  • CPU读取4字节对齐的数据更快

Q3:8位颜色是什么?

A:

  • 每个像素只用8位(1字节)
  • 只能表明256种颜色
  • 使用调色板(颜色查找表)
  • 目前基本不用了,都是24位或32位

Q4:32位图片和24位图片有什么区别?

A:

  • 24位:RGB,不透明
  • 32位:RGBA,支持透明度
  • 32位多了Alpha通道(8位)
  • PNG支持32位,JPG只支持24位

深入理解

图片格式对比

常见格式特点:

格式

压缩

透明

动画

适用场景

BMP

原始数据、开发测试

JPG

有损

照片、复杂图像

PNG

无损

图标、截图、Logo

GIF

无损

简单动画、表情包

WebP

有损/无损

网页图片(新格式)

颜色空间

RGB颜色空间:

红色:RGB(255, 0, 0)
绿色:RGB(0, 255, 0)
蓝色:RGB(0, 0, 255)
白色:RGB(255, 255, 255)
黑色:RGB(0, 0, 0)
黄色:RGB(255, 255, 0)
青色:RGB(0, 255, 255)
品红:RGB(255, 0, 255)

其他颜色空间:

  • HSV:色相、饱和度、明度
  • CMYK:青、品红、黄、黑(印刷)
  • YUV:亮度、色度(文章)

图片压缩原理

无损压缩(PNG):

  • 行程编码(RLE)
  • LZ77算法
  • 预测编码
  • 可以完全还原

有损压缩(JPG):

  • DCT变换
  • 量化
  • 霍夫曼编码
  • 损失部分细节

总结

图片存储的核心概念:

1. 像素与分辨率

  • 像素是最小单元
  • 分辨率决定清晰度
  • 更多像素=更大文件

2. RGB三原色

  • 红绿蓝混合
  • 每个分量0-255
  • 1600万种颜色

3. 文件格式

  • BMP:原始、未压缩
  • JPG:有损、小文件
  • PNG:无损、透明

4. 压缩技术

  • 减小文件大小
  • 无损vs有损
  • 根据场景选择

目前你清楚了吗?

想了解更多,请关注我,我是大话编程!

从像素到文件,图片存储原理大揭秘!

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容