Gjallarhorn: Dynamic Timelapse Slices

This commit is contained in:
Kenneth Endfinger
2022-01-08 23:17:59 -05:00
parent 7a5a27d581
commit 94d644916b
4 changed files with 80 additions and 8 deletions

View File

@ -14,6 +14,9 @@ import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.int
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.Color
import java.awt.Font
import java.awt.font.TextLayout
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.time.Duration import java.time.Duration
import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.ScheduledThreadPoolExecutor
@ -22,6 +25,15 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
private val db by requireObject<Database>() private val db by requireObject<Database>()
private val timelapseIntervalLimit by option("--timelapse-limit", help = "Timelapse Limit Intervals").int() private val timelapseIntervalLimit by option("--timelapse-limit", help = "Timelapse Limit Intervals").int()
private val timelapseMode by option("--timelapse", help = "Timelapse Mode").enum<TimelapseMode> { it.id }.required() private val timelapseMode by option("--timelapse", help = "Timelapse Mode").enum<TimelapseMode> { it.id }.required()
private val timelapseSpeedChangeThreshold by option(
"--timelapse-change-speed-threshold",
help = "Timelapse Change Speed Threshold"
).int()
private val timelapseSpeedChangeMinimumIntervalSeconds by option(
"--timelapse-change-speed-minimum-interval-seconds",
help = "Timelapse Change Speed Minimum Interval Seconds"
).int()
private val render by option("--render", help = "Render Top Down Image").enum<RenderType> { it.id }.required() private val render by option("--render", help = "Render Top Down Image").enum<RenderType> { it.id }.required()
private val considerAirBlocks by option("--consider-air-blocks", help = "Enable Air Block Consideration").flag() private val considerAirBlocks by option("--consider-air-blocks", help = "Enable Air Block Consideration").flag()
@ -32,10 +44,20 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
private val logger = LoggerFactory.getLogger(BlockChangeTimelapseCommand::class.java) private val logger = LoggerFactory.getLogger(BlockChangeTimelapseCommand::class.java)
override fun run() { override fun run() {
val threadPoolExecutor = ScheduledThreadPoolExecutor(8) val threadPoolExecutor = ScheduledThreadPoolExecutor(16)
val changelog = BlockChangelog.query(db) val changelog = BlockChangelog.query(db)
val timelapse = BlockMapTimelapse<BufferedImage>(maybeBuildTrim()) val timelapse = BlockMapTimelapse<BufferedImage>(maybeBuildTrim())
val slices = timelapse.calculateChangelogSlices(changelog, timelapseMode.interval, timelapseIntervalLimit) var slices = timelapse.calculateChangelogSlices(changelog, timelapseMode.interval, timelapseIntervalLimit)
if (timelapseSpeedChangeThreshold != null && timelapseSpeedChangeMinimumIntervalSeconds != null) {
val minimumInterval = Duration.ofSeconds(timelapseSpeedChangeMinimumIntervalSeconds!!.toLong())
val blockChangeThreshold = timelapseSpeedChangeThreshold!!
slices = timelapse.splitChangelogSlicesWithThreshold(changelog, blockChangeThreshold, minimumInterval, slices)
}
logger.info("Timelapse Slices: ${slices.size} slices")
val imagePadCount = slices.size.toString().length val imagePadCount = slices.size.toString().length
val pool = BlockMapRenderPool( val pool = BlockMapRenderPool(
changelog = changelog, changelog = changelog,
@ -44,10 +66,20 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
rendererFactory = { expanse -> render.create(expanse) }, rendererFactory = { expanse -> render.create(expanse) },
threadPoolExecutor = threadPoolExecutor threadPoolExecutor = threadPoolExecutor
) { slice, result -> ) { slice, result ->
val speed = slice.relative.toSeconds().toDouble() / timelapseMode.interval.toSeconds().toDouble()
val graphics = result.createGraphics()
val font = Font.decode("Arial Black").deriveFont(36.0f)
graphics.color = Color.black
graphics.font = font
val context = graphics.fontRenderContext
val layout =
TextLayout("${slice.to} @ ${speed}x (1 frame = ${slice.relative.toSeconds()} seconds)", font, context)
layout.draw(graphics, 60f, 60f)
graphics.dispose()
val index = slices.indexOf(slice) + 1 val index = slices.indexOf(slice) + 1
val suffix = "-${index.toString().padStart(imagePadCount, '0')}" val suffix = "-${index.toString().padStart(imagePadCount, '0')}"
result.savePngFile("${render.id}${suffix}.png") result.savePngFile("${render.id}${suffix}.png")
logger.info("Rendered Timelapse $index") logger.info("Rendered Timelapse Slice $index")
} }
pool.render(slices) pool.render(slices)

View File

@ -10,12 +10,15 @@ class BlockChangelog(
val changes: List<BlockChange> val changes: List<BlockChange>
) { ) {
fun slice(slice: BlockChangelogSlice): BlockChangelog = BlockChangelog(changes.filter { fun slice(slice: BlockChangelogSlice): BlockChangelog = BlockChangelog(changes.filter {
it.time >= slice.first && slice.isTimeWithin(it.time)
it.time <= slice.second
}) })
fun countRelativeChangesInSlice(slice: BlockChangelogSlice): Int = changes.count {
slice.isRelativeWithin(it.time)
}
val changeTimeRange: BlockChangelogSlice val changeTimeRange: BlockChangelogSlice
get() = changes.minOf { it.time } to changes.maxOf { it.time } get() = BlockChangelogSlice(changes.minOf { it.time }, changes.maxOf { it.time })
companion object { companion object {
fun query(db: Database, filter: Op<Boolean> = Op.TRUE): BlockChangelog = transaction(db) { fun query(db: Database, filter: Op<Boolean> = Op.TRUE): BlockChangelog = transaction(db) {

View File

@ -1,5 +1,23 @@
package cloud.kubelet.foundation.gjallarhorn.state package cloud.kubelet.foundation.gjallarhorn.state
import java.time.Duration
import java.time.Instant import java.time.Instant
typealias BlockChangelogSlice = Pair<Instant, Instant> data class BlockChangelogSlice(val from: Instant, val to: Instant, val relative: Duration) {
constructor(from: Instant, to: Instant) : this(from, to, Duration.ofMillis(to.toEpochMilli() - from.toEpochMilli()))
fun changeResolutionTime(): Instant = to.minus(relative)
fun isTimeWithin(time: Instant) = time in from..to
fun isRelativeWithin(time: Instant) = time in changeResolutionTime()..to
fun split(): List<BlockChangelogSlice> {
val half = relative.dividedBy(2)
val initial = to.minus(relative)
val first = initial.plus(half)
return listOf(
BlockChangelogSlice(from, first, half),
BlockChangelogSlice(from, to, half)
)
}
}

View File

@ -19,7 +19,26 @@ class BlockMapTimelapse<T>(val trim: Pair<BlockCoordinate, BlockCoordinate>? = n
if (limit != null) { if (limit != null) {
intervals = intervals.takeLast(limit).toMutableList() intervals = intervals.takeLast(limit).toMutableList()
} }
return intervals.map { start to it } return intervals.map { BlockChangelogSlice(start, it, interval) }
}
fun splitChangelogSlicesWithThreshold(
changelog: BlockChangelog,
targetChangeThreshold: Int,
minimumTimeInterval: Duration,
slices: List<BlockChangelogSlice>
): List<BlockChangelogSlice> {
return slices.flatMap { slice ->
val count = changelog.countRelativeChangesInSlice(slice)
if (count < targetChangeThreshold ||
slice.relative < minimumTimeInterval
) {
return@flatMap listOf(slice)
}
val split = slice.split()
return@flatMap splitChangelogSlicesWithThreshold(changelog, targetChangeThreshold, minimumTimeInterval, split)
}
} }
override fun buildRenderJobs( override fun buildRenderJobs(