Files
test-repo/docs/.vitepress/theme/components/appendix/image-gen-intro/VaeEncoderDemo.vue
T
sanbuphy 0eba9e87e9 fix(eslint): reduce warnings in GitHub Actions deployment
- Disable formatting rules (handled by Prettier)
- Relaxed strict Vue/JS rules for demo code compatibility
- Fix syntax errors in ApiPlayground and VoiceCloningDemo
- Fix duplicate else-if condition in ApiPlayground
- Fix Promise executor async pattern in AutoregressiveAudioDemo
- Add TypeScript file support to ESLint config

Warnings reduced from 295 to 251 problems.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-18 17:38:10 +08:00

386 lines
8.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
VaeEncoderDemo.vue
VAE 编解码器演示组件
用途
展示 VAE 如何将高分辨率图像压缩到潜空间以及如何从潜空间还原图像
帮助用户理解 Latent Space 的概念
交互功能
- 编码/解码模式切换
- 可视化压缩过程
- 展示潜空间表示
- 对比原始图像和重建图像
-->
<template>
<div class="vae-demo">
<el-card shadow="never">
<template #header>
<div class="header-controls">
<span class="title">🔍 VAE 编解码器</span>
<el-radio-group
v-model="mode"
size="small"
>
<el-radio-button label="encode">
<el-icon><ArrowRight /></el-icon> 编码 (Encode)
</el-radio-button>
<el-radio-button label="decode">
<el-icon><ArrowLeft /></el-icon> 解码 (Decode)
</el-radio-button>
</el-radio-group>
</div>
</template>
<div class="vae-flow">
<!-- 输入侧 -->
<div class="stage">
<div class="stage-label">
{{ mode === 'encode' ? '原始图像' : '潜空间表示' }}
</div>
<div class="stage-visual">
<canvas
ref="inputCanvas"
width="200"
height="200"
class="stage-canvas"
/>
</div>
<div class="stage-info">
<el-tag
size="small"
type="info"
>
{{ mode === 'encode' ? '512 × 512 × 3 = 786,432 数值' : '64 × 64 × 4 = 16,384 数值' }}
</el-tag>
</div>
</div>
<!-- 箭头 -->
<div class="arrow-stage">
<el-icon
class="flow-arrow"
:size="32"
>
<component :is="mode === 'encode' ? ArrowRight : ArrowLeft" />
</el-icon>
<div class="compression-ratio">
<el-tag
type="success"
effect="dark"
>
压缩率: 48×
</el-tag>
</div>
</div>
<!-- 输出侧 -->
<div class="stage">
<div class="stage-label">
{{ mode === 'encode' ? '潜空间表示' : '重建图像' }}
</div>
<div class="stage-visual">
<canvas
ref="outputCanvas"
width="200"
height="200"
class="stage-canvas"
/>
</div>
<div class="stage-info">
<el-tag
size="small"
type="info"
>
{{ mode === 'encode' ? '64 × 64 × 4 = 16,384 数值' : '512 × 512 × 3 = 786,432 数值' }}
</el-tag>
</div>
</div>
</div>
<!-- 潜空间可视化 -->
<div
v-if="mode === 'encode'"
class="latent-viz"
>
<div class="latent-title">
潜空间特征图 (4 个通道)
</div>
<div class="latent-channels">
<div
v-for="i in 4"
:key="i"
class="channel-box"
:style="getChannelStyle(i)"
>
<span class="channel-label">Channel {{ i }}</span>
</div>
</div>
</div>
<div class="explanation">
<el-alert
:title="mode === 'encode' ? '编码:图像 → 潜空间' : '解码:潜空间 → 图像'"
:type="mode === 'encode' ? 'warning' : 'success'"
:description="mode === 'encode'
? 'VAE Encoder 将高维图像压缩到低维潜空间,保留关键语义信息,丢弃冗余细节。这就像把一本厚书浓缩成大纲。'
: 'VAE Decoder 从潜空间表示中重建图像。虽然无法完美还原每一个细节,但足以生成高质量的图像。这就像根据大纲重写一本书。'"
show-icon
:closable="false"
/>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>为什么需要 VAE</strong>
直接在像素空间训练扩散模型计算量太大通过 VAE 压缩到潜空间计算效率提升约 48 同时保持图像质量
</p>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { ArrowRight, ArrowLeft } from '@element-plus/icons-vue'
const mode = ref('encode')
const inputCanvas = ref(null)
const outputCanvas = ref(null)
// 绘制示例图像
const drawSampleImage = (canvas) => {
const ctx = canvas.getContext('2d')
const w = canvas.width
const h = canvas.height
// 绘制一个风景图
// 天空
const skyGradient = ctx.createLinearGradient(0, 0, 0, h * 0.6)
skyGradient.addColorStop(0, '#87CEEB')
skyGradient.addColorStop(1, '#E0F7FA')
ctx.fillStyle = skyGradient
ctx.fillRect(0, 0, w, h * 0.6)
// 太阳
ctx.beginPath()
ctx.arc(w * 0.75, h * 0.2, w * 0.1, 0, Math.PI * 2)
ctx.fillStyle = '#FFD700'
ctx.fill()
// 山
ctx.fillStyle = '#4CAF50'
ctx.beginPath()
ctx.moveTo(0, h * 0.6)
ctx.lineTo(w * 0.3, h * 0.3)
ctx.lineTo(w * 0.7, h * 0.5)
ctx.lineTo(w, h * 0.4)
ctx.lineTo(w, h)
ctx.lineTo(0, h)
ctx.fill()
// 草地
ctx.fillStyle = '#8BC34A'
ctx.fillRect(0, h * 0.6, w, h * 0.4)
// 花朵
const colors = ['#FF69B4', '#FFD700', '#FF6347', '#9370DB']
for (let i = 0; i < 8; i++) {
const x = (i * w * 0.12) + 20
const y = h * 0.75 + (i % 2) * 30
ctx.fillStyle = colors[i % colors.length]
ctx.beginPath()
ctx.arc(x, y, 8, 0, Math.PI * 2)
ctx.fill()
}
}
// 绘制潜空间表示(抽象可视化)
const drawLatentRepresentation = (canvas) => {
const ctx = canvas.getContext('2d')
const w = canvas.width
const h = canvas.height
// 生成噪声纹理表示潜空间
const imageData = ctx.createImageData(w, h)
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const i = (y * w + x) * 4
// 使用柏林噪声模拟潜空间特征
const value = Math.sin(x * 0.1) * Math.cos(y * 0.1) * 50 + 128
imageData.data[i] = value + Math.random() * 30
imageData.data[i + 1] = value + Math.random() * 30
imageData.data[i + 2] = value + Math.random() * 30
imageData.data[i + 3] = 255
}
}
ctx.putImageData(imageData, 0, 0)
}
// 获取通道样式
const getChannelStyle = (channel) => {
const hues = [200, 120, 30, 280]
return {
background: `linear-gradient(135deg, hsl(${hues[channel - 1]}, 70%, 50%), hsl(${hues[channel - 1]}, 70%, 30%))`
}
}
// 更新显示
const updateDisplay = () => {
if (!inputCanvas.value || !outputCanvas.value) return
if (mode.value === 'encode') {
drawSampleImage(inputCanvas.value)
drawLatentRepresentation(outputCanvas.value)
} else {
drawLatentRepresentation(inputCanvas.value)
drawSampleImage(outputCanvas.value)
}
}
onMounted(updateDisplay)
watch(mode, updateDisplay)
</script>
<style scoped>
.vae-demo {
margin: 0.5rem 0;
}
.header-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-weight: 600;
}
.vae-flow {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
padding: 24px 0;
flex-wrap: wrap;
}
.stage {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.stage-label {
font-weight: 500;
color: var(--vp-c-text-2);
}
.stage-visual {
width: 200px;
height: 200px;
background: var(--vp-c-bg-mute);
border-radius: 6px;
overflow: hidden;
border: 2px solid var(--vp-c-divider);
}
.stage-canvas {
width: 100%;
height: 100%;
object-fit: cover;
}
.stage-info {
font-size: 0.75rem;
}
.arrow-stage {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.flow-arrow {
color: var(--vp-c-brand);
}
.compression-ratio {
font-size: 0.8rem;
}
.latent-viz {
margin-top: 16px;
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.latent-title {
font-weight: 500;
margin-bottom: 12px;
text-align: center;
}
.latent-channels {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.channel-box {
aspect-ratio: 1;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.channel-label {
position: absolute;
bottom: 4px;
left: 4px;
font-size: 0.7rem;
color: white;
background: rgba(0, 0, 0, 0.5);
padding: 2px 6px;
border-radius: 3px;
}
.explanation {
margin-top: 16px;
}
.info-box {
margin-top: 16px;
padding: 12px;
background: var(--vp-c-bg-mute);
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.6;
}
.icon {
font-size: 1.2em;
}
@media (max-width: 640px) {
.vae-flow {
flex-direction: column;
}
.arrow-stage {
transform: rotate(90deg);
}
.latent-channels {
grid-template-columns: repeat(2, 1fr);
}
}
</style>