图片存储原理:从像素到文件
> 简单的零和一怎么就变成了多姿多彩的图像世界?
引子
神奇的图片
大家会不会很好奇:
- 计算机是怎么用0和1存储和显示图片的呢?
- 简单的0和1怎么就变成了多姿多彩的图像世界?
像素点的秘密
放大显示器
我们先来看这么一张图片:
用手机拍摄的显示器的一小部分:
- 上面显示的是电脑桌面上的一个文件
- 可以看到这个文件的图标边缘是白色的
- 文件名称也是白色的
目前我们把它放大到必定程度看一下:
- 哎,有没有发现
- 显示器上竟然出现了一个个小点
- 没错,这就是我们所说的像素点!
像素的概念
分辨率示例:
- 我的屏幕分辨率是 3840 × 2160
- 所以这个屏幕上就有 3840 × 2160 = 8,294,400 个小点
- 这就是所谓的800万像素了
进一步发现
大家有没有进一步的发现?
放大之后:
- 隐约看到原来白色的文件名
- 出现了一些彩色的东西
这张图片已经放大到极限了:
- 我们换一张用手机超级微距拍摄的图片
- 我们也把这张图片放大到必定的程度
哎,看到了没有?
- 原来白色的地方出现了几种颜色,对不对?
学生质疑: “肯定是你做的手脚,好端端白色地方怎么会变成彩色呢?暗箱操作,真不要脸。”
验证实验
两个验证方法
老师: “早就知道有人会怀疑我,所以目前你可以做两个事情验证一下。”
验证1:拉远距离
- 先暂停文章,把手机放下
- 然后慢慢地拉大你和手机之间的距离
- 大约5到10米的样子
- 再看一下,你看到的是白色的字还是彩色的字?
验证2:近距离拍照
- 用你的手机近距离拍摄电脑屏幕上白色地方的照片
- 然后在手机上放大查看
- 有没有看到彩色的色块?
结果:
- 然后你就会发现:”老师诚不欺我,对不对。”
三原色原理
颜色的秘密
那这是为什么呢?
实则就是三原色混合导致的结果:
- 我们都知道我们所看到的各种颜色
- 是可以由红、绿、蓝三种颜色混合而成的
显示原理
我们的屏幕显示就是利用了这个原理:
在每一个像素点上:
- 都安装上了红、绿、蓝三种颜色的控制小灯
- 通过控制每个小灯的明暗程度
- 就可以得到我们想要的颜色了
再次验证
好的,目前你可以:
- 重新拉远你和手机之间的距离
- 看看亮不同灯的区块都是什么颜色
- 是不是感觉很神奇?
原理:
- 就是由于我们从远处看时,看到的是混合光
- 所以就得到了这些颜色
- 而我们屏幕显示器的这些小灯是超级超级小的
- 所以就相当于你在二三十厘米处看到的就是混合光
计算机存储图片
数据表明
然后我们知道了显示的原理:
- 那计算机是怎么存储图片数据给屏幕进行显示的呢?
如果说:
- 我们用三个数字来表明一个像素点
- 红、绿、蓝三个灯的明暗程度
- 是可以得到不同的混合光数据呢?
唉,没错,计算机也是这样子存储的!
存储示例
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)
互动时间
思考题:
- 为什么BMP文件需要4字节对齐?
- JPG和PNG分别适合什么场景?
- 如何表明半透明的颜色?
如果本文对你有协助,欢迎:
- 点赞支持
- 关注不迷路
- 评论区分享你对图片的理解
- ⭐ 收藏慢慢看
—本文为”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有损
- 根据场景选择
目前你清楚了吗?
想了解更多,请关注我,我是大话编程! ✨
从像素到文件,图片存储原理大揭秘!








![透明背景[png、ico]图标下载工具 - 宋马](https://pic.songma.com/blogimg/20250422/2a6870f249f74a7f8bc7193d17b73d21.png)









暂无评论内容