ファイルの保存先選択
Kivyを使ってAndroidで動くカメラアプリを作っていたところ、撮影した画像をポンポン本体に保存していたらすぐにAndroid本体の容量が足りなくなってしまうので、SDカードに保存されるようにしたくなりました。
保存先をSDカードにする方法を調べてみたところ、アプリで勝手にSDカードに保存先を作ることは出来なさそう(たしかに言われてみれば色々問題ありそう)。本体以外に保存するためにはユーザーで保存先を選べるようにする必要がありそう。
ということで、保存先を選択するやり方を実装サンプルとしてメモしておきます。
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy import platform
from kivy.clock import Clock
from kivy.graphics.texture import Texture
from kivy.uix.camera import Camera
import time
import cv2
import numpy as np
from kivy.properties import StringProperty
from kivy.uix.popup import Popup
from kivy.core.window import Window
currentActivity = None
CLS_Activity = None
CLS_Intent = None
CLS_Bundle = None
REQUEST_STORAGE_ACCESS = 1
class SaveToSelect(BoxLayout):
def play(self, button):
self.cam = Camera(index=0)
self.cam.play = True
Clock.schedule_interval(self.update, 1.0 / 30)
button.disabled = True
self.ids.capture_button.disabled = False
def update(self, dt):
height, width = self.cam.texture.height, self.cam.texture.width
capture = np.frombuffer(self.cam.texture.pixels, np.uint8)
capture = capture.reshape(height, width, 4)
frame = capture[:,:,:3]
if type(frame).__module__ == 'numpy':
ret = True
if platform == "android":
frame = cv2.flip(frame, 0)
frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
self.image_data = frame
if ret:
buf = cv2.flip(frame, 0)
image_texture = Texture.create(size=(frame.shape[1], frame.shape[0]), colorfmt='rgb')
image_texture.blit_buffer(buf.tostring(), colorfmt='rgb', bufferfmt='ubyte')
self.ids.camera_view.texture = image_texture
def capture(self):
if self.image_data is not None:
timestr = time.strftime("%Y%m%d_%H%M%S")
filename = "output_{}.png".format(timestr)
image_data = cv2.cvtColor(self.image_data, cv2.COLOR_RGB2BGR) # openCVの色の並びはBGR
self.android_write_storage(filename, image_data)
def file_save_callback(self, filepath):
if filepath != '':
message='save'
else:
message='cancel'
self.popup = MessagePopup(title='', message=message, separator_height=0)
self.popup.open()
def android_write_storage(self, file_name, image_data):
if platform == "android":
self.save_image_data = image_data
from android import activity
# 保存先選択
intent = CLS_Intent(CLS_Intent.ACTION_CREATE_DOCUMENT)
intent.setType('image/png')
bundle = CLS_Bundle()
bundle.putString(CLS_Intent.EXTRA_TITLE, file_name)
intent.putExtras(bundle)
intent.addCategory(CLS_Intent.CATEGORY_OPENABLE)
currentActivity.startActivityForResult(intent, REQUEST_STORAGE_ACCESS)
activity.unbind(on_activity_result=self.on_activity_result)
activity.bind(on_activity_result=self.on_activity_result)
def on_activity_result(self, request_code, result_code, intent):
try:
if request_code == REQUEST_STORAGE_ACCESS:
if result_code == CLS_Activity.RESULT_CANCELED:
# キャンセル
Clock.schedule_once(lambda dt: self.file_save_callback(''), 0)
if result_code != CLS_Activity.RESULT_OK:
raise NotImplementedError('Unknown result_code "{}"'.format(result_code))
treeUri = intent.getData() # Uri
if treeUri is None:
raise NotImplementedError('no data')
filepath = treeUri.getPath() # String
outputStream = currentActivity.getContentResolver().openOutputStream(treeUri)
# ストリームに書き出し
retval, buf = cv2.imencode('.png', self.save_image_data)
outputStream.write(buf.tobytes())
Clock.schedule_once(lambda dt: self.file_save_callback(filepath), 0)
finally:
from android import activity
activity.unbind(on_activity_result=self.on_activity_result)
class MessagePopup(Popup):
message = StringProperty('')
ok_text = StringProperty('OK')
__events__ = ('on_ok',)
def ok(self):
self.dispatch('on_ok')
self.dismiss()
def on_ok(self):
pass
def __init__(self, **kwargs) -> None:
super(MessagePopup, self).__init__(**kwargs)
self.size_hint = (0.8, 0.3)
self.pos_hint={'center_x':0.5, 'center_y':0.5}
self.auto_dismiss = False
class TestSaveToSelectApp(App):
def build(self):
if platform == 'android':
Window.bind(on_keyboard=self.key_input)
from jnius import autoclass, cast
global currentActivity
global CLS_Activity
global CLS_Intent
global CLS_Bundle
PythonActivity = autoclass('org.kivy.android.PythonActivity')
currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
CLS_Activity = autoclass('android.app.Activity')
CLS_Intent = autoclass('android.content.Intent')
CLS_Bundle = autoclass('android.os.Bundle')
from android.permissions import request_permissions, Permission
request_permissions([Permission.READ_EXTERNAL_STORAGE,
Permission.MANAGE_DOCUMENTS,
Permission.CAMERA,
Permission.WRITE_EXTERNAL_STORAGE])
return SaveToSelect()
def key_input(self, window, key, scancode, codepoint, modifier):
if key == 27:
return True
else:
return False
if __name__ == '__main__':
TestSaveToSelectApp().run()
確認用のKVファイル
<SaveToSelect>:
camera_view:camera_view
BoxLayout:
orientation: 'vertical'
Image:
id: camera_view
Button:
text: 'play'
size_hint_y: 0.1
on_press: root.play(self)
Button:
id:capture_button
text: 'capture'
size_hint_y: 0.1
disabled: True
on_press: root.capture()
<MessagePopup>:
FloatLayout:
Label:
size_hint: 0.8, 0.6
pos_hint: {'x': 0.1, 'y':0.4}
text: root.message
text_size: self.size
halign: 'center'
valign: 'middle'
Button:
size_hint: 0.4, 0.35
pos_hint: {'x':0.5, 'y':0.05}
text: root.ok_text
on_release: root.ok()
サンプルソースが長くなってしまいましたが、メインとなる関数は下記2つです。
・android_write_storage:保存先選択ツールを開きます。
・on_activity_result:選択から戻るときの処理。選択された場所にデータを出力します。
REQUEST_STORAGE_ACCESS は定数にしていますが数値は任意です。request_code はどこから戻ってきたのかを判別するもので、startActivityForResult に渡した値と同じ値で返ってきます。
あと必要なのが、Android API にアクセスするため Pyjnius を使っています。サンプルでは build 内で分岐させていますが頭のグローバル変数宣言時に書いてもいいと思います。
from jnius import autoclass, cast
PythonActivity = autoclass('org.kivy.android.PythonActivity')
currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
Activity = autoclass('android.app.Activity')
Intent = autoclass('android.content.Intent')
Bundle = autoclass('android.os.Bundle')
※サンプルで変数名についてる”CLS_”は、ラップしたJavaクラスだと分かりやすくするために適当につけただけで深い意味はないです。
使い方は、サンプルのcapture関数で呼んでいるように android_write_storage にデフォルトのファイル名とデータを渡してあげるだけです。機種によって見え方は違うと思いますが任意のファイル名で保存先を選ぶことができます。
今回は画像保存に限定しているので、intent.setTypeでPNG指定をしていますが、目的に応じてMIMEタイプを設定してあげれば、他の形式のファイル保存にも応用が利くと思います。(多分)
on_activity_resultの中で出力データをByteデータをストリームに渡して書き込みます。サンプルではデータをOpenCVで扱っていますので imencode を使用しています。画像の扱い方に応じてByteデータで渡せるように調整してください。
file_save_callback の関数に保存後の処理を書いています。呼び出し元と別クラスにしてファイル選択処理を実装する場合は、コールバック関数として渡す形になるかと思います。
ちなみに、サンプルでコールバック関数にファイルパスを返していますが、そのパスを使った画像表示は出来きませんでした。保存先のパスを使って何かしたい場合は、アプリから参照できるパスにするためにもうひと手間必要かもしれません。(必要になったら調べて、うまくできたら投稿するかも)
他のサンプル部分について今回の主旨とは外れますが、動作確認のために、カメラからの画像取得やメッセージ通知のためのクラスを使っています。これらについての使い方は別の記事で書いていますので気になる方はご覧ください。(宣伝)
以上、保存先選択の実装サンプルでした。
(参考)
(おまけ)Androidアクセス用クラス例
上の説明の中で、request_codeでon_activity_result がどこから戻ってきたものかを判別できると書きました。
別の投稿(ギャラリー選択)で、同じようにAndroid API にアクセスしてon_activity_resultで受け取る実装方法をあげていますが、Android API にアクセスする共通クラスとしてまとめる例を載せてみます。(クラスとそれに使っている共通変数以外は省略します。)
from jnius import autoclass, cast
PythonActivity = autoclass('org.kivy.android.PythonActivity')
currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
Activity = autoclass('android.app.Activity')
Intent = autoclass('android.content.Intent')
Bundle = autoclass('android.os.Bundle')
ImagesMedia = autoclass('android.provider.MediaStore$Images$Media')
REQUEST_GALLERY = 1
REQUEST_STORAGE_ACCESS = 2
MediaStore_Images_Media_DATA = '_data'
class AndroidAccess():
def __init__(self, imege_select_callback, file_save_callback):
self.imege_select_callback = imege_select_callback
self.file_save_callback = file_save_callback
def android_select_image(self):
if platform == "android":
from android import activity
# イメージギャラリー
intent = Intent(Intent.ACTION_PICK,ImagesMedia.EXTERNAL_CONTENT_URI)
currentActivity.startActivityForResult(intent, REQUEST_GALLERY)
activity.unbind(on_activity_result=self.on_activity_result)
activity.bind(on_activity_result=self.on_activity_result)
def android_write_storage(self, file_name, image_data):
if platform == 'android':
self.image_data = image_data
from android import activity
intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.setType('image/png')
bundle = Bundle()
bundle.putString(Intent.EXTRA_TITLE, file_name)
intent.putExtras(bundle)
intent.addCategory(Intent.CATEGORY_OPENABLE)
currentActivity.startActivityForResult(intent, REQUEST_STORAGE_ACCESS)
activity.unbind(on_activity_result=self.on_activity_result)
activity.bind(on_activity_result=self.on_activity_result)
def on_activity_result(self, request_code, result_code, intent):
try:
# ファイル選択
if request_code == REQUEST_GALLERY:
if result_code == Activity.RESULT_CANCELED:
# 未選択
Clock.schedule_once(lambda dt: self.imege_select_callback(''), 0)
return
if result_code != Activity.RESULT_OK:
raise NotImplementedError('Unknown result_code "{}"'.format(result_code))
selectedImage = intent.getData() # Uri
filePathColumn = [MediaStore_Images_Media_DATA] # String[]
cursor = currentActivity.getContentResolver().query(selectedImage, filePathColumn, None, None, None) # Cursor
cursor.moveToFirst()
columnIndex = cursor.getColumnIndex(filePathColumn[0]) # int
selectedPicturePath = cursor.getString(columnIndex) # String
Clock.schedule_once(lambda dt: self.select_path(selectedPicturePath), 0)
cursor.close()
# 保存先選択
if request_code == REQUEST_STORAGE_ACCESS:
if result_code == Activity.RESULT_CANCELED:
# キャンセル
Clock.schedule_once(lambda dt: self.file_save_callback(''), 0)
if result_code != Activity.RESULT_OK:
raise NotImplementedError('Unknown result_code "{}"'.format(result_code))
treeUri = intent.getData() # Uri
if treeUri is None:
raise NotImplementedError('no data')
filepath = treeUri.getPath()
outputStream = currentActivity.getContentResolver().openOutputStream(treeUri)
# ストリームに書き出し
# retval, buf = cv2.imencode('.png', self.image_data)
outputStream.write(buf.tobytes())
Clock.schedule_once(lambda dt: self.file_save_callback(filepath), 0)
finally:
from android import activity
activity.unbind(on_activity_result=self.on_activity_result)
ソースの内容はすでに書いているサンプルとほとんど同じですが、例のように startActivityForResult に渡す値を別のものにすることで、request_codeに入ってくる値で判別して処理を両立できます。
※ストリームに書き出しの部分については、データの扱い方に応じてうまくByteデータを渡すよう調整してください。
例ではクラスをインスタンス化する際に、選択後の処理、保存後の処理をそれぞれ渡すようにしています。一応、簡単な使い方イメージも書いてみます。
# クラスインスタンス化
self.AA = AndroidAccess(self.imege_select_callback, self.file_save_callback)
# ファイル選択
self.AA.android_select_image()
# ファイル保存
self.AA.android_write_storage(filename, image_data)
def imege_select_callback(self, filepath):
# ファイル選択後の処理
def file_save_callback(self, filepath):
# ファイル保存後の処理
使うときはいろいろ調整が必要と思いますが、参考になれば幸いです。
コメント