---
# System prepended metadata

title: Pico W 驅動 TFT 顯示屏：使用 micropython-ili9341 顯示 RAW RGB565 圖片
tags: [Micropython]

---

![image](https://hackmd.io/_uploads/BkTHe01wex.png)

micropython-ili9341搭配 [micropython-ili9341](https://github.com/rdagger/micropython-ili9341)控制並顯示圖片



```python=
# -*- coding: utf-8 -*-
"""
一個使用 Streamlit 製作的網站，用於將圖片轉換為 RAW RGB565 格式。
使用者可以上傳圖片，調整其尺寸，然後處理轉換並提供轉換後的 .raw 檔案供下載。
"""

import streamlit as st
from PIL import Image
from struct import pack, unpack # 引入 unpack 用於 RGB565 到圖片的轉換
import io
import os
import math # 引入 math 模組用於數學運算

def convert_image_to_rgb565(img_bytes, target_width, target_height):
    """
    將圖片的位元組數據轉換為 RGB565 格式的位元組。
    在轉換前會先將圖片縮放至指定尺寸。

    Args:
        img_bytes (BytesIO): 從 Streamlit 上傳的圖片位元組數據。
        target_width (int): 縮放後的目標寬度。
        target_height (int): 縮放後的目標高度。

    Returns:
        bytes: RGB565 格式的二進位數據。
    """
    try:
        # 開啟圖片並確保其為 RGB 格式
        img = Image.open(img_bytes).convert('RGB')
        
        # 根據使用者輸入的尺寸，重新調整圖片大小
        # Image.Resampling.LANCZOS 是一種高品質的濾波演算法，適合縮圖
        img_resized = img.resize((target_width, target_height), Image.Resampling.LANCZOS)
        
        pixels = list(img_resized.getdata())
        
        # 使用一個在記憶體中的二進位流來儲存結果
        output_bin = io.BytesIO()

        # 遍歷每個像素並進行轉換
        for pix in pixels:
            # 將 8-bit R, G, B 轉換為 5-bit R, 6-bit G, 5-bit B
            r = (pix[0] >> 3) & 0x1F
            g = (pix[1] >> 2) & 0x3F
            b = (pix[2] >> 3) & 0x1F
            
            # 將轉換後的 R, G, B 值組合成一個 16-bit 的數值
            # 格式: RRRRRGGGGGGBBBBB
            rgb565_pixel = (r << 11) | (g << 5) | b
            
            # 將 16-bit 數值打包成 2 個位元組 (使用大端序)
            output_bin.write(pack('>H', rgb565_pixel))
        
        # 回到流的開頭，以便讀取其內容
        output_bin.seek(0)
        return output_bin.getvalue()

    except Exception as e:
        st.error(f"圖片處理時發生錯誤：{e}")
        return None

def convert_rgb565_to_image(rgb565_data, width, height):
    """
    將 RGB565 格式的位元組數據轉換回 PIL Image 物件。
    此函數用於預覽轉換後的圖片，將 16-bit RGB565 像素解碼回 8-bit RGB。

    Args:
        rgb565_data (bytes): RGB565 格式的二進位數據。
        width (int): 圖片的寬度。
        height (int): 圖片的高度。

    Returns:
        PIL.Image.Image: 轉換後的 PIL Image 物件。
    """
    img = Image.new('RGB', (width, height))
    pixels = []
    # 每個 RGB565 像素佔 2 個位元組
    for i in range(0, len(rgb565_data), 2):
        # 解包 16-bit 數值 (使用大端序)
        # '>H' 表示大端序 (Big-endian) 的無符號短整數 (unsigned short)
        rgb565_pixel = unpack('>H', rgb565_data[i:i+2])[0]

        # 將 16-bit RGB565 轉換回 8-bit R, G, B
        # R (5 bits): 提取最高 5 位，然後左移 3 位並複製最高 2 位到最低 2 位進行近似擴展
        r = (rgb565_pixel >> 11) & 0x1F
        r = (r << 3) | (r >> 2) 
        
        # G (6 bits): 提取中間 6 位，然後左移 2 位並複製最高 2 位到最低 2 位進行近似擴展
        g = (rgb565_pixel >> 5) & 0x3F
        g = (g << 2) | (g >> 4)
        
        # B (5 bits): 提取最低 5 位，然後左移 3 位並複製最高 2 位到最低 2 位進行近似擴展
        b = (rgb565_pixel >> 0) & 0x1F
        b = (b << 3) | (b >> 2)

        pixels.append((r, g, b))
    
    # 將處理後的像素數據放入 PIL Image 物件
    img.putdata(pixels)
    return img

# --- Streamlit 網站介面 ---

# 設定頁面標題和圖示
st.set_page_config(page_title="RGB565 轉換器", page_icon="🖼️")

# 網站主標題
st.title("🖼️ 圖片轉 RGB565 格式工具")

# 網站說明
st.markdown("""
歡迎使用這個小工具！

您可以上傳一張圖片，**調整成您需要的尺寸**，然後系統會將其轉換為 **RAW RGB565** 格式的二進位檔案，並提供給您下載。
轉換完成後，您也可以直接在頁面上預覽轉換後的圖片。
""")

# 檔案上傳元件
uploaded_file = st.file_uploader(
    "請選擇一張圖片...", 
    type=["png", "jpg", "jpeg", "bmp"],
    help="支援常見的圖片格式"
)

# 如果使用者有上傳檔案
if uploaded_file is not None:
    
    # 為了獲取圖片尺寸，需要先用 PIL 開啟
    img_for_size = Image.open(uploaded_file)
    original_width, original_height = img_for_size.size

    # 顯示分隔線
    st.divider()

    # 建立兩欄佈局
    col1, col2 = st.columns(2)

    with col1:
        st.subheader("原始圖片預覽")
        # 顯示使用者上傳的原始圖片
        # 已將 use_column_width=True 更新為 use_container_width=True
        st.image(uploaded_file, caption=f"原始尺寸: {original_width} x {original_height}", use_container_width=True)

    with col2:
        st.subheader("調整尺寸與轉換")
        st.write("請在下方選擇調整方式。")
        
        # 選擇調整方式：指定尺寸或指定檔案大小
        resize_method = st.radio(
            "選擇調整方式：",
            ("指定尺寸", "指定檔案大小"),
            index=0, # 預設為指定尺寸
            help="您可以直接設定圖片的寬高，或設定目標檔案大小讓系統自動計算尺寸。"
        )

        new_width = original_width
        new_height = original_height
        info_message = ""

        if resize_method == "指定尺寸":
            # 讓使用者輸入新的寬度和高度，預設值為原始尺寸
            new_width = st.number_input(
                "新的寬度 (Width)", 
                min_value=1, 
                value=original_width, 
                step=1,
                help="設定轉換後圖片的寬度（單位：像素）"
            )
            new_height = st.number_input(
                "新的高度 (Height)", 
                min_value=1, 
                value=original_height, 
                step=1,
                help="設定轉換後圖片的高度（單位：像素）"
            )
            info_message = f"目標尺寸: {new_width} x {new_height}"

        elif resize_method == "指定檔案大小":
            target_file_size_kb = st.number_input(
                "目標檔案大小 (KB)",
                min_value=1,
                value=int((original_width * original_height * 2) / 1024), # 預設為原始 RGB565 大小
                step=1,
                help="設定轉換後的 .raw 檔案的目標大小（單位：KB）。系統將根據此大小自動調整圖片尺寸。"
            )

            # 計算目標像素數量 (每個 RGB565 像素佔 2 bytes)
            target_pixels = (target_file_size_kb * 1024) / 2
            
            # 原始圖片的像素數量
            original_pixels = original_width * original_height

            if target_pixels >= original_pixels:
                # 如果目標大小大於或等於原始圖片的 RGB565 大小，則使用原始尺寸
                new_width = original_width
                new_height = original_height
                info_message = f"目標檔案大小為 {target_file_size_kb} KB，原始圖片尺寸已符合要求，將使用原始尺寸: {new_width} x {new_height}"
            else:
                # 根據目標像素數量計算新的尺寸，保持長寬比
                aspect_ratio = original_width / original_height
                
                # 計算新的高度和寬度
                calculated_height = math.sqrt(target_pixels / aspect_ratio)
                calculated_width = calculated_height * aspect_ratio

                # 轉換為整數，並確保至少為 1 像素
                new_width = max(1, int(calculated_width))
                new_height = max(1, int(calculated_height))
                
                # 重新檢查計算後的實際像素數，以防浮點數誤差導致超出目標大小
                # 如果計算出的尺寸導致實際像素數過大，則進一步微調
                while (new_width * new_height * 2) / 1024 > target_file_size_kb and (new_width > 1 or new_height > 1):
                    if new_width / aspect_ratio > new_height: # 哪個維度更大，就先縮小哪個
                        new_width = max(1, new_width - 1)
                    else:
                        new_height = max(1, new_height - 1)
                    # 重新計算另一個維度以保持比例
                    if new_width / aspect_ratio < new_height:
                        new_height = max(1, int(new_width / aspect_ratio))
                    else:
                        new_width = max(1, int(new_height * aspect_ratio))
                    
                info_message = f"目標檔案大小為 {target_file_size_kb} KB，自動調整尺寸為: {new_width} x {new_height}"

        # 轉換按鈕
        if st.button("🚀 開始轉換", use_container_width=True, type="primary"):
            st.info(info_message)
            # 進行圖片轉換
            with st.spinner('正在轉換中，請稍候...'):
                raw_data = convert_image_to_rgb565(uploaded_file, new_width, new_height)

            if raw_data:
                # 產生下載檔名
                base_filename = os.path.splitext(uploaded_file.name)[0]
                download_filename = f"{base_filename}_{new_width}x{new_height}_rgb565.raw"

                # 提供下載按鈕
                st.download_button(
                    label="✅ 下載 .raw 檔案",
                    data=raw_data,
                    file_name=download_filename,
                    mime="application/octet-stream",
                    use_container_width=True
                )
                
                # 顯示檔案大小資訊
                raw_data_size_kb = len(raw_data) / 1024
                st.success(f"轉換成功！\n\n實際檔案大小: {raw_data_size_kb:.2f} KB")

                # 顯示轉換後的圖片預覽
                st.subheader("轉換後圖片預覽 (RGB565)")
                # 將 RGB565 數據轉換回圖片以便預覽
                converted_img = convert_rgb565_to_image(raw_data, new_width, new_height)
                # 已將 use_column_width=True 更新為 use_container_width=True
                st.image(converted_img, caption=f"轉換後尺寸: {new_width} x {new_height}", use_container_width=True)

# 頁腳
st.divider()
st.markdown("<div style='text-align: center;'>由 Gemini 協助開發</div>", unsafe_allow_html=True)

```



```python=
# main.py (顯示圖片專用 - 最終且最簡潔的版本)

import time
from machine import Pin, SPI
from ili9341 import Display, color565 # 確保 ili9341.py 和 main.py 在同一個目錄下

# --- 圖片檔案路徑 ---
IMAGE_FILE_PATH = 'rgb565.raw' # 假設圖片直接放在根目錄

# --- 硬體初始化 ---
print("--- Initializing Hardware ---")
PIN_SPI_SCK = 18
PIN_SPI_MOSI = 19
PIN_SPI_MISO = 16 # 通常 MISO 對於 LCD 寫入是不需要的，但保留
PIN_TFT_CS = 13
PIN_TFT_RST = 14
PIN_TFT_DC = 15
PIN_TFT_LED = 12

spi = SPI(0, baudrate=40000000, sck=Pin(PIN_SPI_SCK), mosi=Pin(PIN_SPI_MOSI), miso=Pin(PIN_SPI_MISO))
tft_cs = Pin(PIN_TFT_CS, Pin.OUT, value=1)
tft_rst = Pin(PIN_TFT_RST, Pin.OUT, value=1)
tft_dc = Pin(PIN_TFT_DC, Pin.OUT, value=1)
tft_led = Pin(PIN_TFT_LED, Pin.OUT, value=1) # 通常用於控制背光，保持開啟

# --- 顯示器實例化 ---
print("--- Initializing Display ---")
# 確保 rotation 設定符合你的物理顯示方向
# 如果你的螢幕是 320x240，rotation=270 應會將 240寬 x 320高 轉為 320寬 x 240高
display = Display(spi, dc=tft_dc, cs=tft_cs, rst=tft_rst, width=320, height=240, rotation=270)
display.clear(color565(0, 0, 0)) # 清除螢幕為黑色

# --- 主程式流程 ---
print("--- Starting Image Display ---")

try:
    # 直接呼叫 Display 物件的 draw_image 方法，並傳入檔案路徑
    # 庫內部會處理檔案開啟、讀取、分塊和顯示
    # 預設參數 x=0, y=0, w=320, h=240 剛好符合你的 320x240 圖片需求
    display.draw_image(IMAGE_FILE_PATH)
    print(f"Successfully displayed {IMAGE_FILE_PATH}.")
except OSError as e:
    print(f"Error: Could not load or display image '{IMAGE_FILE_PATH}'.")
    print(f"Reason: {e}")
    print("請確保圖片檔案存在且為正確的 320x240 RGB565 原始格式。")
    display.clear(color565(255, 0, 0)) # 螢幕顯示紅色背景作為錯誤指示
except Exception as e:
    print(f"發生意外錯誤: {e}")
    display.clear(color565(255, 0, 0))

# --- 主迴圈 ---
while True:
    # 圖片會持續顯示，這裡只是讓程式保持運行
    time.sleep(10)

```