Очки сумрачного зрения. Android Camera2 API от чайника, часть 5 внезапная



    Проживая в эпоху технологических прорывов и свершений, взирая на то, как устремляются в небо ракеты Маска и Безоса, мы, простые люди с высшим техническим образованием, часто не замечаем возможности совершить прорыв не там, далеко в космосе, а здесь рядом с нами, буквально не вставая с дивана из-стола.

    Судите сами, к какому открытию может привести чтение обычной статьи о современных смартфонах. Источник приводить не буду, чтобы не делиться будущими доходами.
    Вершиной вычислительной фотографии можно, пожалуй, считать ночную съемку. Примером может служить «Ночной Режим» в смартфонах Google Pixel. В нём IT гиганту пришлось задействовать съёмку в RAW, HDR-стекинг, компенсацию «смазов», распознавание сцен нейросетями. А появление второй камеры в прошлогоднем Pixel 4 сделало «Night Sight» пригодным даже для съемки звезд. В сумме это создает ощущение волшебства: глаза видят кромешную тьму, а на фотографии лёгкие сумерки. Как шутят на форумах, скоро на смартфон можно будет снять чёрную кошку в тёмной комнате и она будет чёткой.

    Другое дело, что ходить ночью и тыриться в экран мобильника как-то неудобно, даже в ночном режиме. И тут мой взгляд случайно упал на VR-гарнитуру для смартфона, валявшуюся на полке. Прорыв свершился! Осталось только, используя её и накопленные за четыре поста знания о Android Camera2 API, направить изображение с «Night Sight» прямо в глаз. Заодно и руки будут свободны, чтобы поймать чёрную кошку в тёмной комнате. Совсем без света, конечно, не получится, фотонов, хоть немного, да нужно. Но по крайней мере уровня котановских гляделок в темноте мы достигнуть (а может, даже превзойти) обязаны.

    Итак, чтобы научиться видеть во тьме, нам понадобится:

    1: гарнитура виртуальной реальности для смартфона, (можно самую дешёвую)



    2: смартфон с поддержкой современных гуглофич для камеры (ну, он точно самым дешевым не окажется)



    3: знание основ Android Camera2 API (это у нас уже есть)

    часть первая
    часть вторая
    часть третья
    часть четвертая

    Открываем новый проект в Android Studio и начинаем ваять код.

    Первым делом надо собрать, собственно VR поверхности, которые будут светить в гарнитуру.

    Макет
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#03061B"
        tools:context=".MainActivity">
    
        <TextureView
            android:id="@+id/textureView"
            android:layout_width="240dp"
            android:layout_height="320dp"
            android:layout_marginTop="28dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.497"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <TextureView
            android:id="@+id/textureView3"
            android:layout_width="240dp"
            android:layout_height="320dp"
            android:layout_marginTop="16dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textureView" />
    
        <LinearLayout
            android:layout_width="165dp"
            android:layout_height="40dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textureView3"
            app:layout_constraintVertical_bias="0.838">
    
            <Button
                android:id="@+id/button1"
                android:layout_width="wrap_content"
                android:layout_height="36dp"
                android:backgroundTint="#3F51B5"
                android:text="вкл"
                android:textColor="#1A87DD" />
    
            <Button
                android:id="@+id/button3"
                android:layout_width="wrap_content"
                android:layout_height="37dp"
                android:backgroundTint="#3F51B5"
                android:text="выкл"
                android:textColor="#2196F3" />
        </LinearLayout>
    
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    На выходе должно получится что-то в этом роде:



    Как видим, текстуры получились прямоугольными, тогда как в любой VR игрушке для смартфона разработчики как-то ухитряются делать их круглыми. Но мы не будем на этом зацикливаться. Дальнейший опыт все равно покажет, что и так сойдёт.

    Для этого, собственно, мы пишем скромную такую Activity, в которой всё уже нам знакомо по предыдущим статьям.

    package com.example.twovideosurfaces;
    import androidx.annotation.RequiresApi;
    import androidx.appcompat.app.AppCompatActivity;
    import androidx.core.content.ContextCompat;
    import android.Manifest;
    import android.content.Context;
    import android.content.pm.ActivityInfo;
    import android.content.pm.PackageManager;
    import android.graphics.SurfaceTexture;
    import android.hardware.camera2.CameraAccessException;
    import android.hardware.camera2.CameraCaptureSession;
    import android.hardware.camera2.CameraDevice;
    import android.hardware.camera2.CameraManager;
    import android.hardware.camera2.CaptureRequest;
    import android.os.Build;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.HandlerThread;
    import android.os.StrictMode;
    import android.util.Log;
    import android.view.Surface;
    import android.view.TextureView;
    import android.view.View;
    import android.widget.Button;
    import java.util.Arrays;
    public class MainActivity extends AppCompatActivity  {
        public static final String LOG_TAG = "myLogs";
        public static Surface surface1 = null;
        public static Surface surface2 = null;
        CameraService[] myCameras = null;
        private CameraManager mCameraManager = null;
        private final int CAMERA1 = 0;
        private Button mOn = null;
        private Button mOff = null;
        public static TextureView mImageViewUp = null;
        public static TextureView mImageViewDown = null;
        private HandlerThread mBackgroundThread;
        private Handler mBackgroundHandler = null;
        private void startBackgroundThread() {
            mBackgroundThread = new HandlerThread("CameraBackground");
            mBackgroundThread.start();
            mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
        }
        private void stopBackgroundThread() {
            mBackgroundThread.quitSafely();
            try {
                mBackgroundThread.join();
                mBackgroundThread = null;
                mBackgroundHandler = null;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        @RequiresApi(api = Build.VERSION_CODES.M)
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
            StrictMode.setThreadPolicy(policy);
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            setContentView(R.layout.activity_main);
            Log.d(LOG_TAG, "Запрашиваем разрешение");
            if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
                    ||
                    (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
            ) {
                requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
            }
            mOn = findViewById(R.id.button1);
            mOff = findViewById(R.id.button3);
            mImageViewUp = findViewById(R.id.textureView);
            mImageViewDown = findViewById(R.id.textureView3);
            mOn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (myCameras[CAMERA1] != null) {// открываем камеру
                        if (!myCameras[CAMERA1].isOpen()) myCameras[CAMERA1].openCamera();
                    }
                }
            });
            mOff.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                }
            });
            mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
            try {
                // Получение списка камер с устройства
                myCameras = new CameraService[mCameraManager.getCameraIdList().length];
                for (String cameraID : mCameraManager.getCameraIdList()) {
                    Log.i(LOG_TAG, "cameraID: " + cameraID);
                    int id = Integer.parseInt(cameraID);
                    // создаем обработчик для камеры
                    myCameras[id] = new CameraService(mCameraManager, cameraID);
                }
            } catch (CameraAccessException e) {
                Log.e(LOG_TAG, e.getMessage());
                e.printStackTrace();
            }
        }
        public class CameraService {
            private String mCameraID;
            private CameraDevice mCameraDevice = null;
            private CameraCaptureSession mSession;
            private CaptureRequest.Builder mPreviewBuilder;
            public CameraService(CameraManager cameraManager, String cameraID) {
                mCameraManager = cameraManager;
                mCameraID = cameraID;
            }
            private CameraDevice.StateCallback mCameraCallback = new CameraDevice.StateCallback() {
                @Override
                public void onOpened(CameraDevice camera) {
                    mCameraDevice = camera;
                    Log.i(LOG_TAG, "Open camera  with id:" + mCameraDevice.getId());
                    startCameraPreviewSession();
                }
                @Override
                public void onDisconnected(CameraDevice camera) {
                    mCameraDevice.close();
                    Log.i(LOG_TAG, "disconnect camera  with id:" + mCameraDevice.getId());
                    mCameraDevice = null;
                }
                @Override
                public void onError(CameraDevice camera, int error) {
                    Log.i(LOG_TAG, "error! camera id:" + camera.getId() + " error:" + error);
                }
            };
            private void startCameraPreviewSession() {
                SurfaceTexture texture = mImageViewUp.getSurfaceTexture();
                texture.setDefaultBufferSize(1280, 1024);
                surface1 = new Surface(texture);
                SurfaceTexture texture2 = mImageViewDown.getSurfaceTexture();
                surface2 = new Surface(texture2);
                texture2.setDefaultBufferSize(1280, 1024);
                try {
                    mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
                    mPreviewBuilder.addTarget(surface1);
                    mPreviewBuilder.addTarget(surface2);
                    mCameraDevice.createCaptureSession(Arrays.asList(surface1,surface2),
                            new CameraCaptureSession.StateCallback() {
                                @Override
                                public void onConfigured(CameraCaptureSession session) {
                                    mSession = session;
    
                                    try {
                                        mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler);
                                    } catch (CameraAccessException e) {
                                        e.printStackTrace();
                                    }
                                }
                                @Override
                                public void onConfigureFailed(CameraCaptureSession session) {
                                }
                            }, mBackgroundHandler);
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
            }
            public boolean isOpen() {
                if (mCameraDevice == null) {
                    return false;
                } else {
                    return true;
                }
            }
            public void openCamera() {
                try {
                    if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
                        mCameraManager.openCamera(mCameraID, mCameraCallback, mBackgroundHandler);
                    }
                } catch (CameraAccessException e) {
                    Log.i(LOG_TAG, e.getMessage());
                }
            }
            public void closeCamera() {
                if (mCameraDevice != null) {
                    mCameraDevice.close();
                    mCameraDevice = null;
                }
            }
        }
        @Override
        public void onPause() {
            if (myCameras[CAMERA1].isOpen()) {
                myCameras[CAMERA1].closeCamera();
            }
            stopBackgroundThread();
            super.onPause();
        }
        @Override
        public void onResume() {
            super.onResume();
            startBackgroundThread();
        }
    }
    

    Да, и не забываем про

    Манифест
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.twovideosurfaces">
        <uses-permission android:name="android.permission.CAMERA" />
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
        <uses-permission android:name="android.permission.INTERNET"/>
        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/Theme.AppCompat.NoActionBar"
            >
            <activity android:name=".MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
    </manifest>
    


    Теперь запихиваем смартфон в VR гарнитуру и ходим по дому, наслаждаясь зрением киборга разрешением 1280 х 1024 на каждый глаз. Ощущения, конечно, странные, с потерей глубины зрения, но всё равно прикольно. Единственно, всё выглядит слегка темновато, но это потому, что мешает передняя полупрозрачная панель гарнитуры. Поэтому в ней надо продырить соответствующее отверстие напротив камеры смартфона. Но опять же, на самых бюджетных VR моделях такой панели вообще может и не быть, и вполне вероятно, что вам и не придется осквернять себя ручным трудом.

    Всё что теперь осталось — так это убедить Google camera API, что у нас тьма кромешная, и хорошо бы задействовать режим Night Vision, а вместе с ним все эти RAW, HDR-стекинг и распознавание сцен нейросетями.

    Для этого всего лишь пропишем в сессии:

    mPreviewBuilder.set(CaptureRequest.CONTROL_SCENE_MODE,
                                                CaptureRequest.CONTROL_SCENE_MODE_NIGHT);
    
    

    и выкрутим по максимуму экспозицию и светочувствительность

     mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE,
            CaptureRequest.CONTROL_AE_MODE_OFF);
    mPreviewBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME,Long.valueOf("100000000"));
     mPreviewBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, 30000);
    


    Ой, я ослеп!



    Вот что, оказывается, видит кот, когда его выкидывают из спальни, где он мешает людям заниматься сексом, в гостиную.

    Но конечно, это перебор и параметры (а их в API немало и здесь приведена всего парочка) надо подкрутить потом опытным путём.

    Теперь нам остаётся только дождаться ночи. Не безлунной, конечно, с плотной облачностью где-нибудь в тайге, а обычной такой ночи со случайными залетными фотонами. И вот что произойдёт…

    Хотя казалось бы, при обычной съёмке практически ничего не видно.



    Но современные камеры творят чудеса и таки находят черную кошку…



    Теперь можно гулять по ночам, потому что днём нельзя из-за карантина. По идее и ночью тоже нельзя, но кто ж вас увидит, крадущихся во мраке ночи с VR-гарнитурами на головах…
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 14

      0
      О, прикольно. Можно ещё попробовать прикрепить ко всему этому тепловизор и совместить картинку, типа Хищник. Я как-то пытался присобачить SeekThermal на HoloLens, получилось не очень, но как раз вот кошек и пр. живность оно палит на ура (конечно, не на нагретом за день асфальте и т. п.).
        0
        А если подсвечивать инфракрасным фонариком, лучше будет?
          0

          IR Фильтр на камерах смартфонов стоит… эффект от подсветки никакой.

            +1
            каким образом тогда у меня получается на смартфоне увидеть мигание ИК светододов от пульта телефизора, ИК сканера айфона, у пультов управления игрушками и т.д.?
              0
              Потому что фильтр не идеальный и мизерную часть он всётаки пропускает.
              Вот если его снять… на противоположной стене комнаты будет видно круглое пятно света от пульта телека.
          +1

          А если по-честному, видно через этот девайс лучше или хуже, чем просто глазами, привыкшими к темноте?


          Я, конечно, верю в чудеса Night Sight'а, но основная причина этих чудес — HDR-стекинг большого количества кадров в течение этак 5 секунд съёмки и потом нескольких секунд обработки. Для видеопотока это всё малоприменимо, поэтому я настроен довольно скептично.

            0

            Забавно, но не более. Человеческий глаз гораздо чувствительней.
            Разве что дольше адаптируется к освещенности и у кого проблемы с ночным зрением.

              0

              Из интересного, на Google Play на днях видел похожее приложение, которое позволяет через меню выбирать спецэффекты для картинки в VR-очках.
              Среди просто "забавных" чб/рисованных и т.д. эффектов так же были и довольно полезные режимы симуляции разных видов дальтонизма. А их уже можно использовать в разработке и тестировании физических интерфейсов в реальном мире.

                0

                Чувствительность глаза оценивается как ISO 800 после 40 минут в темноте. Совершенно не впечатляет. И интеграции светового потока по времени нет — можно долго смотреть на что-то в темноте, но мелкие детали всё-равно не разглядишь.

                0
                Почему-то восхваляется решение гугла и ни слова о лидере ночной съемки — Huawei. Но в любом случае эти ночные режимы только о фото.
                  +2

                  С первым апреля) Пришёл сюда из статьи о тихоходках :D

                    0
                    Два чая этому господину!
                    0
                    смешно, да
                      0
                      наверное видео не смотрели

                      Only users with full accounts can post comments. Log in, please.