From 714edc93be549f93194ad0a1d7e187c32b49d1e8 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 20 Jul 2022 21:38:10 +0000 Subject: [PATCH] Add ContrastProcessor for contrast adjustments. PiperOrigin-RevId: 462232813 --- .../transformer/ConfigurationActivity.java | 1 + .../demo/transformer/TransformerActivity.java | 5 + .../{sample_mp4_first_frame => }/README.md | 4 +- .../exoplayer_logo/maximum_contrast.png | Bin 0 -> 5825 bytes .../media/bitmap/exoplayer_logo/original.png | Bin 0 -> 15323 bytes .../media3/transformer/BitmapTestUtil.java | 12 + .../ContrastProcessorPixelTest.java | 251 ++++++++++++++++++ .../shaders/fragment_shader_contrast_es2.glsl | 33 +++ .../androidx/media3/transformer/Contrast.java | 47 ++++ .../media3/transformer/ContrastProcessor.java | 78 ++++++ 10 files changed, 428 insertions(+), 3 deletions(-) rename libraries/test_data/src/test/assets/media/bitmap/{sample_mp4_first_frame => }/README.md (87%) create mode 100644 libraries/test_data/src/test/assets/media/bitmap/exoplayer_logo/maximum_contrast.png create mode 100644 libraries/test_data/src/test/assets/media/bitmap/exoplayer_logo/original.png create mode 100644 libraries/transformer/src/androidTest/java/androidx/media3/transformer/ContrastProcessorPixelTest.java create mode 100644 libraries/transformer/src/main/assets/shaders/fragment_shader_contrast_es2.glsl create mode 100644 libraries/transformer/src/main/java/androidx/media3/transformer/Contrast.java create mode 100644 libraries/transformer/src/main/java/androidx/media3/transformer/ContrastProcessor.java diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java index d7ee8a7e89..3f21bcdcfb 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java @@ -104,6 +104,7 @@ public final class ConfigurationActivity extends AppCompatActivity { "3D spin", "Overlay logo & timer", "Zoom in start", + "Increase contrast" }; private static final int PERIODIC_VIGNETTE_INDEX = 2; private static final String SAME_AS_INPUT_OPTION = "same as input"; diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java index a8c1671573..bd622f7dd0 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java @@ -41,6 +41,7 @@ import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.util.DebugTextViewHelper; +import androidx.media3.transformer.Contrast; import androidx.media3.transformer.DebugViewProvider; import androidx.media3.transformer.DefaultEncoderFactory; import androidx.media3.transformer.GlEffect; @@ -320,6 +321,10 @@ public final class TransformerActivity extends AppCompatActivity { if (selectedEffects[5]) { effects.add(MatrixTransformationFactory.createZoomInTransition()); } + if (selectedEffects[6]) { + // TODO(b/238630175): Add slider for contrast adjustments. + effects.add(new Contrast(0.75f)); + } transformerBuilder.setVideoEffects(effects.build()); } diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/README.md b/libraries/test_data/src/test/assets/media/bitmap/README.md similarity index 87% rename from libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/README.md rename to libraries/test_data/src/test/assets/media/bitmap/README.md index 78edfebd86..56589d3400 100644 --- a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/README.md +++ b/libraries/test_data/src/test/assets/media/bitmap/README.md @@ -1,6 +1,4 @@ -Expected first frame of -[sample.mp4](https://github.com/androidx/media/blob/main/libraries/test_data/src/test/assets/media/mp4/sample.mp4) -after a +Expected first frame after a [Transformer](https://github.com/androidx/media/tree/main/libraries/transformer) transformation. Used to validate that frame operations produce expected output in diff --git a/libraries/test_data/src/test/assets/media/bitmap/exoplayer_logo/maximum_contrast.png b/libraries/test_data/src/test/assets/media/bitmap/exoplayer_logo/maximum_contrast.png new file mode 100644 index 0000000000000000000000000000000000000000..8ae6837371c2682a664b8355fc6fea92807a4d8f GIT binary patch literal 5825 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 A00000 literal 0 HcmV?d00001 diff --git a/libraries/test_data/src/test/assets/media/bitmap/exoplayer_logo/original.png b/libraries/test_data/src/test/assets/media/bitmap/exoplayer_logo/original.png new file mode 100644 index 0000000000000000000000000000000000000000..b149114b41418c0bafaeca1c88a6046bf5b01538 GIT binary patch literal 15323 zcmeHug;QJ4_crcUDDK`up->=5&|+;VR>6W>pb*@OyA&^_zy~c-XmLx>;O-s>5Zv8D zpuo%b?|6TAX6Me`IlJ@h*?Z1&R!)qLmO42J6A2a;7WvyZDj%@0aA5x{M7WPNCCX{4 zk1t}EH-_$5SZtmDE7(rsp7M{CRyyxLsXjbB%Cbnf9b$=8qflW6HWq5J@V5d+ zE+;tVNkzs zt7LPe_dfySfF|BdtJi+Mx9~@3O8o%TX7!J&Zd)^(BY?t`zRRZvasvKqEiz&U)N<)? z0Dhd~sSq2Soa}^+>)JFe5O)1%`JX7O9~kHRf!4R<&s)o~yv+V#>((-C=+7NT_pR{$ z+K#cyUdmky$yl0wF{%Bb6v@$n*=5t9D`@q+FHNw0x>U*V(gMF9vX5p_OG;eUd;ZG& zz&4wv8S(*od3597OggB1lCZ6E1eHgEY_Lw(z&7NF`CN=$rqKqv33k3PXg_=^Z}5AI z0E}Y3{WILEYXG=_8o>|WF3z#jG|g$7ZOo5@{llVaO5v;>QnvN+Am_U7NC9`}q@U!i zMcn6yoGQiRo%yf4mD$hBSqGnByeNOtoHIa{R(%|*9Cz&SKZ8|MnPjPi<9aB zANxKmQfq8Kc~3ozofFm}`V@(i+9;_yE$iSa5!s-Boly4d*{#a(jZ|Vci)6N(#Ns?XiuW;K%%nlJzVG)ki|JX9nuoNv`7~) z=H*4z>>NKD16k*8izrB+g#nco?HBvD1HmRYt@yTpQ?^&AqbNu)|I(H8aV?DM27bdQ ze_5&!Z4ZaU$<6uYOxokgEpF{4&-XI-`vX-EB7Q#a`AGWu@99t(fX;boi_XK*80Mu;YcluFLMyuGwVoxA(UneGIJI&szCQ0Bwa^(A)Y|8=drmAjBE#?1x>y#t(dFL^ zOgw^K5kcR})2Bf+)XoKfIy0*bH@i*g{#T=>e(RW`N)coRp0l0_z=)C-#VA3=gBuuPlwYiMt$L~1i8@c|C#TZ%9w@46Vhc%Bq z>5H1WF>%@dJKPHJodqr{FUk*DMlDUFS2m_u=~5f_lU+Xhjox@ss<&dEQd9@lBt^!| zXI+x@mF9zc!XOwgdUZM2L=>_c2GimT*wk82IBDCPJpLwbJW_$)MiwX`@lj4lQO*%{ zo1>YP+U1Z}C_uhZB?4*jw{e?#ceS3)HY!T^uA;B2{-AM&IKC(H!@h-=lw-l|tuqQ< zZxJhg$hvzuR=-ocZmb?+vZ!T`bZC6oA6oBgM*AH$Wd_fkiNiqpx@7M*>)D>a zZxJye9euW`jipqsv_Lr-l-Hs!`d)*M(B$u76Z?Z(v6C+9csQ!L>B5hes@ydx0Dj2v zCB5D>Gcdu3@Y*%W9l4Np>7nIF{tVhSHV6ZrW{Id(wt4-iH>nsoS{L7%UTa1`;<^MF z!3_-z;DjEdf-VfAVG27g^y?FMY3O5=+}%|!d1;Aqa|2G_22$-i=f&h)->pgeQO%|k zfjc?~Lk#oQ*4BRTYiN*KSk0mjirmPFAzO zMU-25VAh$okr<*&gZ-ZQA`{Uo7$I$w7BR=e%Uy-R#x!;76h9VsU?EuK=4z6f z^A4T+*+oWaM>wywDn4x+ly{rizQm(OuTZkyfNvI6dBA7cY}B@MyGBof+@Kr3QL{Pf zc{0C7;hn3^kPYH>6S_ToeO`5l3|=r9@LVwuTa}P5rAcyuoEN_Gj^!}9NlzGpQA}FA zq8yV?s9wZKc4gu03OI5*PQ!+qv~xYQ4kptbAwdD~r7k+%CWY2_E?2#~S(F%sf!^gdUq;$#>d0i7_tkpeu2G;vbs7mL4XjmP|SJKe6NY z4O6V~q)2&`Ueo4t(MOjkA^=)gy1(&ipPt`^@*4kBWx#VT!g;xu;6TQ_jX}O^P|<&6 z#_mbg#G*~4+zaW?XmIv>jwp8KoLgTY$zw-q|EY#J6yYoc?aOqly3P-dUTq_TW5wGx za*(XM)V~Ah+0TxLa;UK#&i!$;)?{k5g!(^cG}HbN_G1x3$&?I?KjY5vkAQ)F`lJ_~ zB%|6?CvvrSN-l?}1l(Ve4y#=eIH6aJ3`TVL5YDzwD!lYqGkv!9JHpP1Qyr6C?y}Oq zMZ}MLG8X?hIc z8=>*d<9NwSgk7JiZy?RiL4?XbP33O@|Mtxbz@$jT0DmQZu$^O>(g;;K$TMo4*JplE zK4Il5Fvo0Y?Ym)`N8&TbFr;@ObP!qCP)~6(MHsN}Eodwhz_I}~|C_+uwTH^~rnJ%+ z4h#4DWRbbZRmoJ8VUuiLYQ555{CP{5d>F8XzC7G`ShGmq^$6ue1l4;#s4n$>y=;M# z+fKNxLv^{JmP+1IPPpYZiI)1=Ix#<2mg(ie-g_|?eRR<3zLrtoXqDkQj#(E2KfTAU zx1lwZ>Mq?MIP`g0<^Hs??dbTy*Z-krGHkme9hB@lp$b2wc1`fy^(M6w@*@*MCDb^U zGTCkdFad_l7{IG9)+WE_LNl<$&L5q2&2$-it1D9RlI?k~SFg;fO&!2_7N$GQwVKZ* zqReAjXq3D1oY3LYGx~DX=!B?u@u$yY{{%>*pwClZ90O^NX-yN$xB*cYO z2js=uZ=ph9yGKs>e#h}>&tJ$3YZwNsanuJ4doKOD3wkJO{p+kt%=7CRNs?^=z0GUs zKf@+*nW?z!sm|L?qqiT7GTU;sPrh(Kcc={(r26JBYQLJ?m@rmudZA`usUD>?(%v+S zObJ5uYOqO1kCl`_rN$!{V~QMFZtq2}R3#7GmBu4N2h4eqTvpqN%72D)=9dcGTEI;0 z8~U#vxrlGULUsGCi(YRZd%<~~zu-w~G}e>7)C|!|3#88n*H@+7K)dSCj{PN{wTO@X z4qDEo8u&quc(0i8wNaB9)TaGDEl{7^@fYjy2u!uylg-TQGr&HxGXBvZbmQOriy5&) zP?vw5&jOo1vN}>Z^KB>6Oe6G-zM_>-ZP5E z;_#;p+!Mgs=@ceh-4iz!YJ6iY`4jQu-Xz85O_Sa9Ff7$Z@R$M^#SWZ%0)6l8BF|Zc zm#0$SvD-_5s-VoOR-=S9KC>J;IOz9pcI#AylXC%Iq^LS$tdJe;e7^}EW-BZ1guAAS z^5ushwriiYpXb3nIo@(Y2b{%f=QNpWPHKpK!39C^;}8Bq^|=X1o;o?HUzM~+V83sj z#6{r>mA_Xs%xQW#vw|f7cK#OsHnJ*pGMb-$NQ=(AgisqDbc!&Pf1M~>%2mXR?RnQ; zo?_sAz}E;JZpx~vZF~%mc&R|P>?UwcuX&2}h}{gZU~ZCHqUl;&+7o$?CS9Z3pVkYr z@s3g>7T+Btg-QlZBLDUjyh3R)vc6&|xIAKK&u;VN2z#fPJ}#4HvGa?!r2e<(!DMB> zN!H_}R7s+aIJ{d+DTiTL2~=FEGMOrLq>6f3_;h@Glh;IWt%l3Ev4*%;NwN9gua8@9xJ674G~MOBAyq z`gh$m&K+FYxSvP=lM73-ll&e80BZd0*uA8KJ+h$)$CjEs;eii@Iqq# zPB8#vKJ68;x<@#IAWpO&DPe{`s#Qb5@THyuEhG~on*R)v40<8+pZqfq!X+GnNLE!K+YIG86N$&n1X16$ib$fTISvxmS-fN~m`!n&VX%+Dfi<~9B_^b8K-i#AU zN&YE*cVWE<@*&3pjW$vxs3L;9fjpE3g<*F`U!SLJ1H+yJX5qJ(6iF3XJt(i2DmzD= zZe109B5PkdT9lu;6h6y$s+{*2>EaWCcs7=Ayzm@`;O=+($zLZ|*0tLhdLKx5n3u5%vPM|l zUczwonmZiFu&S*!tf})OwJh|kiv$DSF!M2g4|u7}mm0#qwh&n%^iIVstgTE|Hv=eX zC)VeO4eM{O4N#g4WX0v~_wLGB?@^GmqRA0j;-u zTx@1#)ERbpUP9;y=G78d$-#fA4+LrCvxoJ@8>Z1EvS=7i^m(evvMcW2vL5{sCZ)^C%oCeiK;;t^IT9~8Kt-U;KC z_mhToi9dAu13?U@E#vpq$Jk@BZiFro&g^1DNf_2xZcp&*wzV{S}btp(IEcS?6>#&MT2n zmRvhyE@vQSXnU|N$mzs81VeXZoovFSzwix zB4q!Xfx%n=;6y^|EB}qcPXzDd#ewW#u$|~yi*o>Dr>H=-00-EPtnwnT*!gZ$d7(u& za?v(4yYA&b^bn;R^5}Q=2g7y(gC18m!oSo??BVK&5v-Ljk>}VJ?A@|ez50k)PJ1#* zErkG`4B$&Qca{B7{y0NO;~Xh+{-~+d?_v^KAQVGfI`v&){+EX4wTC*>ojWa`AQl(d zZ-yS0lc;Dlexmi;4y+UuEH6p4~u z1pMDSs)cRv>ETUzkZ0K|D^k+9-b5cwX!ksmMFv`oab{Bi^7meK&zFrbahxCW3`KU3 zAm`uVZC_RI2ngV4#1Vd)pq=88A!qe4PHmasTypzi_2OKq3>Fz?8}q!ocHU8F^Bz!o zK;rcq$I1y*A>jfRa!iICm%Sr$`<`P>ku)9bbVTBh_?>LqJ@71R4`x6wPxi`WrP(1+ zbdY1_EYD-8Goxgrsb$-%z03;eU_t8SDjdR+#N~*~rX5H@`sQ_FSQR*YTAi zrm({b#Ez`%NNQd&b%z_}-si;u1;2vvk%lNJHFntutV7a0n|bX?cqdh6H%YhDi&dle z%maSx?h^M{VLdN-xa@3oJSXlZz=%y*Oix?l$gpgrDIMdLWq&Bj?w(UEbhhvGUwfi8 zUm5rPPS}7V6-DC@sfg#w;@o_0i1O{Ti^R=fX^8cI`&Jh1_s51s`c+f3h_uI$iuPBu zY9D(zm8_~-nei+UfDj#GCZ$d@I-sw)RR=Q(>Ni$pWeXLm;#CI4SrRC%p08#L-{d4L zv`a20Vh(Y@j_a2YkH z;~V*R!!4T)aiTE>jTL6E>P6+bQ;R)+xPXg}H_X>>X5WRcJ}hjr7-lFI*6>=6;+ys` zzv%8UIkd@K&TtnEBXFV(E|uCCBzmH2=^fUeU4AGWx0p+w^TS2k7BU+lg!1)l=BYnM ztvllM6PqjIH-3rZwXg%SWHVLN@)-(gfE`F%B`q17$s7)9dtOr+*q3e!6^=%Xd_(=| zNOw+q`6LOtZsv{?OhIsp%~f6f+x)Nm);9(Ao=;lAs**pa#<|tQ;2g z!h3mE;mzHNKB(IvWry;A1tbwEhMDdbF3r=7$Q(-rKVV3=eV&92i<{0eKAnGu_E!%j z?sUoePGFV$Zbf5EduB(~H3i4YI{q#=joFi&VmRn2V_(EGUA0-o=oT9m)jq=

;;jhp&ZXiVh62 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

WC4-g{7?ZW*8$~>Oz&Te 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 I*XE)B4-Aw)zW@LL literal 0 HcmV?d00001 diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java index f8c2b441bd..05c4e59e6d 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java @@ -37,6 +37,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.Arrays; /** * Utilities for instrumentation tests for the {@link GlEffectsFrameProcessor} and {@link @@ -102,6 +103,17 @@ public class BitmapTestUtil { return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); } + /** + * Returns a solid {@link Bitmap} with every pixel having the same color. + * + * @param color An RGBA color created by {@link Color}. + */ + public static Bitmap createArgb8888BitmapWithSolidColor(int width, int height, int color) { + int[] colors = new int[width * height]; + Arrays.fill(colors, color); + return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); + } + /** * Returns the average difference between the expected and actual bitmaps, calculated using the * maximum difference across all color channels for each pixel, then divided by the total number diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ContrastProcessorPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ContrastProcessorPixelTest.java new file mode 100644 index 0000000000..c03075fc31 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ContrastProcessorPixelTest.java @@ -0,0 +1,251 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.transformer; + +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.transformer.BitmapTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLSurface; +import android.util.Size; +import androidx.media3.common.util.GlUtil; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Pixel test for contrast adjustment via {@link ContrastProcessor}. + * + *

Expected images are taken from an emulator, so tests on different emulators or physical + * devices may fail. To test on other devices, please increase the {@link + * BitmapTestUtil#MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE} and/or inspect the saved output bitmaps + * as recommended in {@link GlEffectsFrameProcessorPixelTest}. + */ +@RunWith(AndroidJUnit4.class) +public class ContrastProcessorPixelTest { + private static final String EXOPLAYER_LOGO_PNG_ASSET_PATH = + "media/bitmap/exoplayer_logo/original.png"; + // TODO(b/239005261): Migrate png to an emulator generated picture. + private static final String MAXIMUM_CONTRAST_PNG_ASSET_PATH = + "media/bitmap/exoplayer_logo/maximum_contrast.png"; + + // OpenGL uses floats in [0, 1] and maps 0.5f to 128 = 256 / 2. + private static final int OPENGL_NEUTRAL_RGB_VALUE = 128; + + private final Context context = getApplicationContext(); + + private @MonotonicNonNull EGLDisplay eglDisplay; + private @MonotonicNonNull EGLContext eglContext; + private @MonotonicNonNull EGLSurface placeholderEglSurface; + private @MonotonicNonNull SingleFrameGlTextureProcessor contrastProcessor; + private int inputTexId; + private int outputTexId; + private int inputWidth; + private int inputHeight; + + @Before + public void createGlObjects() throws Exception { + eglDisplay = GlUtil.createEglDisplay(); + eglContext = GlUtil.createEglContext(eglDisplay); + + Bitmap inputBitmap = BitmapTestUtil.readBitmap(EXOPLAYER_LOGO_PNG_ASSET_PATH); + inputWidth = inputBitmap.getWidth(); + inputHeight = inputBitmap.getHeight(); + + placeholderEglSurface = GlUtil.createPlaceholderEglSurface(eglDisplay); + GlUtil.focusEglSurface(eglDisplay, eglContext, placeholderEglSurface, inputWidth, inputHeight); + inputTexId = BitmapTestUtil.createGlTextureFromBitmap(inputBitmap); + } + + @After + public void release() throws GlUtil.GlException, FrameProcessingException { + if (contrastProcessor != null) { + contrastProcessor.release(); + } + GlUtil.destroyEglContext(eglDisplay, eglContext); + } + + @Test + public void drawFrame_noContrastChange_leavesFrameUnchanged() throws Exception { + String testId = "drawFrame_noContrastChange"; + contrastProcessor = + new Contrast(/* contrast= */ 0.0f).toGlTextureProcessor(context, /* useHdr= */ false); + Size outputSize = contrastProcessor.configure(inputWidth, inputHeight); + setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(EXOPLAYER_LOGO_PNG_ASSET_PATH); + + contrastProcessor.drawFrame(inputTexId, /* presentationTimeUs = */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.getWidth(), outputSize.getHeight()); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_minimumContrast_producesAllGrayFrame() throws Exception { + String testId = "drawFrame_minimumContrast"; + contrastProcessor = + new Contrast(/* contrast= */ -1.0f).toGlTextureProcessor(context, /* useHdr= */ false); + Size outputSize = contrastProcessor.configure(inputWidth, inputHeight); + setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); + Bitmap expectedBitmap = + BitmapTestUtil.createArgb8888BitmapWithSolidColor( + inputWidth, + inputHeight, + Color.rgb( + OPENGL_NEUTRAL_RGB_VALUE, OPENGL_NEUTRAL_RGB_VALUE, OPENGL_NEUTRAL_RGB_VALUE)); + + contrastProcessor.drawFrame(inputTexId, /* presentationTimeUs = */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.getWidth(), outputSize.getHeight()); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_decreaseContrast_decreasesPixelsGreaterEqual128IncreasesBelow() + throws Exception { + String testId = "drawFrame_decreaseContrast"; + contrastProcessor = + new Contrast(/* contrast= */ -0.75f).toGlTextureProcessor(context, /* useHdr= */ false); + Size outputSize = contrastProcessor.configure(inputWidth, inputHeight); + setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); + Bitmap originalBitmap = BitmapTestUtil.readBitmap(EXOPLAYER_LOGO_PNG_ASSET_PATH); + + contrastProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.getWidth(), outputSize.getHeight()); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap); + assertIncreasedOrDecreasedContrast(originalBitmap, actualBitmap, /* increased= */ false); + } + + @Test + public void drawFrame_increaseContrast_increasesPixelsGreaterEqual128DecreasesBelow() + throws Exception { + String testId = "drawFrame_increaseContrast"; + contrastProcessor = + new Contrast(/* contrast= */ 0.75f).toGlTextureProcessor(context, /* useHdr= */ false); + Size outputSize = contrastProcessor.configure(inputWidth, inputHeight); + setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); + Bitmap originalBitmap = BitmapTestUtil.readBitmap(EXOPLAYER_LOGO_PNG_ASSET_PATH); + + contrastProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.getWidth(), outputSize.getHeight()); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap); + assertIncreasedOrDecreasedContrast(originalBitmap, actualBitmap, /* increased= */ true); + } + + @Test + public void drawFrame_maximumContrast_pixelEither0or255() throws Exception { + String testId = "drawFrame_maximumContrast"; + contrastProcessor = + new Contrast(/* contrast= */ 1.0f).toGlTextureProcessor(context, /* useHdr= */ false); + Size outputSize = contrastProcessor.configure(inputWidth, inputHeight); + setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(MAXIMUM_CONTRAST_PNG_ASSET_PATH); + + contrastProcessor.drawFrame(inputTexId, /* presentationTimeUs = */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.getWidth(), outputSize.getHeight()); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + private static void assertIncreasedOrDecreasedContrast( + Bitmap originalBitmap, Bitmap actualBitmap, boolean increased) { + + for (int y = 0; y < actualBitmap.getHeight(); y++) { + for (int x = 0; x < actualBitmap.getWidth(); x++) { + int originalColor = originalBitmap.getPixel(x, y); + int actualColor = actualBitmap.getPixel(x, y); + + int redDifference = Color.red(actualColor) - Color.red(originalColor); + int greenDifference = Color.green(actualColor) - Color.green(originalColor); + int blueDifference = Color.blue(actualColor) - Color.blue(originalColor); + + // If the contrast increases, all pixels with a value greater or equal to + // OPENGL_NEUTRAL_RGB_VALUE must increase (diff is greater or equal to 0) and all pixels + // below OPENGL_NEUTRAL_RGB_VALUE must decrease (diff is smaller or equal to 0). + // If the contrast decreases, all pixels with a value greater or equal to + // OPENGL_NEUTRAL_RGB_VALUE must decrease (diff is smaller or equal to 0) and all pixels + // below OPENGL_NEUTRAL_RGB_VALUE must increase (diff is greater or equal to 0). + // The interval limits 0 and 255 stay unchanged for either contrast in- or decrease. + + if (Color.red(originalColor) >= OPENGL_NEUTRAL_RGB_VALUE) { + assertThat(increased ? redDifference : -redDifference).isAtLeast(0); + } else { + assertThat(increased ? redDifference : -redDifference).isAtMost(0); + } + + if (Color.green(originalColor) >= OPENGL_NEUTRAL_RGB_VALUE) { + assertThat(increased ? greenDifference : -greenDifference).isAtLeast(0); + } else { + assertThat(increased ? greenDifference : -greenDifference).isAtMost(0); + } + + if (Color.blue(originalColor) >= OPENGL_NEUTRAL_RGB_VALUE) { + assertThat(increased ? blueDifference : -blueDifference).isAtLeast(0); + } else { + assertThat(increased ? blueDifference : -blueDifference).isAtMost(0); + } + } + } + } + + private void setupOutputTexture(int outputWidth, int outputHeight) throws GlUtil.GlException { + outputTexId = + GlUtil.createTexture( + outputWidth, outputHeight, /* useHighPrecisionColorComponents= */ false); + int frameBuffer = GlUtil.createFboForTexture(outputTexId); + GlUtil.focusFramebuffer( + checkNotNull(eglDisplay), + checkNotNull(eglContext), + checkNotNull(placeholderEglSurface), + frameBuffer, + outputWidth, + outputHeight); + } +} diff --git a/libraries/transformer/src/main/assets/shaders/fragment_shader_contrast_es2.glsl b/libraries/transformer/src/main/assets/shaders/fragment_shader_contrast_es2.glsl new file mode 100644 index 0000000000..6420451d85 --- /dev/null +++ b/libraries/transformer/src/main/assets/shaders/fragment_shader_contrast_es2.glsl @@ -0,0 +1,33 @@ +#version 100 +// Copyright 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ES 2 fragment shader that samples from a (non-external) texture with +// uTexSampler, copying from this texture to the current output +// while adjusting contrast based on uContrastFactor. + +precision mediump float; +uniform sampler2D uTexSampler; +uniform float uContrastFactor; +varying vec2 vTexSamplingCoord; + +void main() { + vec4 inputColor = texture2D(uTexSampler, vTexSamplingCoord); + + gl_FragColor = vec4( + clamp(uContrastFactor * (inputColor.r - 0.5) + 0.5, 0.0, 1.0), + clamp(uContrastFactor * (inputColor.g - 0.5) + 0.5, 0.0, 1.0), + clamp(uContrastFactor * (inputColor.b - 0.5) + 0.5, 0.0, 1.0), + inputColor.a); +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Contrast.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Contrast.java new file mode 100644 index 0000000000..06db7a3a3a --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Contrast.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.transformer; + +import static androidx.media3.common.util.Assertions.checkArgument; + +import android.content.Context; +import androidx.media3.common.util.UnstableApi; + +/** A {@link GlEffect} to control the contrast of video frames. */ +@UnstableApi +public class Contrast implements GlEffect { + + /** Adjusts the contrast of video frames in the interval [-1, 1]. */ + public final float contrast; + + /** + * Creates a new instance for the given contrast value. + * + *

Contrast values range from -1 (all gray pixels) to 1 (maximum difference of colors). 0 means + * to add no contrast and leaves the frames unchanged. + */ + public Contrast(float contrast) { + checkArgument(-1 <= contrast && contrast <= 1, "Contrast needs to be in the interval [-1, 1]."); + this.contrast = contrast; + } + + @Override + public SingleFrameGlTextureProcessor toGlTextureProcessor(Context context, boolean useHdr) + throws FrameProcessingException { + return new ContrastProcessor(context, this, useHdr); + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ContrastProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ContrastProcessor.java new file mode 100644 index 0000000000..44c9b89cac --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ContrastProcessor.java @@ -0,0 +1,78 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.transformer; + +import android.content.Context; +import android.opengl.GLES20; +import android.opengl.Matrix; +import android.util.Size; +import androidx.media3.common.util.GlProgram; +import androidx.media3.common.util.GlUtil; +import java.io.IOException; + +/** Contrast processor to apply a {@link Contrast} to each frame. */ +/* package */ final class ContrastProcessor extends SingleFrameGlTextureProcessor { + private static final String VERTEX_SHADER_PATH = "shaders/vertex_shader_transformation_es2.glsl"; + private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_contrast_es2.glsl"; + + private final GlProgram glProgram; + private final float contrastFactor; + + public ContrastProcessor(Context context, Contrast contrastEffect, boolean useHdr) + throws FrameProcessingException { + super(useHdr); + // Use 1.0001f to avoid division by zero issues. + contrastFactor = (1 + contrastEffect.contrast) / (1.0001f - contrastEffect.contrast); + + try { + glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH); + } catch (IOException | GlUtil.GlException e) { + throw new FrameProcessingException(e); + } + + // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. + glProgram.setBufferAttribute( + "aFramePosition", + GlUtil.getNormalizedCoordinateBounds(), + GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE); + + float[] identityMatrix = new float[16]; + Matrix.setIdentityM(identityMatrix, /* smOffset= */ 0); + glProgram.setFloatsUniform("uTransformationMatrix", identityMatrix); + glProgram.setFloatsUniform("uTexTransformationMatrix", identityMatrix); + } + + @Override + public Size configure(int inputWidth, int inputHeight) { + return new Size(inputWidth, inputHeight); + } + + @Override + public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException { + try { + glProgram.use(); + glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0); + glProgram.setFloatUniform("uContrastFactor", contrastFactor); + glProgram.bindAttributesAndUniforms(); + + // The four-vertex triangle strip forms a quad. + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); + } catch (GlUtil.GlException e) { + throw new FrameProcessingException(e, presentationTimeUs); + } + } +}