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

Add Draw API #7628

Merged
merged 25 commits into from
May 6, 2023
Merged

Add Draw API #7628

merged 25 commits into from
May 6, 2023

Conversation

Linchenn
Copy link
Collaborator

@Linchenn Linchenn commented Apr 25, 2023

This PR add Draw op to Core and implemented it in CPU backend.

We could get the following effects with a couple lines of codes:

// Setup
tf.setBackend('cpu');
const imgElem = document.getElementById('salad');
const canvasElem = document.getElementById("display");
const image = tf.browser.fromPixels(imgElem);

// TFJS processing
const filter = tf.tensor4d(fval, [3,3,3,3], 'float32');
const normalizedImage = tf.div(image, 255);
const convRes = tf.conv2d(normalizedImage, filter, 1, 'same');
const res =  tf.clipByValue(convRes, 0, 1);

// Draw
const drawOptions = {imageOptions: {alpha: 0.5}};
tf.browser.draw(res, canvasElem, drawOptions);

image

To see the logs from the Cloud Build CI, please join either our discussion or announcement mailing list.

@Linchenn
Copy link
Collaborator Author

The tests for this PR is under CPU-backend, instead of Core, because different backend would use different context and has different implementation to validate the image data.

@Linchenn
Copy link
Collaborator Author

@axinging Could you help review this PR? Hope the API design make sense for WebGPU backend.

}
const ctx = canvas.getContext(contextType,
canvasOptions?.contextAttributes || {}) as CanvasRenderingContext2D ;

Copy link
Contributor

Choose a reason for hiding this comment

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

Check whether ctx is null in case getContext is called before with a different contextType?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

const value = data[i * depth + d];

if (image.dtype === 'float32') {
if (value < 0 || value > 1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we throw an error or just clamp it to the supported range? For webgl/webgpu backend, it's not possible to do this check and throw errors.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For WebGL and WebGPU, I don't think we need to check values and we could just clamp them. For CPU, it's very convenient to make such checks and throwing an error would be helpful for users, because it's common that users pass values out of 255 and get a pure white canvas with no hit message. So, I think this throw-error feature is good to have.

throw new Error(
`toPixels only supports rank 2 or 3 tensors, got rank ${$img.rank}.`);
}
validateImgTensor($img);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe add a warning in this API to tell users that this API will be deprecated and suggest them switch to the more powerful draw API.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done. Thanks!

*/
export function draw(
image: Tensor2D|Tensor3D|TensorLike, canvas: HTMLCanvasElement,
contextOptions?: ContextOptions, drawOptions?: DrawOptions): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it simpler to merge contextOptions and drawOptions into CanvasOptions/CanvasConfiguration? For example, in webgpu, we have GPUCanvasConfiguration

export interface CanvasOptions {
  /**
   * Optional. If it is not set, it would be variable based on the current
   * backend.
   */
  contextType?: string;
  /**
   * Optional.
   */
  contextAttributes?: WebGLContextAttributes;

  /**
   * Optional. A number in range [0-1]. If the image is a 2D tensor or a 3D
   * tensor with 1 or 3 channels, the alpha channels would set as its value;
   * otherwise, it would not make effects.
   */
  alpha?: number;
}

Copy link
Collaborator Author

@Linchenn Linchenn Apr 27, 2023

Choose a reason for hiding this comment

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

GPUCanvasConfiguration has alphaMode, but it is different from the alpha here.

I mean either contextOptions or GPUCanvasConfiguration are describing the setting of canvas or context. However, drawOptions is to customize the kernel computations. Separating them might be more clearer for users?

Also, do you think the name and the definition of contextOptions makes sense for WebGPU?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's ok to custom alpha. I don't have good candidate name for drawOptions. Leave it open if other reviewers have better suggestion or just keep the current name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sorry, I mean contextOptions (updated my comment), does it makes senses for WebGPU? Should I add more optional fields, like GPUCanvasConfiguration? I intend to wrap all canvas and context related settings into it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. It will be good to change contextAttributes?: WebGLContextAttributes; -> contextAttributes?: WebGLContextAttributes|GPUCanvasConfiguration;. GPUCanvasConfiguration makes it possible that draw the tensor to a different device's canvas since it has the device member.

One issue is that webgpu allows "opaque" alphaMode. It may conflict with alpha in drawOption. We need to define the priority. Once alphaMode is opaque. alpha will take no effect or alpha will override alphaMode?

Copy link
Collaborator

@chunnienc chunnienc Apr 28, 2023

Choose a reason for hiding this comment

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

I agree that merging two options into one can cause some confusion in some overlapping but different purpose params like alpha. But I feel a little bit concerned when I see your example usage code:

tf.browser.draw(res, canvasElem, undefined, {alpha: 0.5});

From the user's perspective it is hard to tell whether the first one or second one is drawOptions. We may hurt the readability and usability of this API. Just my personal concerns, throw it out for more discussion. Feel free to disagree.

Also, if we choose to release it as two separate options params, consider to make it:

function draw(
    image: Tensor2D|Tensor3D|TensorLike, canvas: HTMLCanvasElement, drawOptions?: DrawOptions, contextOptions?: ContextOptions)

if you think customizing drawOptions is a normal user feature but specifying contextOptions is more like an feature for advanced users, swapping the order may be more easy to use since most of the users can just do tf.browser.draw(res, canvas, { alpha: 1, ...});

Copy link
Collaborator Author

@Linchenn Linchenn May 1, 2023

Choose a reason for hiding this comment

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

  1. Since we would add GPUCanvasConfiguration to CanvasOptions, I renamed ContextOptions to CanvasOptions.
  2. For usage of the API with two objects, if users follow the coding style, it would be:
const canvasOptions = {};
const drawOptions = {alpha: 0.5}
tf.browser.draw(res, canvasElem, canvasOptions, drawOptions);
  1. I am open to suggestions for whether we want to merge the two options as Options: {CanvasOptions, DrawOptions}.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As collected feedbacks from you and WebML channel, get most votes for unified option object for this. Updated.

@qjia7
Copy link
Contributor

qjia7 commented Apr 27, 2023

The tests for this PR is under CPU-backend, instead of Core, because different backend would use different context and has different implementation to validate the image data.

Maybe we can easily let each backend support both backend preferred canvas context + 2d canvas. Then we can always use 2d canvas to verify the correctness in core.

@Linchenn
Copy link
Collaborator Author

The tests for this PR is under CPU-backend, instead of Core, because different backend would use different context and has different implementation to validate the image data.

Maybe we can easily let each backend support both backend preferred canvas context + 2d canvas. Then we can always use 2d canvas to verify the correctness in core.

Nice catch, done. Thanks!

@Linchenn Linchenn requested a review from qjia7 April 27, 2023 06:23
Copy link
Contributor

@qjia7 qjia7 left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

*/
export function draw(
image: Tensor2D|Tensor3D|TensorLike, canvas: HTMLCanvasElement,
contextOptions?: ContextOptions, drawOptions?: DrawOptions): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's ok to custom alpha. I don't have good candidate name for drawOptions. Leave it open if other reviewers have better suggestion or just keep the current name.

* [0-255].
*
* @param image A rank-2 tensor with shape `[height, width]`, or a rank-3 tensor
* of shape `[height, width, numChannels]`. If rank-2, draws grayscale. If
Copy link
Collaborator

Choose a reason for hiding this comment

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

"draws grayscale" -> "draws in grayscale"
I feel like the comment can be written to be more clear. @mattsoulanille please help us with grammar.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@Linchenn Linchenn requested a review from chunnienc April 27, 2023 20:20
Copy link
Collaborator

@chunnienc chunnienc left a comment

Choose a reason for hiding this comment

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

LGTM except for:

  1. The doc comment can be improved. @mattsoulanille for native speaker review.
  2. The API with two options looks okay to me, but we can have more discussion on that if you want online and/or offline.

@Linchenn Linchenn requested review from pyu10055 and gyagp May 1, 2023 17:47
Comment on lines 373 to 375
console.warn(
'tf.browser.toPixels is not efficient to draw tensor on canvas. ' +
'Please try tf.browser.draw instead.');
Copy link
Member

Choose a reason for hiding this comment

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

Can we print this warning only once? Otherwise, we may spam the console if the user is running toPixels in a loop / requestAnimationFrame.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nice catch. Added a variable in the browser.ts file for the boolean, since the file already has a variable hasToPixelsWarned, which share the coding style.

}
}

describeWithFlags('Draw on 2d context', BROWSER_ENVS, () => {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: If 'Draw on 2d context' should never be tested in WebGL, then we could use a different set of backends than BROWSER_ENVS instead of filtering it out in setup_test.ts. Alternatively, if we intend to implement drawing on a 2d canvas from WebGL in the future, then the current filtering approach looks good.

On the other hand, this might be more complicated than simply filtering the test case out, so feel free to leave it as-is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As Jiajia suggested, we would implement and test 2d context for all backends soon.

Comment on lines 404 to 405
* @param canvasOptions A object to configure the canvas to draw to.
* @param drawOptions A object of options to customize drawing.
Copy link
Member

Choose a reason for hiding this comment

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

Should we describe the settings that people can change on these config and draw options here, or is this documented elsewhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is at the definition of drawOptions and canvasOptions and tfjs-website would add them to the API. Similar as Options of this API.

Linchenn and others added 3 commits May 1, 2023 12:37
@Linchenn Linchenn merged commit db099a4 into tensorflow:master May 6, 2023
@Linchenn Linchenn deleted the draw branch May 6, 2023 23:51
@Jonathhhan
Copy link

Jonathhhan commented Jun 7, 2023

Thanks for the nice lib. I also get the warning: tf.browser.toPixels is not efficient to draw tensor on canvas. Please try tf.browser.draw instead. It only works with tf.setBackend('cpu');, but then it is (in total) much slower than with tf.setBackend('webgl'). Are there any plans to implement tf.browser.draw for the webgl backend?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants