Spaces:
Running
Running
File size: 7,584 Bytes
419db65 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | import { useRef, useState } from "react";
import { Card, Dropdown, Radio } from "@elvis/ui";
import mandrillImage from "./assets/mandrill.png";
import marioImage from "./assets/mario.png";
import racoonImage from "./assets/racoon.png";
import cameramanImage from "./assets/cameraman.png";
import motocrossImage from "./assets/motocross.png";
import peppersImage from "./assets/peppers.png";
import usePoolingProcessing from "./usePoolingProcessing.ts";
import type { PoolingKernel } from "./usePoolingProcessing.ts";
const IMAGE_OPTIONS = ["Upload...", "Enter URL...", "Mandrill", "Cameraman", "Motocross", "Peppers", "Super Mario", "Racoon"] as const;
const POOLING_KERNELS: PoolingKernel[] = [
"Max (per channel)",
"Average (per channel)",
"Max (across channels)",
"Average (across channels)",
];
const MIN_KERNEL_SIZE = 1;
const MAX_KERNEL_SIZE = 20;
export default function PoolingVisualizer() {
const [rawImage, setRawImage] = useState(mandrillImage);
const [useColor, setUseColor] = useState(true);
const [kernel, setKernel] = useState<PoolingKernel>("Max (per channel)");
const [kernelWidth, setKernelWidth] = useState(2);
const [kernelHeight, setKernelHeight] = useState(2);
const [stride, setStride] = useState(2);
const [dilation, setDilation] = useState(1);
const [padding, setPadding] = useState(0);
const result = usePoolingProcessing(rawImage, useColor, kernel, kernelWidth, kernelHeight, stride, dilation, padding);
return (
<div className="grid grid-cols-[minmax(0,7fr)_minmax(0,3fr)] gap-8 h-full min-h-0">
<Card className="grid grid-cols-2 gap-4 h-full min-h-0">
<ImagePane title="Input" image={result.input} dimensions={result.inputSize} />
<ImagePane title="Output" image={result.output} dimensions={result.outputSize} />
</Card>
<Card className="flex flex-col gap-6 h-full min-h-0 overflow-auto">
<ImageControls setImage={setRawImage} useColor={useColor} setUseColor={setUseColor} />
<div className="flex flex-col w-full min-h-0 gap-3">
<ControlRow label="Kernel">
<Dropdown label="" options={POOLING_KERNELS} activeOption={kernel} onChange={(value) => setKernel(value as PoolingKernel)} />
</ControlRow>
<ControlRow label="Size">
<div className="flex items-center gap-2">
<input type="number" min={MIN_KERNEL_SIZE} max={MAX_KERNEL_SIZE} aria-label="Pooling kernel width" className="w-20 px-2 py-1 text-sm text-center rounded border border-gray-300" value={kernelWidth} onChange={(event) => updateNumber(event.currentTarget.valueAsNumber, setKernelWidth, 1)} />
<span className="text-sm">×</span>
<input type="number" min={MIN_KERNEL_SIZE} max={MAX_KERNEL_SIZE} aria-label="Pooling kernel height" className="w-20 px-2 py-1 text-sm text-center rounded border border-gray-300" value={kernelHeight} onChange={(event) => updateNumber(event.currentTarget.valueAsNumber, setKernelHeight, 1)} />
</div>
</ControlRow>
<ControlRow label="Stride">
<input
type="number"
min={1}
max={MAX_KERNEL_SIZE}
aria-label="Pooling stride"
className="w-20 px-2 py-1 text-sm text-center rounded border border-gray-300"
value={stride}
onChange={(event) => updateNumber(event.currentTarget.valueAsNumber, setStride, 1)}
/>
</ControlRow>
<NumericOption label="Dilation" value={dilation} min={1} onChange={setDilation} />
<NumericOption label="Padding" value={padding} min={0} onChange={setPadding} />
</div>
</Card>
</div>
);
}
function updateNumber(value: number, setter: (value: number) => void, min: number) {
if (!Number.isFinite(value)) return;
setter(Math.round(Math.min(MAX_KERNEL_SIZE, Math.max(min, value))));
}
function NumericOption({ label, value, min, onChange }: { label: string; value: number; min: number; onChange: (value: number) => void }) {
return (
<ControlRow label={label}>
<input
type="number"
min={min}
max={MAX_KERNEL_SIZE}
aria-label={`Pooling ${label.toLowerCase()}`}
className="w-20 px-2 py-1 text-sm text-center rounded border border-gray-300"
value={value}
onChange={(event) => updateNumber(event.currentTarget.valueAsNumber, onChange, min)}
/>
</ControlRow>
);
}
function ImagePane({ title, image, dimensions }: { title: string; image: string | null; dimensions: string | null }) {
return (
<div className="flex flex-col items-center justify-center min-h-0">
<h2 className="text-lg font-bold mb-2">{title}</h2>
{image ? <img src={image} alt={title} className="flex-1 w-full h-full min-w-0 min-h-0 object-contain" /> : <p>Processing...</p>}
<div className="h-8 shrink-0 text-xs text-gray-700 pt-2">{dimensions}</div>
</div>
);
}
function ControlRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="grid grid-cols-[4.5rem_minmax(0,1fr)] items-center gap-2">
<span className="text-sm font-bold text-gray-700 text-right">{label}</span>
<div className="justify-self-start text-sm [&_label]:hidden [&_select]:px-2 [&_select]:py-1 [&_select]:text-xs">{children}</div>
</div>
);
}
function ImageControls({ setImage, useColor, setUseColor }: { setImage: (image: string) => void; useColor: boolean; setUseColor: (value: boolean) => void }) {
const fileInput = useRef<HTMLInputElement | null>(null);
const [selected, setSelected] = useState<(typeof IMAGE_OPTIONS)[number]>("Mandrill");
const [url, setUrl] = useState("");
const presets = { Mandrill: mandrillImage, Cameraman: cameramanImage, Motocross: motocrossImage, Peppers: peppersImage, "Super Mario": marioImage, Racoon: racoonImage };
function choose(option: (typeof IMAGE_OPTIONS)[number]) {
if (option === "Upload...") return fileInput.current?.click();
setSelected(option);
if (option !== "Enter URL...") setImage(presets[option]);
}
function applyUrl() { if (url.trim()) setImage(url.trim()); }
return (
<div className="flex flex-col gap-3">
<input ref={fileInput} type="file" accept="image/*" className="hidden" onChange={(event) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => { if (typeof reader.result === "string") setImage(reader.result); };
reader.readAsDataURL(file);
event.target.value = "";
}} />
<ControlRow label="Image"><Dropdown label="" options={IMAGE_OPTIONS} activeOption={selected} onChange={choose} /></ControlRow>
{selected === "Enter URL..." && <ControlRow label=""><input type="url" value={url} placeholder="Enter image URL and press Enter" className="w-full rounded border border-gray-300 px-2 py-1 text-xs" onChange={(e) => setUrl(e.target.value)} onBlur={applyUrl} onKeyDown={(e) => { if (e.key === "Enter") applyUrl(); }} /></ControlRow>}
<div className="grid grid-cols-[4.5rem_minmax(0,1fr)] items-center gap-2">
<span className="text-sm font-bold text-gray-700 text-right">Color</span>
<div className="justify-self-start text-sm">
<Radio
label=""
options={["Grayscale", "Color"] as const}
activeOption={useColor ? "Color" : "Grayscale"}
onChange={(option) => setUseColor(option === "Color")}
/>
</div>
</div>
</div>
);
}
|