From 10b66cd2ff469e53ee5068a3348c4979b05b3123 Mon Sep 17 00:00:00 2001 From: shaoboyan091 Date: Tue, 7 Jan 2025 12:08:43 +0800 Subject: [PATCH] CopyEI2T: Cases To Cover Rotated Images (#4111) Co-authored-by: Kai Ninomiya --- src/resources/README.md | 22 +++ src/resources/four-colors-rotate-180-cw.jpg | Bin 0 -> 3519 bytes src/resources/four-colors-rotate-270-cw.jpg | Bin 0 -> 3576 bytes src/resources/four-colors-rotate-90-cw.jpg | Bin 0 -> 2780 bytes src/resources/four-colors.jpg | Bin 0 -> 2665 bytes .../copyToTexture/image_file.spec.ts | 125 ++++++++++++++++++ src/webgpu/web_platform/util.ts | 83 ++++++++++++ 7 files changed, 230 insertions(+) create mode 100644 src/resources/four-colors-rotate-180-cw.jpg create mode 100644 src/resources/four-colors-rotate-270-cw.jpg create mode 100644 src/resources/four-colors-rotate-90-cw.jpg create mode 100644 src/resources/four-colors.jpg create mode 100644 src/webgpu/web_platform/copyToTexture/image_file.spec.ts diff --git a/src/resources/README.md b/src/resources/README.md index a1ed0604170c..2bc89869a501 100644 --- a/src/resources/README.md +++ b/src/resources/README.md @@ -91,3 +91,25 @@ ffmpeg -display_vflip -i temp.mp4 -c copy four-colors-vp9-bt601-vflip.mp4 rm temp.mp4 ``` + +The test jpg files were generated with by exiftool cmds and other image tools in below steps: +``` +// Generate four-colors.jpg with no orientation metadata +Use a image tool (e.g. "Paint" app on Windows) to create four-colors.jpg from four-colors.png and check with exiftool to ensure no orientation metadata been set. + +// Generate jpg picture with 90 cw rotation metadata +Use a image tool (e.g. "Paint" app on Windows) to create four-colors-hard-rotate-90-ccw.jpg and check with exiftool to ensure no orientation metadata been set. +exiftool -Orientation#=6 four-colors-hard-rotate-90-ccw.jpg -o four-colors-rotate-90-cw.jpg +rm four-colors-hard-rotate-90-ccw.jpg + +// Generate jpg picture with 180 cw rotation metadata +Use a image tool (e.g. "Paint" app on Windows) to create four-colors-hard-rotate-180-ccw.jpg and check with exiftool to ensure no orientation metadata been set. +exiftool -Orientation#=3 four-colors-hard-rotate-180-ccw.jpg -o four-colors-rotate-180-cw.jpg +rm four-colors-hard-rotate-180-ccw.jpg + +// Generate jpg picture with 270 cw rotation metadata +Use a image tool (e.g. "Paint" app on Windows) to create four-colors-hard-rotate-270-ccw.jpg and check with exiftool to ensure no orientation metadata been set. +exiftool -Orientation#=8 four-colors-hard-rotate-270-ccw.jpg -o four-colors-rotate-270-cw.jpg +rm four-colors-hard-rotate-270-ccw.jpg + +``` diff --git a/src/resources/four-colors-rotate-180-cw.jpg b/src/resources/four-colors-rotate-180-cw.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5be3efab39b10a38ca9917d19c7206f1b52ed7bb GIT binary patch literal 3519 zcmeHJdsGuw8vjB938)Q^U|HO57C{Zr7OY@*>8_))0r9aI-sQ9#fvtktwIW>~U6`sB zGzGI@E7~3nFU!Mf7KIjHC$hX0Z7G6+(uzDqs8&cK$uKi_?*vOdd-jCg{ku2w&G~Zg zH~0R2zwf(u26du);J7j*EChHw9_+wBKs~Ttz9VHlz?wDS0{|kh<2eH#dpz#W=Yg%o z2Z-VGaUa)@o5FF@=l&?1Krq?woThO8$>-vLmstQ%sYYE83^qI-cRg)gS8c4y1aBe4gx1z zXOD%;>|7KH!ns=|uk6k(6nQSMtaXiOeeAU;F?El`^uoU(CS+Pi<Raamk6#O3TVC&YZ2P{^HA;bLanASKrXsbmeOEjkfliw>mm+-|6Y?>mL}rKlEUD zbWCqBJ{dQeDUKJ;;v}spX8*>^3G=eSIS>drUOb!a*!WHYTaSfy&dU_Sgsm=fU)e2^ zEYB^hthM)C6!F+KF}2lUwpT#Uyityth1q{cY|sD2Y>L=VyzanET$naad?z5lsFD@w zn)P&~gRJO%t2U>wD067jmoFmt+X+_2CU=PGI7)k+mR>@jzDjlzUl*7Ak$q#z@fLmA z2zxzMH||69sQcB%kk@`)r1GsPC{oG5it)$uR5GpUEYbXql_<*$h3DO6f62U%+A`j( z>{Souyn~=YT|u|hPIQ~-d`3?X2jz{k#5{}@LC`_n_f5VCQj1wPb_If{q%)Z41(j8{ zSPf#QtVo66!$oxIbfFGEi({pFeip~NII4%&h;DCEMSK!ihhX-SlX^n@c-2%% zBRKm@Ez_(vC7C@L#lb7MJRiyM4Xhc{U!+_7swUFhdXdt6t?Tz>J*vndPXu^#JXGIB z(3>r0Ra&|V!K3>Re(!3o?Xq!3jYgf+w{I zBCH?qy|{5e!uzxE9Vn$~e@j{M54S-SqJLp>%LE zf^S0ERS3F-v|=}c)@THWHO#lnf@f)~O_{9-3e- zjZBCu&7T)~<*oGgI({*#hUysK=hwBeo*%e!-B-6QXHg;T;M8(0s`%g4L7Ji=Nw zjdsY%#3NLz`jnE6eV#4%qc0-(0UwI{@dDupIGAkEkoNmXY5YQCG3)ywJ_YmHKat}# zWPKn4oB|7B@`be6C*JB)ZWLC_wE;oJDg=JS#J5DRI|7_ii+;qx^S%?3rd!$RSbSA4 zf}6OM@-@sOEFSp)fh8N#rZ-&~Rnkbvk}9%cD>;mFeyxQa7n?G51_V#5ukUpBdR^t} z^yH>nYSU>yCPHc6iQsMzu9OZ|g8)}vCH?L_%H2PG)>DGg(KnP-iEd1f;8Gpyp=Ah5 z;U$?yY7GJFD0j926O_giy)`;x7&&ks!FySBu#zGt%MMGmcPP2m5I8Ni!Q=R4G!V(q z)1Lil#7OM0chwMiOWPi?-c5Tiyp;_#6el3~l|R`L$VR^;f5s;d$7%Y)Z?YRqwZwsa z5=C5|L?H)jMg|xZamtwT|Nx^iX-jgssYb!($uTz8_N!0Bo zLe0_}Ia&0MS_E%&n4_v)pYhq(6UiJR)~x9hu=6pW#1#s;1gsd3ZfljhvK7*YORYp% zW{8{wv8FVbthNK{DILbVMO{J>VgitmRoTp*~zLiKWWOYHLxJu}K7rkBO<9lxlUID{W5^SJ;m> z6McmS~mAeY2=;cc|k){05d G?SBD=u^Hb0 literal 0 HcmV?d00001 diff --git a/src/resources/four-colors-rotate-270-cw.jpg b/src/resources/four-colors-rotate-270-cw.jpg new file mode 100644 index 0000000000000000000000000000000000000000..62ae07beb092a67dc9c1b0c5aa1100a766ad0fc2 GIT binary patch literal 3576 zcmdT`3sh4_8lIa62@%0ys9Lv8tD*?ogDutv+xGe>D!3esh`73L6szLeiac7tBivP2 zsIf=ZC$)Bo1QnIlEG?8rT`q{IwQ33$ML`h+0^yNJB(Ix$XXhsAcDtLLv%9C=Jv+I7 zGWqA9JKy*J|I9zI1wIC2q83Fi0vLvY4Dt8mt9k zAvO;fxY#7X5o2622Co1qLdr*8VIAXY7>CQ_dm%`oQOKZt41&+)au8TP57EW4&^X|U z`Tj4zI^S!oY`tJovgEa02MdLh7nIfoEN?VS2}?-X?Jat4-1rI42Tq+9G(C9c>$7Ih znH#?F?TAH-mqbRzu83P1zv@@3H@vfP)8@o2zfDbh|AX|5%pH4vzc(lM!+nZFhmRaR zmjChbPgF&xPM;}0d+z*apO=+?QBhe{eYyTh!_{lozq-+M=iBDH_gd~h=;(aX)!n1* z?bAISHa;6MnJrcu%L_vx{H{5P{R1yC;>AHR;0ah>7$+SGSIpzT{HmA#e3@W<^4LkQ z?Gj2B94suY^PU{G+z^nE(kOav%FK?yrz|xGvww})?*E9{--vzB>jCgViOCUj#Q+EP zRFxuxw&Nk9Al2{O311gNNnQxC^#4XhmdRtQ2N7;L925i*1%rkSG%I_XaQo?JxKDLa1jvIh&WeXG-l4tfTGsS|=*fe@rB zsf*d?I@ER&f(pX)7(upU_zHJnV0HL`YQ$P^VB$Slh*-BV;r;AtKOrBEBNGl({}+Pf zOo)p=dFr{6{Qo9EhX-N_&2&P4cyY@a2nv=%&_0#ae@VsiA;60u*cEETk7nklga^lz zMnmw%X$Wefr3ojP%%u?Q`~)wbAvfA^i$4T2de&a=J@;0_Z2NFNW1X3$NV0Z^(!+_& zw{?W37lOx2?e)jgC#z0Tl6K<_In~u@noequkmj$7{(d#zjco@M?oj0o{&Jk1$zyor z&AC4!9%o|wzo2>?CV7t(hywD@T^ z$ReZF+%guWmaReFLu9PmAwsW{$mr7_L7@B$f?B0D_iiu*VN^Z4x`f!W0oM+YhH1?D z5e0dZF{p2yIl4!ByxMlJ@0IL#@pgPQ{fqjW5vv>3gYR^2TicpNDTg)>Y>F!BRBqL; z(Vgf=%&7fZ+7PRI zDCu?{sx6n=|2RwWb7@?megDwHOIpU5otCMgd+dE>vURo#eqHjE7k=>W{etF1ViNTS zy>sr1sSsq6;qOUSy;ZW%I<76(F}c^V>YYSY-Nb0(cavSj-x{!(*Kd zF|KE<$hU90d>db%lq*JC0@b@~R53MvcBh&;0zq3RLTt-qbbBSTc_@DWB+}CoSp@Nl z(r^*|0(TN%{WCgl=&jER$XQao@A;^7N>b$#* VVs*}~^aXLb(gSPoV{IjP|F3eCRlooM literal 0 HcmV?d00001 diff --git a/src/resources/four-colors-rotate-90-cw.jpg b/src/resources/four-colors-rotate-90-cw.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2e2beed8e491e6b2c5f6898dfef88b64f273f239 GIT binary patch literal 2780 zcmex=kRWME@p zU}j`s1+y6#!kC$XVjyuK2o!*@L7YHA1{R38P)VQ=f(=r~z}U8cfnj6+|Jw}C3`~rS zAOr=>tjr+5#=^qP%Ff2l4#FIq+?*U7TpaA|T)bRd+&nxo=LsQGd)Xdz%(#qMz)y>_*(Y^YsE>`O44~(BS|6xR+oBN8z%2xsyT_xof0Rr>M*hBpVI5!G~k6F^Yq^9~eV6nZqUS zxvBhki8eTtZXW7Gze1cjYf~0b6b0q@185KmOyzra0&Lz4YXCq2GMWMzt|**ln!a-< zHCW=NYx2dyfE3?w8DD0Jzgv?&pejxRG+EINFoJ}l$TbtRj3yB!mC`bKlw3|y5gik) zP$(60xiVI%j8Wl`M=w#ws+N$NY(gFX!UBB{i~`1~$7m`MBn(9}6lwz< zRw~0J2#lGaB(zi(g(WFta6@w(mQT|XELJAPcB$9!I!GB={EE~KQ3;mq^2EKGw8J$v zMbgHmR&8$A_{#L6eMh2|OYd8D{{zXZQuM3WtX=nT#v_leH)fi$vLD}Mesb%!y!@x0 zE-2pd!p>d0_msR`w*Qp_<*y!mj^cV2JplfS$HB z>+B!C80<@eXCRdmUzFqkPMVR*R-{J7Z?MR>?@dTdJFL)btg$t(e&oAyim)Ihixise1gp9a;#5v{+*sS5af#BN+{_+Vlf~q;YuHwedBDhkDfU6WTJ3|!+E+d#SPHF6}cUtra_+6;Ua?D0j}){1dXLaJ%TfAli3isWw*|OFuo83 zAuk`++g-0Ca3@FpvmVRUHe$ILNE#bm=*Ka?*WG?SKAWx)=BUH~;?!6LkG=(gQT< literal 0 HcmV?d00001 diff --git a/src/webgpu/web_platform/copyToTexture/image_file.spec.ts b/src/webgpu/web_platform/copyToTexture/image_file.spec.ts new file mode 100644 index 000000000000..aa9a16e16dce --- /dev/null +++ b/src/webgpu/web_platform/copyToTexture/image_file.spec.ts @@ -0,0 +1,125 @@ +export const description = ` +copyExternalImageToTexture from ImageFiles like *.png, *.jpg source. +`; + +import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { TextureUploadingUtils } from '../../util/copy_to_texture.js'; +import { + convertToUnorm8, + GetSourceFromImageFile, + kImageNames, + kImageInfo, + kImageExpectedColors, + kObjectTypeFromFiles, +} from '../util.js'; + +export const g = makeTestGroup(TextureUploadingUtils); + +g.test('from_orientation_metadata_file') + .desc( + ` + Test HTMLImageElements with rotation metadata can be copied to WebGPU texture correctly. + + It creates an ImageBitmap or HTMLImageElement using images in the 'resources' folder. + + Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel + of dst texture, and read one pixel out to compare with the manually documented expected color. + + If 'flipY' in 'GPUCopyExternalImageSourceInfo' is set to 'true', copy will ensure the result + is flipped. + + The tests covers: + - Image with rotation metadata + - Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo' (named 'srcDoFlipYDuringCopy' in cases) + - TODO: partial copy tests should be added + - TODO: all valid dstColorFormat tests should be added. + - TODO(#4108): Make this work in service workers (see GetSourceFromImageFile) + ` + ) + .params(u => + u // + .combine('imageName', kImageNames) + .combine('objectTypeFromFile', kObjectTypeFromFiles) + .combine('srcDoFlipYDuringCopy', [true, false]) + ) + .fn(async t => { + const { imageName, objectTypeFromFile, srcDoFlipYDuringCopy } = t.params; + const kColorFormat = 'rgba8unorm'; + + // Load image file. + const source = await GetSourceFromImageFile(t, imageName, objectTypeFromFile); + const width = source.width; + const height = source.height; + + const dstTexture = t.createTextureTracked({ + size: { width, height }, + format: kColorFormat, + usage: + GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + }); + + t.device.queue.copyExternalImageToTexture( + { + source, + flipY: srcDoFlipYDuringCopy, + }, + { + texture: dstTexture, + }, + { + width, + height, + } + ); + + const expect = kImageInfo[imageName].display; + const presentColors = kImageExpectedColors.srgb; + + if (srcDoFlipYDuringCopy) { + t.expectSinglePixelComparisonsAreOkInTexture({ texture: dstTexture }, [ + // Flipped top-left. + { + coord: { x: width * 0.25, y: height * 0.25 }, + exp: convertToUnorm8(presentColors[expect.bottomLeftColor]), + }, + // Flipped top-right. + { + coord: { x: width * 0.75, y: height * 0.25 }, + exp: convertToUnorm8(presentColors[expect.bottomRightColor]), + }, + // Flipped bottom-left. + { + coord: { x: width * 0.25, y: height * 0.75 }, + exp: convertToUnorm8(presentColors[expect.topLeftColor]), + }, + // Flipped bottom-right. + { + coord: { x: width * 0.75, y: height * 0.75 }, + exp: convertToUnorm8(presentColors[expect.topRightColor]), + }, + ]); + } else { + t.expectSinglePixelComparisonsAreOkInTexture({ texture: dstTexture }, [ + // Top-left. + { + coord: { x: width * 0.25, y: height * 0.25 }, + exp: convertToUnorm8(presentColors[expect.topLeftColor]), + }, + // Top-right. + { + coord: { x: width * 0.75, y: height * 0.25 }, + exp: convertToUnorm8(presentColors[expect.topRightColor]), + }, + // Bottom-left. + { + coord: { x: width * 0.25, y: height * 0.75 }, + exp: convertToUnorm8(presentColors[expect.bottomLeftColor]), + }, + // Bottom-right. + { + coord: { x: width * 0.75, y: height * 0.75 }, + exp: convertToUnorm8(presentColors[expect.bottomRightColor]), + }, + ]); + } + }); diff --git a/src/webgpu/web_platform/util.ts b/src/webgpu/web_platform/util.ts index f000d80e56b9..66a2163b7388 100644 --- a/src/webgpu/web_platform/util.ts +++ b/src/webgpu/web_platform/util.ts @@ -101,6 +101,15 @@ export const kVideoExpectedColors = makeTable({ }, } as const); +export const kImageExpectedColors = { + srgb: { + red: { R: 1.0, G: 0.0, B: 0.0, A: 1.0 }, + green: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 }, + blue: { R: 0.0, G: 0.0, B: 1.0, A: 1.0 }, + yellow: { R: 1.0, G: 1.0, B: 0.0, A: 1.0 }, + }, +} as const; + // MAINTENANCE_TODO: Add BT.2020 video in table. // Video container and codec defines several transform ops to apply to raw decoded frame to display. // Our test cases covers 'visible rect' and 'rotation'. @@ -350,6 +359,7 @@ type VideoName = keyof typeof kVideoInfo; export const kVideoNames: readonly VideoName[] = keysOf(kVideoInfo); export const kPredefinedColorSpace = ['display-p3', 'srgb'] as const; + /** * Starts playing a video and waits for it to be consumable. * Returns a promise which resolves after `callback` (which may be async) completes. @@ -606,3 +616,76 @@ export async function captureCameraFrame(test: GPUTest): Promise { return frame; } + +const kFourColorsInfo = { + display: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, +} as const; + +export const kImageInfo = makeTable({ + table: { + 'four-colors.jpg': kFourColorsInfo, + 'four-colors-rotate-90-cw.jpg': kFourColorsInfo, + 'four-colors-rotate-180-cw.jpg': kFourColorsInfo, + 'four-colors-rotate-270-cw.jpg': kFourColorsInfo, + }, +} as const); + +type ImageName = keyof typeof kImageInfo; +export const kImageNames: readonly ImageName[] = keysOf(kImageInfo); + +type ObjectTypeFromFile = (typeof kObjectTypeFromFiles)[number]; +export const kObjectTypeFromFiles = [ + 'ImageBitmap-from-Blob', + 'ImageBitmap-from-Image', + 'Image', +] as const; + +/** + * Load image file(e.g. *.jpg) from ImageBitmap, blob or HTMLImageElement. And + * convert the result to valid source that GPUCopyExternalImageSource supported. + */ +export async function GetSourceFromImageFile( + test: GPUTest, + imageName: ImageName, + objectTypeFromFile: ObjectTypeFromFile +): Promise { + const imageUrl = getResourcePath(imageName); + + switch (objectTypeFromFile) { + case 'ImageBitmap-from-Blob': { + // MAINTENANCE_TODO: resource folder path when using service worker is not correct. Return + // the correct path to load resource in correct place. + // The wrong path: /out/webgpu/webworker/web_platform/copyToTexture/resources + if (globalThis.constructor.name === 'ServiceWorkerGlobalScope') { + test.skip('Try to load image resource from serivce worker but the path is not correct.'); + } + // Load image file through fetch. + const response = await fetch(imageUrl); + return createImageBitmap(await response.blob()); + } + case 'ImageBitmap-from-Image': + case 'Image': { + // Skip test if HTMLImageElement is not available, e.g. in worker. + if (typeof HTMLImageElement === 'undefined') { + test.skip( + 'Try to use HTMLImage do image file decoding but HTMLImageElement not available.' + ); + } + + // Load image file through HTMLImageElement. + const image = new Image(); + image.src = imageUrl; + await raceWithRejectOnTimeout(image.decode(), 5000, 'decode image timeout'); + if (objectTypeFromFile === 'Image') { + return image; + } + + return createImageBitmap(image); + } + } +}