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

728 lines
16 KiB
Vue
Raw Normal View History

<!--
ControlNetDemo.vue
ControlNet 控制网络演示组件
用途
展示 ControlNet 如何精确控制图像生成包括姿态边缘深度等控制方式
交互功能
- 不同控制类型切换
- 控制强度调节
- 可视化控制信号
- 对比有无 ControlNet 的效果
-->
<template>
<div class="controlnet-demo">
<el-card shadow="never">
<template #header>
<div class="header-title">
<el-icon><Pointer /></el-icon>
<span>🎮 ControlNet精确控制</span>
</div>
</template>
<div class="demo-content">
<!-- 控制类型选择 -->
<div class="control-types">
<div
v-for="control in controlTypes"
:key="control.id"
class="control-card"
:class="{ active: selectedControl === control.id }"
@click="selectedControl = control.id"
>
<div class="control-icon">{{ control.icon }}</div>
<div class="control-name">{{ control.name }}</div>
<div class="control-desc">{{ control.description }}</div>
</div>
</div>
<!-- 可视化流程 -->
<div class="workflow-viz">
<div class="workflow-step">
<div class="step-label">输入图像</div>
<canvas
ref="inputCanvas"
width="200"
height="200"
class="workflow-canvas"
/>
</div>
<div class="workflow-arrow">
<el-icon :size="24"><ArrowRight /></el-icon>
<div class="arrow-label">提取</div>
</div>
<div class="workflow-step">
<div class="step-label">控制信号</div>
<canvas
ref="controlCanvas"
width="200"
height="200"
class="workflow-canvas control-signal"
/>
</div>
<div class="workflow-arrow">
<el-icon :size="24"><ArrowRight /></el-icon>
<div class="arrow-label">+ 提示词</div>
</div>
<div class="workflow-step">
<div class="step-label">生成结果</div>
<canvas
ref="outputCanvas"
width="200"
height="200"
class="workflow-canvas"
/>
</div>
</div>
<!-- 控制强度 -->
<div class="strength-control">
<div class="strength-header">
<span>控制强度 (Control Strength)</span>
<el-tag type="primary" effect="dark">{{ controlStrength }}</el-tag>
</div>
<el-slider
v-model="controlStrength"
:min="0"
:max="2"
:step="0.1"
show-stops
:marks="{
0: '无控制',
1: '平衡',
2: '强控制'
}"
/>
<div class="strength-desc">
{{ getStrengthDescription() }}
</div>
</div>
<!-- 对比展示 -->
<div class="comparison-section">
<div class="comparison-title">对比有无 ControlNet</div>
<div class="comparison-grid">
<div class="comparison-item">
<div class="item-label">
<el-tag type="info">仅文本生成</el-tag>
</div>
<canvas
ref="textOnlyCanvas"
width="180"
height="180"
class="comparison-canvas"
/>
<div class="item-desc">姿态随机不可控</div>
</div>
<div class="comparison-item">
<div class="item-label">
<el-tag type="success">ControlNet 控制</el-tag>
</div>
<canvas
ref="controlNetCanvas"
width="180"
height="180"
class="comparison-canvas"
/>
<div class="item-desc">姿态精确匹配输入</div>
</div>
</div>
</div>
<!-- 应用场景 -->
<div class="use-cases">
<div class="use-cases-title">🎯 典型应用场景</div>
<div class="use-cases-grid">
<div
v-for="useCase in useCases"
:key="useCase.title"
class="use-case-card"
>
<div class="use-case-icon">{{ useCase.icon }}</div>
<div class="use-case-title">{{ useCase.title }}</div>
<div class="use-case-desc">{{ useCase.description }}</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>ControlNet 原理</strong>
ControlNet 是一个附加在扩散模型上的神经网络它学习从输入图像中提取特定的结构信息如姿态边缘并用这些信息引导生成过程实现精确控制
</p>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { Pointer, ArrowRight } from '@element-plus/icons-vue'
const selectedControl = ref('pose')
const controlStrength = ref(1.0)
const inputCanvas = ref(null)
const controlCanvas = ref(null)
const outputCanvas = ref(null)
const textOnlyCanvas = ref(null)
const controlNetCanvas = ref(null)
const controlTypes = [
{
id: 'pose',
name: 'OpenPose',
icon: '🕺',
description: '姿态控制,提取人体骨骼关键点'
},
{
id: 'canny',
name: 'Canny',
icon: '✏️',
description: '边缘检测,提取图像轮廓'
},
{
id: 'depth',
name: 'Depth',
icon: '📐',
description: '深度估计,控制空间结构'
},
{
id: 'scribble',
name: 'Scribble',
icon: '🎨',
description: '涂鸦控制,手绘引导生成'
},
{
id: 'segmentation',
name: 'Segmentation',
icon: '🧩',
description: '语义分割,控制物体布局'
}
]
const useCases = [
{
icon: '👗',
title: '虚拟试衣',
description: '保持人物姿态,更换服装款式'
},
{
icon: '🏠',
title: '室内设计',
description: '基于房间结构,生成不同装修风格'
},
{
icon: '🎭',
title: '角色一致性',
description: '保持角色姿态,改变服装或场景'
},
{
icon: '📐',
title: '产品展示',
description: '固定产品角度,变换背景和光照'
}
]
const getStrengthDescription = () => {
if (controlStrength.value < 0.5) {
return '控制较弱,生成结果更自由,但可能偏离预期结构'
} else if (controlStrength.value < 1.5) {
return '平衡模式,在遵循控制和保持创意之间取得平衡'
} else {
return '强控制模式,严格遵循输入结构,但可能牺牲一些图像质量'
}
}
// 绘制姿态骨架
const drawPoseSkeleton = (ctx, width, height, isControl = false) => {
ctx.clearRect(0, 0, width, height)
if (isControl) {
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, width, height)
ctx.strokeStyle = '#0f0'
ctx.fillStyle = '#0f0'
} else {
ctx.fillStyle = '#f0f0f0'
ctx.fillRect(0, 0, width, height)
ctx.strokeStyle = '#333'
ctx.fillStyle = '#333'
}
ctx.lineWidth = isControl ? 3 : 2
// 头部
ctx.beginPath()
ctx.arc(width * 0.5, height * 0.15, width * 0.08, 0, Math.PI * 2)
ctx.stroke()
// 身体
ctx.beginPath()
ctx.moveTo(width * 0.5, height * 0.23)
ctx.lineTo(width * 0.5, height * 0.5)
ctx.stroke()
// 左臂
ctx.beginPath()
ctx.moveTo(width * 0.5, height * 0.3)
ctx.lineTo(width * 0.25, height * 0.4)
ctx.stroke()
// 右臂
ctx.beginPath()
ctx.moveTo(width * 0.5, height * 0.3)
ctx.lineTo(width * 0.75, height * 0.35)
ctx.stroke()
// 左腿
ctx.beginPath()
ctx.moveTo(width * 0.5, height * 0.5)
ctx.lineTo(width * 0.35, height * 0.8)
ctx.stroke()
// 右腿
ctx.beginPath()
ctx.moveTo(width * 0.5, height * 0.5)
ctx.lineTo(width * 0.65, height * 0.75)
ctx.stroke()
// 关节点
const joints = [
[0.5, 0.23], [0.5, 0.3], [0.5, 0.5],
[0.25, 0.4], [0.75, 0.35],
[0.35, 0.8], [0.65, 0.75]
]
joints.forEach(([x, y]) => {
ctx.beginPath()
ctx.arc(width * x, height * y, isControl ? 4 : 3, 0, Math.PI * 2)
ctx.fill()
})
}
// 绘制边缘检测
const drawCannyEdges = (ctx, width, height) => {
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, width, height)
ctx.strokeStyle = '#fff'
ctx.lineWidth = 2
// 绘制简单的几何形状边缘
ctx.beginPath()
ctx.moveTo(width * 0.2, height * 0.2)
ctx.lineTo(width * 0.8, height * 0.2)
ctx.lineTo(width * 0.8, height * 0.8)
ctx.lineTo(width * 0.2, height * 0.8)
ctx.closePath()
ctx.stroke()
// 内部细节
ctx.beginPath()
ctx.arc(width * 0.5, height * 0.5, width * 0.2, 0, Math.PI * 2)
ctx.stroke()
}
// 绘制深度图
const drawDepthMap = (ctx, width, height) => {
// 创建深度渐变
const gradient = ctx.createRadialGradient(
width * 0.5, height * 0.5, 0,
width * 0.5, height * 0.5, width * 0.5
)
gradient.addColorStop(0, '#fff')
gradient.addColorStop(0.5, '#888')
gradient.addColorStop(1, '#000')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, width, height)
}
// 绘制涂鸦
const drawScribble = (ctx, width, height) => {
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, width, height)
ctx.strokeStyle = '#000'
ctx.lineWidth = 3
// 随机涂鸦线条
ctx.beginPath()
for (let i = 0; i < 5; i++) {
ctx.moveTo(Math.random() * width, Math.random() * height)
ctx.lineTo(Math.random() * width, Math.random() * height)
}
ctx.stroke()
}
// 绘制语义分割
const drawSegmentation = (ctx, width, height) => {
// 天空
ctx.fillStyle = '#87CEEB'
ctx.fillRect(0, 0, width, height * 0.4)
// 地面
ctx.fillStyle = '#8B4513'
ctx.fillRect(0, height * 0.6, width, height * 0.4)
// 建筑
ctx.fillStyle = '#808080'
ctx.fillRect(width * 0.3, height * 0.2, width * 0.4, height * 0.5)
// 树木
ctx.fillStyle = '#228B22'
ctx.beginPath()
ctx.arc(width * 0.15, height * 0.5, width * 0.1, 0, Math.PI * 2)
ctx.fill()
ctx.beginPath()
ctx.arc(width * 0.85, height * 0.5, width * 0.1, 0, Math.PI * 2)
ctx.fill()
}
// 绘制生成结果
const drawOutput = (ctx, width, height, withControl = true) => {
ctx.fillStyle = '#f0f0f0'
ctx.fillRect(0, 0, width, height)
// 根据控制类型绘制不同的输出
if (selectedControl.value === 'pose') {
// 绘制一个人物,姿态与骨架匹配
const strength = withControl ? controlStrength.value : 0.3
// 头部
ctx.fillStyle = '#fdbcb4'
ctx.beginPath()
ctx.arc(width * 0.5, height * 0.15, width * 0.08 * (0.5 + strength * 0.5), 0, Math.PI * 2)
ctx.fill()
// 身体
ctx.fillStyle = '#4a90e2'
ctx.fillRect(
width * (0.5 - 0.08 * strength),
height * 0.23,
width * 0.16 * strength,
height * 0.27
)
// 简单的肢体
ctx.strokeStyle = '#fdbcb4'
ctx.lineWidth = 8 * strength
// 左臂
ctx.beginPath()
ctx.moveTo(width * 0.5, height * 0.3)
ctx.lineTo(width * (0.25 + (0.5 - strength) * 0.3), height * 0.4)
ctx.stroke()
// 右臂
ctx.beginPath()
ctx.moveTo(width * 0.5, height * 0.3)
ctx.lineTo(width * (0.75 - (0.5 - strength) * 0.3), height * 0.35)
ctx.stroke()
} else if (selectedControl.value === 'canny') {
// 边缘控制效果
const strength = withControl ? controlStrength.value : 0.3
ctx.strokeStyle = '#333'
ctx.lineWidth = 2
ctx.beginPath()
ctx.moveTo(width * 0.2, height * 0.2)
ctx.lineTo(width * (0.8 - (1 - strength) * 0.3), height * 0.2)
ctx.lineTo(width * 0.8, height * (0.8 - (1 - strength) * 0.2))
ctx.lineTo(width * (0.2 + (1 - strength) * 0.3), height * 0.8)
ctx.closePath()
ctx.stroke()
}
}
const updateDisplay = () => {
// 输入图像
if (inputCanvas.value) {
const ctx = inputCanvas.value.getContext('2d')
drawPoseSkeleton(ctx, 200, 200, false)
}
// 控制信号
if (controlCanvas.value) {
const ctx = controlCanvas.value.getContext('2d')
switch (selectedControl.value) {
case 'pose':
drawPoseSkeleton(ctx, 200, 200, true)
break
case 'canny':
drawCannyEdges(ctx, 200, 200)
break
case 'depth':
drawDepthMap(ctx, 200, 200)
break
case 'scribble':
drawScribble(ctx, 200, 200)
break
case 'segmentation':
drawSegmentation(ctx, 200, 200)
break
}
}
// 输出
if (outputCanvas.value) {
const ctx = outputCanvas.value.getContext('2d')
drawOutput(ctx, 200, 200, true)
}
// 对比
if (textOnlyCanvas.value) {
const ctx = textOnlyCanvas.value.getContext('2d')
drawOutput(ctx, 180, 180, false)
}
if (controlNetCanvas.value) {
const ctx = controlNetCanvas.value.getContext('2d')
drawOutput(ctx, 180, 180, true)
}
}
onMounted(updateDisplay)
watch([selectedControl, controlStrength], updateDisplay)
</script>
<style scoped>
.controlnet-demo {
margin: 1rem 0;
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.demo-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.control-types {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.control-card {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.control-card:hover {
border-color: var(--vp-c-brand);
}
.control-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-mute);
}
.control-icon {
font-size: 2rem;
margin-bottom: 8px;
}
.control-name {
font-weight: 600;
margin-bottom: 4px;
}
.control-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.workflow-viz {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.workflow-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.step-label {
font-weight: 500;
color: var(--vp-c-text-2);
}
.workflow-canvas {
width: 160px;
height: 160px;
background: var(--vp-c-bg);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
}
.workflow-canvas.control-signal {
background: #000;
}
.workflow-arrow {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
color: var(--vp-c-text-3);
}
.arrow-label {
font-size: 0.75rem;
}
.strength-control {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.strength-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.strength-desc {
margin-top: 12px;
font-size: 0.875rem;
color: var(--vp-c-text-2);
text-align: center;
}
.comparison-section {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.comparison-title {
font-weight: 500;
margin-bottom: 16px;
text-align: center;
}
.comparison-grid {
display: flex;
justify-content: center;
gap: 32px;
flex-wrap: wrap;
}
.comparison-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.item-label {
font-weight: 500;
}
.comparison-canvas {
width: 150px;
height: 150px;
background: var(--vp-c-bg);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
}
.item-desc {
font-size: 0.8rem;
color: var(--vp-c-text-3);
}
.use-cases {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.use-cases-title {
font-weight: 500;
margin-bottom: 16px;
text-align: center;
}
.use-cases-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.use-case-card {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
text-align: center;
}
.use-case-icon {
font-size: 2rem;
margin-bottom: 8px;
}
.use-case-title {
font-weight: 600;
margin-bottom: 4px;
}
.use-case-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) {
.workflow-viz {
flex-direction: column;
}
.workflow-arrow {
transform: rotate(90deg);
margin: 8px 0;
}
}
</style>