Kivy Android摄像头应用开发

发布于:2025-07-06 ⋅ 阅读:(16) ⋅ 点赞:(0)

Kivy Android摄像头应用开发

📱 应用效果展示

  • 实时日志查看:完整的日志系统记录所有操作和错误
  • 画面旋转:支持90度增量旋转摄像头画面
  • 智能摄像头切换:优化切换逻辑防止频繁切换导致的闪退
  • 软件授权限完成后需要重新退出再打开软件即可调用摄像头
通过网盘分享的知识:kivy
链接: https://pan.baidu.com/s/5WzNo_X8FM6g4xfPItvpHfw 
--来自百度网盘超级会员v3的分享

应用界面截图


项目目录结构如下(字体和软件图标及启动图标需自行准备):

./
├── main.py
├── buildozer.spec
├── fonts/
│   └── simkai.ttf
├── data/
│   ├── presplash.png
│   └── icon.png

🐍 Python 核心代码

import os
import sys
import traceback
import time
import logging
from datetime import datetime
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.camera import Camera
from kivy.clock import Clock
from kivy.properties import NumericProperty, StringProperty, BooleanProperty
from kivy.core.text import LabelBase
from kivy.resources import resource_add_path, resource_find
from kivy.graphics import Rotate  # 添加旋转功能

# 字体目录,自己在项目目录下创建一个
fonts_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fonts')
resource_add_path(fonts_path)
LabelBase.register(name='Roboto', fn_regular='simkai.ttf')


# ==================== 增强日志系统 ====================
class CrossPlatformLogHandler(logging.Handler):
    def __init__(self, log_dir_name="logs", file_prefix="app"):
        super().__init__()
        self.setFormatter(logging.Formatter(
            '%(asctime)s [%(levelname)s] %(message)s'
        ))
        self.log_dir_name = log_dir_name
        self.file_prefix = file_prefix
        self._android = self._check_android()

    def _check_android(self):
        try:
            from jnius import autoclass
            autoclass('android.os.Build')
            return True
        except:
            return False

    def get_log_path(self):
        """获取跨平台的日志存储路径"""
        if self._android:
            try:
                from jnius import autoclass
                PythonActivity = autoclass('org.kivy.android.PythonActivity')
                context = PythonActivity.mActivity
                files_dir = context.getExternalFilesDir(None)
                base_path = files_dir.getAbsolutePath()
            except Exception as e:
                base_path = "/sdcard/Android/data/org.kivy.camerademo/files"
        else:
            base_path = os.getcwd()

        full_path = os.path.join(base_path, self.log_dir_name)
        os.makedirs(full_path, exist_ok=True)
        return full_path

    def get_current_filename(self):
        """生成按日期的日志文件名"""
        date_str = datetime.now().strftime("%Y%m%d")
        return f"{self.file_prefix}_{date_str}.log"

    def emit(self, record):
        try:
            log_dir = self.get_log_path()
            filename = self.get_current_filename()
            log_file = os.path.join(log_dir, filename)

            with open(log_file, "a", encoding="utf-8") as f:
                f.write(self.format(record) + "\n")

            # 在Android上同时输出到ADB
            if self._android:
                try:
                    from jnius import autoclass
                    Log = autoclass('android.util.Log')
                    log_msg = self.format(record).replace('\n', ' \\n ')
                    if record.levelno >= logging.ERROR:
                        Log.e("CameraApp", log_msg)
                    elif record.levelno >= logging.WARNING:
                        Log.w("CameraApp", log_msg)
                    elif record.levelno >= logging.INFO:
                        Log.i("CameraApp", log_msg)
                    else:
                        Log.d("CameraApp", log_msg)
                except Exception as adb_ex:
                    sys.stderr.write(f"ADB日志失败: {str(adb_ex)}\n")
        except Exception as e:
            sys.stderr.write(f"日志写入失败: {str(e)}\n")


class LogSystem:
    def __init__(self, level=logging.DEBUG, log_dir="logs", file_prefix="app"):
        self.logger = logging.getLogger("CameraApp")
        self.logger.setLevel(level)

        # 清除所有现有处理器
        for handler in self.logger.handlers[:]:
            self.logger.removeHandler(handler)

        # 添加控制台处理器
        console_handler = logging.StreamHandler(sys.stdout)
        console_handler.setFormatter(logging.Formatter(
            '%(asctime)s [%(levelname)s] %(message)s'
        ))
        self.logger.addHandler(console_handler)

        # 添加跨平台文件处理器
        file_handler = CrossPlatformLogHandler(log_dir, file_prefix)
        self.logger.addHandler(file_handler)

        # 设置全局异常捕获
        sys.excepthook = self._global_except_hook

    def _global_except_hook(self, exctype, value, tb):
        error_msg = "".join(traceback.format_exception(exctype, value, tb))
        self.logger.critical(f"未捕获异常:\n{error_msg}")
        sys.__excepthook__(exctype, value, tb)
        sys.exit(1)


# 初始化日志系统
log_system = LogSystem()
logger = log_system.logger

# ==================== Kivy应用代码 ====================
Builder.load_string('''
<CameraWidget>:
    orientation: 'vertical'
    Label:
        text: root.status_message
        color: (1, 0, 0, 1) if root.error_state else (0, 1, 0, 1)
        size_hint_y: None
        height: '48dp'
    BoxLayout:
        id: camera_container
        size_hint_y: 0.7
        # 添加旋转画布
        canvas.before:
            PushMatrix
            Rotate:
                angle: root.rotation_angle
                origin: self.center
        canvas.after:
            PopMatrix
    BoxLayout:
        size_hint_y: None
        height: '48dp'
        spacing: '10dp'
        padding: '10dp'
        Button:
            text: '切换摄像头' if root.camera_active else '启动摄像头'
            on_press: root.switch_camera() if root.camera_active else root.initialize_camera()
            disabled: not root.permission_granted
        Button:
            text: '停止摄像头'
            on_press: root.stop_camera()
            disabled: not root.camera_active
        Button:
            text: '重试权限'
            on_press: root.retry_permissions()
        Button:
            text: '旋转画面'
            on_press: root.rotate_camera()
            disabled: not root.camera_active
        Button:
            text: '调试日志'
            on_press: root.open_logs()
''')


class CameraWidget(BoxLayout):
    cam_index = NumericProperty(0)
    status_message = StringProperty("正在初始化...")
    camera_active = BooleanProperty(False)
    permission_granted = BooleanProperty(False)
    error_state = BooleanProperty(False)
    rotation_angle = NumericProperty(0)  # 添加旋转角度属性

    # 兼容性设置
    RESOLUTIONS = [
        (640, 480),  # 480p
        (320, 240),  # 240p (低端设备备用)
        (1280, 720),  # 720p
        (1920, 1080)  # 1080p
    ]

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        logger.info("CameraWidget初始化开始")
        self.camera = None
        self.current_resolution = 0
        self.max_cameras = self.detect_camera_count()
        self.permissions_requested = False
        Clock.schedule_once(self.request_permissions, 1)
        logger.info(f"检测到 {self.max_cameras} 个摄像头")

    def detect_camera_count(self):
        """尝试检测可用摄像头数量"""
        try:
            from jnius import autoclass
            Context = autoclass('android.content.Context')
            CameraManager = autoclass('android.hardware.camera2.CameraManager')
            context = autoclass('org.kivy.android.PythonActivity').mActivity
            manager = context.getSystemService(Context.CAMERA_SERVICE)
            return len(manager.getCameraIdList())
        except Exception as e:
            logger.warning(f"摄像头检测失败: {e}")
            return 2  # 默认假设有2个摄像头

    def request_permissions(self, dt):
        """请求摄像头权限"""
        if self.permissions_requested:
            logger.info("权限已请求过,跳过")
            return

        logger.info("开始请求权限")
        self.permissions_requested = True

        try:
            from android.permissions import Permission, request_permissions, check_permission

            # Android 13+ 需要不同的摄像头权限
            android_version = self.get_android_version()
            logger.info(f"Android 版本: {android_version}")

            required_perms = [
                Permission.CAMERA,
                Permission.RECORD_AUDIO
            ]

            # 对于Android 13+,需要单独的摄像头权限
            if android_version >= 33:  # Android 13 (API 33)
                required_perms.append("android.permission.CAMERA")

            # 存储权限(仅Android 10以下需要)
            if android_version < 29:  # Android 10 (API 29)
                required_perms.append(Permission.WRITE_EXTERNAL_STORAGE)
                required_perms.append(Permission.READ_EXTERNAL_STORAGE)

            logger.info(f"需要权限: {required_perms}")

            # 检查是否已有权限
            has_perms = True
            for perm in required_perms:
                if not check_permission(perm):
                    has_perms = False
                    logger.warning(f"缺少权限: {perm}")

            if has_perms:
                self.permission_granted = True
                logger.info("所有权限已授予")
                self.initialize_camera()
            else:
                logger.info("请求权限中...")
                self.status_message = "请求权限中..."

                def callback(permissions, results):
                    logger.info(f"权限回调: {permissions} -> {results}")

                    # 检查是否全部授予
                    granted = True
                    for i, perm in enumerate(permissions):
                        if not results[i]:
                            logger.warning(f"权限被拒绝: {perm}")
                            granted = False

                    self.permission_granted = granted

                    if self.permission_granted:
                        logger.info("用户授予了所有权限")
                        self.initialize_camera()
                    else:
                        logger.error("用户拒绝了某些权限")
                        self.status_message = "权限被拒绝!请点击'重试权限'"
                        self.error_state = True

                request_permissions(required_perms, callback)

        except Exception as e:
            logger.error(f"权限请求异常: {e}")
            # 非Android平台
            self.permission_granted = True
            self.initialize_camera()

    def get_android_version(self):
        """获取Android API级别"""
        try:
            from jnius import autoclass
            Build_VERSION = autoclass('android.os.Build$VERSION')
            return Build_VERSION.SDK_INT
        except:
            return 0  # 未知版本

    def initialize_camera(self):
        """初始化摄像头"""
        if not self.permission_granted:
            logger.warning("尝试初始化但没有权限")
            self.status_message = "需要摄像头权限"
            return

        logger.info(f"初始化摄像头 (索引: {self.cam_index})")
        self.stop_camera()  # 确保先停止现有摄像头
        self.rotation_angle = 0  # 重置旋转角度

        try:
            resolution = self.RESOLUTIONS[self.current_resolution]
            logger.info(f"尝试分辨率: {resolution[0]}x{resolution[1]}")

            # 创建摄像头实例
            self.camera = Camera(
                index=self.cam_index,
                resolution=resolution,
                play=True,
                allow_stretch=True
            )

            # 添加摄像头到界面
            self.ids.camera_container.add_widget(self.camera)

            # 检查摄像头状态
            Clock.schedule_once(self.verify_camera_status, 1.0)

        except Exception as e:
            logger.error(f"摄像头初始化失败: {e}")
            self.try_fallback()
    def verify_camera_status(self, dt):
        """验证摄像头是否真正工作"""
        if not self.camera:
            logger.warning("摄像头实例不存在")
            self.status_message = "摄像头创建失败"
            return

        # 等待摄像头初始化
        if not hasattr(self.camera, '_camera') or self.camera._camera is None:
            logger.error("摄像头硬件连接失败")
            self.status_message = "摄像头硬件连接失败"
            self.error_state = True
            self.try_fallback()
            return

        # 检查纹理
        Clock.schedule_once(self.check_texture, 1.0)

    def check_texture(self, dt):
        """检查摄像头纹理是否可用"""
        if self.camera.texture is None:
            logger.error("摄像头纹理为空,画面获取失败")
            self.status_message = "摄像头画面获取失败"
            self.error_state = True
            self.try_fallback()
        else:
            self.status_message = f"摄像头 {self.cam_index} 已启用"
            self.camera_active = True
            self.error_state = False
            logger.info(f"摄像头成功启动: {self.camera.resolution}")

    def try_fallback(self):
        """尝试备用方案"""
        logger.info("尝试备用方案...")
        self.stop_camera()

        # 1. 先尝试不同分辨率
        self.current_resolution += 1
        if self.current_resolution < len(self.RESOLUTIONS):
            logger.info(f"尝试备用分辨率 #{self.current_resolution}")
            self.initialize_camera()
            return

        # 2. 尝试切换摄像头
        logger.info("尝试切换摄像头")
        self.current_resolution = 0
        self.cam_index = (self.cam_index + 1) % self.max_cameras
        if self.cam_index == 0:
            # 已经尝试了所有摄像头和分辨率
            self.status_message = "无法初始化任何摄像头"
            self.error_state = True
            logger.critical("所有备用方案都失败了")
        else:
            logger.info(f"切换到摄像头 #{self.cam_index}")
            self.initialize_camera()

    def stop_camera(self):
        """停止摄像头"""
        logger.info("停止摄像头")
        if self.camera:
            try:
                self.ids.camera_container.clear_widgets()
                if hasattr(self.camera, 'play'):
                    self.camera.play = False
                self.camera = None
                logger.debug("摄像头资源已释放")
            except Exception as e:
                logger.error(f"停止摄像头错误: {e}")

        self.camera_active = False
        self.rotation_angle = 0  # 停止摄像头时重置旋转角度

    def switch_camera(self):
        """切换前后摄像头"""
        logger.info("切换摄像头")
        if not self.camera_active:
            return

        # 保存当前索引以便回退
        prev_index = self.cam_index

        try:
            self.stop_camera()
            self.cam_index = (self.cam_index + 1) % self.max_cameras
            self.current_resolution = 0  # 重置分辨率尝试
            self.initialize_camera()
        except Exception as e:
            logger.error(f"切换摄像头失败: {e}")
            # 切换失败时回退到之前的摄像头
            self.cam_index = prev_index
            self.status_message = f"切换失败! 使用摄像头 {self.cam_index}"
            self.error_state = True
            # 尝试重新初始化之前的摄像头
            Clock.schedule_once(lambda dt: self.initialize_camera(), 0.5)

    def retry_permissions(self):
        """重新请求权限"""
        logger.info("用户请求重试权限")
        self.permissions_requested = False
        self.error_state = False
        self.status_message = "重新请求权限中..."
        Clock.schedule_once(self.request_permissions, 0.5)
        
    def rotate_camera(self):
        """旋转摄像头画面90度"""
        if not self.camera_active:
            return
            
        # 每次点击增加90度,360度后重置为0
        self.rotation_angle = (self.rotation_angle + 90) % 360
        logger.info(f"旋转画面到: {self.rotation_angle}度")
        self.status_message = f"画面旋转: {self.rotation_angle}度"

    def open_logs(self):
        """打开日志目录"""
        logger.info("用户请求查看日志")
        try:
            if self._is_android():
                self._open_logs_android()
            else:
                self._open_logs_desktop()
        except Exception as e:
            logger.error(f"打开日志失败: {e}")
            self.status_message = f"无法打开日志: {str(e)}"

    def _is_android(self):
        """检查是否在Android上运行"""
        try:
            from jnius import autoclass
            autoclass('android.os.Build')
            return True
        except:
            return False

    def _open_logs_android(self):
        """在Android上打开日志目录"""
        from jnius import autoclass, cast
        try:
            # 获取日志目录路径
            Context = autoclass('android.content.Context')
            PythonActivity = autoclass('org.kivy.android.PythonActivity')
            context = PythonActivity.mActivity.getApplicationContext()
            files_dir = context.getExternalFilesDir(None)
            log_path = os.path.join(files_dir.getAbsolutePath(), "logs")

            # 使用文件浏览器打开
            Intent = autoclass('android.content.Intent')
            Uri = autoclass('android.net.Uri')
            intent = Intent(Intent.ACTION_VIEW)
            intent.setDataAndType(Uri.parse(f"file://{log_path}"), "resource/folder")
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

            # 检查是否有文件浏览器应用
            PackageManager = autoclass('android.content.pm.PackageManager')
            pm = context.getPackageManager()
            if intent.resolveActivity(pm) is not None:
                context.startActivity(intent)
                logger.info("已打开日志目录")
                self.status_message = "已打开日志目录"
            else:
                logger.warning("没有找到文件浏览器应用")
                self.status_message = "请使用文件浏览器查看日志目录"

        except Exception as e:
            logger.error(f"Android打开日志失败: {e}")
            self.status_message = "打开日志失败,请查看ADB日志"

    def _open_logs_desktop(self):
        """在桌面平台上打开日志目录"""
        import platform
        import subprocess
        try:
            log_path = os.path.join(os.getcwd(), "logs")
            os.makedirs(log_path, exist_ok=True)

            system = platform.system()
            if system == "Windows":
                os.startfile(log_path)
            elif system == "Darwin":  # macOS
                subprocess.Popen(["open", log_path])
            else:  # Linux
                subprocess.Popen(["xdg-open", log_path])

            logger.info(f"在桌面平台打开了日志目录: {log_path}")
            self.status_message = f"日志目录已打开: {log_path}"
        except Exception as e:
            logger.error(f"桌面平台打开日志失败: {e}")
            self.status_message = f"无法打开日志: {str(e)}"


class CameraApp(App):
    def build(self):
        logger.info("应用启动")
        return CameraWidget()

    def on_pause(self):
        logger.info("应用进入暂停状态")
        if hasattr(self.root, 'stop_camera'):
            self.root.stop_camera()
        return True

    def on_resume(self):
        logger.info("应用恢复运行")
        if hasattr(self.root, 'initialize_camera'):
            Clock.schedule_once(lambda dt: self.root.initialize_camera(), 0.5)


if __name__ == '__main__':
    try:
        logger.info("===== 应用程序启动 =====")
        CameraApp().run()
    except Exception as e:
        logger.critical(f"应用程序崩溃: {traceback.format_exc()}")
        sys.exit(1)

⚙️ Buildozer 配置 (buildozer.spec)

[app]

# 应用基本信息
title = MyCameraApp
package.name = camerademo
package.domain = org.kivy
version = 1.4.2
source.dir = .

# 主程序入口
source.include_exts = py,png,jpg,kv,atlas,ttf
main.py = main

# 使用 Pyjnius 的 GitHub 主分支(包含修复)
requirements = 
    python3,
    kivy==2.2.0,
    android,
    https://github.com/kivy/pyjnius/archive/master.zip,
    openssl,
    pillow
source.include_patterns = fonts/simkai.ttf

# Android特定配置
android.permissions = CAMERA, RECORD_AUDIO, WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE, WAKE_LOCK, android.permission.CAMERA

android.manifest_features = 
    <uses-feature android:name="android.hardware.camera" android:required="true"/>
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
    <uses-feature android:name="android.hardware.microphone" android:required="true"/>
    
android.add_manifest_application = 
    android:requestLegacyExternalStorage="true",
    android:usesCleartextTraffic="true"

# 硬件配置
android.arch = arm64-v8a, armeabi-v7a
android.api = 33
android.minapi = 21
android.ndk = 25b
android.sdk = 34

# 窗口设置
orientation = portrait
fullscreen = 0
allow.android.permission_requests = 1

# 添加Presplash启动画面
presplash.filename = %(source.dir)s/data/presplash.png
icon.filename = %(source.dir)s/data/icon.png

# 日志级别
log_level = 2

# 预构建命令 - 更可靠的修复
prebuild = 
    # 确保使用正确的 Cython 版本
    pip install --upgrade cython==0.29.36
    
    # 应用 Pyjnius 补丁
    if [ -d "{{ project_dir }}/.buildozer/android/platform" ]; then
        find {{ project_dir }}/.buildozer/android/platform/build-* -name jnius_utils.pxi -exec sed -i 's/(isinstance(arg, long) and arg < 2147483648)/arg < 2147483648/g' {} \;
        find {{ project_dir }}/.buildozer/android/platform/build-* -name jnius_utils.pxi -exec sed -i 's/from __builtin__ import long, int, float/# Removed builtin import/g' {} \;
    fi

[buildozer]
# 构建控制
log_level = 2
warn_on_root = 1
android.accept_sdk_license = True

⚠️ 重要注意事项

🧹 清理构建环境

# 彻底清理之前的构建
buildozer android clean
rm -r .buildozer

🔧 环境依赖

# 安装必要组件
pip install --upgrade cython==0.29.36
sudo apt install -y git unzip openjdk-17-jdk python3-pip autoconf libtool pkg-config zlib1g-dev libncurses5-dev libncursesw5-dev libtinfo5 cmake

📦 构建APK

# 重新构建APK
buildozer android debug

📊 日志调试技巧

# 查看所有日志
adb logcat

# 过滤关键错误
adb logcat -s CameraApp:E

# 保存日志到文件
adb logcat -s CameraApp > debug_log.txt

# 查看Python相关日志
adb logcat | grep python

💡 关键功能说明

功能 描述 实现要点
摄像头切换 前后摄像头切换 防抖机制防止频繁切换导致的闪退
画面旋转 90度增量旋转 使用Kivy的Rotate图形指令
权限管理 动态请求摄像头权限 兼容Android不同版本权限系统
日志系统 跨平台日志记录 同时输出到文件和ADB控制台
异常处理 全局异常捕获 自动记录未捕获的异常信息
兼容性 多分辨率支持 自动降级适配低端设备

提示:在低端设备上,建议优先使用640×480分辨率以确保流畅运行


网站公告

今日签到

点亮在社区的每一天
去签到