kernel-convolution-visualizer / src /components /canvas /StaticConvolutionView.tsx
mihirmithani
Phase 2
25e7739
Raw History Blame
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>
)
}