-
Notifications
You must be signed in to change notification settings - Fork 5
Android
Aos AR-Evacuation Beacon is Android version of the application. AR-Evacuation With Beacons detects the beacons inside of K-SW Square building in the Purdue University. The application estimates user's current indoor location and inform the optimized path to each user to flee safely from fire situation.
BeaconApplication is an application detects nearby beacons which are installed on the ceiling or the wall. In this project, iBeacon protocol was used, setBeaconLayout was set. To detect particular beacon, the UUID was set. After the UUId was set, only the beacons with specific UUID are detected.
val beaconManager = BeaconManager.getInstanceForApplication(this)
BeaconManager.setDebug(true)
beaconManager.beaconParsers.clear()
// setBeaconLayout to find iBeacon
beaconManager.beaconParsers.add(BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24"))
// set beacon period
beaconManager.backgroundScanPeriod = 1000
beaconManager.foregroundScanPeriod = 1000
// set UUID
region = Region("all-beacons", Identifier.parse("$beaconID"), null, null)
beaconManager.startMonitoring(region)
beaconManager.startRangingBeacons(region)
LocalizationManager utilizes RSSI received from the beacons as an input of DNN model and estimates user location from model output. The DNN model is downloaded from firebase. The code block below is how to get model output. Since the each cell location comes out with the probability, the highest probability cell is the estimated location.
private fun getModelOutput(rssiList: MutableList<Float>) {
val labelNum = 31
val input = ByteBuffer.allocateDirect(4 * rssiList.size).order(ByteOrder.nativeOrder())
for (i in 0 until rssiList.size) {
input.putFloat(rssiList[i])
}
// set buffer size
val bufferSize = labelNum * java.lang.Float.SIZE / java.lang.Byte.SIZE
val modelOutput = ByteBuffer.allocateDirect(bufferSize).order(ByteOrder.nativeOrder())
// run DNN model
interpreter?.run(input, modelOutput)
modelOutput.rewind()
val probabilities = modelOutput.asFloatBuffer()
try {
val probabilityArray = mutableListOf<Float>()
for (i in 0 until probabilities.capacity()) {
val probability = probabilities.get(i)
probabilityArray.add(probability)
}
// highest probability is the estimated location
val maxProbability = probabilityArray.maxOrNull()
// map probability values to labels
var mappedLabel: String = BeaconConstants.labelList[probabilityArray.indexOf(maxProbability)]
} catch (e: IOException) {
Log.e("$$$ Output Error $$$", e.toString())
}There was a limit to assume the location of the user using only the model output. So, we used the output of the model and user direction together based on the compass sensor of Android device.
private fun filterErrorWithHeading(previousLocation: Position, currentLocation: Position = Position.unknown): Position {
if (previousLocation == Position.unknown || currentLocation == Position.unknown) return Position.unknown
val adjacentCellList = mutableListOf<String>()
val adjacentCells = AdjacentCell.valueOf(previousLocation.position).cell.toList()
for (cell in adjacentCells) {
if (cell is List<*>) {
val list = cell.filterIsInstance<Position>()
list.forEachIndexed { _, value ->
adjacentCellList.add(value.position)
}
} else {
adjacentCellList.add(cell.toString())
}
}
val userDirection = directionRepository.userDirection.value
// if currentLocation is adjacent to previousLocation
if (adjacentCellList.contains(currentLocation.position)) {
var candidateCells = adjacentCells[userDirection?.index!!]
val newCandidateCells = mutableListOf<Position>()
if (candidateCells is List<*>) {
val list = candidateCells.filterIsInstance<Position>()
list.forEach { value ->
newCandidateCells.add(Position.valueOf(value.position))
}
} else {
newCandidateCells.add(Position.valueOf(candidateCells.toString()))
}
newCandidateCells.removeAll { it == Position.unknown }
// if candidate cell and direction and heading are same, use current cell
return if (newCandidateCells.contains(currentLocation)) {
currentLocation
} else {
// if adjacent cell, direction and heading are not same, use previous cell
previousLocation
}
} else {
// if currentLocation is not adjacent to previousLocation
// user heading and angle between previous location and the result are same, the user moves fast
val currentPair = locationRepository.calculateCenter(currentLocation.position)
val currentX = currentPair.first
val currentY = currentPair.second
val previousPair = locationRepository.calculateCenter(previousLocation.position)
val previousX = previousPair.first
val previousY = previousPair.second
val direction = directionRepository.classifyDirection(directionRepository.vectorBetween2Points(previousX!!, previousY!!, currentX!!, currentY!!))
// user can go to candidate cell in same direction
return if (direction == userDirection) {
var candidateCells = adjacentCells[direction?.index!!]
var newCells = mutableListOf<Position>()
if (candidateCells is List<*>) {
val list = candidateCells.filterIsInstance<Position>()
list.forEach { value ->
newCells.add(Position.valueOf(value.position))
}
} else {
newCells.add(Position.valueOf(candidateCells.toString()))
}
newCells.removeAll { it == Position.unknown }
if (newCells.isEmpty()) {
previousLocation
} else {
newCells.first()
}
} else {
previousLocation
}
}
}DirectionRepository saves direction and heading informations. vectorBetween2Points calculates find out angle between two points using atan2 function.
fun vectorBetween2Points(previousX: Float, previousY: Float, currentX: Float, currentY: Float): Float {
var degree: Float
val tan = atan2(previousX - currentX, previousY - currentY) * 180 / Math.PI
degree = if (tan < 0) {
(-tan).toFloat() + 180f
} else {
180f - tan.toFloat()
}
return degree
}ArRenderable renders 3d arrow object at the center of the camera angle. It extracts camera position and update the worldposition of the anchorNode every per frame.
fun onUpdateFrame(textView: TextView, frameTime: FrameTime) {
val frame = arFragment.arSceneView.arFrame ?: return
val position = frame.camera?.pose?.compose(Pose.makeTranslation(0f, 0f, -2f))?.extractTranslation()
anchorNode.worldPosition = Vector3(position?.tx()!!, position.ty(), position.tz())
...
}After it renders 3D arrow, it updates angle of the arrow based on direction of DirectionRepository. It supposed that user look towards the south.
directionRepository.arrowDegree.value?.let {
val pointDirection = directionRepository.classifyDirection(it)
val headingDirection = directionRepository.classifyDirection(directionRepository.userCurrentHeading.value!!)
when (pointDirection.index - headingDirection.index) {
// forward
0 -> transformableNode.localRotation = Quaternion.eulerAngles(Vector3(0f, 180f, 0f))
// right
-1 -> transformableNode.localRotation = Quaternion.eulerAngles(Vector3(0f, 90f, 0f))
// left
1 -> transformableNode.localRotation = Quaternion.eulerAngles(Vector3(0f, 270f, 0f))
// backward
else -> transformableNode.localRotation = Quaternion.eulerAngles(Vector3(0f, 0f, 0f))
}
}