Compare commits

...

10 Commits

Author SHA1 Message Date
yumoqing
c6371ce257 bugfix 2022-12-21 16:34:31 +08:00
yumoqing
66bc5c375f bugfix 2022-08-08 15:52:18 +08:00
yumoqing
8f461c4523 bugfix 2022-08-08 03:51:15 -04:00
yumoqing
e07bc45ada bugfix 2022-08-08 11:28:20 +08:00
yumoqing
e5e2044775 bugfix 2022-08-08 11:27:29 +08:00
yumoqing
1114629353 bugfix 2022-08-08 11:27:04 +08:00
yumoqing
46dd7593a6 bugfix 2022-08-01 04:14:04 +00:00
yumoqing
136867f804 bugfix 2022-07-14 13:13:03 +08:00
yumoqing
a922670413 add audio and record widget 2022-07-09 22:06:08 +08:00
yumoqing
7d7f4ac472 bugfix 2022-07-01 11:37:36 +08:00
18 changed files with 618 additions and 54 deletions

11
c4k/conf/config.json Normal file
View File

@ -0,0 +1,11 @@
{
"font_sizes":{
"smallest":1.5,
"small":2.5,
"normal":3.5,
"large":4.5,
"huge":5.5,
"hugest":6.5
},
"font_name":"normal"
}

87
c4k/main.py Normal file
View File

@ -0,0 +1,87 @@
from kivy.uix.boxlayout import BoxLayout
from camera4kivy import Preview
from kivyblocks.utils import blocksImage, CSize
from kivyblocks.clickable import ClickableImage
from kivy.app import App
from kivy.utils import platform
from kivy.clock import Clock
class NewCamera(BoxLayout):
def __init__(self, **kw):
super().__init__(orientation='vertical')
self.preview = Preview(**kw)
self.add_widget(self.preview)
box = BoxLayout(orientation='horizontal',
size_hint_y=None,
height=CSize(2))
self.camera = ClickableImage(size_hint=[None, None],
height=CSize(1.6),
width=CSize(1.6),
pos_hint={
'x':self.width/2 - CSize(1.6),
'y':0
},
source=blocksImage('photo.png'),
img_kw={
'size_hint':[None,None],
'height':CSize(1.5),
'width':CSize(1.5)
})
self.lensid = ClickableImage(size_hint=[None, None],
height=CSize(1.6),
width=CSize(1.6),
pos_hint={
'x':self.width/2,
'y':0
},
source=blocksImage('lensid.png'),
img_kw={
'size_hint':[None,None],
'height':CSize(1.5),
'width':CSize(1.5)
})
self.camera.bind(on_press=self.take_a_pic)
self.lensid.bind(on_press=self.change_lensid)
box.add_widget(self.camera)
box.add_widget(self.lensid)
self.add_widget(box)
# self.bind(size=self.change_btn_position)
Clock.schedule_once(self.open_camera, 0.5)
def open_camera(self, *args):
self.preview.connect_camera(aspect_ratio='16:9',
filepath_callback=self.photo_saved)
def photo_saved(self, path:str):
print(f'{path} saved')
def change_lensid(self, *args):
if platform not in [ 'android', 'ios' ]:
return
if self.preview.preview.index == 0:
self.preview.select_camera("1")
else:
self.preview.select_camera("0")
def change_btn_position(self, *args):
self.camera.pos_hint = {
'x':self.width/2 - CSize(1.6),
'y':0
}
self.lensid.pos_hint = {
'x':self.width/2,
'y':0
}
def take_a_pic(self, o):
self.preview.capture_photo()
class C4KApp(App):
def build(self):
x = NewCamera()
return x
if __name__ == '__main__':
C4KApp().run()

View File

@ -16,6 +16,7 @@ class Audio(AppLogger, pyaudio.PyAudio):
self.chunk = 1024 self.chunk = 1024
self.devices = [ self.get_device_info_by_index(i) \ self.devices = [ self.get_device_info_by_index(i) \
for i in range(self.get_device_count()) ] for i in range(self.get_device_count()) ]
self.recording = False
def get_input_device(self): def get_input_device(self):
return [ d for d in self.devices if d.maxInputChannels > 0 ] return [ d for d in self.devices if d.maxInputChannels > 0 ]
@ -27,24 +28,16 @@ class Audio(AppLogger, pyaudio.PyAudio):
x = tempfile.mkstemp(suffix='.wav') x = tempfile.mkstemp(suffix='.wav')
os.close(x[0]) os.close(x[0])
self.temp_filename = x[1] self.temp_filename = x[1]
return self.temp_filename
def record_cb(self, in_data, frame_count, time_info, status): def record_cb(self, in_data, frame_count, time_info, status):
bdata = pickle.dumps(in_data)
self.info('frame_count=%s, time_info=%s, status=%s, bytes count=%s', \
frame_count, time_info, status, len(bdata))
self.rec_frames += frame_count
self.current_ts = time.time()
self.wavfile.writeframesraw(in_data) self.wavfile.writeframesraw(in_data)
if self.running: if self.recording:
return (None, pyaudio.paContinue) return (None, pyaudio.paContinue)
return (None, pyaudio.paComplete) return (None, pyaudio.paComplete)
def replay_cb(self, in_data, frame_count, time_info, status): def replay_cb(self, in_data, frame_count, time_info, status):
data = self.wavfile.readframes(frame_count) data = self.wavfile.readframes(frame_count)
bdata = pickle.dumps(data)
self.info('frame_count=%s, data length in bytes=%s', \
frame_count, len(bdata))
if not data: if not data:
return (None, pyaudio.paComplete) return (None, pyaudio.paComplete)
@ -57,47 +50,74 @@ class Audio(AppLogger, pyaudio.PyAudio):
print(x) print(x)
return dev_cnt - 1 return dev_cnt - 1
def record(self, save_file=None, stop_cond_func=None): def write_audiofile(self, fn, audio_data, channels=2, rate=44100):
filename = save_file wf = wave.open(fn, 'wb')
if filename is None: wf.setnchannels(channels)
self.tmpfile() wf.setsampwidth(2)
filename = self.temp_filename wf.setframerate(rate)
wf.writeframesraw(audio_data)
wf.close()
self.wavfile = wave.open(filename, 'wb') def start_record(self,
self.wavfile.setnchannels(2) savefile=None,
self.wavfile.setsampwidth(2)
self.wavfile.setframerate(44100.00)
self.stream = self.open(format=pyaudio.paInt16,
channels=2, channels=2,
rate=44100, rate=44100
):
if savefile is None:
savefile = self.tmpfile()
self.save_file = savefile
self.wavfile = wave.open(self.save_file, 'wb')
self.wavfile.setnchannels(channels)
self.wavfile.setsampwidth(2)
self.wavfile.setframerate(rate)
self.stream = self.open(format=pyaudio.paInt16,
channels=channels,
rate=rate,
input=True, input=True,
frames_per_buffer=self.chunk, frames_per_buffer=self.chunk,
stream_callback=self.record_cb) stream_callback=self.record_cb)
self.stream.start_stream() self.stream.start_stream()
self.running = True self.recording = True
self.rec_frames = 0
self.start_ts = self.current_ts = time.time() def stop_record(self):
while self.stream.is_active(): if self.recording:
if stop_cond_func and stop_cond_func(): self.recording = False
self.running = False
time.sleep(0.05)
self.stream.stop_stream() self.stream.stop_stream()
self.stream.close() self.stream.close()
self.wavfile.close() self.wavfile.close()
if save_file is None:
self.replay() def record(self, stop_cb,
savefile=None,
channels=2,
rate=44100,
):
self.start_record(savefile=savefile, channels=channels,rate=rate)
while not stop_cb():
time.sleep(0.1)
self.stop_record()
def get_audio_spec(self, audiofile):
wavfile = wave.open(audiofile, 'rb')
sampwidth = wavfile.getsampwidth()
format = self.get_format_from_width(sampwidth)
framerate=wavfile.getframerate()
channels = wavfile.getnchannels()
return {
"format":format,
"sampwidth":sampwidth,
"framerate":framerate,
"channels":channels
}
def replay(self, play_file=None): def replay(self, play_file=None):
idx = self.get_output_index() idx = self.get_output_index()
x = self.get_device_info_by_index(idx) x = self.get_device_info_by_index(idx)
y = self.get_default_input_device_info() y = self.get_default_input_device_info()
self.info('default_input=%s, default_output=%s', y, x)
if play_file is None: if play_file is None:
play_file = self.temp_filename play_file = self.temp_filename
self.wavfile = wave.open(play_file, 'rb') self.wavfile = wave.open(play_file, 'rb')
format = self.get_format_from_width(self.wavfile.getsampwidth()) format = self.get_format_from_width(self.wavfile.getsampwidth())
framerate=self.wavfile.getframerate() framerate=self.wavfile.getframerate()
self.info('format=%s, framerate=%s', format, framerate)
self.stream = self.open(format=format, self.stream = self.open(format=format,
channels=self.wavfile.getnchannels(), channels=self.wavfile.getnchannels(),
rate=framerate, rate=framerate,
@ -112,15 +132,14 @@ class Audio(AppLogger, pyaudio.PyAudio):
self.stream.close() self.stream.close()
self.wavfile.close() self.wavfile.close()
def __del__(self):
if self.temp_filename:
os.remove(self.temp_filename)
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
t_begin = time.time()
def stop_func(audio): def stop_func(audio):
if audio.current_ts - audio.start_ts >= 10: t = time.time()
audio.running = False if t - t_begin >= 10:
return True
return False
create_logger('audio', levelname='debug') create_logger('audio', levelname='debug')
a = Audio() a = Audio()
@ -135,4 +154,4 @@ if __name__ == '__main__':
if len(sys.argv) >= 2: if len(sys.argv) >= 2:
sf = sys.argv[1] sf = sys.argv[1]
f = partial(stop_func, a) f = partial(stop_func, a)
a.record(sf, stop_cond_func=f) a.record(f, savefile=sf)

View File

@ -3,14 +3,14 @@ import tempfile
import pyaudio import pyaudio
import wave import wave
from kivy.event import EventDispatcher from kivy.event import EventDispatcher
from kivy.properties import NumericProperty from kivy.properties import NumericProperty, StringProperty, ObjectProperty
from kivyblocks.baseWidget import HBox from kivyblocks.baseWidget import HBox
from kviycv.micphone import Micphone from kivycv.micphone import Micphone
class AudioRecorder(EventDispatcher): class AudioRecorder(EventDispatcher):
fs = NumericProperty(None) fs = NumericProperty(None)
filename = StringProperty(None) filename = StringProperty(None)
voice_src = OjbectProperty(None) voice_src = ObjectProperty(None)
def __init__(self, **kw): def __init__(self, **kw):
super(AudioRecorder, self).__init__(**kw) super(AudioRecorder, self).__init__(**kw)
self.saving = False self.saving = False

View File

@ -6,7 +6,7 @@ from kivy.uix.camera import Camera
from kivy.properties import NumericProperty from kivy.properties import NumericProperty
from kivy.event import EventDispatcher from kivy.event import EventDispatcher
from .android_rotation import * from kivyblocks.android_rotation import *
if kivy.platform in [ 'win', 'linux', 'macosx' ]: if kivy.platform in [ 'win', 'linux', 'macosx' ]:
from PIL import ImageGrab from PIL import ImageGrab

53
kivycv/recorder.py Normal file
View File

@ -0,0 +1,53 @@
import pyaudio
from kivy.uix.label import Label
from kivy.properties import NumericProperty
from kivyblocks.baseWidget import HBox, VBox
import os
import sys
import time
try:
from .audio import Audio
except:
from audio import Audio
class Recorder(VBox):
rate = NumericProperty(44100)
channel = NumericProperty(2)
def __init__(self, **kw):
super().__init__(**kw)
self.audio = Audio()
self.register_event_type('on_recorded')
def on_recorded(self, record_file):
print('on_recorded:', record_file)
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.audio.start_record(channels=self.channel,
rate=self.rate)
return False
return super().on_touch_down(touch)
def on_touch_up(self, touch):
if self.audio.recording:
self.audio.stop_record()
self.dispatch('on_recorded', self.audio.save_file)
return super().on_touch_up(touch)
if __name__ == '__main__':
from kivy.app import App
class TestApp(App):
def build(self):
x = Recorder()
self.recorder = x
x.bind(on_recorded=self.play_audio)
return x
def play_audio(self, o, f):
print('play file', f)
self.recorder.audio.replay(f)
print('play finished')
# os.remove(f)
TestApp().run()

View File

@ -1,13 +1,13 @@
from kivy.factory import Factory from kivy.factory import Factory
from audio import Audio from .audio import Audio
from audiorecorder import AudioRecorder from .audiorecorder import AudioRecorder
from camerawithmic import CameraWithMic from .camerawithmic import CameraWithMic
from custom_camera import CustomCamera, QrReader from .custom_camera import CustomCamera, QrReader
from kivycamera import KivyCamera from .kivycamera import KivyCamera
from micphone import Micphone from .micphone import Micphone
from qrcodereader import QRCodeReader from .qrcodereader import QRCodeReader
from version import __version__ from .version import __version__
r = Factory.register r = Factory.register
r('Audio', Audio) r('Audio', Audio)
r('AudioRecorder', AudioRecorder) r('AudioRecorder', AudioRecorder)

15
kivycv/xcamera/__init__.py Executable file
View File

@ -0,0 +1,15 @@
"""
Exposes `XCamera` directly in `xcamera` rather than `xcamera.xcamera`.
Also note this may break `pip` since all imports within `xcamera.py` would be
required at setup time. This is because `version.py` (same directory) is used
by the `setup.py` file.
Hence we're not exposing `XCamera` if `pip` is detected.
"""
import os
project_dir = os.path.abspath(
os.path.join(__file__, os.pardir, os.pardir, os.pardir, os.pardir))
using_pip = os.path.basename(project_dir).startswith('pip-')
# only exposes `XCamera` if not within `pip` ongoing install
if not using_pip:
from .xcamera import XCamera # noqa

85
kivycv/xcamera/android_api.py Executable file
View File

@ -0,0 +1,85 @@
from kivy.logger import Logger
from jnius import JavaException, PythonJavaClass, autoclass, java_method
Camera = autoclass('android.hardware.Camera')
AndroidActivityInfo = autoclass('android.content.pm.ActivityInfo')
AndroidPythonActivity = autoclass('org.kivy.android.PythonActivity')
PORTRAIT = AndroidActivityInfo.SCREEN_ORIENTATION_PORTRAIT
LANDSCAPE = AndroidActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
class ShutterCallback(PythonJavaClass):
__javainterfaces__ = ('android.hardware.Camera$ShutterCallback', )
@java_method('()V')
def onShutter(self):
# apparently, it is enough to have an empty shutter callback to play
# the standard shutter sound. If you pass None instead of shutter_cb
# below, the standard sound doesn't play O_o
pass
class PictureCallback(PythonJavaClass):
__javainterfaces__ = ('android.hardware.Camera$PictureCallback', )
def __init__(self, filename, on_success):
super(PictureCallback, self).__init__()
self.filename = filename
self.on_success = on_success
@java_method('([BLandroid/hardware/Camera;)V')
def onPictureTaken(self, data, camera):
s = data.tostring()
with open(self.filename, 'wb') as f:
f.write(s)
Logger.info('xcamera: picture saved to %s', self.filename)
camera.startPreview()
self.on_success(self.filename)
class AutoFocusCallback(PythonJavaClass):
__javainterfaces__ = ('android.hardware.Camera$AutoFocusCallback', )
def __init__(self, filename, on_success):
super(AutoFocusCallback, self).__init__()
self.filename = filename
self.on_success = on_success
@java_method('(ZLandroid/hardware/Camera;)V')
def onAutoFocus(self, success, camera):
if success:
Logger.info('xcamera: autofocus succeeded, taking picture...')
shutter_cb = ShutterCallback()
picture_cb = PictureCallback(self.filename, self.on_success)
camera.takePicture(shutter_cb, None, picture_cb)
else:
Logger.info('xcamera: autofocus failed')
def take_picture(camera_widget, filename, on_success):
# to call the android API, we need access to the underlying
# android.hardware.Camera instance. However, there is no official way to
# retrieve it from the camera widget, so we need to dig into internal
# attributes :-( This works at least on kivy 1.9.1, but it might break any
# time soon.
camera = camera_widget._camera._android_camera
params = camera.getParameters()
params.setFocusMode("auto")
camera.setParameters(params)
cb = AutoFocusCallback(filename, on_success)
Logger.info('xcamera: starting autofocus...')
try:
camera.autoFocus(cb)
except JavaException as e:
Logger.info('Error when calling autofocus: {}'.format(e))
def set_orientation(value):
previous = get_orientation()
AndroidPythonActivity.mActivity.setRequestedOrientation(value)
return previous
def get_orientation():
return AndroidPythonActivity.mActivity.getRequestedOrientation()

BIN
kivycv/xcamera/data/icons.ttf Executable file

Binary file not shown.

BIN
kivycv/xcamera/data/shutter.wav Executable file

Binary file not shown.

43
kivycv/xcamera/main.py Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env python
from kivy.app import App
from kivy.lang import Builder
kv = """
#:import XCamera kivy_garden.xcamera.XCamera
FloatLayout:
orientation: 'vertical'
XCamera:
id: xcamera
on_picture_taken: app.picture_taken(*args)
BoxLayout:
orientation: 'horizontal'
size_hint: 1, None
height: sp(50)
Button:
text: 'Set landscape'
on_release: xcamera.force_landscape()
Button:
text: 'Restore orientation'
on_release: xcamera.restore_orientation()
"""
class CameraApp(App):
def build(self):
return Builder.load_string(kv)
def picture_taken(self, obj, filename):
print('Picture taken and saved to {}'.format(filename))
def main():
CameraApp().run()
if __name__ == '__main__':
main()

37
kivycv/xcamera/platform_api.py Executable file
View File

@ -0,0 +1,37 @@
from kivy.utils import platform
def play_shutter():
# bah, apparently we need to delay the import of kivy.core.audio, lese
# kivy cannot find a camera provider, at lease on linux. Maybe a
# gstreamer/pygame issue?
from kivy.core.audio import SoundLoader
sound = SoundLoader.load("data/shutter.wav")
sound.play()
if platform == 'android':
from .android_api import (
LANDSCAPE, PORTRAIT, take_picture, set_orientation, get_orientation)
else:
# generic fallback for taking pictures. Probably not the best quality,
# they are meant mostly for testing
LANDSCAPE = 'landscape'
PORTRAIT = 'portrait'
def take_picture(camera_widget, filename, on_success):
camera_widget.texture.save(filename, flipped=False)
play_shutter()
on_success(filename)
def set_orientation(value):
previous = get_orientation()
print('FAKE orientation set to {}'.format(value))
get_orientation.value = value
return previous
def get_orientation():
return get_orientation.value
get_orientation.value = PORTRAIT

1
kivycv/xcamera/version.py Executable file
View File

@ -0,0 +1 @@
__version__ = '2020.0613'

41
kivycv/xcamera/xcamera.kv Executable file
View File

@ -0,0 +1,41 @@
#:import xcamera kivyblocks.xcamera.xcamera
<XCameraIconButton>
icon_color: (0, 0, 0, 1)
_down_color: xcamera.darker(self.icon_color)
icon_size: dp(50)
canvas.before:
Color:
rgba: self.icon_color if self.state == 'normal' else self._down_color
Ellipse:
pos: self.pos
size: self.size
size_hint: None, None
size: self.icon_size, self.icon_size
font_size: self.icon_size/2
<XCamera>:
# \ue800 corresponds to the camera icon in the font
icon: u"[font=data/icons.ttf]\ue800[/font]"
icon_color: (0.13, 0.58, 0.95, 0.8)
icon_size: dp(70)
id: camera
resolution: 640, 480 # 1920, 1080
allow_stretch: True
# Shoot button
XCameraIconButton:
id: shoot_button
markup: True
text: root.icon
icon_color: root.icon_color
icon_size: root.icon_size
on_release: root.shoot()
# position
right: root.width - dp(10)
center_y: root.center_y

116
kivycv/xcamera/xcamera.py Executable file
View File

@ -0,0 +1,116 @@
import datetime
import os
from kivy.clock import mainthread
from kivy.lang import Builder
from kivy.properties import ObjectProperty
from kivy.resources import resource_add_path
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.camera import Camera
from kivy.uix.label import Label
from kivy.utils import platform
from .platform_api import LANDSCAPE, set_orientation, take_picture
ROOT = os.path.dirname(os.path.abspath(__file__))
resource_add_path(ROOT)
def darker(color, factor=0.5):
r, g, b, a = color
r *= factor
g *= factor
b *= factor
return r, g, b, a
def get_filename():
return datetime.datetime.now().strftime('%Y-%m-%d %H.%M.%S.jpg')
def is_android():
return platform == 'android'
def check_camera_permission():
"""
Android runtime `CAMERA` permission check.
"""
if not is_android():
return True
from android.permissions import Permission, check_permission
permission = Permission.CAMERA
return check_permission(permission)
def check_request_camera_permission(callback=None):
"""
Android runtime `CAMERA` permission check & request.
"""
had_permission = check_camera_permission()
if not had_permission:
from android.permissions import Permission, request_permissions
permissions = [Permission.CAMERA]
request_permissions(permissions, callback)
return had_permission
class XCameraIconButton(ButtonBehavior, Label):
pass
class XCamera(Camera):
directory = ObjectProperty(None)
_previous_orientation = None
__events__ = ('on_picture_taken', 'on_camera_ready')
def __init__(self, **kwargs):
# Builder.load_file(os.path.join(ROOT, "xcamera.kv"))
super().__init__(**kwargs)
def _on_index(self, *largs):
"""
Overrides `kivy.uix.camera.Camera._on_index()` to make sure
`camera.open()` is not called unless Android `CAMERA` permission is
granted, refs #5.
"""
@mainthread
def on_permissions_callback(permissions, grant_results):
"""
On camera permission callback calls parent `_on_index()` method.
"""
if all(grant_results):
self._on_index_dispatch(*largs)
if check_request_camera_permission(callback=on_permissions_callback):
self._on_index_dispatch(*largs)
def _on_index_dispatch(self, *largs):
super()._on_index(*largs)
self.dispatch('on_camera_ready')
def on_picture_taken(self, filename):
"""
This event is fired every time a picture has been taken.
"""
pass
def on_camera_ready(self):
"""
Fired when the camera is ready.
"""
pass
def shoot(self):
def on_success(filename):
self.dispatch('on_picture_taken', filename)
filename = get_filename()
if self.directory:
filename = os.path.join(self.directory, filename)
take_picture(self, filename, on_success)
def force_landscape(self):
self._previous_orientation = set_orientation(LANDSCAPE)
def restore_orientation(self):
if self._previous_orientation is not None:
set_orientation(self._previous_orientation)

0
requirements.txt Normal file
View File

56
setup.py Executable file
View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
from kivycv.version import __version__
import codecs
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
description = "kivy opencv related modules"
author = "yumoqing"
email = "yumoqing@icloud.com"
version = __version__
depandent_packages = []
with codecs.open('./requirements.txt', 'r', 'utf-8') as f:
b = f.read()
b = ''.join(b.split('\r'))
depandent_packages = b.split('\n')
package_data = {
"kivycv":[
"*.txt",
'xcamera/data/*.ttf',
'xcamera/data/*.wav',
'xcamera/xcamera.kv',
'image_processing/cascades/*.xml'
]
}
setup(
name="kivycv",
ext_modules= [
],
version=version,
# uncomment the following lines if you fill them out in release.py
description=description,
author=author,
author_email=email,
install_requires=depandent_packages,
packages=[
'kivycv',
'kivycv.xcamera',
'kivycv.image_processing'
],
package_data=package_data,
keywords = [
],
classifiers = [
'Development Status :: 3 - Alpha',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3.5',
'Topic :: Software Development :: Libraries :: Python Modules'
],
platforms= 'any'
)