Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CopyEI2T: Cases To Cover Rotated Images #4111

Merged
merged 13 commits into from
Jan 7, 2025
22 changes: 22 additions & 0 deletions src/resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.jpg -o .\four-colors-rotate-90-cw.jpg
rm four-clors-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.jpg -o .\four-colors-rotate-180-cw.jpg
rm four-clors-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.jpg -o .\four-colors-rotate-270-cw.jpg
rm four-colors-hard-rotate-270-ccw.jpg
shaoboyan091 marked this conversation as resolved.
Show resolved Hide resolved

```
shaoboyan091 marked this conversation as resolved.
Show resolved Hide resolved
Binary file added src/resources/four-colors-rotate-180-cw.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/resources/four-colors-rotate-270-cw.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/resources/four-colors-rotate-90-cw.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/resources/four-colors.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
124 changes: 124 additions & 0 deletions src/webgpu/web_platform/copyToTexture/image_file.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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 Images with images under Resource folder.
shaoboyan091 marked this conversation as resolved.
Show resolved Hide resolved

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.
kainino0x marked this conversation as resolved.
Show resolved Hide resolved
`
)
.params(u =>
u //
.combine('imageName', kImageNames)
.combine('objectTypeFromFile', kObjectTypeFromFiles)
kainino0x marked this conversation as resolved.
Show resolved Hide resolved
.combine('srcDoFlipYDuringCopy', [true, false])
)
.fn(async t => {
const { imageName, objectTypeFromFile, srcDoFlipYDuringCopy } = t.params;
const kColorFormat = 'rgba8unorm';

// Load image file.
const source = await GetSourceFromImageFile(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]),
},
]);
}
});
131 changes: 131 additions & 0 deletions src/webgpu/web_platform/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -606,3 +616,124 @@ export async function captureCameraFrame(test: GPUTest): Promise<VideoFrame> {

return frame;
}

export const kImageInfo = makeTable({
table: {
'four-colors.jpg': {
coded: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
'four-colors-rotate-90-cw.jpg': {
coded: {
topLeftColor: 'red',
topRightColor: 'green',
bottomLeftColor: 'yellow',
bottomRightColor: 'blue',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
'four-colors-rotate-180-cw.jpg': {
coded: {
topLeftColor: 'green',
topRightColor: 'blue',
bottomLeftColor: 'red',
bottomRightColor: 'yellow',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
'four-colors-rotate-270-cw.jpg': {
coded: {
topLeftColor: 'blue',
topRightColor: 'yellow',
bottomLeftColor: 'green',
bottomRightColor: 'red',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
},
} as const);
shaoboyan091 marked this conversation as resolved.
Show resolved Hide resolved
shaoboyan091 marked this conversation as resolved.
Show resolved Hide resolved

type ImageName = keyof typeof kImageInfo;
export const kImageNames: readonly ImageName[] = keysOf(kImageInfo);

type ObjectTypeFromFile = (typeof kObjectTypeFromFiles)[number];
export const kObjectTypeFromFiles = ['imageBitmap', 'image', 'blob'] as const;
shaoboyan091 marked this conversation as resolved.
Show resolved Hide resolved

/**
* Load image file(e.g. *.jpg) from ImageBitmap, blob or HTMLImageElement. And
* convert the result to valid source that GPUCopyExternalImageSource supported.
*
* @param imageName: Required image name
* @param objectTypeFromFile: The object to load image file, e.g. ImageBitmap, blob
*
*/
export async function GetSourceFromImageFile(
imageName: ImageName,
objectTypeFromFile: ObjectTypeFromFile
): Promise<ImageBitmap | HTMLImageElement> {
const image = new Image();
const imageUrl = getResourcePath(imageName);
let imageBitmap: ImageBitmap;
shaoboyan091 marked this conversation as resolved.
Show resolved Hide resolved

if (objectTypeFromFile === 'blob') {
// Load image file through fetch.
const blob = await loadXHR(imageUrl);
imageBitmap = await createImageBitmap(blob, { imageOrientation: 'from-image' });
shaoboyan091 marked this conversation as resolved.
Show resolved Hide resolved
return imageBitmap;
} else {
// Load image file through HTMLImageElement.
image.src = imageUrl;
await raceWithRejectOnTimeout(image.decode(), 5000, 'decode image timeout');
kainino0x marked this conversation as resolved.
Show resolved Hide resolved
if (objectTypeFromFile === 'image') {
return image;
}

imageBitmap = await createImageBitmap(image, { imageOrientation: 'from-image' });
return imageBitmap;
}
}

// Use fetch() to load image file as blob
function loadXHR(url: string): Promise<Blob> {
return new Promise<Blob>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'blob';
xhr.onerror = function () {
reject(new Error('Network error.'));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kainino0x I'm not sure what's the best way to handle this reject case. Do you have any suggestions?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, we want any error case to fail the test.

However I don't think XHR should ever be need, rather than fetch() + .blob():
https://developer.mozilla.org/en-US/docs/Web/API/Response/blob

(I'm surprised to see we don't already have any code that uses fetch(), but it seems the only place we use a still-image resource uses HTMLImageElement.)

};
xhr.onload = function () {
if (xhr.status === 200) {
resolve(xhr.response);
} else {
reject(new Error('Loading error:' + xhr.statusText));
}
};
xhr.send();
});
}
shaoboyan091 marked this conversation as resolved.
Show resolved Hide resolved
Loading