Проживая в эпоху технологических прорывов и свершений, взирая на то, как устремляются в небо ракеты Маска и Безоса, мы, простые люди с высшим техническим образованием, часто не замечаем возможности совершить прорыв не там, далеко в космосе, а здесь рядом с нами, буквально не вставая
Судите сами, к какому открытию может привести чтение обычной статьи о современных смартфонах. Источник приводить не буду, чтобы не делиться будущими доходами.
Вершиной вычислительной фотографии можно, пожалуй, считать ночную съемку. Примером может служить «Ночной Режим» в смартфонах 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-гарнитурами на головах…
SquareRootOfZero
О, прикольно. Можно ещё попробовать прикрепить ко всему этому тепловизор и совместить картинку, типа Хищник. Я как-то пытался присобачить SeekThermal на HoloLens, получилось не очень, но как раз вот кошек и пр. живность оно палит на ура (конечно, не на нагретом за день асфальте и т. п.).