Digital_Image_Processing/Lab/Lab2/source/实验2-柯劲帆-21281280.md
2024-09-05 12:56:46 +08:00

702 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<h1><center>实验报告</center></h1>
<div style="text-align: center;">
<div><span style="display: inline-block; width: 65px; text-align: center;">课程名称</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">数字图像处理</span></div>
<div><span style="display: inline-block; width: 65px; text-align: center;">实验题目</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">直方图均衡化处理计算机实现</span></div>
<div><span style="display: inline-block; width: 65px; text-align: center;">学号</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">21281280</span></div>
<div><span style="display: inline-block; width: 65px; text-align: center;">姓名</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">柯劲帆</span></div>
<div><span style="display: inline-block; width: 65px; text-align: center;">班级</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">物联网2101班</span></div>
<div><span style="display: inline-block; width: 65px; text-align: center;">指导老师</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">安高云</span></div>
<div><span style="display: inline-block; width: 65px; text-align: center;">报告日期</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">2024年1月10日</span></div>
</div>
---
**目录**
[TOC]
---
# 1. 直方图均衡化处理程序
本实验中我使用Python实现直方图均衡化处理。对于图像的读取、处理和保存我都使用了按字节进行读写的方式符合实验要求。
## 1.1. BMP格式图片的读写
BMP格式图片的数据分为以下部分
| 内容 | 大小 |
| :------------------------------- | ------ |
| bmp文件头bmp file header | 14字节 |
| 位图信息头bitmap information | 40字节 |
| 调色板color palette | 可选 |
| 位图数据 | |
这里使用Lenna的BMP格式图片的十六进制码作为解读用例。
![Miss](Miss.bmp)
### 1.1.1. BMP文件头内容读取
BMP文件头内容如下
| 内容 | 大小 | 偏移 | Lenna图片 | 备注 |
| ------------------------------ | ----- | ---- | ---------- | ------------------------------------------ |
| bfType 文件类型 | 2字节 | 0x00 | 0x4D42 | 字符显示就是“BM” |
| bfSize 文件大小 | 4字节 | 0x02 | 0x00010438 | |
| bfReserved1 保留 | 2字节 | 0x06 | 0x00 | 必须设置为0 |
| bfReserved2 保留 | 2字节 | 0x08 | 0x00 | 必须设置为0 |
| bfOffBits 从头到位图数据的偏移 | 4字节 | 0x0A | 0x00000436 | = 文件头大小 + 位图信息头大小 + 调色板大小 |
Lenna图片中数据如下图使用VS Code的Hex Editor打开
![1.1.1.1](1.1.1.1.png)
因此读取代码为:
```python
class BmpData:
def __init__(self, file_path:str):
with open(file_path, "rb") as file:
self.file = file
self.bfType = unpack("<H", file.read(2))[0] # 0x00 文件类型
self.bfSize = unpack("<i", file.read(4))[0] # 0x02 文件大小
self.bfReserved1 = unpack("<H", file.read(2))[0] # 0x06 保留必须设置为0
self.bfReserved2 = unpack("<H", file.read(2))[0] # 0x08 保留必须设置为0
self.bfOffBits = unpack("<i", file.read(4))[0] # 0x0a 从头到位图数据的偏移
```
### 1.1.2. BMP位图信息头读取
BMP位图信息头内容如下
| 内容 | 大小 | 偏移 | Lenna图片 | 备注 |
| ----------------------------------------------- | ----- | ---- | ---------- | ------------------------------------------------------- |
| biSize 信息头的大小 | 4字节 | 0x0E | 0x00000028 | |
| biWidth 图像的宽度(以像素为单位) | 4字节 | 0x12 | 0x00000100 | |
| biHeight 图像的高度(以像素为单位) | 4字节 | 0x16 | 0x00000100 | 如果是正的,说明图像是倒立的;反之正立 |
| biPlanes 颜色平面数 | 2字节 | 0x1A | 0x0001 | |
| biBitCount 每像素的比特数 | 2字节 | 0x1C | 0x0008 | |
| biCompression 压缩类型 | 4字节 | 0x1E | 0x00000000 | |
| biSizeImage 位图数据的大小 | 4字节 | 0x22 | 0x00000000 | = 文件大小 - 位图偏移bfOffBits用BI_RGB格式时可设置为0 |
| biXPelsPerMeter 水平分辨率 | 4字节 | 0x26 | 0x00000B12 | 单位是像素/米,有符号整数 |
| biYPelsPerMeter 垂直分辨率 | 4字节 | 0x2A | 0x00000B12 | 单位是像素/米,有符号整数 |
| biClrUsed 位图使用的调色板中的颜色索引数 | 4字节 | 0x2E | 0x00000000 | 如果是0说明使用所有颜色 |
| biClrImportant 对图像显示有重要影响的颜色索引数 | 4字节 | 0x32 | 0x00000000 | 如果是0说明都重要 |
Lenna图片中数据如下图
![1.1.1.2](1.1.1.2.png)
因此读取代码为:
```python
self.biSize = unpack("<i", file.read(4))[0] # 0x0e 信息头的大小
self.biWidth = unpack("<i", file.read(4))[0] # 0x12 图像的宽度(以像素为单位)
self.biHeight = unpack("<i", file.read(4))[0] # 0x16 图像的高度(以像素为单位)(负说明图像是倒立的)
self.biPlanes = unpack("<H", file.read(2))[0] # 0x1a 颜色平面数
self.biBitCount = unpack("<H", file.read(2))[0] # 0x1c 比特数/像素数
self.biCompression = unpack("<i", file.read(4))[0] # 0x1e 压缩类型
self.biSizeImage = unpack("<i", file.read(4))[0] # 0x22 位图数据的大小
self.biXPelsPerMeter = unpack("<i", file.read(4))[0] # 0x26 水平分辨率
self.biYPelsPerMeter = unpack("<i", file.read(4))[0] # 0x2a 垂直分辨率
self.biClrUsed = unpack("<i", file.read(4))[0] # 0x2e 位图使用的调色板中的颜色索引数
self.biClrImportant = unpack("<i", file.read(4))[0] # 0x32 对图像显示有重要影响的颜色索引数(0说明都重要)
```
### 1.1.3. BMP调色板读取
调色板是可选的不过这里的8位色图有调色板。那么接下来的数据就是调色板了。
调色板就是一个颜色的索引这里是8位色图一共有256中颜色由于每个颜色都有RGB三原色也就是要3个字节表示这样的话256个颜色就不能表示所有的颜色。
所以需要一个索引用一个字节的索引指向4个字节表示的颜色B/G/R/Alpha四个值。一个颜色用4个字节表示有N个颜色那么调色板就是一个N*4的二维数组。
Lenna图片中数据如下图
![1.1.1.3](1.1.1.3.png)
调色板数据较长,这里只截了一部分。
可以看出调色板从0x36开始是0x00到0xFF顺序排列的B/G/R/Alpha四个值。
不完全列举如下:
| 范围 | 颜色编号 | B | G | R | Alpha |
| --------------- | -------- | ---- | ---- | ---- | ----- |
| 0x36 - 0x39 | 0 | 0x00 | 0x00 | 0x00 | 0x00 |
| 0x3A - 0x3D | 1 | 0x01 | 0x01 | 0x01 | 0x01 |
| 0x3E - 0x41 | 2 | 0x02 | 0x02 | 0x02 | 0x02 |
| 0x0042 - 0x0431 | 3 - 256 | ... | ... | ... | ... |
| 0x0432 - 0x0435 | 255 | 0xFF | 0xFF | 0xFF | 0xFF |
这里0x00到0xFF即0到255能够覆盖所有颜色范围。如果每像素的比特数biBitCount不足8位那么调色板就不能覆盖所有256个颜色那么说明图片里没有用到的颜色不会出现在调色板里。
因此读取代码为:
```python
def get_color_palette(self) -> np.ndarray:
if (self.bfOffBits == 0x36): # 16/24位图像不需要调色板起始位置就等于0x36
return None
color_alette_size = 2 ** int(self.biBitCount) # 多少字节调色板颜色就有2^n个
color_palette = np.zeros((color_alette_size, 3), dtype=np.int32)
self.file.seek(0x36)
for i in range(color_alette_size):
b = unpack("B", self.file.read(1))[0]
g = unpack("B", self.file.read(1))[0]
r = unpack("B", self.file.read(1))[0]
alpha = unpack("B", self.file.read(1))[0]
color_palette[i][0] = b
color_palette[i][1] = g
color_palette[i][2] = r
return color_palette
```
### 1.1.4. BMP位图数据读取
接下来是位图数据。由于是8位色图所以每个像素用1个字节表示取出每个字节从调色盘中获取对应的R/G/B/Alpha数值忽略掉Alpha值放入三维数组中就是图片数据了。如果是24位色图按照BGR的顺序排列32位色图按照BGRAlpha排列。
读取颜色值的代码如下:
```python
def get_RGB(self, pixel_data:str):
if len(pixel_data) <= 8:
color_index = int(pixel_data, 2)
return self.color_palette[color_index]
elif len(pixel_data) == 16:
b = int(pixel_data[1:6], 2) * 8
g = int(pixel_data[6:11], 2) * 8
r = int(pixel_data[11:16], 2) * 8
return [r, g, b]
elif len(pixel_data) == 24:
b = int(pixel_data[0:8], 2)
g = int(pixel_data[8:16], 2)
r = int(pixel_data[16:24], 2)
return [r, g, b]
elif len(pixel_data) == 32:
b = int(pixel_data[0:8], 2)
g = int(pixel_data[8:16], 2)
r = int(pixel_data[16:24], 2)
alpha = int(pixel_data[24:32], 2)
return [r, g, b]
```
Lenna图片的biHeight为正数说明图像倒立从左下角开始到右上角以行为主序排列。
位图数据排列还有一个规则,就是对齐。
Windows默认的扫描的最小单位是4字节如果数据对齐满足这个值的话对于数据的获取速度等都是有很大的增益的。因此BMP图像顺应了这个要求要求每行的数据的长度必须是4的倍数如果不够需要以0填充这样可以达到按行的快速存取。
每行的的长度为:
$$
Rowsize = 4 \times \left \lceil \frac{bfOffBits \times biWidth}{32} \right \rceil
$$
用代码实现为:
```python
Rowsize = ((biWidth * biBitCount + 31) >> 5) << 2
```
补零的数量就为:
$$
Rowsize = 4 \times \left \lceil \frac{bfOffBits \times biWidth}{32} \right \rceil - (bfOffBits \times biWidth)
$$
获取图片三维数组的代码如下:
```python
def get_numpy_img(self) -> np.ndarray:
biHeight = abs(self.biHeight)
img_np = np.zeros((biHeight, self.biWidth, 3), dtype=np.int32)
self.file.seek(self.bfOffBits)
for x in range(biHeight):
row_byte_count = ((self.biWidth * self.biBitCount + 31) >> 5) << 2
row_bits = self.file.read(row_byte_count)
row_bits = ''.join(format(byte, '08b') for byte in row_bits)
for y in range(self.biWidth):
pixel_data = row_bits[y * self.biBitCount: (y + 1) * self.biBitCount]
if self.biHeight > 0: # 图像倒立
img_np[biHeight - 1 - x][y] = self.get_RGB(pixel_data)
else:
img_np[x][y] = self.get_RGB(pixel_data)
return img_np
```
### 1.1.5. BMP图片的写入
将图片三维数组按照BMP格式写入二进制文件即可。这里我以8位色图写入。
```python
def save_img(self, image:np.ndarray, save_path:str):
with open(save_path, "wb") as file:
file.write(int(self.bfType).to_bytes(2, byteorder='little')) # 0x00 文件类型
file.write(int(0x36 + 0x100 * 4 + self.biWidth * abs(self.biHeight)).to_bytes(4, byteorder='little')) # 0x02 文件大小
file.write(int(0).to_bytes(4, byteorder='little')) # 0x06 保留必须设置为0
file.write(int(0x36 + 0x100 * 4).to_bytes(4, byteorder='little')) # 0x0a 从头到位图数据的偏移
file.write(int(40).to_bytes(4, byteorder='little')) # 0x0e 信息头的大小
file.write(int(self.biWidth).to_bytes(4, byteorder='little')) # 0x12 图像的宽度
file.write(int(self.biHeight).to_bytes(4, byteorder='little')) # 0x16 图像的高度
file.write(int(self.biPlanes).to_bytes(2, byteorder='little')) # 0x1a 颜色平面数
file.write(int(8).to_bytes(2, byteorder='little')) # 0x1c 比特数/像素数
file.write(int(self.biCompression).to_bytes(4, byteorder='little')) # 0x1e 压缩类型
file.write(int(self.biSizeImage).to_bytes(4, byteorder='little')) # 0x22 位图数据的大小
file.write(int(self.biXPelsPerMeter).to_bytes(4, byteorder='little')) # 0x26 水平分辨率
file.write(int(self.biYPelsPerMeter).to_bytes(4, byteorder='little')) # 0x2a 垂直分辨率
file.write(int(0x100 * 4).to_bytes(4, byteorder='little')) # 0x2e 位图使用的调色板中的颜色索引数
file.write(int(0).to_bytes(4, byteorder='little')) # 0x32 对图像显示有重要影响的颜色索引数
for i in range(256):
file.write(int(i).to_bytes(1, byteorder='little'))
file.write(int(i).to_bytes(1, byteorder='little'))
file.write(int(i).to_bytes(1, byteorder='little'))
file.write(int(0).to_bytes(1, byteorder='little'))
for x in range(abs(self.biHeight)):
for y in range(self.biWidth):
if self.biHeight > 0:
file.write(int(image[self.biHeight - 1 - x][y]).to_bytes(1, byteorder='little'))
else:
file.write(int(image[x][y]).to_bytes(1, byteorder='little'))
file.write(b'0' * ((((self.biWidth * 8 + 31) >> 5) << 2) - 8 * self.biWidth))
file.close()
```
## 1.2. 直方图均衡化处理
直方图均衡化的步骤如下:
1. 将彩色图转换为灰度图;
2. 统计每个色阶的像素数,转换为频率;
3. 将各个色阶的频率依次累加,得到前缀和;
4. 将各个色阶的频率前缀和转换到相近的灰度色阶值,作为该色阶内像素的均衡化后的灰度值;
5. 将原图的各个像素变换到对应得到灰度值。
### 1.2.1. 灰度化
这里灰度化的方法采用
$$
grey\space value=0.299\times R + 0.587 \times G + 0.114\times B
$$
灰度转化代码如下:
```python
def get_gray_img(self) -> np.ndarray:
biHeight = abs(self.biHeight)
gray_img = np.dot(self.img_np.reshape((biHeight * self.biWidth, 3)).astype(np.float32),
[0.299, 0.587, 0.114]).astype(np.int32)
gray_img = gray_img.reshape((biHeight, self.biWidth))
return gray_img
```
### 1.2.2. 直方图均衡化
按照步骤,均衡化代码如下:
```python
def equalize(self, level:int):
biHeight = abs(self.biHeight)
self.hist = np.zeros(256, dtype=np.int32)
max_value = self.gray.max()
min_value = self.gray.min()
gap = (max_value - min_value + 1) / level
for x in range(biHeight):
for y in range(self.biWidth):
self.hist[self.gray[x, y]] += 1
hist = np.zeros(level, dtype=np.float32)
for i in range(level):
hist[i] = np.sum(self.hist[min_value + int(i * gap) : min_value + int((i + 1) * gap)])
hist /= biHeight * self.biWidth
for i in range(1, level):
hist[i] += hist[i - 1]
hist *= level
hist = np.around(hist)
hist /= level
hist = np.floor(hist * 255).astype(np.int32)
self.equalized_img = np.zeros_like(self.gray)
self.equalized_hist = np.zeros(256, dtype=np.int32)
for x in range(biHeight):
for y in range(self.biWidth):
self.equalized_img[x, y] = hist[int((self.gray[x, y] - min_value) / gap)]
self.equalized_hist[self.equalized_img[x, y]] += 1
return self.equalized_img, self.hist, self.equalized_hist
```
## 1.3. GUI界面设计和程序逻辑
```python
def choosepic():
global path_
path_ = tkinter.filedialog.askopenfilename(title='请选择图片文件', filetypes=[('图片', '.bmp')])
if path_ == '':
return
img_temp = Image.open(path_).resize((int(256 * 0.8), int(256 * 0.8))) # 图片读取和加载
img = ImageTk.PhotoImage(img_temp)
label_image1.config(image=img)
label_image1.image = img
def equalize():
if path_ == '':
return
image = BmpData(path_)
# img = Image.fromarray(image.img_np.astype(np.uint8))
# img.show()
equalized_img, hist, equalized_hist = image.equalize(8) # 分别为均衡化的图/直方图/均衡化后的直方图
equalized_img = Image.fromarray(equalized_img.astype(np.uint8))
# equalized_img.show()
name_parts = path_.split('.')
name_parts[-2] += "_equalized"
new_file_name = '.'.join(name_parts)
image.save_equalized_img(new_file_name)
equalized_img = equalized_img.resize((int(256 * 0.8), int(256 * 0.8)))
equalized_img = ImageTk.PhotoImage(equalized_img)
label_image2.config(image=equalized_img)
label_image2.image = equalized_img # 处理后的图片的显示
if __name__ == "__main__":
root = tkinter.Tk()
root.title('21281280柯劲帆') # 标题
width, height = 600, 400
width_max, height_max = root.maxsize()
s_center = '%dx%d+%d+%d' % (width, height, (width_max - width) / 2, (height_max - height) / 2) # 将页面显示在正中间
root.geometry(s_center)
root.resizable(width=False, height=False) # 窗口不可移动
l = tkinter.Label(root, text='实验二', width=60, height=2, fg='black', font=("微软雅黑", 16), anchor=tkinter.CENTER)
l.pack()
label_image1 = tkinter.Label(root, width=int(256 * 0.8), height=int(256 * 0.8), bg='whitesmoke', anchor=tkinter.NE)
label_image1.pack()
label_image1.place(x=45, y=70, width=int(256 * 0.8), height=int(256 * 0.8))
label_image2 = tkinter.Label(root, width=int(256 * 0.8), height=int(256 * 0.8), bg='whitesmoke', anchor=tkinter.NE)
label_image2.place(x=350, y=70, width=int(256 * 0.8), height=int(256 * 0.8))
# 文本按钮
Image_Input = tkinter.Button(root, text='Choose', command=choosepic)
Image_Input.place(x=105, y=300, width=80, height=30)
# 直方图均衡化
Fun1 = tkinter.Button(root, text='直方图均衡化', command=equalize)
Fun1.place(x=265, y=300, width=80, height=30)
# 退出
Quit = tkinter.Button(root, text='Quit', command=sys.exit)
Quit.place(x=415, y=300, width=80, height=30)
end = tkinter.Label(root, text='21281280 柯劲帆', fg='silver', font=("微软雅黑", 10))
end.place(x=215, y=360, width=200, height=20)
root.mainloop()
```
# 2. 实验过程
编好代码后对Python代码进行封装变成exe可执行程序。
在命令行中配置环境并封装:
```sh
> pip install pyinstaller
> Pyinstaller -F -w read_bmp.py
```
在文件资源管理器窗口中双击exe文件即可运行。
# 3. 实验结果及分析
这里我准备了手机拍摄的3张图片图片内容相同但是在拍摄的过程中调整亮度得到3张偏暗、正常、偏亮的图片直方图均衡化处理如下
<table>
<tr>
<td></td>
<td>偏暗</td>
<td>正常</td>
<td>偏亮</td>
</tr>
<tr>
<td>原图</td>
<td><img src="my_image_dark.bmp" alt="my_image_dark"></td>
<td><img src="my_image_normal.bmp" alt="my_image_normal"></td>
<td><img src="my_image_light.bmp" alt="my_image_light"></td>
</tr>
<tr>
<td>直方图均衡化过程</td>
<td><img src="3.dark.png" alt="dark"></td>
<td><img src="3.normal.png" alt="normal"></td>
<td><img src="3.light.png" alt="light"></td>
</tr>
<tr>
<td>直方图均衡化结果</td>
<td><img src="my_image_dark_equalized.bmp" alt="my_image_dark_equalized"></td>
<td><img src="my_image_normal_equalized.bmp" alt="my_image_normal_equalized"></td>
<td><img src="my_image_light_equalized.bmp" alt="my_image_light_equalized"></td>
</tr>
</table>
可见偏暗的图进行直方图均衡化处理后,辨识度增加;
正常的图进行直方图均衡化处理后,由于将颜色集中到几个色阶上,所以层次感增强;
偏亮的图进行直方图均衡化处理后,效果不好,辨识度甚至下降了。
# 4. 心得体会
在本次直方图均衡化处理的数字图像处理实验中,我学习和掌握了以下几点:
1. 熟练掌握了BMP格式图片的读取和写入包括文件头、信息头、调色板以及位图数据的解析。这让我对图像文件的格式和结构有了更深入的理解。
2. 实现了直方图均衡化处理的关键步骤,包括灰度化、计算直方图、直方图均衡化变换等。这让我对直方图均衡化算法的原理有了更清晰的认识。
3. 通过编程实现直方图均衡化处理,并通过对不同曝光的图片进行处理,观察结果发现:偏暗图片效果好,正常图片层次增强,偏亮图片效果不佳。这让我理解到直方图均衡化处理的适用场景。
4. 熟练使用Python中的Numpy、PIL等库进行图像处理,并编写GUI界面。这进一步提高了我的编程能力。
5. 通过把Python代码打包成exe文件,实现可直接运行。这让我掌握了把代码封装成软件产品的方法。
通过本次实验,我对数字图像处理理论知识和编程实现能力都得到了提高。
# 5. 源代码
```python
import numpy as np
from struct import unpack
from PIL import Image, ImageTk
import sys
import tkinter
import tkinter.filedialog
class BmpData:
def __init__(self, file_path:str):
with open(file_path, "rb") as file:
self.file = file
self.bfType = unpack("<H", file.read(2))[0] # 0x00 文件类型
self.bfSize = unpack("<i", file.read(4))[0] # 0x02 文件大小
self.bfReserved1 = unpack("<H", file.read(2))[0] # 0x06 保留必须设置为0
self.bfReserved2 = unpack("<H", file.read(2))[0] # 0x08 保留必须设置为0
self.bfOffBits = unpack("<i", file.read(4))[0] # 0x0a 从头到位图数据的偏移
self.biSize = unpack("<i", file.read(4))[0] # 0x0e 信息头的大小
self.biWidth = unpack("<i", file.read(4))[0] # 0x12 图像的宽度(以像素为单位)
self.biHeight = unpack("<i", file.read(4))[0] # 0x16 图像的高度(以像素为单位)(负说明图像是倒立的)
self.biPlanes = unpack("<H", file.read(2))[0] # 0x1a 颜色平面数
self.biBitCount = unpack("<H", file.read(2))[0] # 0x1c 比特数/像素数
self.biCompression = unpack("<i", file.read(4))[0] # 0x1e 压缩类型
self.biSizeImage = unpack("<i", file.read(4))[0] # 0x22 位图数据的大小
self.biXPelsPerMeter = unpack("<i", file.read(4))[0] # 0x26 水平分辨率
self.biYPelsPerMeter = unpack("<i", file.read(4))[0] # 0x2a 垂直分辨率
self.biClrUsed = unpack("<i", file.read(4))[0] # 0x2e 位图使用的调色板中的颜色索引数
self.biClrImportant = unpack("<i", file.read(4))[0] # 0x32 对图像显示有重要影响的颜色索引数(0说明都重要)
self.color_palette = self.get_color_palette()
self.img_np = self.get_numpy_img()
self.gray = self.get_gray_img()
file.close()
def get_color_palette(self) -> np.ndarray:
if (self.bfOffBits == 0x36): # 16/24位图像不需要调色板起始位置就等于0x36
return None
color_alette_size = 2 ** int(self.biBitCount) # 多少字节调色板颜色就有2^n个
color_palette = np.zeros((color_alette_size, 3), dtype=np.int32)
self.file.seek(0x36)
for i in range(color_alette_size):
b = unpack("B", self.file.read(1))[0]
g = unpack("B", self.file.read(1))[0]
r = unpack("B", self.file.read(1))[0]
alpha = unpack("B", self.file.read(1))[0]
color_palette[i][0] = b
color_palette[i][1] = g
color_palette[i][2] = r
return color_palette
def get_numpy_img(self) -> np.ndarray:
biHeight = abs(self.biHeight)
img_np = np.zeros((biHeight, self.biWidth, 3), dtype=np.int32)
self.file.seek(self.bfOffBits)
for x in range(biHeight):
row_byte_count = ((self.biWidth * self.biBitCount + 31) >> 5) << 2
row_bits = self.file.read(row_byte_count)
row_bits = ''.join(format(byte, '08b') for byte in row_bits)
for y in range(self.biWidth):
pixel_data = row_bits[y * self.biBitCount: (y + 1) * self.biBitCount]
if self.biHeight > 0: # 图像倒立
img_np[biHeight - 1 - x][y] = self.get_RGB(pixel_data)
else:
img_np[x][y] = self.get_RGB(pixel_data)
return img_np
def get_gray_img(self) -> np.ndarray:
biHeight = abs(self.biHeight)
gray_img = np.dot(self.img_np.reshape((biHeight * self.biWidth, 3)).astype(np.float32),
[0.299, 0.587, 0.114]).astype(np.int32)
gray_img = gray_img.reshape((biHeight, self.biWidth))
return gray_img
def get_RGB(self, pixel_data:str):
if len(pixel_data) <= 8:
color_index = int(pixel_data, 2)
return self.color_palette[color_index]
elif len(pixel_data) == 16:
b = int(pixel_data[1:6], 2) * 8
g = int(pixel_data[6:11], 2) * 8
r = int(pixel_data[11:16], 2) * 8
return [r, g, b]
elif len(pixel_data) == 24:
b = int(pixel_data[0:8], 2)
g = int(pixel_data[8:16], 2)
r = int(pixel_data[16:24], 2)
return [r, g, b]
elif len(pixel_data) == 32:
b = int(pixel_data[0:8], 2)
g = int(pixel_data[8:16], 2)
r = int(pixel_data[16:24], 2)
alpha = int(pixel_data[24:32], 2)
return [r, g, b]
def equalize(self, level:int):
biHeight = abs(self.biHeight)
self.hist = np.zeros(256, dtype=np.int32)
max_value = self.gray.max()
min_value = self.gray.min()
gap = (max_value - min_value + 1) / level
for x in range(biHeight):
for y in range(self.biWidth):
self.hist[self.gray[x, y]] += 1
hist = np.zeros(level, dtype=np.float32)
for i in range(level):
hist[i] = np.sum(self.hist[min_value + int(i * gap) : min_value + int((i + 1) * gap)])
hist /= biHeight * self.biWidth
for i in range(1, level):
hist[i] += hist[i - 1]
hist *= level
hist = np.around(hist)
hist /= level
hist = np.floor(hist * 255).astype(np.int32)
self.equalized_img = np.zeros_like(self.gray)
self.equalized_hist = np.zeros(256, dtype=np.int32)
for x in range(biHeight):
for y in range(self.biWidth):
self.equalized_img[x, y] = hist[int((self.gray[x, y] - min_value) / gap)]
self.equalized_hist[self.equalized_img[x, y]] += 1
return self.equalized_img, self.hist, self.equalized_hist
def save_equalized_img(self, save_path:str):
self.save_img(image=self.equalized_img, save_path=save_path)
def save_img(self, image:np.ndarray, save_path:str):
with open(save_path, "wb") as file:
file.write(int(self.bfType).to_bytes(2, byteorder='little')) # 0x00 文件类型
file.write(int(0x36 + 0x100 * 4 + self.biWidth * abs(self.biHeight)).to_bytes(4, byteorder='little')) # 0x02 文件大小
file.write(int(0).to_bytes(4, byteorder='little')) # 0x06 保留必须设置为0
file.write(int(0x36 + 0x100 * 4).to_bytes(4, byteorder='little')) # 0x0a 从头到位图数据的偏移
file.write(int(40).to_bytes(4, byteorder='little')) # 0x0e 信息头的大小
file.write(int(self.biWidth).to_bytes(4, byteorder='little')) # 0x12 图像的宽度
file.write(int(self.biHeight).to_bytes(4, byteorder='little')) # 0x16 图像的高度
file.write(int(self.biPlanes).to_bytes(2, byteorder='little')) # 0x1a 颜色平面数
file.write(int(8).to_bytes(2, byteorder='little')) # 0x1c 比特数/像素数
file.write(int(self.biCompression).to_bytes(4, byteorder='little')) # 0x1e 压缩类型
file.write(int(self.biSizeImage).to_bytes(4, byteorder='little')) # 0x22 位图数据的大小
file.write(int(self.biXPelsPerMeter).to_bytes(4, byteorder='little')) # 0x26 水平分辨率
file.write(int(self.biYPelsPerMeter).to_bytes(4, byteorder='little')) # 0x2a 垂直分辨率
file.write(int(0x100 * 4).to_bytes(4, byteorder='little')) # 0x2e 位图使用的调色板中的颜色索引数
file.write(int(0).to_bytes(4, byteorder='little')) # 0x32 对图像显示有重要影响的颜色索引数
for i in range(256):
file.write(int(i).to_bytes(1, byteorder='little'))
file.write(int(i).to_bytes(1, byteorder='little'))
file.write(int(i).to_bytes(1, byteorder='little'))
file.write(int(0).to_bytes(1, byteorder='little'))
for x in range(abs(self.biHeight)):
for y in range(self.biWidth):
if self.biHeight > 0:
file.write(int(image[self.biHeight - 1 - x][y]).to_bytes(1, byteorder='little'))
else:
file.write(int(image[x][y]).to_bytes(1, byteorder='little'))
file.write(b'0' * ((((self.biWidth * 8 + 31) >> 5) << 2) - 8 * self.biWidth))
file.close()
def choosepic():
global path_
path_ = tkinter.filedialog.askopenfilename(title='请选择图片文件', filetypes=[('图片', '.jpg .png .bmp .jpeg')])
if path_ == '':
return
img_temp = Image.open(path_).resize((int(256 * 0.8), int(256 * 0.8))) # 图片读取和加载
img = ImageTk.PhotoImage(img_temp)
label_image1.config(image=img)
label_image1.image = img
def equalize():
if path_ == '':
return
image = BmpData(path_)
# img = Image.fromarray(image.img_np.astype(np.uint8))
# img.show()
equalized_img, hist, equalized_hist = image.equalize(8) # 分别为均衡化的图/直方图/均衡化后的直方图
equalized_img = Image.fromarray(equalized_img.astype(np.uint8))
# equalized_img.show()
name_parts = path_.split('.')
name_parts[-2] += "_equalized"
new_file_name = '.'.join(name_parts)
image.save_equalized_img(new_file_name)
equalized_img = equalized_img.resize((int(256 * 0.8), int(256 * 0.8)))
equalized_img = ImageTk.PhotoImage(equalized_img)
label_image2.config(image=equalized_img)
label_image2.image = equalized_img # 处理后的图片的显示
if __name__ == "__main__":
root = tkinter.Tk()
root.title('21281280柯劲帆') # 标题
width, height = 600, 400
width_max, height_max = root.maxsize()
s_center = '%dx%d+%d+%d' % (width, height, (width_max - width) / 2, (height_max - height) / 2) # 将页面显示在正中间
root.geometry(s_center)
root.resizable(width=False, height=False) # 窗口不可移动
l = tkinter.Label(root, text='实验二', width=60, height=2, fg='black', font=("微软雅黑", 16), anchor=tkinter.CENTER)
l.pack()
label_image1 = tkinter.Label(root, width=int(256 * 0.8), height=int(256 * 0.8), bg='whitesmoke', anchor=tkinter.NE)
label_image1.pack()
label_image1.place(x=45, y=70, width=int(256 * 0.8), height=int(256 * 0.8))
label_image2 = tkinter.Label(root, width=int(256 * 0.8), height=int(256 * 0.8), bg='whitesmoke', anchor=tkinter.NE)
label_image2.place(x=350, y=70, width=int(256 * 0.8), height=int(256 * 0.8))
# 文本按钮
Image_Input = tkinter.Button(root, text='Choose', command=choosepic)
Image_Input.place(x=105, y=300, width=80, height=30)
# 直方图均衡化
Fun1 = tkinter.Button(root, text='直方图均衡化', command=equalize)
Fun1.place(x=265, y=300, width=80, height=30)
# 退出
Quit = tkinter.Button(root, text='Quit', command=sys.exit)
Quit.place(x=415, y=300, width=80, height=30)
end = tkinter.Label(root, text='21281280 柯劲帆', fg='silver', font=("微软雅黑", 10))
end.place(x=215, y=360, width=200, height=20)
root.mainloop()
```