cnn_visualizer / frontends /react /src /PoolingVisualizer.tsx
nanye's picture
added more options for convolution and added pooling visualization
419db65
Raw History Blame Contribute Delete
7.58 kB
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>
);
}