onnx-web-upscale / src /components /ControlPanel.vue
notaneimu's picture
match colors, exposure, grain
44f343d
<script setup lang="ts">
import { computed, ref } from "vue";
import type { InspectorControls, ModelGroup } from "../lib/types";
const props = defineProps<{
controls: InspectorControls;
onnxVersionText: string;
statusText: string;
statusTone: string;
modelStatusLabel: string;
modelStatusMeta: string;
imageStatusText: string;
predefinedModelGroups: ModelGroup[];
isRunning: boolean;
isModelLoading: boolean;
}>();
const emit = defineEmits<{
modelFile: [file: File | null];
imageFile: [file: File | null];
reload: [];
run: [];
cancel: [];
reset: [];
}>();
const modelInputRef = ref<HTMLInputElement | null>(null);
const imageInputRef = ref<HTMLInputElement | null>(null);
const controlsDisabled = computed(() => props.isRunning || props.isModelLoading);
function openModelPicker() {
modelInputRef.value?.click();
}
function openImagePicker() {
imageInputRef.value?.click();
}
function handleModelChange(event: Event) {
const input = event.target as HTMLInputElement;
emit("modelFile", input.files?.[0] || null);
input.value = "";
}
function handleImageChange(event: Event) {
const input = event.target as HTMLInputElement;
emit("imageFile", input.files?.[0] || null);
input.value = "";
}
</script>
<template>
<aside class="w-full lg:w-80 lg:shrink-0">
<section class="flex flex-col gap-3 rounded-xl border border-stone-300 bg-white p-4 shadow-sm">
<div class="flex flex-col gap-2">
<div class="flex items-start justify-between gap-3">
<div>
<h1 class="m-0 text-lg font-semibold text-stone-900">ONNX in Browser</h1>
<div class="mt-1 text-[11px] text-stone-500">{{ onnxVersionText }}</div>
</div>
<button
type="button"
class="shrink-0 text-[11px] font-normal text-stone-500 underline decoration-stone-300 underline-offset-2 transition hover:text-stone-700 disabled:cursor-not-allowed disabled:text-stone-300 disabled:decoration-stone-200"
:disabled="controlsDisabled"
@click="emit('reset')"
>
Reset
</button>
</div>
<div
:class="[
'w-full px-3 py-2 text-center text-xs font-medium select-none',
statusTone === 'success' && 'bg-emerald-100 text-emerald-900',
statusTone === 'error' && 'bg-rose-100 text-rose-900',
statusTone === 'busy' && 'bg-amber-100 text-amber-900',
!statusTone && 'bg-stone-100 text-stone-600',
]"
>
{{ statusText }}
</div>
<button
v-if="!isRunning"
type="button"
class="inline-flex w-full items-center justify-center rounded-md bg-stone-900 px-3.5 py-2.5 text-sm font-semibold text-white transition hover:bg-stone-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-500 disabled:cursor-not-allowed disabled:bg-stone-300 disabled:text-stone-500"
:disabled="isModelLoading"
@click="emit('run')"
>
Run
</button>
<button
v-else
type="button"
class="inline-flex w-full items-center justify-center rounded-md bg-stone-700 px-3.5 py-2.5 text-sm font-semibold text-white transition hover:bg-stone-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-500 disabled:cursor-not-allowed disabled:bg-stone-300 disabled:text-stone-500"
@click="emit('cancel')"
>
Cancel
</button>
</div>
<div class="flex flex-col gap-2.5">
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Model</span>
<div class="relative">
<select
v-model="props.controls.selectedPredefinedModelId"
:disabled="controlsDisabled"
class="w-full appearance-none rounded-lg border border-stone-300 bg-white px-3 py-2 pr-11 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
>
<option value="">-- Select a model or choose a file --</option>
<optgroup
v-for="group in predefinedModelGroups"
:key="group.label"
:label="group.label"
>
<option
v-for="option in group.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</optgroup>
</select>
<svg
class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-stone-500"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
</div>
</label>
<label class="flex flex-col gap-1">
<span class="flex w-full items-center justify-between gap-2 text-xs font-medium text-stone-600">
<span>Model file</span>
<a
class="font-normal text-stone-500 underline decoration-stone-300 underline-offset-2 transition hover:text-stone-700"
href="https://huggingface.co/spaces/notaneimu/pth2onnx-converter"
target="_blank"
rel="noopener noreferrer"
>
Convert from pth
</a>
</span>
<div class="flex flex-col gap-1.5">
<input
ref="modelInputRef"
class="sr-only"
type="file"
accept=".onnx,application/octet-stream"
:disabled="controlsDisabled"
@change="handleModelChange"
/>
<button
type="button"
class="inline-flex w-full items-center justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-stone-900 ring-1 ring-inset ring-stone-300 transition hover:bg-stone-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-400 disabled:cursor-not-allowed disabled:bg-stone-100 disabled:text-stone-400"
:disabled="controlsDisabled"
@click="openModelPicker"
>
Choose model file
</button>
</div>
</label>
<div class="flex min-w-0 flex-col gap-0.5 rounded-xl border border-stone-300 bg-stone-50 px-3 py-2.5">
<span class="min-w-0 break-words text-sm font-semibold text-stone-900">{{ modelStatusLabel }}</span>
<span class="min-w-0 break-words text-[11px] text-stone-600">{{ modelStatusMeta }}</span>
</div>
<div class="flex flex-col gap-2.5">
<label class="flex items-center justify-between rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
<span class="text-xs font-medium text-stone-600">Auto loading</span>
<input
v-model="props.controls.autoLoadModel"
type="checkbox"
:disabled="controlsDisabled"
class="h-4 w-4 accent-stone-700 disabled:opacity-60"
/>
</label>
<button
type="button"
class="inline-flex w-full items-center justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-stone-900 ring-1 ring-inset ring-stone-300 transition hover:bg-stone-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-400 disabled:cursor-not-allowed disabled:bg-stone-100 disabled:text-stone-400"
:disabled="controlsDisabled"
@click="emit('reload')"
>
Load model
</button>
</div>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Image</span>
<div class="flex flex-col gap-1.5">
<input
ref="imageInputRef"
class="sr-only"
type="file"
accept="image/*"
:disabled="controlsDisabled"
@change="handleImageChange"
/>
<button
type="button"
class="inline-flex w-full items-center justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-stone-900 ring-1 ring-inset ring-stone-300 transition hover:bg-stone-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-400 disabled:cursor-not-allowed disabled:bg-stone-100 disabled:text-stone-400"
:disabled="controlsDisabled"
@click="openImagePicker"
>
Choose image file
</button>
<p class="m-0 min-h-4 text-xs text-stone-500">{{ imageStatusText }}</p>
</div>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Execution provider</span>
<div class="relative">
<select
v-model="props.controls.provider"
:disabled="controlsDisabled"
class="w-full appearance-none rounded-lg border border-stone-300 bg-white px-3 py-2 pr-11 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
>
<option value="webgpu">WebGPU (default)</option>
<option value="wasm">WASM</option>
<option value="webgl">WebGL</option>
</select>
<svg
class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-stone-500"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
</div>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Tile size</span>
<input
v-model="props.controls.tileSize"
type="number"
min="1"
step="1"
placeholder="(empty disables tiling)"
:disabled="controlsDisabled"
class="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
<label class="flex items-center justify-between rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
<span class="text-xs font-medium text-stone-600">Tile blending</span>
<input
v-model="props.controls.tileBlendingEnabled"
type="checkbox"
:disabled="controlsDisabled"
class="h-4 w-4 accent-stone-700 disabled:opacity-60"
/>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Initial width</span>
<input
v-model="props.controls.width"
type="number"
min="1"
step="1"
placeholder="auto"
:disabled="controlsDisabled"
class="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Initial height</span>
<input
v-model="props.controls.height"
type="number"
min="1"
step="1"
placeholder="auto"
:disabled="controlsDisabled"
class="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Pre resize algorithm</span>
<div class="relative">
<select
v-model="props.controls.preResizeAlgorithm"
:disabled="controlsDisabled"
class="w-full appearance-none rounded-lg border border-stone-300 bg-white px-3 py-2 pr-11 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
>
<option value="lanczos">Lanczos</option>
<option value="area">Area</option>
<option value="bicubic">Bicubic</option>
<option value="nearest">Nearest neighbor</option>
</select>
<svg
class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-stone-500"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
</div>
<p class="m-0 text-xs italic leading-snug text-stone-500">Used when resizing the source image to the model input size.</p>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Final scale</span>
<input
v-model="props.controls.outputScale"
type="number"
min="1"
max="4"
step="0.1"
placeholder="off (keep model output)"
:disabled="controlsDisabled"
class="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
<p class="m-0 text-xs italic leading-snug text-stone-500">Optional downscale after inference. Uses the input size as 1x and caps at the model output.</p>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Post resize algorithm</span>
<div class="relative">
<select
v-model="props.controls.outputScaleAlgorithm"
:disabled="controlsDisabled"
class="w-full appearance-none rounded-lg border border-stone-300 bg-white px-3 py-2 pr-11 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
>
<option value="lanczos">Lanczos</option>
<option value="area">Area</option>
<option value="bicubic">Bicubic</option>
<option value="nearest">Nearest neighbor</option>
</select>
<svg
class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-stone-500"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
</div>
<p class="m-0 text-xs italic leading-snug text-stone-500">Used only for the optional output resize after inference.</p>
</label>
<label class="flex items-center justify-between rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
<span class="text-xs font-medium text-stone-600">Compare with classic upscale</span>
<input
v-model="props.controls.compareWithClassicUpscale"
type="checkbox"
:disabled="controlsDisabled"
class="h-4 w-4 accent-stone-700 disabled:opacity-60"
/>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Classic compare algorithm</span>
<div class="relative">
<select
v-model="props.controls.classicCompareAlgorithm"
:disabled="controlsDisabled || !props.controls.compareWithClassicUpscale"
class="w-full appearance-none rounded-lg border border-stone-300 bg-white px-3 py-2 pr-11 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
>
<option value="lanczos">Lanczos</option>
<option value="area">Area</option>
<option value="bicubic">Bicubic</option>
<option value="nearest">Nearest neighbor</option>
</select>
<svg
class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-stone-500"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
</div>
<p class="m-0 text-xs italic leading-snug text-stone-500">Builds a non-AI baseline upscaled to the same final resolution as the model result.</p>
</label>
<details class="group">
<summary class="flex cursor-pointer list-none items-center justify-between py-1 text-sm font-semibold text-stone-700">
<span>Additional params</span>
<span class="text-xs text-stone-500 group-open:hidden">▶</span>
<span class="hidden text-xs text-stone-500 group-open:inline">▼</span>
</summary>
<div class="flex flex-col gap-2.5 pt-2">
<label class="flex items-center justify-between rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
<span class="text-xs font-medium text-stone-600">Color correct final image</span>
<input
v-model="props.controls.colorCorrectionEnabled"
type="checkbox"
:disabled="controlsDisabled"
class="h-4 w-4 accent-stone-700 disabled:opacity-60"
/>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Color correction strength (%)</span>
<input
v-model="props.controls.colorCorrectionStrength"
type="number"
min="0"
max="100"
step="5"
placeholder="100"
:disabled="controlsDisabled || !props.controls.colorCorrectionEnabled"
class="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
<p class="m-0 text-xs italic leading-snug text-stone-500">Matches RGB color balance and black/white levels back toward the source image.</p>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Black/white clip (%)</span>
<input
v-model="props.controls.colorCorrectionClip"
type="number"
min="0"
max="10"
step="0.1"
placeholder="0.6"
:disabled="controlsDisabled || !props.controls.colorCorrectionEnabled"
class="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
<p class="m-0 text-xs italic leading-snug text-stone-500">Ignores tiny outliers when matching source black and white points.</p>
</label>
<label class="flex items-center justify-between rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
<span class="text-xs font-medium text-stone-600">Add film grain</span>
<input
v-model="props.controls.filmGrainEnabled"
type="checkbox"
:disabled="controlsDisabled"
class="h-4 w-4 accent-stone-700 disabled:opacity-60"
/>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Film grain amount (%)</span>
<input
v-model="props.controls.filmGrainAmount"
type="number"
min="0"
max="100"
step="1"
placeholder="6"
:disabled="controlsDisabled || !props.controls.filmGrainEnabled"
class="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Film grain size (px)</span>
<input
v-model="props.controls.filmGrainSize"
type="number"
min="0.5"
max="8"
step="0.1"
placeholder="1.2"
:disabled="controlsDisabled || !props.controls.filmGrainEnabled"
class="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
<label class="flex items-center justify-between rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
<span class="text-xs font-medium text-stone-600">Monochrome grain</span>
<input
v-model="props.controls.filmGrainMonochrome"
type="checkbox"
:disabled="controlsDisabled || !props.controls.filmGrainEnabled"
class="h-4 w-4 accent-stone-700 disabled:opacity-60"
/>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Seam blend width (input px)</span>
<input
v-model="props.controls.seamBlendWidth"
type="number"
min="0"
step="1"
placeholder="auto"
:disabled="controlsDisabled || !props.controls.tileBlendingEnabled"
class="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
<p class="m-0 text-xs italic leading-snug text-stone-500">Blank uses auto seam width. Set 0 to disable seam feather blending.</p>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Seam correction strength (%)</span>
<input
v-model="props.controls.seamCorrectionStrength"
type="number"
min="0"
max="300"
step="5"
placeholder="100"
:disabled="controlsDisabled || !props.controls.tileBlendingEnabled"
class="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
<p class="m-0 text-xs italic leading-snug text-stone-500">0 disables exposure matching. 100 is default, higher values apply stronger correction.</p>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Graph optimization</span>
<div class="relative">
<select
v-model="props.controls.optLevel"
:disabled="controlsDisabled"
class="w-full appearance-none rounded-lg border border-stone-300 bg-white px-3 py-2 pr-11 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
>
<option value="all">All (default)</option>
<option value="extended">Extended</option>
<option value="basic">Basic</option>
<option value="disabled">Disabled</option>
</select>
<svg
class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-stone-500"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
</div>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">WebGPU layout</span>
<div class="relative">
<select
v-model="props.controls.webgpuLayout"
:disabled="controlsDisabled"
class="w-full appearance-none rounded-lg border border-stone-300 bg-white px-3 py-2 pr-11 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
>
<option value="NCHW">NCHW (default)</option>
<option value="NHWC">NHWC</option>
</select>
<svg
class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-stone-500"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
</div>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">WebGPU validation</span>
<div class="relative">
<select
v-model="props.controls.webgpuValidation"
:disabled="controlsDisabled"
class="w-full appearance-none rounded-lg border border-stone-300 bg-white px-3 py-2 pr-11 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
>
<option value="basic">Basic (default)</option>
<option value="full">Full</option>
<option value="wgpuOnly">WebGPU-only</option>
<option value="disabled">Disabled</option>
</select>
<svg
class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-stone-500"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
</div>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Input layout</span>
<div class="relative">
<select
v-model="props.controls.layout"
:disabled="controlsDisabled"
class="w-full appearance-none rounded-lg border border-stone-300 bg-white px-3 py-2 pr-11 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
>
<option value="nchw">NCHW</option>
<option value="nhwc">NHWC</option>
</select>
<svg
class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-stone-500"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
</div>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Channel order</span>
<div class="relative">
<select
v-model="props.controls.channelOrder"
:disabled="controlsDisabled"
class="w-full appearance-none rounded-lg border border-stone-300 bg-white px-3 py-2 pr-11 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
>
<option value="rgb">RGB</option>
<option value="bgr">BGR</option>
</select>
<svg
class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-stone-500"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
</div>
</label>
<label class="flex items-center justify-between rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
<span class="text-xs font-medium text-stone-600">Scale pixels to 0..1</span>
<input
v-model="props.controls.normalize"
type="checkbox"
:disabled="controlsDisabled"
class="h-4 w-4 accent-stone-700 disabled:opacity-60"
/>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Mean (comma separated)</span>
<input
v-model="props.controls.mean"
type="text"
:disabled="controlsDisabled"
class="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-stone-600">Std (comma separated)</span>
<input
v-model="props.controls.std"
type="text"
:disabled="controlsDisabled"
class="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-800 outline-none transition focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
</div>
</details>
</div>
</section>
</aside>
</template>