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

Allow setting sketch color default #4979

Merged
merged 14 commits into from Jul 25, 2023
7 changes: 7 additions & 0 deletions .changeset/smooth-dragons-ask.md
@@ -0,0 +1,7 @@
---
"@gradio/app": minor
"@gradio/image": minor
"gradio": minor
---

feat:Allow setting sketch color default
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@
- Added autofocus argument to Textbox by [@aliabid94](https://github.com/aliabid94) in [PR 4978](https://github.com/gradio-app/gradio/pull/4978)
- The `gr.ChatInterface` UI now converts the "Submit" button to a "Stop" button in ChatInterface while streaming, which can be used to pause generation. By [@abidlabs](https://github.com/abidlabs) in [PR 4971](https://github.com/gradio-app/gradio/pull/4971).
- Add a `border_color_accent_subdued` theme variable to add a subdued border color to accented items. This is used by chatbot user messages. Set the value of this variable in `Default` theme to `*primary_200`. By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4989](https://github.com/gradio-app/gradio/pull/4989)
- Add default sketch color argument `brush_color`. Also, masks drawn on images are now slightly translucent (and mask color can also be set via brush_color). By [@aliabid94](https://github.com/aliabid94) in [PR 4979](https://github.com/gradio-app/gradio/pull/4979)

### Bug Fixes:

Expand Down
10 changes: 10 additions & 0 deletions gradio/components/image.py
Expand Up @@ -81,6 +81,7 @@ def __init__(
elem_classes: list[str] | str | None = None,
mirror_webcam: bool = True,
brush_radius: float | None = None,
brush_color: str = "#000000",
show_share_button: bool | None = None,
**kwargs,
):
Expand Down Expand Up @@ -109,9 +110,11 @@ def __init__(
elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
mirror_webcam: If True webcam will be mirrored. Default is True.
brush_radius: Size of the brush for Sketch. Default is None which chooses a sensible default
brush_color: Default color of the brush for Sketch, should be hex code such as "#FF0000".
aliabid94 marked this conversation as resolved.
Show resolved Hide resolved
aliabid94 marked this conversation as resolved.
Show resolved Hide resolved
show_share_button: If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.
"""
self.brush_radius = brush_radius
self.brush_color = brush_color
self.mirror_webcam = mirror_webcam
valid_types = ["numpy", "pil", "filepath"]
if type not in valid_types:
Expand Down Expand Up @@ -178,6 +181,7 @@ def get_config(self):
"streaming": self.streaming,
"mirror_webcam": self.mirror_webcam,
"brush_radius": self.brush_radius,
"brush_color": self.brush_color,
"selectable": self.selectable,
"show_share_button": self.show_share_button,
"show_download_button": self.show_download_button,
Expand All @@ -198,6 +202,7 @@ def update(
interactive: bool | None = None,
visible: bool | None = None,
brush_radius: float | None = None,
brush_color: str | None = None,
show_share_button: bool | None = None,
):
return {
Expand All @@ -213,6 +218,7 @@ def update(
"visible": visible,
"value": value,
"brush_radius": brush_radius,
"brush_color": brush_color,
"show_share_button": show_share_button,
"__type__": "update",
}
Expand Down Expand Up @@ -276,6 +282,10 @@ def preprocess(

if self.tool == "sketch" and self.source in ["upload", "webcam"]:
mask_im = processing_utils.decode_base64_to_image(mask)

if mask_im.mode == "RGBA": # whiten any opaque pixels in the mask
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't follow this. Can you explain what's happening?

Copy link
Collaborator Author

@aliabid94 aliabid94 Jul 24, 2023

Choose a reason for hiding this comment

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

yep, so previously in the frontend, whenever a user drew a mask, we were drawing it twice - once on the visible layer directly on the image, and then again on a hidden layer called "mask", which instead drew with a white brush on a black background. This mask layer was what was getting sent to the backend on submit.
I simplified the frontend so that there is a single mask layer, now visible, that hovers over the drawing. A user is no longer drawing directly on the image, but instead on this visible mask layer. On submit, we send this mask layer. However, this mask layer is no longer the white on black image of before - it has opacity and may be whatever color that brush_color has been set to. So now we convert this mask layer into the B+W mask of before in the backend with these lines of code.
We support "backwards compatibility" in case someone uses a client, with if mask_im.mode == "RGBA": , because previously masks were only RGB.
cc @pngwn for context to the frontend changes

Copy link
Member

Choose a reason for hiding this comment

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

Are we certain that this conversion is always accurate (going from colours to b+w, especially since the sketches are antialiased)? Masks are typically alpha layers and lack any colours, I'm curious about why we would ever want a mask layer with colour.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The mask layer only has color because that's how that canvas is rendered now, as a color on a transparent background. I assumed the antialiasing would manifest in the alpha, as in the smooth edges would have an alpha between 0 and 255, which would get translated to some gray between 0 and 255 in the backend logic where we take the alpha value and set it as the R, G, and B values. Will confirm if this antialiasing works.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed that antialiasing still works - there are values between 0 and 255 at the edges of the mask

alpha_data = mask_im.getchannel("A").convert("L")
mask_im = _Image.merge("RGB", [alpha_data, alpha_data, alpha_data])
return {
"image": self._format_image(im),
"mask": self._format_image(mask_im),
Expand Down
2 changes: 2 additions & 0 deletions js/app/src/components/Image/Image.svelte
Expand Up @@ -25,6 +25,7 @@
export let mirror_webcam: boolean;
export let shape: [number, number];
export let brush_radius: number;
export let brush_color: string;
export let selectable = false;
export let container = true;
export let scale: number | null = null;
Expand Down Expand Up @@ -78,6 +79,7 @@
{:else}
<Image
{brush_radius}
{brush_color}
{shape}
bind:value
{source}
Expand Down
4 changes: 1 addition & 3 deletions js/image/src/Image.svelte
Expand Up @@ -28,6 +28,7 @@
export let pending: boolean = false;
export let mirror_webcam: boolean;
export let brush_radius: number;
export let brush_color = "#000000";
export let selectable: boolean = false;

let sketch: Sketch;
Expand Down Expand Up @@ -139,9 +140,6 @@
mode = "editor";
}
}

$: brush_color = mode == "mask" ? "#000000" : "#000";

let value_img;
let max_height;
let max_width;
Expand Down
121 changes: 35 additions & 86 deletions js/image/src/Sketch.svelte
Expand Up @@ -66,31 +66,28 @@
function mid_point(p1, p2) {
return {
x: p1.x + (p2.x - p1.x) / 2,
y: p1.y + (p2.y - p1.y) / 2
y: p1.y + (p2.y - p1.y) / 2,
};
}

const canvas_types = [
{
name: "interface",
zIndex: 15
zIndex: 15,
},
{
name: "drawing",
zIndex: 11
name: "mask",
zIndex: 13,
opacity: 0.7,
},
{
name: "temp",
zIndex: 12
name: "drawing",
zIndex: 11,
},
{
name: "mask",
zIndex: -1
name: "temp",
zIndex: 12,
},
{
name: "temp_fake",
zIndex: -2
}
];

let canvas = {};
Expand Down Expand Up @@ -185,8 +182,8 @@
enabled: true,
initialPoint: {
x: width / 2,
y: height / 2
}
y: height / 2,
},
});

canvas_observer = new ResizeObserver((entries, observer, ...rest) => {
Expand Down Expand Up @@ -243,9 +240,6 @@

lines = _lines;
ctx.drawing.drawImage(canvas.temp, 0, 0, width, height);
if (mode === "mask") {
ctx.mask.drawImage(canvas.temp_fake, 0, 0, width, height);
}

if (lines.length == 0) {
dispatch("clear");
Expand All @@ -270,7 +264,7 @@
return JSON.stringify({
lines: lines,
width: canvas_width,
height: canvas_height
height: canvas_height,
});
};

Expand All @@ -280,16 +274,9 @@
draw_points({
points: _points,
brush_color,
brush_radius
brush_radius,
mask: mode === "mask",
});

if (mode === "mask") {
draw_fake_points({
points: _points,
brush_color,
brush_radius
});
}
});

saveLine({ brush_color, brush_radius });
Expand Down Expand Up @@ -351,15 +338,14 @@

const container_dimensions = {
height: container_height,
width: container_height * (dimensions.width / dimensions.height)
width: container_height * (dimensions.width / dimensions.height),
};

await Promise.all([
set_canvas_size(canvas.interface, dimensions, container_dimensions),
set_canvas_size(canvas.drawing, dimensions, container_dimensions),
set_canvas_size(canvas.temp, dimensions, container_dimensions),
set_canvas_size(canvas.temp_fake, dimensions, container_dimensions),
set_canvas_size(canvas.mask, dimensions, container_dimensions, false)
set_canvas_size(canvas.mask, dimensions, container_dimensions, false),
]);

if (!brush_radius) {
Expand Down Expand Up @@ -418,7 +404,7 @@

return {
x: ((clientX - rect.left) / rect.width) * width,
y: ((clientY - rect.top) / rect.height) * height
y: ((clientY - rect.top) / rect.height) * height,
};
};

Expand All @@ -434,69 +420,39 @@
draw_points({
points: points,
brush_color,
brush_radius
brush_radius,
mask: mode === "mask",
});

if (mode === "mask") {
draw_fake_points({
points: points,
brush_color,
brush_radius
});
}
}
mouse_has_moved = true;
};

let draw_points = ({ points, brush_color, brush_radius }) => {
if (!points || points.length < 2) return;
ctx.temp.lineJoin = "round";
ctx.temp.lineCap = "round";

ctx.temp.strokeStyle = brush_color;
ctx.temp.lineWidth = brush_radius;
if (!points || points.length < 2) return;
let p1 = points[0];
let p2 = points[1];
ctx.temp.moveTo(p2.x, p2.y);
ctx.temp.beginPath();
for (var i = 1, len = points.length; i < len; i++) {
var midPoint = mid_point(p1, p2);
ctx.temp.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
p1 = points[i];
p2 = points[i + 1];
}

ctx.temp.lineTo(p1.x, p1.y);
ctx.temp.stroke();
};

let draw_fake_points = ({ points, brush_color, brush_radius }) => {
let draw_points = ({ points, brush_color, brush_radius, mask }) => {
if (!points || points.length < 2) return;
let target_ctx = mask ? ctx.mask : ctx.temp;
target_ctx.lineJoin = "round";
target_ctx.lineCap = "round";

ctx.temp_fake.lineJoin = "round";
ctx.temp_fake.lineCap = "round";
ctx.temp_fake.strokeStyle = "#fff";
ctx.temp_fake.lineWidth = brush_radius;
target_ctx.strokeStyle = brush_color;
target_ctx.lineWidth = brush_radius;
let p1 = points[0];
let p2 = points[1];
ctx.temp_fake.moveTo(p2.x, p2.y);
ctx.temp_fake.beginPath();
target_ctx.moveTo(p2.x, p2.y);
target_ctx.beginPath();
for (var i = 1, len = points.length; i < len; i++) {
var midPoint = mid_point(p1, p2);
ctx.temp_fake.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
target_ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
p1 = points[i];
p2 = points[i + 1];
}

ctx.temp_fake.lineTo(p1.x, p1.y);
ctx.temp_fake.stroke();
target_ctx.lineTo(p1.x, p1.y);
target_ctx.stroke();
};

let save_mask_line = () => {
if (points.length < 1) return;
points.length = 0;
ctx.mask.drawImage(canvas.temp_fake, 0, 0, width, height);

trigger_on_change();
};
Expand All @@ -507,7 +463,7 @@
lines.push({
points: points.slice(),
brush_color: brush_color,
brush_radius
brush_radius,
});

if (mode !== "mask") {
Expand Down Expand Up @@ -540,15 +496,7 @@
ctx.temp.fillRect(0, 0, width, height);

if (mode === "mask") {
ctx.temp_fake.clearRect(
0,
0,
canvas.temp_fake.width,
canvas.temp_fake.height
);
ctx.mask.clearRect(0, 0, width, height);
ctx.mask.fillStyle = "#000";
ctx.mask.fillRect(0, 0, width, height);
ctx.mask.clearRect(0, 0, canvas.mask.width, canvas.mask.height);
}
}

Expand Down Expand Up @@ -587,7 +535,7 @@

export function get_image_data() {
return mode === "mask"
? canvas.mask.toDataURL("image/jpg")
? canvas.mask.toDataURL("image/png")
: canvas.drawing.toDataURL("image/jpg");
}
</script>
Expand All @@ -603,10 +551,11 @@
Start drawing
</div>
{/if}
{#each canvas_types as { name, zIndex }}
{#each canvas_types as { name, zIndex, opacity }}
<canvas
key={name}
style=" z-index:{zIndex};"
style:opacity
class:lr={add_lr_border}
class:tb={!add_lr_border}
bind:this={canvas[name]}
Expand Down