1. 项目缘起一个看似简单却暗藏玄机的需求最近在折腾一块行空板想在上面做个横屏显示的仪表盘其中有个小功能需要在屏幕顶部显示一行状态文字。需求很简单但有个小“怪癖”因为屏幕是横着放的而且安装位置比较特殊操作者是从屏幕底部向上看的。这就导致了一个问题——如果按常规方式显示文字从那个角度看过去字是倒着的。这让我想起了以前在树莓派上做项目时也遇到过类似需要旋转显示的情况。但这次在行空板上我最初的想法是直接用PIL库把文字画出来然后旋转整个图像。然而实际操作下来发现事情没这么简单。行空板的屏幕驱动、PIL的坐标系、还有横屏模式下的坐标变换这几个东西凑在一起稍微不注意显示出来的文字位置就飘到九霄云外去了或者旋转轴心不对根本达不到“颠倒镜像”的效果。所以这个“[Python]如何在行空板上显示颠倒镜像文字横屏显示,PIL库”的问题本质上是在一个特定的硬件行空板、特定的显示模式横屏下如何精准地控制图形库PIL去生成并呈现一个经过特定几何变换180度旋转可能的镜像的文本图像。这不仅仅是一个Image.rotate(180)就能搞定的事它涉及到从图像内存创建、绘图、变换到最终像素输出的完整链条任何一个环节理解偏差都会导致显示异常。2. 核心工具链拆解PIL、行空板与GUI框架在动手之前我们必须把手里这几样工具的特性摸透知道它们的能耐和脾气才能让它们乖乖配合。2.1 PIL库不仅仅是打开图片那么简单PILPython Imaging Library或者其活跃分支Pillow是我们处理图像的核心。在这个项目里我们主要用到它的两个核心子模块Image负责图像的创建、加载、基本变换如旋转、翻转和保存。ImageDraw与ImageFont前者是在Image对象上绘图的“画笔”后者是管理字体的“字库”。关键点在于理解PIL的坐标系和变换逻辑。PIL的图像有一个原点(0,0)默认在左上角X轴向右Y轴向下。当我们调用Image.rotate(angle, expandTrue)时angle旋转角度正数为逆时针旋转。所以180度就是上下颠倒。expand这个参数至关重要。如果为False默认旋转后的图像尺寸不变边角部分会被裁剪掉。如果为True则会自动扩大画布以适应旋转后的整个图像但图像的中心点旋转的轴心可能会发生变化。对于“颠倒”文字旋转180度是核心。但“镜像”通常指的是水平翻转对应PIL的方法是Image.transpose(Image.FLIP_LEFT_RIGHT)。我们需要根据实际需求决定是只颠倒还是颠倒镜像。2.2 行空板与Unihiker库硬件的抽象层行空板是一款集成了屏幕、多种传感器和GPIO接口的单板计算机非常适合做交互式项目。官方为其提供了unihiker库它封装了底层的显示和输入输出操作让我们能用更简单的API来控制它。unihiker库的GUI部分其显示核心通常基于某个底层的图形框架如Tkinter、SDL等。对我们来说最关键的是它提供了一个GUI类我们可以获取到其对应的Canvas画布或直接显示PIL图像的方法。一个常见的模式是from unihiker import GUI gui GUI() # 假设有一个显示图像的方法例如更新某个Image控件的图像 # 或者更直接地在某些版本中可以直接将PIL图像绘制到板子的帧缓冲上我们需要查阅当前行空板固件和unihiker库的文档找到最有效、性能最好的显示PIL图像的方法。通常会有一个方法接受PIL的Image对象作为参数。2.3 横屏模式的坐标映射行空板默认可能是竖屏模式。当我们将系统或应用设置为横屏时整个显示坐标系发生了90度旋转。此时物理屏幕的左上角在软件坐标系里可能对应的是右上角、右下角或左下角。这带来的直接影响是我们在PIL中创建的图像基于其自身的坐标系如果直接送给横屏模式下的行空板显示可能会出现90度偏移。因此我们的解决方案必须考虑这个“横屏因子”。有时这需要在行空板系统层面进行设置如旋转帧缓冲有时则需要我们在应用层对PIL生成的图像做一次额外的90度旋转来补偿。3. 实战步骤从创建图像到稳定显示理论清晰了我们来一步步实现。假设我们的最终需求是在行空板横屏状态下于屏幕顶部区域显示一行颠倒的文字仅旋转180度不水平镜像。3.1 步骤一环境准备与基础图像创建首先确保行空板已连接网络并通过SSH或直接在其桌面环境中操作。安装必要的库如果尚未安装pip install Pillowunihiker库通常已在行空板系统中预装。接着我们创建一个基础的Python脚本初始化GUI并创建一个用于绘制的图像from PIL import Image, ImageDraw, ImageFont from unihiker import GUI import time # 初始化行空板GUI gui GUI() # 定义显示区域的尺寸。这里需要根据横屏模式下的实际屏幕分辨率来设定。 # 例如行空板屏幕分辨率可能是320x240竖屏横屏后就是240x320。 # 我们假设横屏后有效显示区域宽度为width高度为height。 width 320 # 横屏后的宽度请根据实际情况调整 height 240 # 横屏后的高度请根据实际情况调整 # 创建一个与显示区域等大的纯色背景图像模式为RGB base_image Image.new(RGB, (width, height), color(50, 50, 50)) # 深灰色背景 draw ImageDraw.Draw(base_image) # 加载一个字体。行空板系统通常自带一些字体例如DejaVuSans。 # 需要指定正确的字体文件路径。 try: font ImageFont.truetype(/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf, 24) except IOError: # 如果指定字体不存在回退到默认字体可能像素化 font ImageFont.load_default() print(警告未找到指定字体使用默认字体。)注意字体路径是关键。不同系统、不同固件版本的字体存放位置可能不同。如果遇到字体文件找不到的错误可以通过在终端执行fc-list或find /usr -name *.ttf来查找可用的字体文件路径。3.2 步骤二绘制并变换文字图像我们不直接在base_image上画文字而是先在一个“文本图像”上画然后对这个文本图像进行变换最后再贴到背景上。这样做更灵活便于控制位置。# 1. 创建一块专门用于绘制文字的临时图像尺寸稍大于文本 bounding box text_content 状态运行中 # 获取文本的预估尺寸 text_bbox draw.textbbox((0, 0), text_content, fontfont) text_width text_bbox[2] - text_bbox[0] text_height text_bbox[3] - text_bbox[1] # 留一些边距 padding 10 text_img_width text_width padding * 2 text_img_height text_height padding * 2 # 创建透明背景的文本图像模式RGBA支持透明度 text_image Image.new(RGBA, (text_img_width, text_img_height), color(0, 0, 0, 0)) text_draw ImageDraw.Draw(text_image) # 2. 在临时图像的正中心绘制文字颜色白色 # 计算文字起始坐标使其在临时图像中居中 text_x (text_img_width - text_width) // 2 text_y (text_img_height - text_height) // 2 text_draw.text((text_x, text_y), text_content, fontfont, fill(255, 255, 255, 255)) # 白色不透明 # 3. 对文本图像进行180度旋转实现颠倒 # 使用expandTrue确保旋转后图像完整背景透明区域会被保留为透明 rotated_text_image text_image.rotate(180, expandTrue) # 此时rotated_text_image的尺寸可能发生了变化因为expandTrue。 # 我们需要计算旋转后图像的新尺寸以便准确定位。 rotated_width, rotated_height rotated_text_image.size3.3 步骤三将处理后的图像合成并显示现在我们需要把旋转后的文字图像粘贴到背景图像的指定位置例如屏幕顶部居中。# 4. 定义文字在最终背景上的目标位置顶部居中 # 假设我们希望文字块显示在距离顶部10像素的位置并且水平居中。 target_y 10 target_x (width - rotated_width) // 2 # 5. 将旋转后的文字图像RGBA粘贴到底图RGB上。 # 由于底图是RGB模式没有Alpha通道直接粘贴RGBA图像会出问题。 # 我们需要先将旋转后的文字图像转换一下或者使用其Alpha通道作为掩码。 # 方法将RGBA图像分离出RGB和Alpha然后用Alpha作为掩码粘贴RGB部分。 if rotated_text_image.mode RGBA: # 分离通道 r, g, b, a rotated_text_image.split() # 将RGB三个通道合并为一个RGB图像 rgb_text_image Image.merge(RGB, (r, g, b)) # 将RGB图像粘贴到底图上使用Alpha通道作为掩码 base_image.paste(rgb_text_image, (target_x, target_y), maska) else: # 如果不是RGBA理论上不会直接粘贴 base_image.paste(rotated_text_image, (target_x, target_y)) # 6. 关键一步考虑横屏显示。 # 此时base_image是按照我们理解的“横屏逻辑坐标系”宽度高度创建的。 # 但行空板的硬件帧缓冲framebuffer可能仍然是竖屏的原始物理坐标系。 # 因此我们需要将最终图像旋转90度或270度以适应硬件横屏设置。 # 具体旋转角度取决于横屏的方向是左转还是右转。 # 假设横屏是原始竖屏顺时针旋转90度那么我们需要将图像逆时针旋转90度来补偿。 # 这里我们旋转270度即逆时针90度来测试。 final_image_for_display base_image.rotate(270, expandTrue) # 注意expandTrue会改变图像尺寸旋转270度后宽高会互换。 # 行空板的显示方法可能要求图像尺寸与屏幕物理分辨率严格匹配。 # 所以更稳妥的做法是在创建base_image时就使用横屏后的**物理分辨率**。 # 然后在最后一步根据横屏方向对图像进行旋转但使用expandFalse并确保旋转轴心是图像中心可能需要提前计算好裁剪或填充。 # 这是一个容易踩坑的地方需要根据实际显示效果进行调试。3.4 步骤四在行空板上显示图像最后调用unihiker库的方法将最终的PIL图像显示出来。具体方法取决于库的版本。# 方法A如果GUI类有直接显示图像的方法例如设置背景图 # 注意该方法可能要求图像尺寸与屏幕完全一致。 try: gui.show_image(final_image_for_display) # 这是一个假设的API实际名称可能不同 except AttributeError: # 方法B通过创建一个Image控件来显示更常见 # 首先清除画布上可能已有的旧控件 gui.clear() # 创建一个Image控件并设置其图像和位置 # 这里假设Image控件可以接受PIL图像对象并自动适应。 img_widget gui.draw_image(x0, y0, imagefinal_image_for_display) # 或者如果draw_image不支持PIL对象可能需要先保存为临时文件 # temp_path /tmp/display.png # final_image_for_display.save(temp_path) # img_widget gui.draw_image(x0, y0, filepathtemp_path) # 保持程序运行否则图像可能会一闪而过 time.sleep(30) # 显示30秒4. 调试与排坑为什么我的文字不见了或者位置不对按照上面的步骤理论上应该能显示出来。但如果没出来或者位置很奇怪别慌我们一步步排查。4.1 坑一字体加载失败回退到默认像素字体这是最常见的问题之一。如果ImageFont.truetype抛出了IOError并且你使用了load_default()那么显示的文字会是非常小的像素点可能根本看不清就像“消失”了一样。排查与解决确认路径在行空板的终端里使用find命令查找.ttf或.otf文件。find /usr/share/fonts -name *.ttf | head -20使用绝对路径选择其中一个字体文件的绝对路径替换代码中的路径。测试字体可以写一个简单的脚本仅仅尝试加载字体并打印一些信息确保字体文件可读。备用方案将喜欢的字体文件如.ttf直接上传到行空板的项目目录中然后使用相对路径如./myfont.ttf加载。4.2 坑二旋转后图像尺寸变化导致定位坐标失效当我们使用Image.rotate(180, expandTrue)时如果原始图像不是正方形旋转后的图像尺寸会变化宽高互换。我们代码中计算target_x和target_y时使用的是旋转前的text_img_width和text_img_height但粘贴时使用的是旋转后的rotated_text_image。如果expandTrue导致尺寸变化这里的计算就会出错文字可能被贴到画面外。排查与解决打印尺寸在旋转前后打印text_image.size和rotated_text_image.size。如果它们不同说明expandTrue导致了尺寸变化。修正计算所有基于图像尺寸的计算如居中计算都必须使用变换后图像的实际尺寸。在我们的代码中target_x的计算应该使用rotated_width和rotated_height。考虑使用expandFalse如果你能确保文字图像在旋转后即使被裁剪一部分也无所谓或者你事先将画布做得足够大可以使用expandFalse来保持尺寸不变这样坐标计算会更简单。但需要仔细检查旋转轴心默认是图像中心是否会导致你想要的内容被裁掉。4.3 坑三横屏补偿旋转的方向和轴心错误这是最令人头疼的部分。final_image_for_display base_image.rotate(270, expandTrue)这一行是假设性的补偿。实际上可能需要旋转90度、270度或者根本不需要旋转而是需要在行空板系统层面进行配置。排查与解决先屏蔽横屏补偿注释掉最终的旋转步骤直接显示base_image。观察显示效果。建立参照物在base_image上画一个简单的、非对称的图形比如在左上角画一个红色矩形在右下角画一个蓝色圆形。这样你就能清晰地看出图像在屏幕上的实际朝向。系统级横屏设置查阅行空板官方文档看是否有配置横屏的启动参数、环境变量或配置文件。有时在/boot目录下的配置文件中设置display_rotate等参数会更根本。如果系统级设置了横屏那么你的应用可能就不需要做任何旋转补偿。逐角度测试如果确定需要在应用层补偿就像调试机械零件一样尝试0, 90, 180, 270四个旋转角度配合expandTrue和False观察哪个角度能让你的参照物“正”过来。轴心问题rotate方法默认围绕图像中心旋转。如果你希望围绕左上角旋转需要先平移坐标系旋转后再平移回来或者使用Image.rotate(angle, center(x, y))参数Pillow新版本支持。4.4 坑四图像粘贴时的混合模式问题我们的代码中使用了base_image.paste(rgb_text_image, (target_x, target_y), maska)。这里maska意味着Alpha通道中透明值为0的部分不会覆盖底图不透明值为255的部分会完全覆盖。这通常是我们想要的。但如果你的文字图像背景不是完全透明比如是某种半透明的颜色或者rotated_text_image在旋转后边缘产生了抗锯齿的半透明像素粘贴效果可能会发虚或带有底色。排查与解决检查图像模式确保text_image创建时模式是‘RGBA’并且背景色是(0,0,0,0)全透明。检查旋转后的Alpha通道可以将rotated_text_image保存为PNG文件传到电脑上用图片查看器检查确认文字区域为白色实心背景是棋盘格表示透明。简化调试可以先不用透明背景就用白色背景黑色文字直接粘贴排除Alpha混合带来的复杂度。等文字能正确显示并定位后再改为透明背景。5. 优化与扩展让代码更健壮、功能更丰富解决了基本显示问题后我们可以让代码变得更实用。5.1 封装成函数提高复用性将核心的“创建颠倒文字图像”功能封装起来方便多次调用。def create_upside_down_text(text, font_path, font_size, text_color(255,255,255,255), bg_colorNone): 创建一个包含颠倒文字的透明图像。 参数: text: 要显示的字符串 font_path: 字体文件路径 font_size: 字体大小 text_color: 文字颜色RGBA元组 bg_color: 图像背景色RGBA元组。None表示全透明。 返回: PIL.Image.Image对象RGBA模式 if bg_color is None: bg_color (0, 0, 0, 0) try: font ImageFont.truetype(font_path, font_size) except IOError: font ImageFont.load_default() print(f警告字体 {font_path} 未找到使用默认字体。) # 临时绘图以获取文本尺寸 temp_img Image.new(RGBA, (1, 1), bg_color) temp_draw ImageDraw.Draw(temp_img) bbox temp_draw.textbbox((0, 0), text, fontfont) text_width bbox[2] - bbox[0] text_height bbox[3] - bbox[1] padding font_size // 2 # 边距设为字体大小的一半 img_width text_width padding * 2 img_height text_height padding * 2 text_img Image.new(RGBA, (img_width, img_height), bg_color) draw ImageDraw.Draw(text_img) # 绘制文字居中 draw.text((padding, padding), text, fontfont, filltext_color) # 旋转180度 rotated_img text_img.rotate(180, expandTrue, resampleImage.Resampling.BICUBIC) # 使用高质量重采样 return rotated_img # 使用示例 text_img create_upside_down_text(温度25.6℃, /usr/share/fonts/xxx.ttf, 28, text_color(0, 255, 0, 255)) # 然后将text_img粘贴到你的背景图上5.2 动态更新内容仪表盘的文字内容通常是动态变化的。我们不需要每次都重新创建整个背景图可以只更新文字部分。# 假设base_image是背景text_image_widget是之前创建的图像控件 def update_displayed_text(new_text): # 1. 重新生成文字图像 new_text_img create_upside_down_text(new_text, font_path, font_size) # 2. 创建一个背景的副本避免修改原图 current_display_image base_image.copy() # 3. 计算位置并粘贴覆盖旧文字区域 # 注意这里需要先清除旧文字区域或者背景本身就是静态的。 # 如果背景是纯色可以直接在base_image上重新粘贴。 # 如果背景复杂可能需要每次从“干净的”背景开始合成。 target_x (width - new_text_img.width) // 2 target_y 10 if new_text_img.mode RGBA: r, g, b, a new_text_img.split() rgb_part Image.merge(RGB, (r, g, b)) current_display_image.paste(rgb_part, (target_x, target_y), maska) else: current_display_image.paste(new_text_img, (target_x, target_y)) # 4. 横屏补偿旋转如果需要 final_img current_display_image.rotate(270, expandTrue) # 5. 更新GUI控件 img_widget.config(imagefinal_img) # 假设Tkinter风格的API # 或者 gui.update_image(img_widget, final_img)5.3 添加抗锯齿和阴影效果为了让文字更美观Pillow的ImageDraw在绘制文字时默认抗锯齿效果可能有限。在旋转时使用高质量的重采样过滤器如Image.Resampling.BICUBIC可以减少锯齿。此外可以通过绘制两次文字一次稍大的深色文字作为阴影一次正常大小的亮色文字来模拟简单的阴影效果增加可读性。def create_fancy_upside_down_text(text, font_path, font_size, text_color(255,255,255), shadow_color(0,0,0,100), shadow_offset(2,2)): 创建带阴影的颠倒文字 # ... 计算尺寸创建画布 ... # 先画阴影位置偏移颜色半透明 draw.text((paddingshadow_offset[0], paddingshadow_offset[1]), text, fontfont, fillshadow_color) # 再画前景文字 draw.text((padding, padding), text, fontfont, filltext_color) # ... 旋转 ... return rotated_img6. 总结与核心要点回顾在行空板上实现横屏颠倒文字显示是一个结合了图像处理PIL和嵌入式GUI显示unihiker的典型任务。整个过程的关键在于对每个环节坐标系统和变换行为的精确理解。坐标系是基石始终明确PIL的图像坐标系原点左上角、你的“逻辑显示坐标系”横屏后的宽高以及行空板屏幕的“物理坐标系”之间的关系。通过绘制参照物来验证是最高效的调试方法。分离关注点将“文字生成与变换”和“图像显示”分开。先专注于用PIL在内存中生成一张正确的、颠倒的文字图片确保其颜色、透明度、尺寸符合预期。然后再处理如何将它放到屏幕的正确位置。变换的副作用rotate操作特别是expandTrue时会改变图像尺寸和内容位置。所有后续的定位计算都必须基于变换后的新尺寸。系统级配置优先如果行空板支持系统级的屏幕旋转优先使用该方式。这可以简化应用层逻辑避免复杂的补偿旋转性能也更好。字体字体字体嵌入式环境下的字体路径问题是高频故障点。务必通过系统命令确认字体文件的存在和路径并做好加载失败的异常处理。最后代码的健壮性来自于对细节的把握。例如处理RGBA图像的粘贴、考虑横屏补偿、封装功能函数以便复用这些都能让项目从“能跑”升级到“好用”。当颠倒的文字稳稳地显示在行空板横屏的顶端时那种对底层细节完全掌控的感觉正是嵌入式开发乐趣的一部分。