experimental deblocking filter

This commit is contained in:
minjaesong
2025-09-06 00:06:15 +09:00
parent 99e5183b42
commit 4dfea6df11
2 changed files with 193 additions and 8 deletions

View File

@@ -3,6 +3,7 @@
// Usage: playtev moviefile.tev [options]
// Options: -i (interactive), -debug-mv (show motion vector debug visualization)
// -deinterlace=algorithm (yadif or bwdif, default: yadif)
// -nodeblock (disble deblocking filter)
const WIDTH = 560
const HEIGHT = 448
@@ -40,9 +41,26 @@ let subtitleVisible = false
let subtitleText = ""
let subtitlePosition = 0 // 0=bottom center (default)
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
const debugMotionVectors = exec_args[2] && exec_args[2].toLowerCase() == "-debug-mv"
const deinterlaceAlgorithm = "yadif"
// Parse command line options
let interactive = false
let debugMotionVectors = false
let deinterlaceAlgorithm = "yadif"
let enableDeblocking = true // Default: enabled (use -nodeblock to disable)
if (exec_args.length > 2) {
for (let i = 2; i < exec_args.length; i++) {
const arg = exec_args[i].toLowerCase()
if (arg === "-i") {
interactive = true
} else if (arg === "-debug-mv") {
debugMotionVectors = true
} else if (arg === "-nodeblock") {
enableDeblocking = false
} else if (arg.startsWith("-deinterlace=")) {
deinterlaceAlgorithm = arg.substring(13)
}
}
}
const fullFilePath = _G.shell.resolvePathInput(exec_args[1])
const FILE_LENGTH = files.open(fullFilePath.full).size
@@ -374,7 +392,7 @@ if (version !== TEV_VERSION_YCOCG && version !== TEV_VERSION_XYB) {
let colorSpace = (version === TEV_VERSION_XYB) ? "XYB" : "YCoCg-R"
if (interactive) {
con.move(1,1)
println(`Push and hold Backspace to exit | TEV Format ${version} (${colorSpace})`)
println(`Push and hold Backspace to exit | TEV Format ${version} (${colorSpace}) | Deblocking: ${enableDeblocking ? 'ON' : 'OFF'}`)
}
let width = seqread.readShort()
@@ -628,7 +646,7 @@ try {
// Hardware-accelerated TEV decoding to RGB buffers (YCoCg-R or XYB based on version)
try {
// duplicate every 1000th frame (pass a turn every 1000n+501st) if NTSC
if (!isInterlaced || frameCount % 1000 != 501 || frameDuped) {
if (!isNTSC || frameCount % 1000 != 501 || frameDuped) {
frameDuped = false
let decodeStart = sys.nanoTime()
@@ -637,14 +655,14 @@ try {
if (isInterlaced) {
// For interlaced: decode current frame into currentFieldAddr
// For display: use prevFieldAddr as current, currentFieldAddr as next
graphics.tevDecode(blockDataPtr, nextFieldAddr, currentFieldAddr, width, decodingHeight, [qualityY, qualityCo, qualityCg], trueFrameCount, debugMotionVectors, version)
graphics.tevDecode(blockDataPtr, nextFieldAddr, currentFieldAddr, width, decodingHeight, [qualityY, qualityCo, qualityCg], trueFrameCount, debugMotionVectors, version, enableDeblocking)
graphics.tevDeinterlace(trueFrameCount, width, decodingHeight, prevFieldAddr, currentFieldAddr, nextFieldAddr, CURRENT_RGB_ADDR, deinterlaceAlgorithm)
// Rotate field buffers for next frame: NEXT -> CURRENT -> PREV
rotateFieldBuffers()
} else {
// Progressive or first frame: normal decoding without temporal prediction
graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, decodingHeight, [qualityY, qualityCo, qualityCg], trueFrameCount, debugMotionVectors, version)
graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, decodingHeight, [qualityY, qualityCo, qualityCg], trueFrameCount, debugMotionVectors, version, enableDeblocking)
}
decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 // Convert to milliseconds

View File

@@ -1964,6 +1964,167 @@ class GraphicsJSR223Delegate(private val vm: VM) {
return xybData
}
/**
* Advanced TEV Deblocking Filter - Reduces blocking artifacts from 16x16 macroblocks
*
* Uses gradient analysis and adaptive filtering to handle:
* - Quantized smooth gradients appearing as discrete blocks
* - Diagonal edges crossing block boundaries causing color banding
* - Texture preservation to avoid over-smoothing genuine edges
*
* @param rgbAddr RGB frame buffer address (24-bit: R,G,B per pixel)
* @param width Frame width in pixels
* @param height Frame height in pixels
* @param blockSize Size of blocks (16 for TEV format)
* @param strength Filter strength (0.0-1.0, higher = more smoothing)
*/
private fun tevDeblockingFilter(rgbAddr: Long, width: Int, height: Int,
blockSize: Int = 16, strength: Float = 0.4f) {
val blocksX = (width + blockSize - 1) / blockSize
val blocksY = (height + blockSize - 1) / blockSize
val thisAddrIncVec: Long = if (rgbAddr < 0) -1 else 1
// Helper function to get pixel value safely
fun getPixel(x: Int, y: Int, c: Int): Int {
if (x < 0 || y < 0 || x >= width || y >= height) return 0
val offset = (y.toLong() * width + x) * 3 + c
return vm.peek(rgbAddr + offset * thisAddrIncVec)!!.toUint().toInt()
}
// Helper function to set pixel value safely
fun setPixel(x: Int, y: Int, c: Int, value: Int) {
if (x < 0 || y < 0 || x >= width || y >= height) return
val offset = (y.toLong() * width + x) * 3 + c
vm.poke(rgbAddr + offset * thisAddrIncVec, value.coerceIn(0, 255).toByte())
}
// Detect if pixels form a smooth gradient (quantized)
fun isQuantizedGradient(p0: Int, p1: Int, p2: Int, p3: Int): Boolean {
// Check for step-like transitions typical of quantized gradients
val d01 = kotlin.math.abs(p1 - p0)
val d12 = kotlin.math.abs(p2 - p1)
val d23 = kotlin.math.abs(p3 - p2)
// Look for consistent small steps (quantized gradient)
val avgStep = (d01 + d12 + d23) / 3.0f
val stepVariance = kotlin.math.abs(d01 - avgStep) + kotlin.math.abs(d12 - avgStep) + kotlin.math.abs(d23 - avgStep)
return avgStep in 3.0f..25.0f && stepVariance < avgStep * 0.8f
}
// Apply horizontal deblocking (vertical edges between blocks)
for (by in 0 until blocksY) {
for (bx in 1 until blocksX) {
val blockEdgeX = bx * blockSize
if (blockEdgeX >= width) continue
for (y in (by * blockSize) until minOf((by + 1) * blockSize, height)) {
for (c in 0..2) { // RGB components
// Sample 4 pixels across the block boundary: [left2][left1] | [right1][right2]
val left2 = getPixel(blockEdgeX - 2, y, c)
val left1 = getPixel(blockEdgeX - 1, y, c)
val right1 = getPixel(blockEdgeX, y, c)
val right2 = getPixel(blockEdgeX + 1, y, c)
val edgeDiff = kotlin.math.abs(right1 - left1)
// Skip strong edges (likely genuine features)
if (edgeDiff > 50) continue
// Check for quantized gradient pattern
if (isQuantizedGradient(left2, left1, right1, right2)) {
// Apply gradient-preserving smoothing
val gradientLeft = left1 - left2
val gradientRight = right2 - right1
val avgGradient = (gradientLeft + gradientRight) / 2.0f
val smoothedLeft1 = (left2 + avgGradient).toInt()
val smoothedRight1 = (right2 - avgGradient).toInt()
// Blend with original based on strength
val blendLeft = (left1 * (1.0f - strength) + smoothedLeft1 * strength).toInt()
val blendRight = (right1 * (1.0f - strength) + smoothedRight1 * strength).toInt()
setPixel(blockEdgeX - 1, y, c, blendLeft)
setPixel(blockEdgeX, y, c, blendRight)
}
// Check for color banding on diagonal features
else if (edgeDiff in 8..35) {
// Look at diagonal context to detect banding
val diagContext = kotlin.math.abs(getPixel(blockEdgeX - 1, y - 1, c) - getPixel(blockEdgeX, y + 1, c))
if (diagContext < edgeDiff * 1.5f) {
// Likely diagonal banding - apply directional smoothing
val blend = 0.3f * strength
val blendLeft = (left1 * (1.0f - blend) + right1 * blend).toInt()
val blendRight = (right1 * (1.0f - blend) + left1 * blend).toInt()
setPixel(blockEdgeX - 1, y, c, blendLeft)
setPixel(blockEdgeX, y, c, blendRight)
}
}
}
}
}
}
// Apply vertical deblocking (horizontal edges between blocks)
for (by in 1 until blocksY) {
for (bx in 0 until blocksX) {
val blockEdgeY = by * blockSize
if (blockEdgeY >= height) continue
for (x in (bx * blockSize) until minOf((bx + 1) * blockSize, width)) {
for (c in 0..2) { // RGB components
// Sample 4 pixels across the block boundary: [top2][top1] | [bottom1][bottom2]
val top2 = getPixel(x, blockEdgeY - 2, c)
val top1 = getPixel(x, blockEdgeY - 1, c)
val bottom1 = getPixel(x, blockEdgeY, c)
val bottom2 = getPixel(x, blockEdgeY + 1, c)
val edgeDiff = kotlin.math.abs(bottom1 - top1)
// Skip strong edges (likely genuine features)
if (edgeDiff > 50) continue
// Check for quantized gradient pattern
if (isQuantizedGradient(top2, top1, bottom1, bottom2)) {
// Apply gradient-preserving smoothing
val gradientTop = top1 - top2
val gradientBottom = bottom2 - bottom1
val avgGradient = (gradientTop + gradientBottom) / 2.0f
val smoothedTop1 = (top2 + avgGradient).toInt()
val smoothedBottom1 = (bottom2 - avgGradient).toInt()
// Blend with original based on strength
val blendTop = (top1 * (1.0f - strength) + smoothedTop1 * strength).toInt()
val blendBottom = (bottom1 * (1.0f - strength) + smoothedBottom1 * strength).toInt()
setPixel(x, blockEdgeY - 1, c, blendTop)
setPixel(x, blockEdgeY, c, blendBottom)
}
// Check for color banding on diagonal features
else if (edgeDiff in 8..35) {
// Look at diagonal context to detect banding
val diagContext = kotlin.math.abs(getPixel(x - 1, blockEdgeY - 1, c) - getPixel(x + 1, blockEdgeY, c))
if (diagContext < edgeDiff * 1.5f) {
// Likely diagonal banding - apply directional smoothing
val blend = 0.3f * strength
val blendTop = (top1 * (1.0f - blend) + bottom1 * blend).toInt()
val blendBottom = (bottom1 * (1.0f - blend) + top1 * blend).toInt()
setPixel(x, blockEdgeY - 1, c, blendTop)
setPixel(x, blockEdgeY, c, blendBottom)
}
}
}
}
}
}
}
/**
* Hardware-accelerated TEV frame decoder for YCoCg-R 4:2:0 format
* Decodes compressed TEV block data directly to framebuffer
@@ -1978,7 +2139,8 @@ class GraphicsJSR223Delegate(private val vm: VM) {
*/
fun tevDecode(blockDataPtr: Long, currentRGBAddr: Long, prevRGBAddr: Long,
width: Int, height: Int, qualityIndices: IntArray, frameCounter: Int,
debugMotionVectors: Boolean = false, tevVersion: Int = 2) {
debugMotionVectors: Boolean = false, tevVersion: Int = 2,
enableDeblocking: Boolean = true) {
// height doesn't change when interlaced, because that's the encoder's output
@@ -2369,6 +2531,11 @@ class GraphicsJSR223Delegate(private val vm: VM) {
}
}
}
// Apply deblocking filter if enabled to reduce blocking artifacts
if (enableDeblocking) {
tevDeblockingFilter(currentRGBAddr, width, height)
}
}
fun tevDeinterlace(frameCounter: Int, width: Int, height: Int, prevField: Long, currentField: Long, nextField: Long, outputRGB: Long, algorithm: String = "yadif") {