Gabriel's picture
Upload folder using huggingface_hub
d192d6a verified
<svelte:options accessors={true} />
<script lang="ts">
import { onMount, onDestroy, tick } from "svelte";
import {
Application,
Container,
Graphics,
Text,
Sprite,
Texture,
TextStyle,
} from "pixi.js";
import type { Gradio } from "@gradio/utils";
import { Block, BlockLabel } from "@gradio/atoms";
import { Image as ImageIcon } from "@gradio/icons";
import { StatusTracker } from "@gradio/statustracker";
import type { LoadingStatus } from "@gradio/statustracker";
import type { FileData } from "@gradio/client";
export let elem_id = "";
export let elem_classes: string[] = [];
export let visible: boolean | "hidden" = true;
export let value: {
image: FileData;
polygons: Array<{
id: string;
coordinates: number[][];
color: string;
mask_opacity?: number;
stroke_width?: number;
stroke_opacity?: number;
selected_mask_opacity?: number;
selected_stroke_opacity?: number;
display_text?: string | null;
display_font_size?: number | null;
display_text_color?: string;
}>;
selected_polygons?: string[] | null;
} | null = null;
export let label: string;
export let show_label: boolean;
export let height: number | string | undefined = undefined;
export let width: number | string | undefined = undefined;
export let container = true;
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
export let root: string;
export let gradio: Gradio<{
change: never;
upload: never;
clear: never;
select: { index: number | null; value: any };
clear_status: LoadingStatus;
}>;
let canvasContainer: HTMLDivElement;
let app: Application;
let imageSprite: Sprite | null = null;
let polygonGraphics: Map<string, Graphics> = new Map();
let polygonTexts: Map<string, Text> = new Map();
let textContainer: Container | null = null;
let selectedPolygonIds: string[] = [];
let viewportContainer: Container | null = null;
let isDragging = false;
let lastPointerPosition = { x: 0, y: 0 };
let initialScale = 1;
let initialPosition = { x: 0, y: 0 };
let minScale = 0.5;
let maxScale = 3;
let isMouseOverCanvas = false;
type ImageRect = {
left: number;
top: number;
right: number;
bottom: number;
width: number;
height: number;
naturalWidth: number;
naturalHeight: number;
};
let imageRect: ImageRect = {
left: 0,
top: 0,
right: 1,
bottom: 1,
width: 1,
height: 1,
naturalWidth: 2,
naturalHeight: 2,
};
$: (value, handleValueChange());
async function handleValueChange() {
if (!app || !value) return;
selectedPolygonIds = value.selected_polygons || [];
await renderAnnotations();
}
function updateSelection(newSelectedIds: string[]) {
if (!value || !imageSprite) return;
polygonGraphics.forEach((graphics, polygonId) => {
const polygon = value.polygons.find((p) => p.id === polygonId);
if (!polygon) return;
const originalMaskAlpha = polygon.mask_opacity ?? 0.2;
const selectedMaskAlpha = polygon.selected_mask_opacity ?? 0.5;
const originalStrokeAlpha = polygon.stroke_opacity ?? 0.6;
const selectedStrokeAlpha = polygon.selected_stroke_opacity ?? 1.0;
const strokeWidth = polygon.stroke_width ?? 0.7;
graphics.clear();
if (newSelectedIds.includes(polygonId)) {
drawPolygonPath(
graphics,
polygon,
selectedMaskAlpha,
strokeWidth,
selectedStrokeAlpha,
);
} else {
drawPolygonPath(
graphics,
polygon,
originalMaskAlpha,
strokeWidth,
originalStrokeAlpha,
);
}
});
}
function handlePolygonSelection(polygonId: string, event: any) {
if (!value) return;
const isMultiSelect = event.ctrlKey || event.metaKey;
if (selectedPolygonIds.includes(polygonId)) {
const newSelectedIds = selectedPolygonIds.filter(
(id) => id !== polygonId,
);
updateSelection(newSelectedIds);
selectedPolygonIds = newSelectedIds;
gradio.dispatch("select", {
index:
newSelectedIds.length > 0
? value.polygons.findIndex(
(p) =>
p.id ===
newSelectedIds[
newSelectedIds.length - 1
],
)
: null,
value:
newSelectedIds.length > 0
? newSelectedIds
: null,
});
return;
}
let newSelectedIds: string[];
if (isMultiSelect) {
newSelectedIds = [...selectedPolygonIds, polygonId];
} else {
newSelectedIds = [polygonId];
}
updateSelection(newSelectedIds);
selectedPolygonIds = newSelectedIds;
gradio.dispatch("select", {
index: value.polygons.findIndex(
(p) => p.id === polygonId,
),
value: newSelectedIds,
});
}
async function initPixiApp() {
if (!canvasContainer) return;
const containerWidth = canvasContainer.clientWidth || 800;
const containerHeight = canvasContainer.clientHeight || 600;
app = new Application();
await app.init({
width: containerWidth,
height: containerHeight,
backgroundColor: 0xf0f0f0,
antialias: true,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
});
canvasContainer.appendChild(app.canvas as HTMLCanvasElement);
app.stage.eventMode = "static";
app.stage.hitArea = app.screen;
viewportContainer = new Container();
viewportContainer.eventMode = "static";
app.stage.addChild(viewportContainer);
setupControls();
}
function setupControls() {
if (!app || !viewportContainer) return;
window.addEventListener("keydown", handleKeydown);
// Track mouse hover state for canvas area
if (canvasContainer) {
canvasContainer.addEventListener("mouseenter", () => {
isMouseOverCanvas = true;
});
canvasContainer.addEventListener("mouseleave", () => {
isMouseOverCanvas = false;
});
}
app.stage.on("pointerdown", (event) => {
if (event.button === 1 || (event.button === 0 && event.shiftKey)) {
isDragging = true;
lastPointerPosition = { x: event.global.x, y: event.global.y };
app.canvas.style.cursor = "grabbing";
}
});
app.stage.on("pointermove", (event) => {
if (isDragging && viewportContainer) {
const dx = event.global.x - lastPointerPosition.x;
const dy = event.global.y - lastPointerPosition.y;
viewportContainer.x += dx;
viewportContainer.y += dy;
lastPointerPosition = { x: event.global.x, y: event.global.y };
}
});
app.stage.on("pointerup", () => {
isDragging = false;
app.canvas.style.cursor = "default";
});
app.stage.on("pointerupoutside", () => {
isDragging = false;
app.canvas.style.cursor = "default";
});
app.canvas.addEventListener("wheel", handleWheel, { passive: false });
}
function handleKeydown(event: KeyboardEvent) {
if (!viewportContainer || !isMouseOverCanvas) return;
switch(event.key) {
case "=":
case "+":
zoomIn();
event.preventDefault();
break;
case "-":
case "_":
zoomOut();
event.preventDefault();
break;
case "0":
if (event.ctrlKey || event.metaKey) {
resetView();
event.preventDefault();
}
break;
case "ArrowLeft":
viewportContainer!.x += 20;
event.preventDefault();
break;
case "ArrowRight":
viewportContainer!.x -= 20;
event.preventDefault();
break;
case "ArrowUp":
viewportContainer!.y += 20;
event.preventDefault();
break;
case "ArrowDown":
viewportContainer!.y -= 20;
event.preventDefault();
break;
}
}
function handleWheel(event: WheelEvent) {
if (!viewportContainer || !app) return;
event.preventDefault();
const delta = event.deltaY < 0 ? 1.05 : 0.95;
const newScale = viewportContainer!.scale.x * delta;
if (newScale >= minScale && newScale <= maxScale) {
const worldPos = {
x: (event.offsetX - viewportContainer!.x) / viewportContainer!.scale.x,
y: (event.offsetY - viewportContainer!.y) / viewportContainer!.scale.y,
};
viewportContainer!.scale.x = newScale;
viewportContainer!.scale.y = newScale;
viewportContainer!.x = event.offsetX - worldPos.x * newScale;
viewportContainer!.y = event.offsetY - worldPos.y * newScale;
}
}
function zoomIn() {
if (!viewportContainer || !app) return;
const newScale = Math.min(viewportContainer!.scale.x * 1.1, maxScale);
const center = { x: app.screen.width / 2, y: app.screen.height / 2 };
const worldPos = {
x: (center.x - viewportContainer!.x) / viewportContainer!.scale.x,
y: (center.y - viewportContainer!.y) / viewportContainer!.scale.y,
};
viewportContainer!.scale.x = newScale;
viewportContainer!.scale.y = newScale;
viewportContainer!.x = center.x - worldPos.x * newScale;
viewportContainer!.y = center.y - worldPos.y * newScale;
}
function zoomOut() {
if (!viewportContainer || !app) return;
const newScale = Math.max(viewportContainer!.scale.x * 0.9, minScale);
const center = { x: app.screen.width / 2, y: app.screen.height / 2 };
const worldPos = {
x: (center.x - viewportContainer!.x) / viewportContainer!.scale.x,
y: (center.y - viewportContainer!.y) / viewportContainer!.scale.y,
};
viewportContainer!.scale.x = newScale;
viewportContainer!.scale.y = newScale;
viewportContainer!.x = center.x - worldPos.x * newScale;
viewportContainer!.y = center.y - worldPos.y * newScale;
}
function resetView() {
if (!viewportContainer) return;
viewportContainer!.scale.x = initialScale;
viewportContainer!.scale.y = initialScale;
viewportContainer!.x = initialPosition.x;
viewportContainer!.y = initialPosition.y;
}
async function renderAnnotations() {
if (!app || !value || !viewportContainer) return;
viewportContainer!.removeChildren();
polygonGraphics.clear();
polygonTexts.forEach((text) => text.destroy());
polygonTexts.clear();
textContainer = new Container();
textContainer.zIndex = 1000;
viewportContainer!.sortableChildren = true;
viewportContainer!.addChild(textContainer);
if (value.image) {
let imageUrl = "";
if (typeof value.image === "string") {
imageUrl = value.image;
} else if (value.image.url) {
imageUrl = value.image.url;
} else if (value.image.path) {
if (root && !value.image.path.startsWith("http")) {
imageUrl = `${root}/file=${value.image.path}`;
} else {
imageUrl = value.image.path;
}
}
if (imageUrl) {
try {
const img = new Image();
img.crossOrigin = "anonymous";
const imageLoadPromise = new Promise<HTMLImageElement>(
(resolve, reject) => {
img.onload = () => resolve(img);
img.onerror = reject;
img.src = imageUrl;
},
);
const loadedImage = await imageLoadPromise;
const texture = Texture.from(loadedImage);
imageSprite = new Sprite(texture);
const scaleX = app.screen.width / texture.width;
const scaleY = app.screen.height / texture.height;
const scale = Math.min(scaleX, scaleY);
imageSprite.scale.set(scale);
const displayWidth = texture.width * scale;
const displayHeight = texture.height * scale;
imageSprite.x = (app.screen.width - displayWidth) / 2;
imageSprite.y = (app.screen.height - displayHeight) / 2;
imageRect = {
left: imageSprite.x,
top: imageSprite.y,
right: imageSprite.x + displayWidth,
bottom: imageSprite.y + displayHeight,
width: displayWidth,
height: displayHeight,
naturalWidth: texture.width,
naturalHeight: texture.height,
};
viewportContainer!.addChild(imageSprite);
initialScale = 1;
initialPosition.x = 0;
initialPosition.y = 0;
viewportContainer!.scale.x = 1;
viewportContainer!.scale.y = 1;
viewportContainer!.x = 0;
viewportContainer!.y = 0;
} catch (error) {
return;
}
}
}
if (value.polygons && value.polygons.length > 0 && imageSprite) {
value.polygons.forEach((polygon) => {
const graphics = new Graphics();
let color = 0xff0000;
try {
if (polygon.color) {
const colorStr = polygon.color.replace("#", "");
color = parseInt(colorStr, 16);
}
} catch (e) {
color = 0xff0000;
}
const polygonMaskOpacity = polygon.mask_opacity ?? 0.2;
const selectedMaskAlpha = polygon.selected_mask_opacity ?? 0.5;
const polygonStrokeOpacity = polygon.stroke_opacity ?? 0.6;
const selectedStrokeAlpha =
polygon.selected_stroke_opacity ?? 1.0;
const strokeWidth = polygon.stroke_width ?? 0.7;
const initialMaskAlpha = selectedPolygonIds.includes(polygon.id)
? selectedMaskAlpha
: polygonMaskOpacity;
const initialStrokeAlpha = selectedPolygonIds.includes(
polygon.id,
)
? selectedStrokeAlpha
: polygonStrokeOpacity;
if (polygon.coordinates && polygon.coordinates.length > 0) {
drawPolygonPath(
graphics,
polygon,
initialMaskAlpha,
strokeWidth,
initialStrokeAlpha,
);
}
graphics.eventMode = "static";
graphics.cursor = "pointer";
const originalMaskAlpha = polygonMaskOpacity;
const hoverMaskAlpha = Math.min(polygonMaskOpacity + 0.1, 1.0);
const hoverStrokeAlpha = Math.min(
polygonStrokeOpacity + 0.2,
1.0,
);
graphics.on("pointerover", () => {
if (!selectedPolygonIds.includes(polygon.id)) {
graphics.clear();
drawPolygonPath(
graphics,
polygon,
hoverMaskAlpha,
strokeWidth,
hoverStrokeAlpha,
);
}
});
graphics.on("pointerout", () => {
if (!selectedPolygonIds.includes(polygon.id)) {
graphics.clear();
drawPolygonPath(
graphics,
polygon,
originalMaskAlpha,
strokeWidth,
polygonStrokeOpacity,
);
}
});
graphics.on("pointerdown", (event) => {
handlePolygonSelection(polygon.id, event);
});
viewportContainer!.addChild(graphics);
polygonGraphics.set(polygon.id, graphics);
if (polygon.display_text && polygon.display_font_size && polygon.display_font_size > 0) {
const text = createPolygonText(polygon);
const center = calculatePolygonCenter(polygon.coordinates);
text.anchor.set(0.5, 0.5);
text.x = center.x;
text.y = center.y;
text.eventMode = "static";
text.cursor = "pointer";
text.on("pointerdown", (event) => {
handlePolygonSelection(polygon.id, event);
});
textContainer!.addChild(text);
polygonTexts.set(polygon.id, text);
}
});
}
}
function createPolygonText(polygon: any): Text {
const style = new TextStyle({
fontSize: polygon.display_font_size || 14,
fill: polygon.display_text_color || "#000000",
fontWeight: "bold",
align: "center"
});
return new Text({
text: polygon.display_text,
style: style
});
}
function calculatePolygonCenter(coordinates: number[][]): { x: number; y: number } {
const displayCoords = coordinates.map((coord) => [
(coord[0] / (imageRect.naturalWidth - 1)) * imageRect.width + imageRect.left,
(coord[1] / (imageRect.naturalHeight - 1)) * imageRect.height + imageRect.top,
]);
let centerX = 0;
let centerY = 0;
displayCoords.forEach((coord) => {
centerX += coord[0];
centerY += coord[1];
});
return {
x: centerX / displayCoords.length,
y: centerY / displayCoords.length
};
}
function drawPolygonPath(
graphics: Graphics,
polygon: any,
maskAlpha: number = 0.2,
strokeWidth: number = 0.7,
strokeAlpha: number = 0.6,
) {
if (polygon.coordinates && polygon.coordinates.length > 0) {
const displayCoords = polygon.coordinates.map((coord: number[]) => {
return [
(coord[0] / (imageRect.naturalWidth - 1)) *
imageRect.width +
imageRect.left,
(coord[1] / (imageRect.naturalHeight - 1)) *
imageRect.height +
imageRect.top,
];
});
let color = 0xff0000;
try {
if (polygon.color) {
const colorStr = polygon.color.replace("#", "");
color = parseInt(colorStr, 16);
}
} catch (e) {
color = 0xff0000;
}
graphics.poly(displayCoords.flat());
graphics.fill({ color: color, alpha: maskAlpha });
graphics.stroke({
width: strokeWidth,
color: color,
alpha: strokeAlpha,
});
}
}
onMount(async () => {
await tick();
await initPixiApp();
if (value) {
await renderAnnotations();
}
});
onDestroy(() => {
if (app) {
app.canvas.removeEventListener("wheel", handleWheel);
window.removeEventListener("keydown", handleKeydown);
if (canvasContainer) {
canvasContainer.removeEventListener("mouseenter", () => {});
canvasContainer.removeEventListener("mouseleave", () => {});
}
app.destroy(true, { children: true, texture: true });
}
});
async function handleResize() {
if (!canvasContainer || !app) return;
const newWidth = canvasContainer.clientWidth;
const newHeight = canvasContainer.clientHeight;
if (newWidth !== app.screen.width || newHeight !== app.screen.height) {
app.renderer.resize(newWidth, newHeight);
await renderAnnotations();
}
}
$: if (canvasContainer) {
handleResize();
}
$: (value, gradio.dispatch("change"));
</script>
<Block
{visible}
variant={"solid"}
padding={false}
{elem_id}
{elem_classes}
allow_overflow={false}
{container}
{scale}
{min_width}
{height}
{width}
>
<StatusTracker
autoscroll={gradio.autoscroll}
i18n={gradio.i18n}
{...loading_status}
on:clear_status={() => gradio.dispatch("clear_status", loading_status)}
/>
<BlockLabel
{show_label}
Icon={ImageIcon}
label={label || "Image Annotations"}
/>
<div class="container">
<div class="canvas-container" bind:this={canvasContainer} />
</div>
</Block>
<style>
.container {
display: flex;
position: relative;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.canvas-container {
position: relative;
width: 100%;
height: 100%;
min-height: 400px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background-color: #f0f0f0;
}
:global(.canvas-container canvas) {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
</style>