Skip to content

Instantly share code, notes, and snippets.

@awasisto
Last active November 15, 2019 15:33
Show Gist options
  • Save awasisto/ea60b86bf0c619bfe689a461e0fd7f1f to your computer and use it in GitHub Desktop.
Save awasisto/ea60b86bf0c619bfe689a461e0fd7f1f to your computer and use it in GitHub Desktop.
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()
}
}
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