如何在Python中不依赖外部库裁剪BMP并保存?代码问题排查
解决BMP图像裁剪变形问题并替换PIL实现手动保存
我来帮你搞定这两个核心问题:图像变形和移除PIL依赖手动保存BMP文件。
一、修复图像变形问题
你的代码里有两个关键错误直接导致了图像变形:
1. 列索引判断逻辑错误
在repack_sub_pixels函数中,判断水平裁剪范围时错误地用了origin_y(垂直起始位置),而不是origin_x(水平起始位置)。因为每个像素占3字节,正确的列索引范围应该是origin_x * 3到(origin_x + width) * 3。
2. 未处理BMP行对齐填充字节
BMP格式强制要求每行的字节数必须是4的倍数,不足的部分会用空字节(通常是\x00)填充。你的read_rows函数没有跳过这些填充字节,导致读取的行数据混入了无效填充,打乱了像素排列顺序。
二、替换PIL,手动实现BMP文件保存
我们需要严格遵循BMP格式规范,手动构建文件头、信息头,处理像素数据的行对齐,最后按BMP要求的"从下到上"顺序写入像素数据。
修改后的完整代码
import numpy as np import struct def read_rows(path, origin_x, origin_y, height, width): image_file = open(path, "rb") # 读取BMP原图的宽度和高度 image_file.seek(18, 0) bmp_w = struct.unpack('I', image_file.read(4))[0] bmp_h = struct.unpack('I', image_file.read(4))[0] # 检查裁剪范围是否超出原图边界 if origin_x + width > bmp_w or origin_y + height > bmp_h: print(f'Dimensions are outside the figure size, choose between {bmp_w}x{bmp_h} pixels') return None # 计算每行的实际字节数(含对齐填充) row_pixel_bytes = bmp_w * 3 padding = (4 - (row_pixel_bytes % 4)) % 4 total_row_bytes = row_pixel_bytes + padding image_file.seek(54) # 跳过14字节文件头+40字节信息头,直接定位到像素数据 rows = [] for _ in range(bmp_h): row = [] # 读取一行的有效像素(BMP存储顺序是BGR) for _ in range(bmp_w): b = ord(image_file.read(1)) g = ord(image_file.read(1)) r = ord(image_file.read(1)) row.append(b) row.append(g) row.append(r) # 跳过当前行的填充字节 image_file.read(padding) # 插入到列表开头,因为BMP像素是从下到上存储的 rows.insert(0, row) image_file.close() return rows def repack_sub_pixels(rows, origin_x, origin_y, height, width): print("Repacking pixels...") sub_pixels = [] # 遍历目标垂直范围的行 for jdx in range(origin_y, origin_y + height): row = rows[jdx] # 截取目标水平范围的像素字节 start_idx = origin_x * 3 end_idx = (origin_x + width) * 3 sub_pixels.extend(row[start_idx:end_idx]) print(f"Packed {len(sub_pixels)} sub-pixels.") return sub_pixels def save_bmp(output_name, pixels, width, height): # 计算裁剪后图像的行填充字节 row_pixel_bytes = width * 3 padding = (4 - (row_pixel_bytes % 4)) % 4 total_pixel_size = len(pixels) + (height * padding) # 构建BMP文件头(14字节) file_header = struct.pack('<2sIHHIH', b'BM', # 文件标识 54 + total_pixel_size, # 文件总大小 0, 0, # 保留字段 54) # 像素数据起始偏移量 # 构建BMP信息头(40字节,BITMAPINFOHEADER) info_header = struct.pack('<IIIHHIIIIII', 40, # 信息头大小 width, # 图像宽度 height, # 图像高度 1, # 颜色平面数 24, # 每像素位数(24位真彩色) 0, # 无压缩 total_pixel_size, # 像素数据总大小(含填充) 0, 0, # 默认分辨率 0, 0) # 24位无需颜色表 # 处理像素行对齐,生成最终像素数据(BMP要求从下到上存储) pixel_data = b'' padding_bytes = b'\x00' * padding for i in reversed(range(height)): start = i * row_pixel_bytes end = start + row_pixel_bytes row_bytes = bytes(pixels[start:end]) pixel_data += row_bytes + padding_bytes # 写入文件 with open(output_name, 'wb') as f: f.write(file_header) f.write(info_header) f.write(pixel_data) print(f'File saved as {output_name}') def crop(file_name, output_name, a, b, h, w): ''' Takes input file name , output file name, origin x from left , origin y from top , height of cropped image from origin , width of cropped image from origin. ''' rows = read_rows(file_name, a, b, h, w) if not rows: return sub_pixels = repack_sub_pixels(rows, a, b, h, w) save_bmp(output_name, sub_pixels, w, h) # 测试调用 crop('tiger.bmp', 'new_output.bmp', 0, 100, 100, 100)
关键修改点说明
read_rows函数:
- 新增了行填充字节的计算与跳过逻辑,确保读取的行数据仅包含有效像素。
- 简化了像素读取流程,直接按BMP的BGR顺序收集通道数据。
repack_sub_pixels函数:
- 修正了水平范围的判断逻辑,用
origin_x替代origin_y,确保裁剪区域正确。 - 改用
extend方法高效收集像素数据,避免循环append的冗余操作。
- 修正了水平范围的判断逻辑,用
save_bmp函数:
- 严格按照BMP格式规范构建文件头和信息头,确保文件能被系统正常识别。
- 处理了裁剪后图像的行对齐填充,保证每行字节数为4的倍数。
- 按BMP要求的"从下到上"顺序写入像素数据,避免图像倒置。
内容的提问来源于stack exchange,提问作者caxefaizan
相关产品推荐
相关产品推荐

