自动绘图AI:程序如何画出动漫美少女
说明
本文发布较早,了解最新动态,请查看 GitHub 项目。(2024 年 4 月 注)
准备
全新的图形引擎与 AI 算法,高效流畅地绘出任何一副美丽的图像。
IDE:VisualStudio
Language:VB.NET / C#
Graphics:AutoPaint.NET
第一节 背景
背景是图画里衬托主体事物的景象。
图1-1 先画个蓝蓝的天空
蓝天、白云和大地,程序最擅长这种色调单一的涂抹了。
第二节 轮廓
轮廓是物体的外周或图形的外框。
图2-2 勾勒人物和衣饰轮廓
现在 AI 要控制笔触大小和颜色,让图像的主体显现出来。
第三节 光影
光影是物体在光的照射下呈现出明与暗的关系。
图3-1 光影提升画面质感
AI 可不懂什么是光影,在上一步的基础上优化细节即可。
第四节 润色
润色是增加物体本身及其周围的色彩。
图4-1 画面润色
这是关键一步,AI需要将丢失的颜色细节补缺回来。
第五节 成型
大功告成!前面所有的步骤都是为这一步铺垫。
图5-1 人物已经栩栩如生啦
事实上 AI 只进行这一步也可以画出完整的图像,但没有过渡会显得生硬。
第六节 算法
算法思路很简单,计算画笔轨迹后一遍遍重绘,感觉上是人类画手的效果。
不再是二值化
因为现在要绘制全彩图像,将图像划分为只有黑和白的效果已经没有什么意义,二值化不再适用
适用的方法是将 RGB 颜色空间划分为若干个颜色子空间,然后逐个处理一幅图像中属于某个子空间的区域
自动循迹
循迹算法没有大的变动,仍是早前博客里贴出的代码
彩色图像线条较短,可以不再计算点周围的权值用来中断轨迹
重绘
程序先选择笔触较大、颜色淡的画笔绘制一遍,然后在这基础上逐步减小笔触并加深色彩
直接按照标准笔触可以一遍成型,但会显得突兀和生硬,毕竟这个AI不是真的在思考如何画一幅图像
Imports System.Numerics ''' <summary> ''' 表示自动循迹并生成绘制序列的AI ''' </summary> Public Class SequenceAI ''' <summary> ''' 线条序列List ''' </summary> ''' <returns></returns> Public Property Sequences As List(Of PointSequence) ''' <summary> ''' 扫描方式 ''' </summary> Public Property ScanMode As ScanMode = ScanMode.Rect Dim xArray() As Integer = {-1, 0, 1, 1, 1, 0, -1, -1} Dim yArray() As Integer = {-1, -1, -1, 0, 1, 1, 1, 0} Dim NewStart As Boolean ''' <summary> ''' 创建并初始化一个可自动生成绘制序列AI的实例 ''' </summary> Public Sub New(BolArr(,) As Integer) Sequences = New List(Of PointSequence) CalculateSequence(BolArr) For Each SubItem In Sequences SubItem.CalcSize() Next End Sub ''' <summary> ''' 新增一个序列 ''' </summary> Private Sub CreateNewSequence() Sequences.Add(New PointSequence) End Sub ''' <summary> ''' 在序列List末尾项新增一个点 ''' </summary> Private Sub AddPoint(point As Vector2) Sequences.Last.Points.Add(point) End Sub ''' <summary> ''' 计算序列 ''' </summary> Private Sub CalculateSequence(BolArr(,) As Integer) If ScanMode = ScanMode.Rect Then ScanRect(BolArr) Else ScanCircle(BolArr) End If End Sub ''' <summary> ''' 圆形扫描 ''' </summary> ''' <param name="BolArr"></param> Private Sub ScanCircle(BolArr(,) As Integer) Dim xCount As Integer = BolArr.GetUpperBound(0) Dim yCount As Integer = BolArr.GetUpperBound(1) Dim CP As New Point(xCount / 2, yCount / 2) Dim R As Integer = 0 For R = 0 To If(xCount > yCount, xCount, yCount) For Theat = 0 To Math.PI * 2 Step 1 / R Dim dx As Integer = CInt(CP.X + R * Math.Cos(Theat)) Dim dy As Integer = CInt(CP.Y + R * Math.Sin(Theat)) If Not (dx > 0 And dy > 0 And dx < xCount And dy < yCount) Then Continue For If BolArr(dx, dy) = 1 Then BolArr(dx, dy) = 0 Me.CreateNewSequence() Me.AddPoint(New Vector2(dx, dy)) CheckMove(BolArr, dx, dy, 0) NewStart = True End If Next Next End Sub ''' <summary> ''' 矩形扫描 ''' </summary> ''' <param name="BolArr"></param> Private Sub ScanRect(BolArr(,) As Integer) Dim xCount As Integer = BolArr.GetUpperBound(0) Dim yCount As Integer = BolArr.GetUpperBound(1) For i = 0 To xCount - 1 For j = 0 To yCount - 1 Dim dx As Integer = i Dim dy As Integer = j If Not (dx > 0 And dy > 0 And dx < xCount And dy < yCount) Then Continue For If BolArr(dx, dy) = 1 Then BolArr(dx, dy) = 0 Me.CreateNewSequence() Me.AddPoint(New Vector2(dx, dy)) CheckMove(BolArr, dx, dy, 0) NewStart = True End If Next Next End Sub ''' <summary> ''' 递归循迹算法 ''' </summary> Private Sub CheckMove(ByRef bolArr(,) As Integer, ByVal x As Integer, ByVal y As Integer, ByVal StepNum As Integer) If StepNum > 1000 Then Return Dim xBound As Integer = bolArr.GetUpperBound(0) Dim yBound As Integer = bolArr.GetUpperBound(1) Dim dx, dy As Integer Dim AroundValue As Integer = GetAroundValue(bolArr, x, y) '根据点权值轨迹将在当前点断开 'If AroundValue > 2 AndAlso AroundValue < 8 Then 'Return 'End If For i = 0 To 7 dx = x + xArray(i) dy = y + yArray(i) If Not (dx > 0 And dy > 0 And dx < xBound And dy < yBound) Then Return ElseIf bolArr(dx, dy) = 1 Then bolArr(dx, dy) = 0 If NewStart = True Then Me.CreateNewSequence() Me.AddPoint(New Vector2(dx, dy)) NewStart = False Else Me.AddPoint(New Vector2(dx, dy)) End If CheckMove(bolArr, dx, dy, StepNum + 1) NewStart = True End If Next End Sub ''' <summary> ''' 返回点权值 ''' </summary> Private Function GetAroundValue(ByRef BolArr(,) As Integer, ByVal x As Integer, ByVal y As Integer) As Integer Dim dx, dy, ResultValue As Integer Dim xBound As Integer = BolArr.GetUpperBound(0) Dim yBound As Integer = BolArr.GetUpperBound(1) For i = 0 To 7 dx = x + xArray(i) dy = y + yArray(i) If dx > 0 And dy > 0 And dx < xBound And dy < yBound Then If BolArr(dx, dy) = 1 Then ResultValue += 1 End If End If Next Return ResultValue End Function End Class ''' <summary> ''' 线条扫描方式 ''' </summary> Public Enum ScanMode ''' <summary> ''' 矩形扫描 ''' </summary> Rect ''' <summary> ''' 圆形扫描 ''' </summary> Circle End Enum
Imports System.Numerics ''' <summary> ''' 表示由一系列点向量组成的线条 ''' </summary> Public Class PointSequence Public Property Points As New List(Of Vector2) Public Property Sizes As Single() ''' <summary> ''' 计算画笔大小 ''' </summary> Public Sub CalcSize() If Points.Count < 1 Then Exit Sub Static Mid, PenSize As Single ReDim Sizes(Points.Count - 1) For i = 0 To Points.Count - 1 Mid = CSng(Math.Abs(i - Points.Count / 2)) PenSize = 1 - Mid / Points.Count * 2 Sizes(i) = PenSize Next End Sub End Class
using System; using System.Collections; using System.Collections.Generic; using System.Data; using System.Diagnostics; using System.Numerics; /// <summary> /// 表示自动循迹并生成绘制序列的AI /// </summary> public class SequenceAI { /// <summary> /// 线条序列List /// </summary> /// <returns></returns> public List<PointSequence> Sequences { get; set; } /// <summary> /// 扫描方式 /// </summary> public ScanMode ScanMode { get; set; } int[] xArray = { -1, 0, 1, 1, 1, 0, -1, -1 }; int[] yArray = { -1, -1, -1, 0, 1, 1, 1, 0 }; bool NewStart; /// <summary> /// 创建并初始化一个可自动生成绘制序列AI的实例 /// </summary> public SequenceAI(int[,] BolArr) { Sequences = new List<PointSequence>(); CalculateSequence(BolArr); foreach (object SubItem_loopVariable in Sequences) { SubItem = SubItem_loopVariable; SubItem.CalcSize(); } } /// <summary> /// 新增一个序列 /// </summary> private void CreateNewSequence() { Sequences.Add(new PointSequence()); } /// <summary> /// 在序列List末尾项新增一个点 /// </summary> private void AddPoint(Vector2 point) { Sequences.Last.Points.Add(point); } /// <summary> /// 计算序列 /// </summary> private void CalculateSequence(int[,] BolArr) { if (ScanMode == ScanMode.Rect) { ScanRect(BolArr); } else { ScanCircle(BolArr); } } /// <summary> /// 圆形扫描 /// </summary> /// <param name="BolArr"></param> private void ScanCircle(int[,] BolArr) { int xCount = BolArr.GetUpperBound(0); int yCount = BolArr.GetUpperBound(1); Point CP = new Point(xCount / 2, yCount / 2); int R = 0; for (R = 0; R <= xCount > yCount ? xCount : yCount; R++) { for (Theat = 0; Theat <= Math.PI * 2; Theat += 1 / R) { int dx = Convert.ToInt32(CP.X + R * Math.Cos(Theat)); int dy = Convert.ToInt32(CP.Y + R * Math.Sin(Theat)); if (!(dx > 0 & dy > 0 & dx < xCount & dy < yCount)) continue; if (BolArr[dx, dy] == 1) { BolArr[dx, dy] = 0; this.CreateNewSequence(); this.AddPoint(new Vector2(dx, dy)); CheckMove(ref BolArr, dx, dy, 0); NewStart = true; } } } } /// <summary> /// 矩形扫描 /// </summary> /// <param name="BolArr"></param> private void ScanRect(int[,] BolArr) { int xCount = BolArr.GetUpperBound(0); int yCount = BolArr.GetUpperBound(1); for (i = 0; i <= xCount - 1; i++) { for (j = 0; j <= yCount - 1; j++) { int dx = i; int dy = j; if (!(dx > 0 & dy > 0 & dx < xCount & dy < yCount)) continue; if (BolArr[dx, dy] == 1) { BolArr[dx, dy] = 0; this.CreateNewSequence(); this.AddPoint(new Vector2(dx, dy)); CheckMove(ref BolArr, dx, dy, 0); NewStart = true; } } } } /// <summary> /// 递归循迹算法 /// </summary> private void CheckMove(ref int[,] bolArr, int x, int y, int StepNum) { if (StepNum > 1000) return; int xBound = bolArr.GetUpperBound(0); int yBound = bolArr.GetUpperBound(1); int dx = 0; int dy = 0; int AroundValue = GetAroundValue(ref bolArr, x, y); //根据点权值轨迹将在当前点断开 //If AroundValue > 2 AndAlso AroundValue < 8 Then //Return //End If for (i = 0; i <= 7; i++) { dx = x + xArray[i]; dy = y + yArray[i]; if (!(dx > 0 & dy > 0 & dx < xBound & dy < yBound)) { return; } else if (bolArr[dx, dy] == 1) { bolArr[dx, dy] = 0; if (NewStart == true) { this.CreateNewSequence(); this.AddPoint(new Vector2(dx, dy)); NewStart = false; } else { this.AddPoint(new Vector2(dx, dy)); } CheckMove(ref bolArr, dx, dy, StepNum + 1); NewStart = true; } } } /// <summary> /// 返回点权值 /// </summary> private int GetAroundValue(ref int[,] BolArr, int x, int y) { int dx = 0; int dy = 0; int ResultValue = 0; int xBound = BolArr.GetUpperBound(0); int yBound = BolArr.GetUpperBound(1); for (i = 0; i <= 7; i++) { dx = x + xArray[i]; dy = y + yArray[i]; if (dx > 0 & dy > 0 & dx < xBound & dy < yBound) { if (BolArr[dx, dy] == 1) { ResultValue += 1; } } } return ResultValue; } } /// <summary> /// 线条扫描方式 /// </summary> public enum ScanMode { /// <summary> /// 矩形扫描 /// </summary> Rect, /// <summary> /// 圆形扫描 /// </summary> Circle }
using System; using System.Collections; using System.Collections.Generic; using System.Data; using System.Diagnostics; using System.Numerics; /// <summary> /// 表示由一系列点向量组成的线条 /// </summary> public class PointSequence { public List<Vector2> Points { get; set; } public float[] Sizes { get; set; } float static_CalcSize_Mid; /// <summary> /// 计算画笔大小 /// </summary> float static_CalcSize_PenSize; public void CalcSize() { if (Points.Count < 1) return; Sizes = new float[Points.Count]; for (i = 0; i <= Points.Count - 1; i++) { static_CalcSize_Mid = Convert.ToSingle(Math.Abs(i - Points.Count / 2)); static_CalcSize_PenSize = 1 - static_CalcSize_Mid / Points.Count * 2; Sizes[i] = static_CalcSize_PenSize; } } }
视频
附录
GitHub:EDGameEngine.AutoDraw
早期博客:程序如何实现自动绘图
早期博客:更优秀的自动绘图程序
创意分享:儿童涂鸦遇上程序绘图