-
-
Save awasisto/ea60b86bf0c619bfe689a461e0fd7f1f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.wasisto.camrng | |
import android.content.Context | |
import android.graphics.BitmapFactory | |
import android.graphics.ImageFormat | |
import android.graphics.Rect | |
import android.hardware.camera2.* | |
import android.hardware.camera2.params.RggbChannelVector | |
import android.media.ImageReader | |
import android.os.Handler | |
import android.os.HandlerThread | |
import androidx.core.graphics.get | |
import androidx.lifecycle.MutableLiveData | |
class ImageNoiseRng private constructor( | |
context: Context, | |
var mode: Mode, | |
var onError: (Throwable) -> Unit | |
) : Rng() { | |
enum class Mode { | |
THERMAL_NOISE_BLUE_PIXEL, | |
THERMAL_NOISE_RED_PIXEL, | |
SHOT_NOISE_GREEN_PIXEL | |
} | |
companion object { | |
private const val MOVING_AVERAGE_DATA_LENGTH = 30 | |
@Volatile | |
private var instance: ImageNoiseRng? = null | |
@Synchronized | |
fun getInstance(context: Context, mode: Mode, onError: (Throwable) -> Unit): ImageNoiseRng { | |
return instance ?: ImageNoiseRng(context.applicationContext, mode, onError).also { | |
instance = it | |
} | |
} | |
} | |
override val liveBoolean = MutableLiveData<Boolean>() | |
private var cameraDevice: CameraDevice? = null | |
private val handler = Handler( | |
HandlerThread("BackgroundThread").also { | |
it.start() | |
}.looper | |
) | |
private val movingAverageData = mutableListOf<Float>() | |
init { | |
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager | |
val cameraId = cameraManager.cameraIdList.find { | |
cameraManager.getCameraCharacteristics(it)[CameraCharacteristics.LENS_FACING] == CameraCharacteristics.LENS_FACING_BACK | |
}!! | |
val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId) | |
val size = cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!.getOutputSizes(ImageFormat.JPEG).maxBy { it.width * it.height }!! | |
try { | |
cameraManager.openCamera( | |
cameraId, | |
object : CameraDevice.StateCallback() { | |
override fun onOpened(cameraDevice: CameraDevice) { | |
this@ImageNoiseRng.cameraDevice = cameraDevice | |
val imageReader = ImageReader.newInstance(size.width, size.height, ImageFormat.JPEG, 2).apply { | |
setOnImageAvailableListener( | |
{ | |
val image = it.acquireNextImage() | |
val imageWidth = image.width | |
val imageHeight = image.height | |
val buffer = image.planes[0].buffer | |
val bytes = ByteArray(buffer.remaining()) | |
buffer.get(bytes) | |
image.close() | |
val centerPixel = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)[imageWidth / 2, imageHeight / 2] | |
val red = (centerPixel shr 16 and 0xff).toFloat() | |
val green = (centerPixel shr 8 and 0xff).toFloat() | |
val blue = (centerPixel and 0xff).toFloat() | |
movingAverageData += when (mode) { | |
Mode.THERMAL_NOISE_BLUE_PIXEL -> blue | |
Mode.THERMAL_NOISE_RED_PIXEL -> red | |
Mode.SHOT_NOISE_GREEN_PIXEL -> green | |
} | |
if (movingAverageData.size == MOVING_AVERAGE_DATA_LENGTH) { | |
liveBoolean.postValue( | |
when (mode) { | |
Mode.THERMAL_NOISE_BLUE_PIXEL -> blue >= movingAverageData.average() | |
Mode.THERMAL_NOISE_RED_PIXEL -> red >= movingAverageData.average() | |
Mode.SHOT_NOISE_GREEN_PIXEL -> green >= movingAverageData.average() | |
} | |
) | |
movingAverageData.removeAt(0) | |
} | |
}, | |
handler) | |
} | |
cameraDevice.createCaptureSession( | |
listOf(imageReader.surface), | |
object : CameraCaptureSession.StateCallback() { | |
override fun onConfigured(session: CameraCaptureSession) { | |
val captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_MANUAL).apply { | |
addTarget(imageReader.surface) | |
set(CaptureRequest.SENSOR_SENSITIVITY, cameraCharacteristics[CameraCharacteristics.SENSOR_MAX_ANALOG_SENSITIVITY]) // max iso | |
set(CaptureRequest.SCALER_CROP_REGION, Rect(size.width / 2, size.height / 2, size.width / 2 + 1, size.height / 2 + 1)) // max zoom | |
set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) // auto exposure | |
set(CaptureRequest.COLOR_CORRECTION_GAINS, RggbChannelVector(3.0f, 1.9f, 1.9f, 2.6f)) // daylight white balance | |
set(CaptureRequest.LENS_FOCUS_DISTANCE, 0.0f) // infinity focus | |
} | |
session.setRepeatingRequest(captureRequestBuilder.build(), object : CameraCaptureSession.CaptureCallback() {}, null) | |
} | |
override fun onConfigureFailed(session: CameraCaptureSession) { | |
cameraDevice.close() | |
onError(Exception("Failed to configure capture session")) | |
} | |
}, | |
null | |
) | |
} | |
override fun onDisconnected(camera: CameraDevice) { | |
cameraDevice?.close() | |
} | |
override fun onError(camera: CameraDevice, error: Int) { | |
cameraDevice?.close() | |
onError(Exception("Failed to open camera. Error code: $error")) | |
} | |
}, | |
null | |
) | |
} catch (e: SecurityException) { | |
onError(e) | |
} | |
} | |
fun close() { | |
cameraDevice?.close() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.wasisto.camrng | |
import androidx.lifecycle.LiveData | |
import androidx.lifecycle.MediatorLiveData | |
import androidx.lifecycle.Observer | |
import kotlinx.coroutines.Dispatchers | |
import kotlinx.coroutines.runBlocking | |
import java.util.concurrent.CountDownLatch | |
abstract class Rng { | |
abstract val liveBoolean: LiveData<Boolean> | |
val liveByte by lazy { createLiveDataForType(Byte::class.java) } | |
val liveInt by lazy { createLiveDataForType(Int::class.java) } | |
val liveLong by lazy { createLiveDataForType(Long::class.java) } | |
val liveFloat by lazy { createLiveDataForType(Float::class.java) } | |
val liveDouble by lazy { createLiveDataForType(Double::class.java) } | |
fun nextBoolean() = nextData(liveBoolean) | |
fun nextByte() = nextData(liveByte) | |
fun nextInt() = nextData(liveInt) | |
fun nextLong() = nextData(liveLong) | |
fun nextFloat() = nextData(liveFloat) | |
fun nextDouble() = nextData(liveDouble) | |
@Suppress("UNCHECKED_CAST") | |
private fun <T> createLiveDataForType(type: Class<T>): LiveData<T> { | |
val bits = when (type) { | |
Byte::class.java -> Byte.SIZE_BITS | |
Int::class.java -> Int.SIZE_BITS | |
Long::class.java -> Long.SIZE_BITS | |
Float::class.java -> 24 | |
Double::class.java -> 53 | |
else -> throw UnsupportedOperationException() | |
} | |
val bitStringBuilder = StringBuilder() | |
return MediatorLiveData<T>().apply { | |
addSource(liveBoolean) { | |
bitStringBuilder.append(if (it) '1' else '0') | |
if (bitStringBuilder.length == bits) { | |
when (type) { | |
Byte::class.java -> { | |
value = if (bitStringBuilder[0] == '1') { | |
// convert 2's complement to decimal | |
-(bitStringBuilder.toString().replace('0', ' ').replace('1', '0').replace(' ', '1').toByte(2) + 1) as T | |
} else { | |
bitStringBuilder.toString().toByte(2) as T | |
} | |
} | |
Int::class.java -> { | |
value = if (bitStringBuilder[0] == '1') { | |
// convert 2's complement to decimal | |
-(bitStringBuilder.toString().replace('0', ' ').replace('1', '0').replace(' ', '1').toInt(2) + 1) as T | |
} else { | |
bitStringBuilder.toString().toInt(2) as T | |
} | |
} | |
Long::class.java -> { | |
value = if (bitStringBuilder[0] == '1') { | |
// convert 2's complement to decimal | |
-(bitStringBuilder.toString().replace('0', ' ').replace('1', '0').replace(' ', '1').toLong(2) + 1) as T | |
} else { | |
bitStringBuilder.toString().toLong(2) as T | |
} | |
} | |
Float::class.java -> { | |
// based on java.util.Random#nextFloat() | |
value = (bitStringBuilder.toString().toInt(2) / (1 shl 24).toFloat()) as T | |
} | |
Double::class.java -> { | |
// based on java.util.Random#nextDouble() | |
value = (((bitStringBuilder.substring(0, 26).toLong(2) shl 27) + bitStringBuilder.substring(26, 53).toLong(2)) * 1.0 / (1L shl 53)) as T | |
} | |
} | |
bitStringBuilder.clear() | |
} | |
} | |
} | |
} | |
@Suppress("UNCHECKED_CAST") | |
private fun <T> nextData(liveData: LiveData<T>): T { | |
var data: T? = null | |
val latch = CountDownLatch(2) | |
runBlocking(Dispatchers.Main) { | |
liveData.observeForever(object : Observer<T> { | |
override fun onChanged(t: T) { | |
data = t | |
latch.countDown() | |
if (latch.count == 0L) { | |
liveData.removeObserver(this) // prevent memory leak | |
} | |
} | |
}) | |
} | |
latch.await() | |
return data as T | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment