Download src/components/canvas/StaticConvolutionView.tsx from TechAvenger/kernel-convolution-visualizer: direct link, hf CLI and curl.
- Browser
- Download file 18.6 kB
-
https://huggingface.co/spaces/TechAvenger/kernel-convolution-visualizer/resolve/25e773913dbd4e9dcd6b40f787d68b280ccdfca1/src/components/canvas/StaticConvolutionView.tsx
- Command line
-
hf download hf://spaces/TechAvenger/kernel-convolution-visualizer@25e773913dbd4e9dcd6b40f787d68b280ccdfca1/src/components/canvas/StaticConvolutionView.tsx
-
curl -L -o StaticConvolutionView.tsx https://huggingface.co/spaces/TechAvenger/kernel-convolution-visualizer/resolve/25e773913dbd4e9dcd6b40f787d68b280ccdfca1/src/components/canvas/StaticConvolutionView.tsx
18.6 kB
| 'use client' | |
| import { useCallback, useMemo, useState } from 'react' | |
| import { useStaticConvolution } from '@/hooks/useStaticConvolution' | |
| import { useAppStore } from '@/store' | |
| import { Card, CardContent, Badge, Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui' | |
| import { cn } from '@/utils/helpers' | |
| import { Calculator, Grid, Palette } from 'lucide-react' | |
| interface StaticConvolutionViewProps { | |
| className?: string | |
| inspectedPosition: { x: number; y: number } | null | |
| onClearInspection: () => void | |
| } | |
| export function StaticConvolutionView({ className, inspectedPosition, onClearInspection }: StaticConvolutionViewProps) { | |
| const { | |
| computeStep, | |
| getPaddedImagePreview, | |
| kernel, | |
| normalize, | |
| padding, | |
| imageWidth, | |
| imageHeight, | |
| hasImage, | |
| hasKernel, | |
| } = useStaticConvolution() | |
| const { settings } = useAppStore() | |
| const [activeChannel, setActiveChannel] = useState(0) | |
| const [activeTab, setActiveTab] = useState<string>('overview') | |
| if (!hasImage || !hasKernel) { | |
| return ( | |
| <Card className={cn('w-full', className)}> | |
| <CardContent className="p-6 text-center text-gray-500 dark:text-gray-400"> | |
| <Grid className="h-12 w-12 mx-auto mb-3 opacity-50" /> | |
| <p>Upload an image and select a kernel to inspect convolution steps</p> | |
| </CardContent> | |
| </Card> | |
| ) | |
| } | |
| const step = inspectedPosition ? computeStep(inspectedPosition.x, inspectedPosition.y, activeChannel) : null | |
| const channelLabels = ['Red', 'Green', 'Blue', 'Alpha'] | |
| const channelColors = ['text-red-600', 'text-green-600', 'text-blue-600', 'text-gray-600'] | |
| // Get padded image preview for visualization | |
| const paddedPreview = inspectedPosition ? getPaddedImagePreview(kernel.length) : null | |
| const halfKernel = Math.floor(kernel.length / 2) | |
| const centerPx = halfKernel | |
| const centerPy = halfKernel | |
| return ( | |
| <Card className={cn('w-full flex flex-col', className)}> | |
| <CardContent className="p-4 flex-1 flex flex-col min-h-0"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between mb-4"> | |
| <h3 className="font-medium flex items-center gap-2"> | |
| <Calculator className="h-5 w-5" /> | |
| Static Convolution Inspector | |
| </h3> | |
| <div className="flex items-center gap-2"> | |
| <Badge variant="outline" className="text-xs"> | |
| {padding} padding | |
| </Badge> | |
| {normalize && <Badge variant="secondary" className="text-xs">Normalized</Badge>} | |
| </div> | |
| </div> | |
| {/* Inspected Position Info */} | |
| {inspectedPosition && ( | |
| <div className="mb-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-4"> | |
| <span className="font-medium">Position: ({inspectedPosition.x}, {inspectedPosition.y})</span> | |
| <div className="flex items-center gap-2"> | |
| <Palette className="h-4 w-4 text-gray-500" /> | |
| <select | |
| value={activeChannel} | |
| onChange={(e) => setActiveChannel(parseInt(e.target.value))} | |
| className="text-sm border border-gray-300 rounded px-2 py-1 dark:border-gray-600 dark:bg-gray-700" | |
| > | |
| {channelLabels.map((label, i) => ( | |
| <option key={i} value={i} className={channelColors[i]}> | |
| {label} Channel | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| </div> | |
| <button | |
| onClick={onClearInspection} | |
| className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" | |
| > | |
| Clear Selection | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Kernel Matrix Display */} | |
| <div className="mb-4"> | |
| <h4 className="text-sm font-medium mb-2">Kernel Matrix ({kernel.length}×{kernel.length})</h4> | |
| <div className="grid gap-1 mb-4" style={{ gridTemplateColumns: `repeat(${kernel.length}, 1fr)` }}> | |
| {kernel.map((row, i) => | |
| row.map((val, j) => ( | |
| <div | |
| key={`${i}-${j}`} | |
| className={cn( | |
| 'p-2 text-center text-sm font-mono rounded border', | |
| val > 0 | |
| ? 'bg-green-50 text-green-800 dark:bg-green-900/30 dark:text-green-300 border-green-200' | |
| : val < 0 | |
| ? 'bg-red-50 text-red-800 dark:bg-red-900/30 dark:text-red-300 border-red-200' | |
| : 'bg-gray-50 text-gray-800 dark:bg-gray-800 dark:text-gray-300 border-gray-200' | |
| )} | |
| > | |
| {val.toFixed(val === Math.round(val) ? 0 : 2)} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| {/* Step Details Tabs */} | |
| {step && ( | |
| <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col min-h-0"> | |
| <TabsList className="grid w-full grid-cols-5 mb-4"> | |
| <TabsTrigger value="overview">Overview</TabsTrigger> | |
| <TabsTrigger value="patch">Image Patch</TabsTrigger> | |
| <TabsTrigger value="multiplications">Multiplications</TabsTrigger> | |
| <TabsTrigger value="summation">Summation</TabsTrigger> | |
| <TabsTrigger value="padding">Padding</TabsTrigger> | |
| </TabsList> | |
| <TabsContent value="overview" className="flex-1 overflow-auto space-y-4"> | |
| <div className="grid gap-4 sm:grid-cols-2"> | |
| <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"> | |
| <h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Input Patch (Normalized 0-1)</h5> | |
| <div className="grid gap-0.5 text-xs font-mono" style={{ gridTemplateColumns: `repeat(${kernel.length}, 1fr)` }}> | |
| {step.inputPatch.map((row, i) => | |
| row.map((val, j) => ( | |
| <div | |
| key={`${i}-${j}`} | |
| className={cn( | |
| 'p-1.5 text-center rounded border', | |
| val > 0.5 ? 'bg-blue-50 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-700' | |
| )} | |
| > | |
| {val.toFixed(3)} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"> | |
| <h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Kernel Values</h5> | |
| <div className="grid gap-0.5 text-xs font-mono" style={{ gridTemplateColumns: `repeat(${kernel.length}, 1fr)` }}> | |
| {step.kernel.map((row, i) => | |
| row.map((val, j) => ( | |
| <div | |
| key={`${i}-${j}`} | |
| className={cn( | |
| 'p-1.5 text-center rounded border', | |
| val > 0 ? 'bg-green-50 text-green-800 dark:bg-green-900/30 dark:text-green-300' : | |
| val < 0 ? 'bg-red-50 text-red-800 dark:bg-red-900/30 dark:text-red-300' : | |
| 'bg-gray-50 text-gray-800 dark:bg-gray-800 dark:text-gray-300' | |
| )} | |
| > | |
| {val.toFixed(val === Math.round(val) ? 0 : 2)} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg sm:col-span-2"> | |
| <h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Computation Summary</h5> | |
| <div className="grid gap-2 sm:grid-cols-3 text-sm"> | |
| <div className="font-mono"> | |
| <span className="text-gray-500">Raw Sum: </span> | |
| <span className="text-blue-600 dark:text-blue-400">{step.sum.toFixed(6)}</span> | |
| </div> | |
| <div className="font-mono"> | |
| <span className="text-gray-500">Normalized: </span> | |
| <span className="text-green-600 dark:text-green-400">{step.normalizedValue.toFixed(6)}</span> | |
| </div> | |
| <div className="font-mono"> | |
| <span className="text-gray-500">Output (0-255): </span> | |
| <span className="text-purple-600 dark:text-purple-400">{Math.round(step.normalizedValue * 255)}</span> | |
| </div> | |
| <div className="font-mono sm:col-span-3"> | |
| <span className="text-gray-500">Progress: </span> | |
| <span>{Math.round(step.progress * 100)}%</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </TabsContent> | |
| <TabsContent value="patch" className="flex-1 overflow-auto"> | |
| <h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"> | |
| Image Patch Under Kernel at ({inspectedPosition?.x}, {inspectedPosition?.y}) - Channel: {channelLabels[activeChannel]} | |
| </h5> | |
| <div className="grid gap-1" style={{ gridTemplateColumns: `repeat(${kernel.length}, 1fr)` }}> | |
| {step.inputPatch.map((row, i) => | |
| row.map((val, j) => ( | |
| <div | |
| key={`${i}-${j}`} | |
| className={cn( | |
| 'p-3 text-center text-sm font-mono rounded border aspect-square flex items-center justify-center', | |
| val > 0.5 ? 'bg-blue-50 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border-blue-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 border-gray-200' | |
| )} | |
| > | |
| <div className="text-xs text-gray-500">{val.toFixed(3)}</div> | |
| <div className="text-lg font-bold">{val.toFixed(3)}</div> | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </TabsContent> | |
| <TabsContent value="multiplications" className="flex-1 overflow-auto"> | |
| <h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"> | |
| Element-wise Multiplication: Pixel × Kernel | |
| </h5> | |
| <div className="space-y-4"> | |
| {step.multiplications.map((row, i) => | |
| row.map((mult, j) => { | |
| const pixelVal = step.inputPatch[i]?.[j] ?? 0 | |
| const kernelVal = step.kernel[i]?.[j] ?? 0 | |
| return ( | |
| <div key={`${i}-${j}`} className="flex items-center justify-center gap-4 p-2 bg-gray-50 dark:bg-gray-800 rounded"> | |
| <div className="w-20 text-right text-sm font-mono text-gray-600 dark:text-gray-400"> | |
| {pixelVal.toFixed(3)} | |
| </div> | |
| <span className="text-gray-400">×</span> | |
| <div className="w-20 text-center text-sm font-mono" style={{ color: kernelVal > 0 ? '#16a34a' : kernelVal < 0 ? '#dc2626' : '#6b7280' }}> | |
| {kernelVal.toFixed(2)} | |
| </div> | |
| <span className="text-gray-400">=</span> | |
| <div className="w-24 text-left text-sm font-mono font-medium text-purple-600 dark:text-purple-400"> | |
| {mult.toFixed(6)} | |
| </div> | |
| </div> | |
| ) | |
| }) | |
| )} | |
| </div> | |
| </TabsContent> | |
| <TabsContent value="summation" className="flex-1 overflow-auto"> | |
| <h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"> | |
| Summation: Σ(Pixel × Kernel) | |
| </h5> | |
| <div className="space-y-2 font-mono text-sm"> | |
| {step.multiplications.map((row, i) => | |
| row.map((mult, j) => { | |
| const pixelVal = step.inputPatch[i]?.[j] ?? 0 | |
| const kernelVal = step.kernel[i]?.[j] ?? 0 | |
| return ( | |
| <div key={`${i}-${j}`} className="flex items-center gap-2 text-gray-700 dark:text-gray-300"> | |
| <span className="text-gray-400">({pixelVal.toFixed(3)} × {kernelVal.toFixed(2)})</span> | |
| <span className="text-purple-600 dark:text-purple-400">= {mult.toFixed(6)}</span> | |
| </div> | |
| ) | |
| }) | |
| )} | |
| <div className="border-t border-gray-300 dark:border-gray-600 pt-2 mt-2 flex items-center justify-between"> | |
| <span className="font-medium">Sum:</span> | |
| <span className="font-bold text-blue-600 dark:text-blue-400">{step.sum.toFixed(6)}</span> | |
| </div> | |
| <div className="flex items-center justify-between text-green-600 dark:text-green-400"> | |
| <span>After normalization (÷ {kernel.flat().reduce((a, b) => a + b, 0).toFixed(normalize ? 0 : 2)}):</span> | |
| <span className="font-bold">{step.normalizedValue.toFixed(6)}</span> | |
| </div> | |
| <div className="flex items-center justify-between text-purple-600 dark:text-purple-400 border-t border-gray-300 dark:border-gray-600 pt-2 mt-2"> | |
| <span className="font-medium">Final Output (0-255):</span> | |
| <span className="font-bold text-lg">{Math.round(step.normalizedValue * 255)}</span> | |
| </div> | |
| </div> | |
| </TabsContent> | |
| <TabsContent value="padding" className="flex-1 overflow-auto"> | |
| <h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"> | |
| Padded Image Preview ({padding} padding) - Kernel centered at ({inspectedPosition?.x}, {inspectedPosition?.y}) | |
| </h5> | |
| {paddedPreview && ( | |
| <div className="space-y-4"> | |
| <div className="text-sm text-gray-500 dark:text-gray-400"> | |
| Padded dimensions: {paddedPreview.width} × {paddedPreview.height} (original: {imageWidth} × {imageHeight}) | |
| <br /> | |
| Kernel center position in padded image: ({centerPx}, {centerPy}) | |
| </div> | |
| <div className="grid gap-0.5" style={{ gridTemplateColumns: `repeat(${paddedPreview.width}, 1fr)` }}> | |
| {Array.from({ length: paddedPreview.height }, (_, py) => | |
| Array.from({ length: paddedPreview.width }, (_, px) => { | |
| // Show only first 3 channels (RGB) as grayscale | |
| const idx = (py * paddedPreview.width + px) * 4 | |
| const r = paddedPreview.data[idx] ?? 0 | |
| const g = paddedPreview.data[idx + 1] ?? 0 | |
| const b = paddedPreview.data[idx + 2] ?? 0 | |
| const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b) | |
| const isCenter = px === centerPx && py === centerPy | |
| const isKernelArea = | |
| px >= centerPx - halfKernel && | |
| px < centerPx + halfKernel + 1 && | |
| py >= centerPy - halfKernel && | |
| py < centerPy + halfKernel + 1 | |
| return ( | |
| <div | |
| key={`${px}-${py}`} | |
| className={cn( | |
| 'aspect-square text-center text-xs font-mono rounded border flex items-center justify-center', | |
| isCenter | |
| ? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border-blue-400 font-bold' | |
| : isKernelArea | |
| ? 'bg-yellow-50 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 border-yellow-300' | |
| : px < halfKernel || px >= paddedPreview.width - halfKernel || py < halfKernel || py >= paddedPreview.height - halfKernel | |
| ? 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400 border-gray-200' | |
| : 'bg-white text-gray-800 dark:bg-gray-900 dark:text-gray-200 border-gray-200' | |
| )} | |
| title={`Padded[${px},${py}] = ${gray}`} | |
| > | |
| {gray} | |
| </div> | |
| ) | |
| }) | |
| )} | |
| </div> | |
| <div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400"> | |
| <span className="flex items-center gap-1"> | |
| <span className="w-3 h-3 rounded border bg-blue-100 dark:bg-blue-900/30" /> | |
| Kernel Center | |
| </span> | |
| <span className="flex items-center gap-1"> | |
| <span className="w-3 h-3 rounded border bg-yellow-50 dark:bg-yellow-900/30" /> | |
| Kernel Area | |
| </span> | |
| <span className="flex items-center gap-1"> | |
| <span className="w-3 h-3 rounded border bg-gray-100 dark:bg-gray-800" /> | |
| Padding Area | |
| </span> | |
| <span className="flex items-center gap-1"> | |
| <span className="w-3 h-3 rounded border bg-white dark:bg-gray-900" /> | |
| Original Image | |
| </span> | |
| </div> | |
| </div> | |
| )} | |
| </TabsContent> | |
| </Tabs> | |
| )} | |
| {!inspectedPosition && ( | |
| <div className="flex-1 flex items-center justify-center text-gray-400"> | |
| <p className="text-center">Click on the image to inspect a convolution step</p> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| ) | |
| } |