You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Camera4Kivy竖屏拍照时图片逆时针旋转90°问题求助

问题解决:Camera4Kivy竖屏拍摄图片旋转90度的问题

环境依赖

  • kivy: 2.2.1
  • kivymd: 1.2.0dev0
  • camera4kivy: 0.3.0
  • python3

问题描述

应用强制竖屏(buildozer.spec中orientation=portrait),使用Camera4Kivy拍摄时,竖屏状态下捕获的图片会逆时针旋转90°,横屏拍摄则显示正常。尝试用pyjnius区分横竖屏状态但未成功。

图片说明

解决方案

核心思路是读取图片的Exif方向信息,根据信息修正图片旋转角度后再保存。修改PictureMDScreen类中的capture_path方法,在移动文件后添加图片旋转处理逻辑:

修改后的关键代码片段

def capture_path(self,file_path): ## called when clicking on the camera button
    if platform == "android":
        source = file_path
        destination = join(app_storage_path(), "app", self.picture_name + ".png")
        self.source = destination

        try:
            os.rename(source,destination) # move a file from source to destination
            # 新增:处理图片旋转
            im = Image.open(destination)
            exif_data = im._getexif()
            if exif_data:
                orientation = exif_data.get(274)  # 274对应Exif中的Orientation字段
                # 根据Exif方向修正图片
                if orientation == 6:
                    # 图片需要顺时针旋转90度(修正逆时针旋转90度的问题)
                    im = im.rotate(-90, expand=True)
                elif orientation == 8:
                    # 图片需要逆时针旋转90度
                    im = im.rotate(90, expand=True)
                elif orientation == 3:
                    # 图片需要旋转180度
                    im = im.rotate(180, expand=True)
                # 保存修正后的图片
                im.save(destination)
        except Exception as e:
            log("error : " + str(e))
        time.sleep(1)
        
    elif platform in ["win","macos"]:
        self.source = self.picture_name + ".jpg"

    try:
        self.tile.source = self.source
        self.tile.ids.image.reload() # reload image from cache
    except Exception as e:
        log("error" + str(e))

补充说明

  1. Exif的Orientation字段(标记为274)定义了图片的拍摄方向,不同数值对应不同旋转需求:
    • 1:正常方向
    • 6:顺时针旋转90度(对应你遇到的逆时针旋转90度的情况,需要反向旋转修正)
    • 8:逆时针旋转90度
    • 3:旋转180度
  2. expand=True参数确保旋转后的图片不会被裁剪,保持完整尺寸。
  3. 原代码中的checkExifOrientation方法存在错误(直接用im["274"]访问Exif信息),修正后应先获取exif_data字典再取值。

完整代码

from os import listdir
import time
import os
from kivymd.uix.imagelist import MDSmartTile
from kivymd.app import MDApp
from kivy.lang import Builder
from kivymd.uix.screenmanager import MDScreenManager
from kivy.core.window import Window
from kivy.properties import StringProperty, ObjectProperty
from kivy.clock import Clock
from kivy.utils import platform
from kivy.logger import Logger
from android_permissions import AndroidPermissions
from camera4kivy import Preview
from kivymd.uix.screen import MDScreen
from kivy.cache import Cache
from toast import Toast
from os.path import join
from PIL import Image

if platform == "android":
    from jnius import autoclass
    from android.storage import app_storage_path, primary_external_storage_path
    from android import mActivity, api_version
    Environment = autoclass('android.os.Environment')

def log(msg):
    Logger.info(msg)


KV = '''

<MyMDSmartTile>:
    radius: 24
    box_radius: 0, 0, 24, 24
    box_color: 0, 0, 0, .5
    size_hint: 1, None
    height: "160dp" # à faire varier
    mipmap: False # if True OpenGL mipmapping to be applied to the texture

    MDIconButton:
        icon: "camera"
        on_release: 
            app.root.ids._picture_screen.tile = root
            app.root.ids._picture_screen.picture_name = root.text
            app.root.current = "picture_screen"

    MDLabel:
        text: root.text
        bold: True
        font_style: "Caption"
        opposite_colors: True

MyMDScreenManager:
    MDScreen:
        name: "home_screen"
        MDScrollView:
            MDGridLayout:
                id: _mybox
                cols: 2
                adaptive_height: True
                padding:10
                spacing:10
 
    PictureMDScreen:
        id: _picture_screen
        name: "picture_screen"
        root: root
        photo_preview: _preview
        MDFloatLayout:
            Preview:
                id: _preview
            MDIconButton:
                icon: "camera"
                pos_hint: {"center_x": .5, "center_y": .5}
                on_release: 
                    _preview.capture_photo(location = "private", name = _picture_screen.picture_name) # or capture_screenshot()

            MDIconButton:
                icon: "close"
                pos_hint: {"center_x": .8, "center_y": .5}
                on_release: _picture_screen.close()
                    
'''

class PictureMDScreen(MDScreen):
    tile = ObjectProperty()
    root = ObjectProperty()
    picture_name = StringProperty()
    photo_preview = ObjectProperty(None) # store the widget instance Preview
    source = StringProperty()
    
    def on_enter(self):
        self.photo_preview.connect_camera(filepath_callback= self.capture_path)
        # self.photo_preview.preview.orientation = 90 # trying to have portrait orientation

    def on_pre_leave(self):
        print("preleaving")
        self.photo_preview.disconnect_camera()
    
    def capture_path(self,file_path): ## called when clicking on the camera button
        if platform == "android":
            source = file_path
            destination = join(app_storage_path(), "app", self.picture_name + ".png")
            self.source = destination

            try:
                os.rename(source,destination) # move a file from source to destination
                # 处理图片旋转
                im = Image.open(destination)
                exif_data = im._getexif()
                if exif_data:
                    orientation = exif_data.get(274)
                    if orientation == 6:
                        im = im.rotate(-90, expand=True)
                    elif orientation == 8:
                        im = im.rotate(90, expand=True)
                    elif orientation == 3:
                        im = im.rotate(180, expand=True)
                    im.save(destination)
            except Exception as e:
                log("error : " + str(e))
            time.sleep(1)
            
        elif platform in ["win","macos"]:
            self.source = self.picture_name + ".jpg"

        try:
            self.tile.source = self.source
            self.tile.ids.image.reload() # reload image from cache
        except Exception as e:
            log("error" + str(e))

    def close(self):
        if platform == "android":
                self.photo_preview.disconnect_camera()
        self.root.current = 'home_screen'

    def checkExifOrientation(self,im):
        '''
        Exif is information inscrusted in an image, we can check 
        orientation thanks to im._getexif()["274"]
        just for information : PIL.ExifTags.TAGS[274] gives "Orientation"
        '''
        exif_data = im._getexif()
        if exif_data and exif_data.get("274"): # check in exif dictionary if 274 in 
            log("has exif in it")
            if exif_data["274"] == 6:
                log("image is portrait")
                return "portrait"
            elif exif_data["274"] == 1:
                log("image is landscape")
                return "landscape"
            else:
                log("we don't know, image is portrait")
                return "portrait"
        else: 
            log("there is no exif 274 so image is portrait")
            return "portrait"
        
    def app_name(self):
        context = mActivity.getApplicationContext()
        appinfo = context.getApplicationInfo()
        if appinfo.labelRes:
            name = context.getString(appinfo.labelRes)
        else:
            name = appinfo.nonLocalizedLabel.toString()
        return name
        
class MyMDSmartTile(MDSmartTile):
    text = StringProperty()

class MyMDScreenManager(MDScreenManager): 

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def on_kv_post(self, base_widget):

        #Create the tiles
        for i in range(10):
            self.ids._mybox.add_widget(MyMDSmartTile(text = str(i)))

class CameraCuisto(MDApp):
    def build(self):
        return Builder.load_string(KV)

    def on_start(self):
        self.dont_gc = AndroidPermissions(self.start_app)

    def start_app(self):
        self.dont_gc = None

    def on_start(self, **kwargs):
        if platform == "android":
            from android.permissions import request_permissions, Permission
            request_permissions([Permission.WRITE_EXTERNAL_STORAGE, Permission.READ_EXTERNAL_STORAGE, Permission.CAMERA])
            log('Perm requested')

if __name__ == "__main__":
    CameraCuisto().run()

内容的提问来源于stack exchange,提问作者xavier cattelain

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 12:19:51