Skip to content

Commit 8af8ac3

Browse files
committed
Improve image reader lookup performance
Signed-off-by: Kyle Corry <kylecorry31@gmail.com>
1 parent 1d17ea8 commit 8af8ac3

5 files changed

Lines changed: 100 additions & 71 deletions

File tree

app/src/main/java/com/kylecorry/trail_sense/shared/data/EncodedDataImageReader.kt

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,41 @@ class EncodedDataImageReader(
1515
private val reader: ImageReader,
1616
private val treatZeroAsNaN: Boolean = false,
1717
private val maxChannels: Int? = null,
18-
private val decoder: (Int?) -> List<Float> = { listOf(it?.toFloat() ?: 0f) }
18+
private val decoder: (Int) -> FloatArray = { floatArrayOf(it.toFloat()) }
1919
) : DataImageReader {
2020
override fun getSize(): Size {
2121
return reader.getSize()
2222
}
2323

2424
override suspend fun getRegion(rect: Rect, config: Bitmap.Config): Pair<FloatBitmap, Boolean>? {
25-
var pixelGrid: Array<Array<FloatArray>>? = null
25+
var pixelGrid: FloatBitmap? = null
2626
var isAllNaN = true
2727
reader.getRegion(rect)?.use {
2828
val pixels = getPixels()
2929
val w = width
30-
pixelGrid = Array(height) { y ->
31-
Array(width) { x ->
30+
val h = height
31+
val channels = if (maxChannels != null) {
32+
val firstPixel = if (pixels.isNotEmpty()) decoder(pixels[0]) else floatArrayOf()
33+
minOf(firstPixel.size, maxChannels)
34+
} else {
35+
if (pixels.isNotEmpty()) decoder(pixels[0]).size else 0
36+
}
37+
38+
pixelGrid = FloatBitmap(w, h, channels)
39+
for (y in 0 until h) {
40+
for (x in 0 until w) {
3241
val pixel = pixels[y * w + x]
3342
val decoded = decoder(pixel)
34-
val channels = if (maxChannels != null) minOf(
35-
decoded.size,
36-
maxChannels
37-
) else decoded.size
38-
FloatArray(channels) { i ->
39-
val value = decoded[i]
40-
if ((treatZeroAsNaN && SolMath.isZero(value)) || value.isNaN()) {
41-
Float.NaN
42-
} else {
43-
isAllNaN = false
44-
value
45-
}
43+
for (i in 0 until channels) {
44+
val value = if (i < decoded.size) decoded[i] else Float.NaN
45+
val finalValue =
46+
if ((treatZeroAsNaN && SolMath.isZero(value)) || value.isNaN()) {
47+
Float.NaN
48+
} else {
49+
isAllNaN = false
50+
value
51+
}
52+
pixelGrid.set(x, y, i, finalValue)
4653
}
4754
}
4855
}
@@ -61,37 +68,37 @@ class EncodedDataImageReader(
6168
a: Double,
6269
b: Double,
6370
convertZero: Boolean = true
64-
): (Int?) -> List<Float> {
71+
): (Int) -> FloatArray {
6572
return {
66-
val red = it?.red?.toFloat() ?: 0f
67-
val green = it?.green?.toFloat() ?: 0f
68-
val blue = it?.blue?.toFloat() ?: 0f
69-
val alpha = it?.alpha?.toFloat() ?: 0f
73+
val red = it.red.toFloat()
74+
val green = it.green.toFloat()
75+
val blue = it.blue.toFloat()
76+
val alpha = it.alpha.toFloat()
7077

7178
if (!convertZero && red == 0f && green == 0f && blue == 0f) {
72-
listOf(0f, 0f, 0f, alpha)
79+
floatArrayOf(0f, 0f, 0f, alpha)
7380
} else {
74-
listOf(
75-
red / a - b,
76-
green / a - b,
77-
blue / a - b,
78-
alpha / a - b
79-
).map { it.toFloat() }
81+
floatArrayOf(
82+
(red / a - b).toFloat(),
83+
(green / a - b).toFloat(),
84+
(blue / a - b).toFloat(),
85+
(alpha / a - b).toFloat()
86+
)
8087
}
8188
}
8289
}
8390

84-
fun split16BitDecoder(a: Double = 1.0, b: Double = 0.0): (Int?) -> List<Float> {
91+
fun split16BitDecoder(a: Double = 1.0, b: Double = 0.0): (Int) -> FloatArray {
8592
return {
86-
val red = it?.red ?: 0
87-
val green = it?.green ?: 0
88-
val blue = it?.blue ?: 0
89-
val alpha = it?.alpha ?: 0
93+
val red = it.red
94+
val green = it.green
95+
val blue = it.blue
96+
val alpha = it.alpha
9097

91-
listOf(
92-
green shl 8 or red,
93-
alpha shl 8 or blue
94-
).map { (it.toDouble() / a - b).toFloat() }
98+
floatArrayOf(
99+
((green shl 8 or red).toDouble() / a - b).toFloat(),
100+
((alpha shl 8 or blue).toDouble() / a - b).toFloat()
101+
)
95102

96103
}
97104
}

app/src/main/java/com/kylecorry/trail_sense/shared/data/FloatBitmapInterpolator.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ class FloatBitmapInterpolator(interpolationOrder: Int) {
1212
)
1313

1414
fun getValue(bitmap: FloatBitmap, rect: Rect, x: Float, y: Float): FloatArray {
15-
if (bitmap.isEmpty() || bitmap[0].isEmpty() || bitmap[0][0].isEmpty()) {
15+
if (bitmap.width == 0 || bitmap.height == 0 || bitmap.channels == 0) {
1616
return FloatArray(0)
1717
}
1818

19-
val interpolated = FloatArray(bitmap[0][0].size)
19+
val interpolated = FloatArray(bitmap.channels)
2020

2121
for (i in interpolated.indices) {
2222
val localPixel = PixelCoordinate(

app/src/main/java/com/kylecorry/trail_sense/shared/data/GeographicImageSource.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,24 @@ import com.kylecorry.sol.science.geology.CoordinateBounds
88
import com.kylecorry.sol.units.Coordinate
99
import kotlin.math.floor
1010

11-
typealias FloatBitmap = Array<Array<FloatArray>>
11+
class FloatBitmap(val width: Int, val height: Int, val channels: Int) {
12+
private val data = FloatArray(width * height * channels)
13+
14+
fun get(x: Int, y: Int, channel: Int): Float {
15+
return data[(y * width + x) * channels + channel]
16+
}
17+
18+
fun set(x: Int, y: Int, channel: Int, value: Float) {
19+
data[(y * width + x) * channels + channel] = value
20+
}
21+
22+
fun getOrNull(x: Int, y: Int, channel: Int): Float? {
23+
if (x !in 0 until width || y !in 0 until height || channel !in 0 until channels) {
24+
return null
25+
}
26+
return get(x, y, channel)
27+
}
28+
}
1229

1330
class GeographicImageSource(
1431
private val reader: DataImageReader,
@@ -83,7 +100,7 @@ class GeographicImageSource(
83100
val rect = getRect(region, interpolationOrder) ?: continue
84101
val (pixelGrid, hasData) = reader.getRegion(rect) ?: continue
85102
val interpolator = FloatBitmapInterpolator(interpolationOrder)
86-
val empty = FloatArray(pixelGrid[0][0].size)
103+
val empty = FloatArray(pixelGrid.channels)
87104
for (pixel in region) {
88105
var interpolated = empty
89106
if (hasData) {

app/src/main/java/com/kylecorry/trail_sense/shared/data/PixelInterpolators.kt

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,21 @@ import kotlin.math.roundToInt
99
interface PixelInterpolator {
1010
fun interpolate(
1111
pixel: PixelCoordinate,
12-
pixels: Array<Array<FloatArray>>,
12+
pixels: FloatBitmap,
1313
channel: Int
1414
): Float?
1515
}
1616

1717
class NearestInterpolator : PixelInterpolator {
1818
override fun interpolate(
1919
pixel: PixelCoordinate,
20-
pixels: Array<Array<FloatArray>>,
20+
pixels: FloatBitmap,
2121
channel: Int
2222
): Float? {
2323
// TODO: Extract this
24-
if (pixels.isEmpty()) {
25-
return null
26-
}
27-
val height = pixels.size
28-
val width = pixels.first().size
29-
if (width == 0) {
24+
val height = pixels.height
25+
val width = pixels.width
26+
if (width == 0 || height == 0) {
3027
return null
3128
}
3229

@@ -44,7 +41,7 @@ class NearestInterpolator : PixelInterpolator {
4441
if (cx < 0 || cy < 0 || cy >= height || cx >= width) {
4542
return
4643
}
47-
val value = pixels[cy][cx][channel]
44+
val value = pixels.get(cx, cy, channel)
4845
if (value.isNaN()) {
4946
return
5047
}
@@ -89,18 +86,18 @@ class NearestInterpolator : PixelInterpolator {
8986
class BilinearInterpolator : PixelInterpolator {
9087
override fun interpolate(
9188
pixel: PixelCoordinate,
92-
pixels: Array<Array<FloatArray>>,
89+
pixels: FloatBitmap,
9390
channel: Int
9491
): Float? {
9592
// Find the 4 corners
9693
val xFloor = pixel.x.toInt()
9794
val yFloor = pixel.y.toInt()
9895
val xCeil = xFloor + 1
9996
val yCeil = yFloor + 1
100-
val x1y1 = pixels.getOrNull(yFloor)?.getOrNull(xFloor)?.getOrNull(channel)
101-
val x1y2 = pixels.getOrNull(yCeil)?.getOrNull(xFloor)?.getOrNull(channel)
102-
val x2y1 = pixels.getOrNull(yFloor)?.getOrNull(xCeil)?.getOrNull(channel)
103-
val x2y2 = pixels.getOrNull(yCeil)?.getOrNull(xCeil)?.getOrNull(channel)
97+
val x1y1 = pixels.getOrNull(xFloor, yFloor, channel)
98+
val x1y2 = pixels.getOrNull(xFloor, yCeil, channel)
99+
val x2y1 = pixels.getOrNull(xCeil, yFloor, channel)
100+
val x2y2 = pixels.getOrNull(xCeil, yCeil, channel)
104101

105102
// Not enough values to interpolate
106103
if (x1y1 == null || x1y2 == null || x2y1 == null || x2y2 == null ||
@@ -133,7 +130,7 @@ class BicubicInterpolator : PixelInterpolator {
133130

134131
override fun interpolate(
135132
pixel: PixelCoordinate,
136-
pixels: Array<Array<FloatArray>>,
133+
pixels: FloatBitmap,
137134
channel: Int
138135
): Float? {
139136
val x = pixel.x
@@ -145,13 +142,13 @@ class BicubicInterpolator : PixelInterpolator {
145142
val fx = x - xInt
146143
val fy = y - yInt
147144

148-
val rowVals = mutableListOf<Float>()
145+
val rowVals = FloatArray(4)
149146
for (i in 0 until 4) {
150147
var value = 0f
151148
for (j in 0 until 4) {
152149
val currentX = xInt + j - 1
153150
val currentY = yInt + i - 1
154-
val pixelValue = pixels.getOrNull(currentY)?.getOrNull(currentX)?.getOrNull(channel)
151+
val pixelValue = pixels.getOrNull(currentX, currentY, channel)
155152
?: return null
156153

157154
if (pixelValue.isNaN()) {
@@ -160,7 +157,7 @@ class BicubicInterpolator : PixelInterpolator {
160157

161158
value += pixelValue * cubic(fx - (j - 1).toFloat())
162159
}
163-
rowVals.add(value)
160+
rowVals[i] = value
164161
}
165162

166163
var result = 0f

app/src/main/java/com/kylecorry/trail_sense/shared/dem/DEM.kt

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import com.kylecorry.andromeda.core.coroutines.onDefault
1010
import com.kylecorry.andromeda.core.coroutines.onIO
1111
import com.kylecorry.andromeda.core.tryOrDefault
1212
import com.kylecorry.luna.cache.LRUCache
13-
import com.kylecorry.luna.coroutines.ParallelCoroutineRunner
13+
import com.kylecorry.luna.coroutines.Parallel
1414
import com.kylecorry.sol.math.SolMath
1515
import com.kylecorry.sol.math.SolMath.cosDegrees
1616
import com.kylecorry.sol.math.SolMath.toRadians
@@ -41,9 +41,9 @@ import kotlin.math.hypot
4141
import kotlin.math.sin
4242

4343
object DEM {
44-
private val cacheDistance = 10f
45-
private val cacheSize = 500
46-
private var cache = GeospatialCache<Float>(Distance.meters(cacheDistance), size = cacheSize)
44+
private const val CACHE_DISTANCE = 10f
45+
private const val CACHE_SIZE = 500
46+
private var cache = GeospatialCache<Float>(Distance.meters(CACHE_DISTANCE), size = CACHE_SIZE)
4747
private val multiElevationLookupLock = Mutex()
4848
private var gridCache = LRUCache<String, List<List<Pair<Coordinate, Float>>>>(1)
4949
private var cachedSources: List<GeographicImageSource>? = null
@@ -121,8 +121,8 @@ object DEM {
121121

122122
val allElevations = getElevations(toLookupCoordinates, bounds)
123123
var i = 0
124-
latitudes.map { lat ->
125-
longitudes.map { lon ->
124+
latitudes.map {
125+
longitudes.map {
126126
val location = toLookupCoordinates[i++]
127127
location to (allElevations[location] ?: 0f)
128128
}
@@ -140,18 +140,26 @@ object DEM {
140140
): List<Contour> = onDefault {
141141
val grid = getElevationGrid(bounds, resolution)
142142

143-
val minElevation = grid.minOfOrNull { it.minOf { it.second } } ?: 0f
144-
val maxElevation = grid.maxOfOrNull { it.maxOf { it.second } } ?: 0f
143+
var minElevation = Float.MAX_VALUE
144+
var maxElevation = Float.MIN_VALUE
145+
for (row in grid) {
146+
for (point in row) {
147+
if (point.second < minElevation) {
148+
minElevation = point.second
149+
}
150+
if (point.second > maxElevation) {
151+
maxElevation = point.second
152+
}
153+
}
154+
}
145155

146156
val thresholds = Interpolation.getMultiplesBetween(
147157
minElevation,
148158
maxElevation,
149159
interval
150160
)
151161

152-
val parallelThresholds = ParallelCoroutineRunner(16)
153-
parallelThresholds.map(thresholds) { threshold ->
154-
162+
Parallel.map(thresholds) { threshold ->
155163
val segments = Interpolation.getIsoline(
156164
grid,
157165
threshold,
@@ -388,7 +396,7 @@ object DEM {
388396
}
389397

390398
fun invalidateCache() {
391-
cache = GeospatialCache(Distance.meters(cacheDistance), size = cacheSize)
399+
cache = GeospatialCache(Distance.meters(CACHE_DISTANCE), size = CACHE_SIZE)
392400
cachedSources = null
393401
cachedIsExternal = null
394402
}

0 commit comments

Comments
 (0)