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>
  );
}