Gabriel's picture
Upload folder using huggingface_hub
bfd1654 verified
<svelte:options accessors={true} />
<script lang="ts">
import { onMount, onDestroy, tick } from "svelte";
import * as PIXI 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;
}>;
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: PIXI.Application;
let imageSprite: PIXI.Sprite | null = null;
let polygonGraphics: Map<string, PIXI.Graphics> = new Map();
let selectedPolygonIds: string[] = [];
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, imageSprite!, selectedMaskAlpha, strokeWidth, selectedStrokeAlpha);
} else {
drawPolygonPath(graphics, polygon, imageSprite!, originalMaskAlpha, strokeWidth, originalStrokeAlpha);
}
});
}
async function initPixiApp() {
if (!canvasContainer) return;
const containerWidth = canvasContainer.clientWidth || 800;
const containerHeight = canvasContainer.clientHeight || 600;
app = new PIXI.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;
}
async function renderAnnotations() {
if (!app || !value) return;
app.stage.removeChildren();
polygonGraphics.clear();
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 = PIXI.Texture.from(loadedImage);
imageSprite = new PIXI.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,
};
app.stage.addChild(imageSprite);
} catch (error) {
console.error("Failed to load image:", error);
return;
}
}
}
if (value.polygons && value.polygons.length > 0 && imageSprite) {
value.polygons.forEach((polygon) => {
const graphics = new PIXI.Graphics();
let color = 0xff0000;
try {
if (polygon.color) {
const colorStr = polygon.color.replace("#", "");
color = parseInt(colorStr, 16);
}
} catch (e) {
console.error("Error parsing color:", e);
}
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) {
const displayCoords = polygon.coordinates.map((coord) => {
return [
(coord[0] / (imageRect.naturalWidth - 1)) *
imageRect.width +
imageRect.left,
(coord[1] / (imageRect.naturalHeight - 1)) *
imageRect.height +
imageRect.top,
];
});
graphics.poly(displayCoords.flat());
graphics.fill({ color: color, alpha: initialMaskAlpha });
graphics.stroke({ width: strokeWidth, color: color, alpha: 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,
imageSprite!,
hoverMaskAlpha,
strokeWidth,
hoverStrokeAlpha,
);
}
});
graphics.on("pointerout", () => {
if (!selectedPolygonIds.includes(polygon.id)) {
graphics.clear();
drawPolygonPath(
graphics,
polygon,
imageSprite!,
originalMaskAlpha,
strokeWidth,
polygonStrokeOpacity,
);
}
});
graphics.on("pointerdown", (event) => {
// Check if Ctrl/Cmd key is held for multi-selection
const isMultiSelect = event.ctrlKey || event.metaKey;
if (selectedPolygonIds.includes(polygon.id)) {
// Deselect this polygon
const newSelectedIds = selectedPolygonIds.filter(
(id) => id !== polygon.id,
);
updateSelection(newSelectedIds);
selectedPolygonIds = newSelectedIds;
// Dispatch deselection event to Gradio
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;
}
// Select polygon
let newSelectedIds: string[];
if (isMultiSelect) {
// Add to existing selection
newSelectedIds = [...selectedPolygonIds, polygon.id];
} else {
// Replace selection
newSelectedIds = [polygon.id];
}
updateSelection(newSelectedIds);
selectedPolygonIds = newSelectedIds;
// Dispatch select event to Gradio
gradio.dispatch("select", {
index: value.polygons.findIndex(
(p) => p.id === polygon.id,
),
value: newSelectedIds,
});
});
app.stage.addChild(graphics);
polygonGraphics.set(polygon.id, graphics);
});
}
}
function drawPolygonPath(
graphics: PIXI.Graphics,
polygon: any,
imageSprite: PIXI.Sprite,
maskAlpha: number = 0.2,
strokeWidth: number = 0.7,
strokeAlpha: number = 0.6,
) {
if (polygon.coordinates && polygon.coordinates.length > 0) {
// Transform coordinates from natural image space to display space
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) {
console.error("Error parsing color in drawPolygonPath:", e);
}
// Use Pixi.js 8 drawing API
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.destroy(true, { children: true, texture: true });
}
});
// Handle canvas resize
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);
// Re-render annotations with updated canvas dimensions
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>