package org.jcodec.codecs.vpx;
import static java.lang.Math.abs;
import static org.jcodec.codecs.vpx.FilterUtil.Segment.horizontal;
import static org.jcodec.codecs.vpx.FilterUtil.Segment.vertical;
import org.jcodec.api.NotImplementedException;
import org.jcodec.codecs.vpx.Macroblock.Subblock;
/**
* This class is part of JCodec ( www.jcodec.org ) This software is distributed
* under FreeBSD License
*
* @author The JCodec project
*
*/
public class FilterUtil {
/**
* Clamp, then convert signed number back to pixel value.
*/
private static int clipPlus128(int v) {
return (int) (clipSigned(v) + 128);
}
public static class Segment {
/**
* pixels before edge
*/
int p0, p1, p2, p3;
/**
* pixels after edge
*/
int q0, q1, q2, q3;
/**
* All functions take (among other things) a segment (of length at most 4 + 4 = 8) symmetrically
* straddling an edge. The pixel values (or pointers) are always given in order, from the
* "beforemost" to the "aftermost". So, for a horizontal edge (written "|"), an 8-pixel segment
* would be ordered p3 p2 p1 p0 | q0 q1 q2 q3.
*
* Filtering is disabled if the difference between any two adjacent "interior" pixels in the
* 8-pixel segment exceeds the relevant threshold (I). A more complex thresholding calculation
* is done for the group of four pixels that straddle the edge, in line with the calculation in simple_segment() above.
* @interior limit on interior differences
* @edge limit at the edge
*/
public boolean isFilterRequired(int interior, int edge) {
return ((abs(p0 - q0)<<2) + (abs(p1 - q1)>>2)) <= edge
&& abs(p3 - p2) <= interior
&& abs(p2 - p1) <= interior
&& abs(p1 - p0) <= interior
&& abs(q3 - q2) <= interior
&& abs(q2 - q1) <= interior
&& abs(q1 - q0) <= interior;
}
/**
* HEV - Hight Edge Variance. Filtering is altered
* if (at least) one of the differences on either side of
* the edge exceeds a threshold (we have "high edge variance").
* @param threshold
* @param p1 before
* @param p0 before
* @param q0 after
* @param q1 after
* @return
*/
public boolean isHighVariance(int threshold) {
return abs(p1 - p0) > threshold || abs(q1 - q0) > threshold;
}
public Segment getSigned() {
Segment seg = new Segment();
seg.p3 = minus128(this.p3);
seg.p2 = minus128(this.p2);
seg.p1 = minus128(this.p1);
seg.p0 = minus128(this.p0);
seg.q0 = minus128(this.q0);
seg.q1 = minus128(this.q1);
seg.q2 = minus128(this.q2);
seg.q3 = minus128(this.q3);
return seg;
}
public static Segment horizontal(Subblock right, Subblock left, int a) {
Segment seg = new Segment();
seg.p0 = left.val[3*4+a];
seg.p1 = left.val[2*4+a];
seg.p2 = left.val[1*4+a];
seg.p3 = left.val[0*4+a];
seg.q0 = right.val[0*4+a];
seg.q1 = right.val[1*4+a];
seg.q2 = right.val[2*4+a];
seg.q3 = right.val[3*4+a];
return seg;
}
public static Segment vertical(Subblock lower, Subblock upper, int a) {
Segment seg = new Segment();
seg.p0 = upper.val[a*4+3];
seg.p1 = upper.val[a*4+2];
seg.p2 = upper.val[a*4+1];
seg.p3 = upper.val[a*4+0];
seg.q0 = lower.val[a*4+0];
seg.q1 = lower.val[a*4+1];
seg.q2 = lower.val[a*4+2];
seg.q3 = lower.val[a*4+3];
return seg;
}
public void applyHorizontally(Subblock right, Subblock left, int a) {
left.val[3*4+a] = this.p0;
left.val[2*4+a] = this.p1;
left.val[1*4+a] = this.p2;
left.val[0*4+a] = this.p3;
right.val[0*4+a] = this.q0;
right.val[1*4+a] = this.q1;
right.val[2*4+a] = this.q2;
right.val[3*4+a] = this.q3;
}
public void applyVertically(Subblock lower, Subblock upper, int a) {
upper.val[a*4+3] = this.p0;
upper.val[a*4+2] = this.p1;
upper.val[a*4+1] = this.p2;
upper.val[a*4+0] = this.p3;
lower.val[a*4+0] = this.q0;
lower.val[a*4+1] = this.q1;
lower.val[a*4+2] = this.q2;
lower.val[a*4+3] = this.q3;
}
/**
*
* @param hevThreshold detect high edge variance
* @param interiorLimit possibly disable filter
* @param edgeLimit
* @param this
*/
void filterMb(int hevThreshold, int interiorLimit, int edgeLimit) {
Segment signedSeg = this.getSigned();
if (signedSeg.isFilterRequired(interiorLimit, edgeLimit)) {
if (!signedSeg.isHighVariance(hevThreshold)) {
// Same as the initial calculation in "common_adjust",
// w is something like twice the edge difference
int w = clipSigned(clipSigned(signedSeg.p1 - signedSeg.q1) + 3 * (signedSeg.q0 - signedSeg.p0));
// 9/64 is approximately 9/63 = 1/7 and 1<<7 = 128 = 2*64.
// So this a, used to adjust the pixels adjacent to the edge,
// is something like 3/7 the edge difference.
int a = (27 * w + 63) >> 7;
q0 = clipPlus128(signedSeg.q0 - a);
p0 = clipPlus128(signedSeg.p0 + a);
// Next two are adjusted by 2/7 the edge difference
a = (18 * w + 63) >> 7;
// System.out.println("a: "+a);
q1 = clipPlus128(signedSeg.q1 - a);
p1 = clipPlus128(signedSeg.p1 + a);
// Last two are adjusted by 1/7 the edge difference
a = (9 * w + 63) >> 7;
q2 = clipPlus128(signedSeg.q2 - a);
p2 = clipPlus128(signedSeg.p2 + a);
} else
// if hev, do simple filter
this.adjust(true); // using outer taps
}
}
/**
*
* @param hev_threshold detect high edge variance
* @param interior_limit disable filter
* @param edge_limit
* @param this
*/
public void filterSb(int hev_threshold,
int interior_limit,
int edge_limit) {
Segment signedSeg = this.getSigned();
if (signedSeg.isFilterRequired(interior_limit, edge_limit)) {
boolean hv = signedSeg.isHighVariance(hev_threshold);
int a = (this.adjust(hv) + 1) >> 1;
if (!hv) {
this.q1 = clipPlus128(signedSeg.q1 - a);
this.p1 = clipPlus128(signedSeg.p1 + a);
}
}
}
/**
* filter is 2 or 4 taps wide
*/
private int adjust(boolean use_outer_taps) {
/**
* retrieve and convert all 4 pixels
*/
int p1 = minus128(this.p1);
int p0 = minus128(this.p0);
int q0 = minus128(this.q0);
int q1 = minus128(this.q1);
/**
* Disregarding clamping, when "use_outer_taps" is false, "a" is 3*(q0-p0).
* Since we are about to divide "a" by 8, in this case we end up multiplying
* the edge difference by 5/8. When "use_outer_taps" is true (as for the
* simple filter), "a" is p1 - 3*p0 + 3*q0 - q1, which can be thought of
* as a refinement of 2*(q0 - p0) and the adjustment is something like
* (q0 - p0)/4.
*/
int a = clipSigned((use_outer_taps ? clipSigned(p1 - q1) : 0) + 3 * (q0 - p0));
/**
* b is used to balance the rounding of a/8 in the case where the "fractional"
* part "f" of a/8 is exactly 1/2.
*/
int b = (clipSigned(a + 3)) >> 3;
/**
* Divide a by 8, rounding up when f >= 1/2. Although not strictly part
* of the "C" language, the right-shift is assumed to propagate the sign bit.
*/
a = clipSigned(a + 4) >> 3;
/**
* Subtract "a" from q0, "bringing it closer" to p0.
*/
this.q0 = clipPlus128(q0 - a);
/**
* Add "a" (with adjustment "b") to p0, "bringing it closer" to q0. T
* he clamp of "a+b", while present in the reference decoder, is
* superfluous; we have -16 <= a <= 15 at this point.
*/
this.p0 = clipPlus128(p0 + b);
return a;
}
}
private static int clipSigned(int v) {
return (int) (v < -128 ? -128 : (v > 127 ? 127 : v));
}
/* Convert pixel value (0 <= v <= 255) to an 8-bit signed number. */
private static int minus128(int v) {
return (int) (v - 128);
}
public static void loopFilterUV(Macroblock[][] mbs, int sharpnessLevel, boolean keyFrame) {
for (int y = 0; y < (mbs.length-2); y++) {
for (int x = 0; x < (mbs[0].length-2); x++) {
Macroblock rmb = mbs[y+1][x+1];
Macroblock bmb = mbs[y+1][x+1];
int loop_filter_level = rmb.filterLevel;
if (loop_filter_level != 0) {
int interior_limit = rmb.filterLevel;
if (sharpnessLevel > 0) {
interior_limit >>= sharpnessLevel > 4 ? 2 : 1;
if (interior_limit > 9 - sharpnessLevel)
interior_limit = 9 - sharpnessLevel;
}
if (interior_limit == 0)
interior_limit = 1;
int hev_threshold = 0;
if (keyFrame) /* current frame is a key frame */ {
if (loop_filter_level >= 40)
hev_threshold = 2;
else if (loop_filter_level >= 15)
hev_threshold = 1;
} else /* current frame is an interframe */ {
throw new NotImplementedException("TODO: non-key frames are not supported yet.");
// if (loop_filter_level >= 40)
// hev_threshold = 3;
// else if (loop_filter_level >= 20)
// hev_threshold = 2;
// else if (loop_filter_level >= 15)
// hev_threshold = 1;
}
/* Luma and Chroma use the same inter-macroblock edge limit */
int mbedge_limit = ((loop_filter_level + 2) * 2) + interior_limit;
/* Luma and Chroma use the same inter-subblock edge limit */
int sub_bedge_limit = (loop_filter_level * 2) + interior_limit;
if (x > 0) {
Macroblock lmb = mbs[y+1][x+1-1];
for (int b = 0; b < 2; b++) {
Subblock rsbU = rmb.uSubblocks[b][0];
Subblock lsbU = lmb.uSubblocks[b][1];
Subblock rsbV = rmb.vSubblocks[b][0];
Subblock lsbV = lmb.vSubblocks[b][1];
for (int a = 0; a < 4; a++) {
Segment seg = horizontal(rsbU, lsbU, a);
seg.filterMb(hev_threshold, interior_limit, mbedge_limit);
seg.applyHorizontally(rsbU, lsbU, a);
seg = horizontal(rsbV, lsbV, a);
seg.filterMb(hev_threshold, interior_limit, mbedge_limit);
seg.applyHorizontally(rsbV, lsbV, a);
}
}
}
// sb left
if (!rmb.skipFilter) {
for (int a = 1; a < 2; a++) {
for (int b = 0; b < 2; b++) {
Subblock lsbU = rmb.uSubblocks[b][a - 1];
Subblock rsbU = rmb.uSubblocks[b][a];
Subblock lsbV = rmb.vSubblocks[b][a - 1];
Subblock rsbV = rmb.vSubblocks[b][a];
for (int c = 0; c < 4; c++) {
Segment seg = horizontal(rsbU, lsbU, c);
seg.filterSb(hev_threshold, interior_limit, sub_bedge_limit);
seg.applyHorizontally(rsbU, lsbU, c);
seg = horizontal(rsbV, lsbV, c);
seg.filterSb(hev_threshold, interior_limit, sub_bedge_limit);
seg.applyHorizontally(rsbV, lsbV, c);
}
}
}
}
// top
if (y > 0) {
Macroblock tmb = mbs[y+1-1][x+1];
for (int b = 0; b < 2; b++) {
Subblock tsbU = tmb.uSubblocks[1][b];
Subblock bsbU = bmb.uSubblocks[0][b];
Subblock tsbV = tmb.vSubblocks[1][b];
Subblock bsbV = bmb.vSubblocks[0][b];
for (int a = 0; a < 4; a++) {
// System.out.println("l");
Segment seg = vertical(bsbU, tsbU, a);
seg.filterMb(hev_threshold, interior_limit, mbedge_limit);
seg.applyVertically(bsbU, tsbU, a);
seg = vertical(bsbV, tsbV, a);
seg.filterMb(hev_threshold, interior_limit, mbedge_limit);
seg.applyVertically(bsbV, tsbV, a);
}
}
}
// sb top
if (!rmb.skipFilter) {
for (int a = 1; a < 2; a++) {
for (int b = 0; b < 2; b++) {
Subblock tsbU = bmb.uSubblocks[a-1][b];
Subblock bsbU = bmb.uSubblocks[a][b];
Subblock tsbV = bmb.vSubblocks[a-1][b];
Subblock bsbV = bmb.vSubblocks[a][b];
for (int c = 0; c < 4; c++) {
Segment seg = vertical(bsbU, tsbU, c);
seg.filterSb(hev_threshold, interior_limit, sub_bedge_limit);
seg.applyVertically(bsbU, tsbU, c);
seg = vertical(bsbV, tsbV, c);
seg.filterSb(hev_threshold, interior_limit, sub_bedge_limit);
seg.applyVertically(bsbV, tsbV, c);
}
}
}
}
}
}
}
}
public static void loopFilterY(Macroblock[][] mbs, int sharpnessLevel, boolean keyFrame) {
for (int y = 0; y < (mbs.length-2); y++) {
for (int x = 0; x < (mbs[0].length-2); x++) {
Macroblock rmb = mbs[y+1][x+1];
Macroblock bmb = mbs[y+1][x+1];
int loopFilterLevel = rmb.filterLevel;
if (loopFilterLevel != 0) {
int interiorLimit = rmb.filterLevel;
if (sharpnessLevel > 0) {
interiorLimit >>= sharpnessLevel > 4 ? 2 : 1;
if (interiorLimit > 9 - sharpnessLevel)
interiorLimit = 9 - sharpnessLevel;
}
if (interiorLimit == 0)
interiorLimit = 1;
int varianceThreshold = 0;
if (keyFrame) /* current frame is a key frame */ {
if (loopFilterLevel >= 40)
varianceThreshold = 2;
else if (loopFilterLevel >= 15)
varianceThreshold = 1;
} else /* current frame is an interframe */ {
throw new NotImplementedException("TODO: non-key frames are not supported yet");
// if (loopFilterLevel >= 40)
// varianceThreshold = 3;
// else if (loop_filter_level >= 20)
// varianceThreshold = 2;
// else if (loop_filter_level >= 15)
// varianceThreshold = 1;
}
/**
* Luma and Chroma use the same inter-macroblock edge limit
*/
int edgeLimitMb = ((loopFilterLevel + 2) * 2) + interiorLimit;
/**
* Luma and Chroma use the same inter-subblock edge limit
*/
int edgeLimitSb = (loopFilterLevel * 2) + interiorLimit;
// left
if (x > 0) {
Macroblock lmb = mbs[y+1][x-1+1];
for (int b = 0; b < 4; b++) {
Subblock rsb = rmb.ySubblocks[b][0];
Subblock lsb = lmb.ySubblocks[b][3];
for (int a = 0; a < 4; a++) {
Segment seg = horizontal(rsb, lsb, a);
seg.filterMb(varianceThreshold, interiorLimit, edgeLimitMb);
seg.applyHorizontally(rsb, lsb, a);
}
}
}
// sb left
if (!rmb.skipFilter) {
for (int a = 1; a < 4; a++) {
for (int b = 0; b < 4; b++) {
Subblock lsb = rmb.ySubblocks[b][a-1];
Subblock rsb = rmb.ySubblocks[b][a];
for (int c = 0; c < 4; c++) {
Segment seg = horizontal(rsb, lsb, c);
seg.filterSb(varianceThreshold, interiorLimit, edgeLimitSb);
seg.applyHorizontally(rsb, lsb, c);
}
}
}
}
// top
if (y > 0) {
Macroblock tmb = mbs[y-1+1][x+1];
for (int b = 0; b < 4; b++) {
Subblock tsb = tmb.ySubblocks[3][b];
Subblock bsb = bmb.ySubblocks[0][b];
for (int a = 0; a < 4; a++) {
Segment seg = vertical(bsb, tsb, a);
seg.filterMb(varianceThreshold, interiorLimit, edgeLimitMb);
seg.applyVertically(bsb, tsb, a);
}
}
}
// sb top
if (!rmb.skipFilter) {
for (int a = 1; a < 4; a++) {
for (int b = 0; b < 4; b++) {
Subblock tsb = bmb.ySubblocks[a-1][b];
Subblock bsb = bmb.ySubblocks[a][b];
for (int c = 0; c < 4; c++) {
Segment seg = vertical(bsb, tsb, c);
seg.filterSb(varianceThreshold, interiorLimit, edgeLimitSb);
seg.applyVertically(bsb, tsb, c);
}
}
}
}
}
}
}
}
}