Новости

Урок 132. Камера. Висновок зображення на екран. Розмір preview. Обробка повороту пристрою

  1. Розмір прев'ю
  2. поворот превью
  3. інше

У цьому уроці:

- використовуємо об'єкт Camera для отримання зображення з камери
- підганяємо зображення під розміри екрану
- враховуємо поворот пристрою

Розберемося, які основні об'єкти нам знадобляться для виведення зображення з камери на екран. Їх три: Camera , SurfaceView , SurfaceHolder .

Camera використовується, щоб отримати зображення з камери. А щоб це зображення в додатку відобразити, будемо використовувати SurfaceView.

Нормального перекладу слова Surface я не зміг підібрати. «Поверхность» - якось занадто абстрактно. Тому так і буду називати - surface. Це буде означати якийсь компонент, який відображає зображення з камери.

Робота з surface ведеться не безпосередньо, а через посередника - SurfaceHolder (далі holder). Саме з цим об'єктом вміє працювати Camera. Також, holder буде повідомляти нам про те, що surface готовий до роботи, змінений або більше недоступний.

А якщо підсумувати, то: Camera бере holder і з його допомогою виводить зображення на surface.

Напишемо програму, в якому реалізуємо виведення зображення з камери на екран.

Створимо проект:

Project name: P1321_CameraScreen
Build Target: Android 2.3.3
Application name: CameraScreen
Package name: ru.startandroid.develop.p1321camerascreen
Create Activity: MainActivity

Екран main.xml:

<? Xml version = "1.0" encoding = "utf-8"?> <FrameLayout xmlns: android = "http://schemas.android.com/apk/res/android" xmlns: tools = "http: // schemas .android.com / tools "android: layout_width =" match_parent "android: layout_height =" match_parent "> <surfaceView android: id =" @ + id / surfaceView "android: layout_width =" wrap_content "android: layout_height =" wrap_content "android : layout_gravity = "center"> </ SurfaceView> </ FrameLayout>

SurfaceView по центру екрана.

У маніфест додайте права на камеру: <uses-permission android: name = "android.permission.CAMERA" />

MainActivity.java:

package ru.startandroid.develop.p1321camerascreen; import java.io.IOException; import android.app.Activity; import android.graphics.Matrix; import android.graphics.RectF; import android.hardware.Camera; import android.hardware.Camera.CameraInfo; import android.hardware.Camera.Size; import android.os.Bundle; import android.view.Display; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.Window; import android.view.WindowManager; public class MainActivity extends Activity {SurfaceView sv; SurfaceHolder holder; HolderCallback holderCallback; Camera camera; final int CAMERA_ID = 0; final boolean FULL_SCREEN = true; @Override protected void onCreate (Bundle savedInstanceState) {super.onCreate (savedInstanceState); requestWindowFeature (Window.FEATURE_NO_TITLE); getWindow (). setFlags (WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView (R.layout.main); sv = (SurfaceView) findViewById (R.id.surfaceView); holder = sv.getHolder (); holder.setType (SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); holderCallback = new HolderCallback (); holder.addCallback (holderCallback); } @Override protected void onResume () {super.onResume (); camera = Camera.open (CAMERA_ID); setPreviewSize (FULL_SCREEN); } @Override protected void onPause () {super.onPause (); if (camera! = null) camera.release (); camera = null; } Class HolderCallback implements SurfaceHolder.Callback {@Override public void surfaceCreated (SurfaceHolder holder) {try {camera.setPreviewDisplay (holder); camera.startPreview (); } Catch (IOException e) {e.printStackTrace (); }} @Override public void surfaceChanged (SurfaceHolder holder, int format, int width, int height) {camera.stopPreview (); setCameraDisplayOrientation (CAMERA_ID); try {camera.setPreviewDisplay (holder); camera.startPreview (); } Catch (Exception e) {e.printStackTrace (); }} @Override public void surfaceDestroyed (SurfaceHolder holder) {}} void setPreviewSize (boolean fullScreen) {// отримуємо розміри екрану Display display = getWindowManager (). GetDefaultDisplay (); boolean widthIsMax = display.getWidth ()> display.getHeight (); // визначаємо розміри превью камери Size size = camera.getParameters (). GetPreviewSize (); RectF rectDisplay = new RectF (); RectF rectPreview = new RectF (); // RectF екрану, відповідає розмірам екрану rectDisplay.set (0, 0, display.getWidth (), display.getHeight ()); // RectF перший if (widthIsMax) {// превью в горизонтальній орієнтації rectPreview.set (0, 0, size.width, size.height); } Else {// превью в вертикальної орієнтації rectPreview.set (0, 0, size.height, size.width); } Matrix matrix = new Matrix (); // підготовка матриці перетворення if (! FullScreen) {// якщо превью буде "втиснутий" в екран (другий варіант з уроку) matrix.setRectToRect (rectPreview, rectDisplay, Matrix.ScaleToFit.START); } Else {// якщо екран буде "втиснутий" в превью (третій варіант з уроку) matrix.setRectToRect (rectDisplay, rectPreview, Matrix.ScaleToFit.START); matrix.invert (matrix); } // перетворення matrix.mapRect (rectPreview); // установка розмірів surface з отриманого перетворення sv.getLayoutParams (). Height = (int) (rectPreview.bottom); sv.getLayoutParams (). width = (int) (rectPreview.right); } Void setCameraDisplayOrientation (int cameraId) {// визначаємо наскільки повернуть екран від нормального положення int rotation = getWindowManager (). GetDefaultDisplay (). GetRotation (); int degrees = 0; switch (rotation) {case Surface.ROTATION_0: degrees = 0; break; case Surface.ROTATION_90: degrees = 90; break; case Surface.ROTATION_180: degrees = 180; break; case Surface.ROTATION_270: degrees = 270; break; } Int result = 0; // отримуємо інфо по камері cameraId CameraInfo info = new CameraInfo (); Camera.getCameraInfo (cameraId, info); // задня камера if (info.facing == CameraInfo.CAMERA_FACING_BACK) {result = ((360 - degrees) + info.orientation); } Else // передня камера if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {result = ((360 - degrees) - info.orientation); result + = 360; } Result = result% 360; camera.setDisplayOrientation (result); }}

Дивимося код.

У onCreate налаштовуємо Activity так, щоб воно було без заголовка і в повний екран. Потім ми визначаємо surface, отримуємо його holder і встановлюємо його тип = SURFACE_TYPE_PUSH_BUFFERS (Настройка типу потрібна тільки в Android версії нижче 3.0).

Далі для holder створюємо callback об'єкт HolderCallback (про нього трохи далі), через який holder буде повідомляти нам про станах surface.

У onResume отримуємо доступ до камери, використовуючи метод open . На вхід передаємо id камери, якщо їх декілька (задня і передня). Цей метод доступний з API level 9. В кінці цього уроку є інформація про те, як отримати id камери.

Також існує метод open без вимоги id на вхід. Він дасть доступ до задньої камері. Він доступний і в більш ранніх версіях.

Після цього викликаємо метод setPreviewSize, в якому налаштовуємо розмір surface. Його детально обговоримо нижче.

У onPause звільняємо камеру методом release , Щоб інші програми могли її використовувати.

Клас HolderCallback, реалізує інтерфейс SurfaceHolder.Callback . Нагадаю, що через нього holder повідомляє нам про стан surface.

У ньому три методи:

surfaceCreated - surface створений. Ми можемо дати камері об'єкт holder за допомогою методу setPreviewDisplay і почати транслювати зображення методом startPreview .

surfaceChanged - був змінений формат або розмір surface. В цьому випадку ми зупиняємо перегляд ( stopPreview ), Налаштовуємо камеру з урахуванням повороту пристрою (setCameraDisplayOrientation, подробиці нижче), і знову запускаємо перегляд.

surfaceDestroyed - surface більш недоступний. Чи не використовуємо цей метод.

З цими методами, до речі, є одна дивина. У хелпе до методу surfaceChanged написано, що він обов'язково буде викликаний не тільки при зміні, але і при створенні surface, тобто відразу після surfaceCreated. Але при цьому в хелпе до камери методи запуску перегляду (setPreviewDisplay, startPreview) викликаються і в surfaceCreated і в surfaceChanged. Тобто при створенні surface ми чогось два рази стартуємо перегляд. Мені незрозуміло, навіщо потрібно це дублювання.

Якщо очистити метод surfaceCreated, то все продовжує працювати. Але в уроці я, мабуть НЕ ризикну так робити. Раптом я чогось не розумію і в цьому є якийсь сенс. Якщо хто знає - пишіть на форумі.

Розмір прев'ю

Метод setPreviewSize. Трохи нетривіальний, особливо якщо ви ніколи не працювали з об'єктами Matrix і RectF .

У ньому ми визначаємо розміри surface з урахуванням екрану і зображення з камери, щоб картинка відображалася з вірним співвідношенням сторін і на весь екран.

Подальші викладки можна пропустити, якщо не хочеться мозок ламати і вникати в механізм. Хоча я постарався зробити ці викладки зрозумілими, цікавими і навіть картинки намалював. Якщо ви все зрозумієте, буде відмінно!) Коли-небудь ці знання знадобляться.

Отже, у нас є картинка, яка приходить з камери - назвемо її превью. І у нас є екран, на якому нам треба це превью відобразити.

Розглянемо конкретний приклад, щоб було наочніше. Планшет Galaxy Tab, задня камера, нормальне горизонтальне положення.

Є екран. Розмір: 1280x752. Співвідношення сторін: 1280/752 = 1,70

Співвідношення сторін: 1280/752 = 1,70

Є превью. Розмір: 640x480. Співвідношення сторін: 640/480 = 1,33.

Припустимо, що ми камеру навели на якийсь круг.

Ми хочемо отримати картинку на весь екран. Які є варіанти? Їх три.

1) Розтягнути превью на екран. Поганий варіант, тому що для цього співвідношення сторін повинно бути однаковим, а у нас воно різне. Але все ж спробуємо, щоб побачити результат.

Для цього нам ширину превью треба помножити на 1280/640 = 2. А висоту на 752/480 = 1,57. У підсумку маємо:

У підсумку маємо:

видно, що картинка деформувалася і стала розтягнутої по горизонталі. Нам це не підходить.

2) Втиснути превью в екран зі збереженням пропорцій. Для цього ми будемо міняти розміри превью (зберігаючи співвідношення сторін), поки воно зсередини не упреться в межі екрану по висоті або ширині. У нашому випадку воно упреться по висоті.

Для цього нам треба помножити ширину і висоту превью на менше з чисел: 1280/640 = 2 і 752/480 = 1,57, тобто на 1.57.

Дивимося, чого вийшло

Дивимося, чого вийшло

стало набагато краще. Тепер картинка прев'ю не спотворюючи. Єдине, що трохи бентежить - порожні області з боків екрану. Але нічого не заважає зафарбувати їх чорним і нехай всі думають, що так і задумано. Зате ми будемо бачити повну і неспотворену картинку. Так, наприклад, зазвичай робиться в відео-плеєрах.

3) Втиснути екран в превью. Тобто зробити другий варіант навпаки. Міняти розмір екрану (зберігаючи співвідношення сторін) до тих пір поки він зсередини не упреться в межі превью по висоті або ширині.

Для цього нам треба було б ширину і висоту екрану розділити на більше з чисел: 1280/640 = 2 і 752/480 = 1,57, тобто на 2.

Але тому що змінювати розміри екрана ми не можемо фізично, то ми будемо міняти розміри превью щоб досягти описаного результату.

Для цього нам треба помножити ширину і висоту превью на більше з чисел: 1280/640 = 2 і 752/480 = 1,57, тобто на 2.

результат

результат

Зображення не спотворена і займає повний екран. Але є нюанс. Ми не бачимо всього зображення. Воно виходить за межі екрану зверху і знизу.

Про всяк випадок зазначу, що це лише один приклад. В інших може бути по іншому. Наприклад, у другому варіанті порожні області можуть бути не з боків, а зверху і знизу. А на дрібних аксесуарах розмір превью буде більше розміру екрана. Але загальний зміст і алгоритм від цього не змінюються.

Ми розглянули три варіанти, і побачили, що перший зовсім поганий, а другий і третій цілком годяться для реалізації.

Від картинок повертаємося до коду. Метод setPreviewSize (boolean fullScreen) реалізує другий (якщо fullScreen == false) і третій (якщо fullScreen == true) варіанти.

Краса методу в тому, що всі перетворення за нас робить Matrix (матриця). І нам самим не треба буде нічого множити або ділити.

Спочатку ми отримуємо розміри екрану і превью. Для екрану відразу з'ясовуємо що більше: ширина або висота. Тобто якщо ширина більше, то пристрій знаходиться в горизонтальній орієнтації, якщо висота більше - у вертикальній.

Для перетворень матриця зажадає від нас RectF об'єкти. Якщо ніколи ще не працювали з ними, то це просто об'єкт, який містить координати прямокутника: left, top, right, bottom.

Як left і top ми завжди використовуватимемо 0, а в right і bottom поміщати ширину і висоту екрана або превью. Тим самим ми будемо отримувати прямокутники точно збігаються за розмірами з екраном і превью.

rectDisplay - екран, rectPreview - превью. У превью зазвичай ширина завжди більше висоти. Якщо пристрій в горизонтальній орієнтації, то ми створюємо rectPreview відповідно його розмірами. А якщо пристрій вертикально, то зображення з камери буде також повернуто вертикально, отже ширина і висота поміняються місцями.

Тепер найцікавіше - підготовка перетворення. використовуємо метод setRectToRect . Він бере на вхід два RectF. І обчислює, які перетворення треба зробити, щоб перший втиснути в другій. Про третій параметр методу я зараз розповідати не буду, ми завжди використовуємо START. (Якщо все ж цікаво, задавайте питання на форумі, там обговоримо)

Тобто цей метод поки не змінює об'єкти. Це тільки настройка матриці. Тепер матриця знає, які розрахунки їй треба буде зробити з координатами об'єкта, який ми їй пізніше надамо.

Дивимося код. Якщо (! FullScreen), то це другий варіант, тобто превью буде втиснутий в екран. Для цього ми просто повідомляємо матриці, що нам об'єкт з розмірами превью треба буде втиснути в об'єкт з розмірами екрану. Тобто якщо звернутися до другого варіанту, то матриця зрозуміла, що їй треба буде помножити сторони об'єкта на 1.57. І коли ми їй потім надамо об'єкт з розмірами превью - вона це зробить і ми отримаємо необхідні нам розміри.

Якщо ж fullScreen (третій варіант), то алгоритм трохи складніше. Ми повідомляємо матриці, що нам треба об'єкт з розмірами екрану втиснути в об'єкт з розмірами превью. Дивимося третій варіант. Спочатку ми з'ясували, що екран треба буде розділити на два. Але потім ми зрозуміли, що ми не можемо змінювати розміри екрану і нам треба робити навпаки - не екран ділити на два, а превью помножити на два. Це ми можемо пояснити і матриці викликавши метод invert. Матриця візьме алгоритм з переданої їй матриці (тобто з самої себе), і зробить все навпаки. Тобто замість того, щоб розділити на два - примножить.

Маю велику надію, що виклав зрозуміло. Якщо ж не зрозуміло - перечитайте раз 5 і звіряйтеся з описом варіантів і картинками в прикладі вище. Якщо все одно не зрозуміло, поверніться до цього де-нить через тиждень. Мозок на той час уже засвоїть і як-то укладе цю інфу. І повторне прочитання може пройти набагато легше. Принаймні у мене зазвичай це так) Я можу щось прочитати - нічого не зрозуміти. Але через тиждень / місяць / півроку знову заглянути туди і здивуватися: «а що тут власне незрозумілого то було?»

Отже, ми підготували матрицю до перетворення, залишилося тільки вручити їй об'єкт, який вона цим перетворенням піддасть. Для цього використовуємо метод mapRect і передаємо йому об'єкт з розмірами превью. Як і в прикладі вище, все перетворення ми будемо проводити з ним.

Після проведення перетворень ми беремо отримані координати і налаштовуємо по ним surface, яке відображає превью.

поворот превью

Якщо мозок ще не зруйнований, зараз ми це виправимо! Розбираємо метод setCameraDisplayOrientation, який буде превью обертати.

Знову розглянемо приклад, коли використовується планшет в горизонтальному стані, камера - задня. Припустимо, ми через камеру дивимося на такий об'єкт:

Припустимо, ми через камеру дивимося на такий об'єкт:

Бачимо його на екрані, все ок.

Важливе зауваження. Через стандартний додаток камери нижчеописаний приклад не відтвориться, тому що стандартний додаток обробляє поворот пристрою. А я хочу продемонструвати, що було б якби не обробляли.

Я повертаю планшет за годинниковою (направо) на 90 градусів. При цьому, зрозуміло повертається і камера. На екрані я бачу тепер таку картинку:

На екрані я бачу тепер таку картинку:

До речі, таку ж картинку побачите і ви, якщо нахиліть голову вправо на 90 градусів)

Тобто система хоч і зреагувала на поворот і повернула основне зображення, але камера повертає нам саме такий повернений вид. Його ми і бачимо.

Що треба зробити, щоб це виправити? Повернути картинку на 90 за годинниковою. Тобто зробити той же поворот, що зробила камера.

Вийшла Аксіома Повороту Камери: наскільки і в яку сторону повернута камера, на стільки ж і в ту ж сторону нам треба повертати і превью, щоб отримувати правильну картинку.

Для цього будемо використовувати метод setDisplayOrientation . Він приймає на вхід кут, на який камера буде повертати за годинниковою превью, перед тим як віддати його нам. Тобто від нас чекають кут повороту превью за годинниковою. Його ми можемо дізнатися, з'ясувавши наскільки повернута за годинниковою камера (див. Аксіому Повороту Камери).

Для цього використовуємо таку конструкцію - getWindowManager (). GetDefaultDisplay (). GetRotation (). Вона повертає нам градуси, на які система повертає зображення за годинниковою, щоб воно нормально відображалося при поворотах пристрою.

Тобто коли ви нахиляє пристрій на 90 проти годинникової, система повинна повертати зображення на 90 за годинниковою, щоб компенсувати поворот. (Зараз мова не про камеру, а просто про зображення яке показує телефон, наприклад - Home)

Аксіома Повороту Пристрої: наскільки і в яку сторону повернута пристрій, на стільки ж, але в іншу сторону система повертає зображення, щоб отримувати його в правильній орієнтації.

Звідси випливає, що getWindowManager (). GetDefaultDisplay (). GetRotation () повідомляє нам наскільки пристрій повернуто проти годинникової.

До речі, від getRotation ми отримуємо константи, а далі в switch перетворимо їх в градуси.

Отже, змінна degrees містить кількість градусів, на які повернуто телефон проти годинникової.

До сих пір мозок цілий? Тоді тримайте такий факт: камера в пристрої може бути повернута щодо цього пристрою.

Так зазвичай робиться на смартфонах. Тобто там камера повернута на 90 градусів. І її нормальна орієнтація збігається з горизонтальною орієнтацією пристрою. Щоб і в превью і на екрані ширина виходила більше висоти.

І ось цей поворот нам теж треба враховувати при повороті превью. Отримати дані про камері можна методом getCameraInfo . На вхід вимагає id камери і об'єкт CameraInfo , В який буде поміщена інфа про камері.

Нас цікавить поле CameraInfo.orientation , Яке повертає на скільки за годинниковою треба повернути превью, щоб отримати нормальне зображення. Тобто виходячи з Аксіоми Повороту Камери - на стільки ж повернута за годинниковою і сама камера.

Ну і добиваємо мозок таким фактом. Камера може бути задньої і передньої (фронтальної). І для них по різному треба вважати повороти)

поле CameraInfo.facing містить інфу про те, яка камера - задня або передня.

Спробуємо порахувати. Нагадаю, що метод setDisplayOrientation чекає від нас градус повороту превью за годинниковою. Тобто ми можемо просто порахувати поворот камери за годинниковою (Аксіома Повороту Камери) і отримаємо потрібне значення.

Щоб дізнатися підсумковий поворот камери за годинниковою в просторі - треба скласти поворот пристрою за годинниковою і CameraInfo.orientation. Це для задньої камери. А для передньої - треба CameraInfo.orientation відняти, тому що вона дивиться в наш бік. І все, що для нас за годинниковою, для неї - проти.

Все, вважаємо. У нас є degrees - к-ть градусів, на які повернуто телефон проти годинникової. Щоб конвертнуть це к-ть в градуси за годинниковою, треба просто відняти їх з 360.

Тобто (360 - degrees) - це поворот пристрою за годінніковою. Я спеціально віділів в коді цею віслів дужками для наочності. Далі ми до цього значення додаємо або віднімаємо (задня або передня камера) вбудований поворот камери. У випадку з передньою камерою на всякий випадок додаємо 360 градусів, щоб не вийшло від'ємне число. І в кінці визначаємо підсумкове кількість градусів в межах від 0 до 360, обчислюючи залишок від ділення на 360.

І урочисто передаємо камері це значення.

На рідкість мозгодробітельная штука - робота з камерою, правда? У підсумку, коли ви все це запустіть, ви повинні бачити адекватне зображення з камери.

На початку коду є дві константи: CAMERA_ID і FULL_SCREEN.

Якщо у вас дві камери, ви можете передати в CAMERA_ID НЕ 0, а 1, і отримаєте картинку з передньої камери.

Ну а змінюючи FULL_SCREEN змінюйте вид превью.

інше

Як визначити, чи є камера в пристрої? Про це повідомить конструкція context.getPackageManager (). HasSystemFeature (PackageManager.FEATURE_CAMERA)

Отримати id камери, можна використовуючи метод getNumberOfCameras (Доступний з API Level 9). Він поверне нам якесь кількість камер N, які доступні на пристрої. Відповідно їх ID будуть 0, 1, ..., N-1. З цього id вже отримуєте CameraInfo і визначаєте, що це за камера.

Метод open може повернути Exception при запуску якщо з якихось причин не вдалося отримати доступ до камери. Має сенс це обробляти і видавати повідомлення користувачеві, а не вилітати з помилкою.

Обробка повороту може криво працювати на деяких девайсах. Наприклад, я тестіл на HTC Desire (4.2.2) і Samsung Galaxy Tab (4.2.2) - було все ок. А на Samsung Galaxy Ace (2.3.6) склалося відчуття, що камера просто ігнор градус повороту, який я їй повідомляю.

На наступному уроці:

- робимо знімок
- пишемо відео

Приєднуйтесь до нас в Telegram:

- в каналі StartAndroid публікуються посилання на нові статті з сайту startandroid.ru і цікаві матеріали з Хабра, medium.com і т.п.

- в чатах вирішуємо ці запитання і проблеми з різних тем: Android , Kotlin , RxJava , Dagger , тестування

- ну і якщо просто хочеться поговорити з колегами по розробці, тобто чат флудильня

- новий чат Performance для обговорення проблем продуктивності та для ваших побажань за змістом курсу по цій темі


Encoding = "utf-8"?
Які є варіанти?
Але через тиждень / місяць / півроку знову заглянути туди і здивуватися: «а що тут власне незрозумілого то було?
Що треба зробити, щоб це виправити?
До сих пір мозок цілий?
На рідкість мозгодробітельная штука - робота з камерою, правда?

Уважаемые партнеры, если Вас заинтересовала наша продукция, мы готовы с Вами сотрудничать. Вам необходимо заполнить эту форму и отправить нам. Наши менеджеры в оперативном режиме обработают Вашу заявку, свяжутся с Вами и ответят на все интересующее Вас вопросы.

Или позвоните нам по телефонам: (048) 823-25-64

Организация (обязательно) *

Адрес доставки

Объем

Как с вами связаться:

Имя

Телефон (обязательно) *

Мобильный телефон

Ваш E-Mail

Дополнительная информация: