Files
test-repo/docs/.vitepress/theme/components/appendix/image-gen-intro/CFGScaleDemo.vue
T

592 lines
14 KiB
Vue
Raw Normal View History

<!--
CFGScaleDemo.vue
CFG Scale 演示组件
用途
展示 Classifier-Free Guidance (CFG) Scale 如何影响生成结果帮助用户理解提示词遵循度的概念
交互功能
- CFG Scale 滑动调节
- 实时对比不同 CFG 值的效果
- 可视化 CFG 对图像的影响
-->
<template>
<div class="cfg-scale-demo">
<el-card shadow="never">
<template #header>
<div class="header-title">
<el-icon><ScaleToOriginal /></el-icon>
<span> CFG Scale提示词遵循度</span>
</div>
</template>
<div class="demo-content">
<!-- CFG 控制 -->
<div class="cfg-control">
<div class="cfg-slider-section">
<div class="cfg-label">
<span>CFG Scale</span>
<el-tag
type="primary"
effect="dark"
size="large"
>
{{ cfgScale }}
</el-tag>
</div>
<el-slider
v-model="cfgScale"
:min="1"
:max="15"
:step="0.5"
show-stops
:marks="{
1: '1\n(自由创作)',
7: '7\n(平衡)',
15: '15\n(严格遵循)'
}"
/>
</div>
<div class="cfg-presets">
<el-button
v-for="preset in cfgPresets"
:key="preset.value"
:type="cfgScale === preset.value ? 'primary' : ''"
size="small"
@click="cfgScale = preset.value"
>
{{ preset.label }}
</el-button>
</div>
</div>
<!-- 对比展示 -->
<div class="comparison-display">
<div class="comparison-item">
<div class="item-label">
<el-tag type="info">
无条件生成
</el-tag>
<span class="cfg-value">CFG = 1</span>
</div>
<canvas
ref="uncondCanvas"
width="200"
height="200"
class="comparison-canvas"
/>
<div class="item-desc">
忽略提示词自由发挥
</div>
</div>
<div class="comparison-arrow">
<el-icon :size="32">
<ArrowRight />
</el-icon>
<div class="guidance-formula">
<div class="formula">
输出 = 无条件 + CFG × (有条件 - 无条件)
</div>
<div class="formula-desc">
CFG 越大提示词影响越强
</div>
</div>
</div>
<div class="comparison-item">
<div class="item-label">
<el-tag type="success">
当前设置
</el-tag>
<span class="cfg-value">CFG = {{ cfgScale }}</span>
</div>
<canvas
ref="currentCanvas"
width="200"
height="200"
class="comparison-canvas"
/>
<div class="item-desc">
{{ getCfgDescription() }}
</div>
</div>
</div>
<!-- CFG 效果展示 -->
<div class="cfg-effects">
<div class="effects-title">
不同 CFG 值的效果对比
</div>
<div class="effects-grid">
<div
v-for="effect in cfgEffects"
:key="effect.value"
class="effect-item"
:class="{ active: cfgScale === effect.value }"
@click="cfgScale = effect.value"
>
<canvas
:ref="el => setEffectCanvas(el, effect.value)"
width="120"
height="120"
class="effect-canvas"
/>
<div class="effect-label">
CFG {{ effect.value }}
</div>
<div class="effect-desc">
{{ effect.desc }}
</div>
</div>
</div>
</div>
<!-- 推荐设置 -->
<div class="recommendations">
<div class="rec-title">
🎯 推荐设置
</div>
<div class="rec-grid">
<div class="rec-item">
<div class="rec-scenario">
创意探索
</div>
<div class="rec-value">
CFG 3-5
</div>
<div class="rec-desc">
AI 更多自由适合艺术探索
</div>
</div>
<div class="rec-item">
<div class="rec-scenario">
平衡模式
</div>
<div class="rec-value">
CFG 7-9
</div>
<div class="rec-desc">
大多数场景的最佳选择
</div>
</div>
<div class="rec-item">
<div class="rec-scenario">
精确控制
</div>
<div class="rec-value">
CFG 12-15
</div>
<div class="rec-desc">
严格遵循提示词但可能过饱和
</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>CFG Scale 原理</strong>
CFG (Classifier-Free Guidance) 控制生成结果对提示词的遵循程度值越高图像越符合提示词描述但过高会导致图像过饱和或失真
</p>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { ScaleToOriginal, ArrowRight } from '@element-plus/icons-vue'
const cfgScale = ref(7.5)
const uncondCanvas = ref(null)
const currentCanvas = ref(null)
const effectCanvases = ref({})
const cfgPresets = [
{ label: '自由 (3)', value: 3 },
{ label: '平衡 (7)', value: 7 },
{ label: '严格 (12)', value: 12 }
]
const cfgEffects = [
{ value: 1, desc: '完全自由' },
{ value: 3, desc: '创意优先' },
{ value: 5, desc: '轻度引导' },
{ value: 7, desc: '平衡' },
{ value: 9, desc: '严格遵循' },
{ value: 12, desc: '非常严格' },
{ value: 15, desc: '过度饱和' }
]
const setEffectCanvas = (el, value) => {
if (el) {
effectCanvases.value[value] = el
}
}
// 绘制目标图像
const drawTargetImage = (ctx, width, height, cfgValue) => {
// 基础图像(提示词:一只蓝色的猫)
const baseColor = { r: 100, g: 150, b: 200 }
// 根据 CFG 值调整颜色饱和度
const saturationBoost = Math.min((cfgValue - 1) / 7, 1.5)
const color = {
r: Math.min(255, baseColor.r + saturationBoost * 50),
g: Math.max(0, baseColor.g - saturationBoost * 30),
b: Math.min(255, baseColor.b + saturationBoost * 30)
}
// 背景
ctx.fillStyle = '#f0f0f0'
ctx.fillRect(0, 0, width, height)
// 猫的形状
ctx.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`
// 身体
ctx.beginPath()
ctx.ellipse(width / 2, height * 0.65, width * 0.25, height * 0.2, 0, 0, Math.PI * 2)
ctx.fill()
// 头
ctx.beginPath()
ctx.arc(width / 2, height * 0.4, width * 0.18, 0, Math.PI * 2)
ctx.fill()
// 耳朵
ctx.beginPath()
ctx.moveTo(width * 0.35, height * 0.3)
ctx.lineTo(width * 0.3, height * 0.15)
ctx.lineTo(width * 0.42, height * 0.25)
ctx.fill()
ctx.beginPath()
ctx.moveTo(width * 0.65, height * 0.3)
ctx.lineTo(width * 0.7, height * 0.15)
ctx.lineTo(width * 0.58, height * 0.25)
ctx.fill()
// 眼睛
ctx.fillStyle = '#fff'
ctx.beginPath()
ctx.ellipse(width * 0.45, height * 0.38, width * 0.05, height * 0.04, 0, 0, Math.PI * 2)
ctx.fill()
ctx.beginPath()
ctx.ellipse(width * 0.55, height * 0.38, width * 0.05, height * 0.04, 0, 0, Math.PI * 2)
ctx.fill()
// 瞳孔
ctx.fillStyle = '#000'
ctx.beginPath()
ctx.arc(width * 0.45, height * 0.38, width * 0.025, 0, Math.PI * 2)
ctx.fill()
ctx.beginPath()
ctx.arc(width * 0.55, height * 0.38, width * 0.025, 0, Math.PI * 2)
ctx.fill()
// 添加噪声(模拟低 CFG 的自由度)
if (cfgValue < 5) {
const imageData = ctx.getImageData(0, 0, width, height)
const noiseAmount = (5 - cfgValue) / 5 * 30
for (let i = 0; i < imageData.data.length; i += 4) {
const noise = (Math.random() - 0.5) * noiseAmount
imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + noise))
imageData.data[i + 1] = Math.max(0, Math.min(255, imageData.data[i + 1] + noise))
imageData.data[i + 2] = Math.max(0, Math.min(255, imageData.data[i + 2] + noise))
}
ctx.putImageData(imageData, 0, 0)
}
// 添加过饱和效果(高 CFG
if (cfgValue > 10) {
const imageData = ctx.getImageData(0, 0, width, height)
const oversaturation = (cfgValue - 10) / 5
for (let i = 0; i < imageData.data.length; i += 4) {
// 增强对比度
const avg = (imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]) / 3
imageData.data[i] = Math.min(255, imageData.data[i] + (imageData.data[i] - avg) * oversaturation)
imageData.data[i + 1] = Math.min(255, imageData.data[i + 1] + (imageData.data[i + 1] - avg) * oversaturation)
imageData.data[i + 2] = Math.min(255, imageData.data[i + 2] + (imageData.data[i + 2] - avg) * oversaturation)
}
ctx.putImageData(imageData, 0, 0)
}
}
const getCfgDescription = () => {
if (cfgScale.value <= 3) return '自由创作,AI 有更多发挥空间'
if (cfgScale.value <= 7) return '平衡模式,兼顾创意和遵循'
if (cfgScale.value <= 10) return '严格遵循提示词'
return '过度控制,可能导致图像失真'
}
const updateDisplay = () => {
// 更新无条件生成
if (uncondCanvas.value) {
const ctx = uncondCanvas.value.getContext('2d')
drawTargetImage(ctx, 200, 200, 1)
}
// 更新当前设置
if (currentCanvas.value) {
const ctx = currentCanvas.value.getContext('2d')
drawTargetImage(ctx, 200, 200, cfgScale.value)
}
// 更新效果网格
cfgEffects.forEach(effect => {
const canvas = effectCanvases.value[effect.value]
if (canvas) {
const ctx = canvas.getContext('2d')
drawTargetImage(ctx, 120, 120, effect.value)
}
})
}
onMounted(updateDisplay)
watch(cfgScale, updateDisplay)
</script>
<style scoped>
.cfg-scale-demo {
margin: 0.5rem 0;
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.demo-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.cfg-control {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 20px;
}
.cfg-slider-section {
margin-bottom: 16px;
}
.cfg-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.cfg-label span {
font-weight: 500;
}
.cfg-presets {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.comparison-display {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
flex-wrap: wrap;
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.comparison-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.item-label {
display: flex;
align-items: center;
gap: 8px;
}
.cfg-value {
font-weight: 600;
color: var(--vp-c-text-2);
}
.comparison-canvas {
width: 180px;
height: 180px;
background: var(--vp-c-bg);
border-radius: 6px;
border: 2px solid var(--vp-c-divider);
}
.item-desc {
font-size: 0.8rem;
color: var(--vp-c-text-3);
text-align: center;
}
.comparison-arrow {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: var(--vp-c-brand);
}
.guidance-formula {
text-align: center;
max-width: 200px;
}
.formula {
font-size: 0.75rem;
font-family: var(--vp-font-family-mono);
background: var(--vp-c-bg);
padding: 8px;
border-radius: 4px;
margin-bottom: 4px;
}
.formula-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.cfg-effects {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 20px;
}
.effects-title {
font-weight: 500;
margin-bottom: 16px;
text-align: center;
}
.effects-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 16px;
}
.effect-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 12px;
background: var(--vp-c-bg);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.effect-item:hover {
border-color: var(--vp-c-brand);
}
.effect-item.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-mute);
}
.effect-canvas {
width: 100px;
height: 100px;
border-radius: 6px;
}
.effect-label {
font-weight: 500;
font-size: 0.875rem;
}
.effect-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
text-align: center;
}
.recommendations {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 20px;
}
.rec-title {
font-weight: 500;
margin-bottom: 16px;
text-align: center;
}
.rec-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.rec-item {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 16px;
text-align: center;
}
.rec-scenario {
font-weight: 500;
margin-bottom: 8px;
}
.rec-value {
font-size: 1.25rem;
font-weight: 600;
color: var(--vp-c-brand);
margin-bottom: 8px;
}
.rec-desc {
font-size: 0.8rem;
color: var(--vp-c-text-3);
}
.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) {
.comparison-display {
flex-direction: column;
}
.comparison-arrow {
transform: rotate(90deg);
margin: 8px 0;
}
}
</style>