Retina显示器下Pillow默认72DPI显示模糊,如何实现高DPI适配?
解决Mac M2 Retina显示器上Pillow图片显示像素化问题
问题背景
在Mac M2 Retina显示器上使用Pillow处理150×150像素图片并添加圆形遮罩后,因默认72DPI导致显示像素化;使用更大图片会占用过多屏幕空间,且无法利用Retina的高DPI特性。尝试过放大重采样、手动设置DPI、优化抗锯齿、CustomTkinter、Quartz API等方法均无效,需寻求绕过Pillow限制实现高DPI显示的方案。
相关代码
# Constants for general settings WINDOW_MIN_WIDTH = 700 WINDOW_MIN_HEIGHT = 350 GRID_COLUMNS = 9 # Number of columns for the grid layout IMAGE_DIAMETER = 132 # Diameter for the circular images border_width = 4 DEFAULT_THEME = "blue" APPEARANCE_MODE = "dark" IMAGE_BRIGHTNESS = 0.5 #.... Omitted a bunch of code which is more about how to find the images using os pointless for this dpi issue def create_circular_image(self, image_path, diameter, border_width): """ Create a circular image with a border and anti-aliasing for smoother edges. """ try: # Load the image and ensure it's square img = Image.open(image_path) square_size = min(img.width, img.height) img = img.crop((0, 0, square_size, square_size)) # Resize with high-quality resampling to reduce pixelation img = img.resize((diameter, diameter), Image.LANCZOS) # Apply a circular mask with anti-aliasing mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(mask) draw.ellipse((0, 0, diameter, diameter), fill=255) img.putalpha(mask) # Determine the group (subdirectory) and border color group = os.path.basename(os.path.dirname(image_path)) ring_color = GROUP_COLORS.get(group, "white") # Default to white if not found # Create a new border with anti-aliasing new_diameter = diameter + 2 * border_width bordered_img = Image.new("RGBA", (new_diameter, new_diameter), ring_color) border_mask = Image.new("L", (new_diameter, new_diameter), 0) border_draw = ImageDraw.Draw(border_mask) border_draw.ellipse((0, 0, new_diameter, new_diameter), fill=255, outline=0) # Calculate the position to center the circular image within the bordered frame img_pos = ((new_diameter - img.width) // 2, (new_diameter - img.height) // 2) # Apply the circular mask to ensure smooth edges bordered_img.paste(img, img_pos, mask) bordered_img.putalpha(border_mask) # Convert to CTkImage for high DPI support ctk_image = CTkImage(light_image=bordered_img, dark_image=bordered_img, size=(new_diameter, new_diameter)) return ctk_image #... Omitted more until I reference the actual creation for the buttons which calls that function def create_phenotype_controls(self, image_paths): """ Create controls for images and sliders. """ phenotype_names = self.extract_phenotypes_from_paths(image_paths) for idx, (image_path, phenotype_name) in enumerate(itertools.zip_longest(image_paths, phenotype_names)): frame = ctk.CTkFrame(self.grid_frame) frame.grid(row=idx // GRID_COLUMNS, column=idx % GRID_COLUMNS, padx=2.5, pady=2.5) # Create circular image image = self.create_circular_image(image_path, IMAGE_DIAMETER, border_width) image_widget = ctk.CTkButton(frame, image=image, text="",command=lambda idx=idx: self.toggle_phenotype(idx),fg_color="gray12") # Ensure no text image_widget.pack(padx=0,pady=0, fill="both") # Fill the available space self.buttons.append(image_widget) # Store the button reference #... Omitted last bit of code here
解决方案
1. 基于Retina设备像素比创建2倍分辨率图片
Retina显示器默认采用2x设备像素比(DPR),即1个逻辑像素对应2个物理像素。按目标显示尺寸的2倍创建图片,再通过CTkImage指定实际显示的逻辑尺寸,让系统自动适配高DPI:
def create_circular_image(self, image_path, diameter, border_width): # 适配Retina的2x缩放因子 retina_diameter = diameter * 2 retina_border_width = border_width * 2 try: img = Image.open(image_path) square_size = min(img.width, img.height) img = img.crop((0, 0, square_size, square_size)) # 用2倍尺寸进行高画质重采样 img = img.resize((retina_diameter, retina_diameter), Image.LANCZOS) # 圆形遮罩同步使用2倍尺寸,保证边缘抗锯齿精度 mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(mask) draw.ellipse((0, 0, retina_diameter, retina_diameter), fill=255) img.putalpha(mask) group = os.path.basename(os.path.dirname(image_path)) ring_color = GROUP_COLORS.get(group, "white") # 边框也使用2倍尺寸创建 new_retina_diameter = retina_diameter + 2 * retina_border_width bordered_img = Image.new("RGBA", (new_retina_diameter, new_retina_diameter), ring_color) border_mask = Image.new("L", (new_retina_diameter, new_retina_diameter), 0) border_draw = ImageDraw.Draw(border_mask) border_draw.ellipse((0, 0, new_retina_diameter, new_retina_diameter), fill=255, outline=0) img_pos = ((new_retina_diameter - img.width) // 2, (new_retina_diameter - img.height) // 2) bordered_img.paste(img, img_pos, mask) bordered_img.putalpha(border_mask) # CTkImage指定原逻辑尺寸,系统自动将高分辨率图片渲染到物理像素上 display_size = (diameter + 2 * border_width, diameter + 2 * border_width) ctk_image = CTkImage(light_image=bordered_img, dark_image=bordered_img, size=display_size) return ctk_image except Exception as e: print(f"Error creating circular image: {e}") return None
2. 禁用Pillow的DPI元数据干扰
不要手动设置图片DPI元数据,Retina的显示逻辑依赖系统设备像素比,而非图片的DPI信息。确保处理后的图片不添加多余的DPI配置,让CTkImage直接处理尺寸适配。
3. 优化CustomTkinter的高DPI配置
在应用初始化阶段,确保CustomTkinter正确启用高DPI支持:
import customtkinter as ctk ctk.set_appearance_mode(APPEARANCE_MODE) ctk.set_default_color_theme(DEFAULT_THEME) # 若自动缩放存在问题,可尝试手动关闭自动DPI感知,依赖系统原生缩放 ctk.deactivate_automatic_dpi_awareness()
原理说明
通过创建2倍分辨率的图片,让系统将高像素密度的内容渲染到Retina显示器的物理像素上,既保证了显示清晰度,又不会占用额外的屏幕逻辑空间,完美适配Retina的高DPI特性。
内容的提问来源于stack exchange,提问作者Chad Ify
相关产品推荐
相关产品推荐

