最近讀研期間上了計算機視覺化的課,老師也對計算機圖形學的實現佈置了相關的作業。雖然我沒有系統地學過圖形視覺化的課,但是我之前逆向過一些遊戲引擎,除了保護驅動之外,因為要做透視,接觸過一些計算機圖形學的基礎常識。這次的作業主要分為2個主要模組,一個是實現畫線,畫圓的演算法,還有填充的演算法,以及裁剪的演算法。
之前工作的時候雖然參與過一些資料視覺化大屏的設計,但是當時主要的工作使用Echarts或者G2做業務元件開發,並沒有對畫線,填充,裁剪等基礎演算法做過實現。這次就著這個機會我就想了解一些。實現的效果如下(動圖載入可能有些慢):
掃描線填充過程
裁剪過程(根據滑鼠位置,實時裁剪多邊形,右邊的藍色是裁剪後的圖形)
為什麼選擇win32 api畫圖
選擇win32的原因是我想做一些與眾不同的實現方法,比起用D3或者Echarts這種webGL的實現方式,我更想直接在顯示器上畫出影像,看起來更極客一些。這也導致了錄屏軟體沒辦法捕捉,只能用手機來錄製?
為什麼不選C++而選擇python
主要是python能對記憶體做個管理,C++直接調這種底層的介面會把記憶體搞壞掉,導致電腦變得特別卡。。不信大家可以在電腦上編譯執行這段程式碼2分鐘試一試,如果你電腦沒炸,算你有錢。。
#include <windows.h>
// g++ a.cpp -o a.exe -lgdi32 && a.exe
void bresenham(int x0,int y0,int x1,int y1){
int dx = abs(x1-x0);
int dy = abs(y1-y0);
int sx = x0<x1 ? 1 : -1;
int sy = y0<y1 ? 1 : -1;
int err = dx-dy;
int e2;
while(1){
SetPixel(GetDC(0),x0,y0,RGB(255,0,0));
if(x0==x1 && y0==y1) break;
e2 = 2*err;
if(e2>-dy){
err = err-dy;
x0 = x0+sx;
}
if(e2<dx){
err = err+dx;
y0 = y0+sy;
}
}
}
void draw_polygon(int x[], int y[], int n)
{
int i;
for (i = 0; i < n - 1; i++)
bresenham(x[i], y[i], x[i + 1], y[i + 1]);
bresenham(x[n - 1], y[n - 1], x[0], y[0]);
}
int main()
{
HDC hdc = GetDC(0);
int x[4] = {100,200,300,100};
int y[4] = {100,100,200,200};
while (1)
{
draw_polygon(x,y,4);
ReleaseDC(0, hdc);
}
return 0;
}
畫線
對於畫線部分,我這裡使用了一個叫bresenham演算法。。雖然我念不出名字,但是這個演算法能夠幫助我們實現畫線運算,還有後面的中心圓填充,多邊形繪畫等方法。而且不通過浮點數的運算,直接變成整數運算,演算法實現的函式如下所示,看起來比較簡單,執行速度也很快。
def bresenham(x0, y0, x1, y1 , color):
dx = abs(x1 - x0)
dy = abs(y1 - y0)
sx = 1 if x0 < x1 else -1
sy = 1 if y0 < y1 else -1
err = dx - dy
while True:
win32gui.SetPixel(dc, x0, y0, color)
if x0 == x1 and y0 == y1:
break
e2 = 2 * err
if e2 > -dy:
err -= dy
x0 += sx
if e2 < dx:
err += dx
y0 += sy
我的螢幕解析度是1920x1080的,只要在電腦裡呼叫這個函式,把兩個點的座標填進去,就可以在顯示器螢幕上畫一條線。
中心圓演算法
這個中心圓演算法相對來說就比畫線的演算法在理解上面難很多,但是實現起來更簡單一些,分成8個關於直線的和座標軸對稱的區域畫圓,因此知道一個就可以畫出其他幾個,下面是實現過程。
def draw_circle(x, y, r):
x0 = 0
y0 = r
d = 3 - 2 * r
while x0 <= y0:
win32gui.SetPixel(dc, x + x0, y + y0, 0xffffff)
win32gui.SetPixel(dc, x + y0, y + x0, 0xffffff)
win32gui.SetPixel(dc, x - y0, y + x0, 0xffffff)
win32gui.SetPixel(dc, x - x0, y + y0, 0xffffff)
win32gui.SetPixel(dc, x - x0, y - y0, 0xffffff)
win32gui.SetPixel(dc, x - y0, y - x0, 0xffffff)
win32gui.SetPixel(dc, x + y0, y - x0, 0xffffff)
win32gui.SetPixel(dc, x + x0, y - y0, 0xffffff)
if d < 0:
d += 4 * x0 + 6
else:
d += 4 * (x0 - y0) + 10
y0 -= 1
x0 += 1
在中心圓填充這裡,可以取個巧,把幾個頂點直接用畫線的演算法一行一行填充上去。就可以實現下面的效果。程式碼如下
# 畫實心圓
def draw_circle_fill(x0, y0, r):
x = 0
y = r
d = 3 - 2 * r
while x <= y:
time.sleep(0.01)
bresenham(x0 + x, y0 + y, x0 - x, y0 + y)
time.sleep(0.01)
bresenham(x0 + x, y0 - y, x0 - x, y0 - y)
time.sleep(0.01)
bresenham(x0 + y, y0 + x, x0 - y, y0 + x)
time.sleep(0.01)
bresenham(x0 + y, y0 - x, x0 - y, y0 - x)
if d < 0:
d += 4 * x + 6
else:
d += 4 * (x - y) + 10
y -= 1
x += 1
掃描線填充
掃描線填充的演算法就比較難實現了,需要找到起始的種子,還有每行的種子,因為我這裡僅僅用頂點實現起來過於複雜,就索性偷懶用了陣列。下面的演算法實現部分僅供參考,具體的實現包括種子的選擇等等,可以更好一些。
maps = [[0 for x in range(0,400)] for x in range(0,400)]
for i in range(200,300):
maps[i][200] = 1
maps[200][i] = 1
maps[i][300] = 1
maps[300][i] = 1
for i in range(230,270):
maps[i][230] = 1
maps[i][270] = 1
maps[230][i] = 1
maps[270][i] = 1
# 掃描填充maps
def scan_fill():
seed = (271,296)
stack = []
stack.append(seed)
while len(stack) > 0:
(x,y) = stack.pop()
# 如果已經被填充過,則跳過
if(maps[x][y] == 1):
continue
# 橫向填充並記錄lx rx
i=0
time.sleep(0.01)
while(maps[x+i][y] == 0):
maps[x+i][y] = 1
win32gui.SetPixel(dc, x+i, y, 0xffffff)
i += 1
rx = x+i-1
i=1
while(maps[x-i][y] == 0):
maps[x-i][y] = 1
win32gui.SetPixel(dc, x-i, y, 0xffffff)
i+=1
lx = x-i+1
# 下一個種子
if y+1>=300:
continue
i=0
while(maps[lx+i][y+1] == 0):
if(maps[lx+i+1][y+1]==1):
stack.append((lx+i,y+1))
break
i+=1
i=0
while(maps[rx-i][y+1] == 0):
if(maps[rx-i-1][y+1]==1):
stack.append((rx-i,y+1))
break
i+=1
if y-1<=0:
continue
i=0
while(maps[lx+i][y-1] == 0):
if(maps[lx+i+1][y-1]==1):
stack.append((lx+i,y-1))
break
i+=1
i=0
while(maps[rx-i][y-1] == 0):
if(maps[rx-i-1][y-1]==1):
stack.append((rx-i,y-1))
break
i+=1
scan_fill()
這裡是所有程式碼
上面的程式碼都是剪下過的,完整的程式碼如下所示,執行後大家就可以在顯示器上看到執行過程:
import time
import win32gui
dc = win32gui.GetDC(0)
maps = [[0 for x in range(0,400)] for x in range(0,400)]
for i in range(200,300):
maps[i][200] = 1
maps[200][i] = 1
maps[i][300] = 1
maps[300][i] = 1
for i in range(230,270):
maps[i][230] = 1
maps[i][270] = 1
maps[230][i] = 1
maps[270][i] = 1
# 中點演算法畫圓
def draw_circle(x, y, r):
x0 = 0
y0 = r
d = 3 - 2 * r
while x0 <= y0:
time.sleep(0.01)
win32gui.SetPixel(dc, x + x0, y + y0, 0xffffff)
time.sleep(0.01)
win32gui.SetPixel(dc, x + y0, y + x0, 0xffffff)
time.sleep(0.01)
win32gui.SetPixel(dc, x - y0, y + x0, 0xffffff)
time.sleep(0.01)
win32gui.SetPixel(dc, x - x0, y + y0, 0xffffff)
time.sleep(0.01)
win32gui.SetPixel(dc, x - x0, y - y0, 0xffffff)
time.sleep(0.01)
win32gui.SetPixel(dc, x - y0, y - x0, 0xffffff)
time.sleep(0.01)
win32gui.SetPixel(dc, x + y0, y - x0, 0xffffff)
time.sleep(0.01)
win32gui.SetPixel(dc, x + x0, y - y0, 0xffffff)
if d < 0:
d += 4 * x0 + 6
else:
d += 4 * (x0 - y0) + 10
y0 -= 1
x0 += 1
# 畫線
def bresenham(x0, y0, x1, y1):
dx = abs(x1 - x0)
dy = abs(y1 - y0)
sx = 1 if x0 < x1 else -1
sy = 1 if y0 < y1 else -1
err = dx - dy
while True:
#time.sleep(0.01)
win32gui.SetPixel(dc, x0, y0, 0xffffff)
if x0 == x1 and y0 == y1:
break
e2 = 2 * err
if e2 > -dy:
err -= dy
x0 += sx
if e2 < dx:
err += dx
y0 += sy
# 畫實心圓
def draw_circle_fill(x0, y0, r):
x = 0
y = r
d = 3 - 2 * r
while x <= y:
time.sleep(0.01)
bresenham(x0 + x, y0 + y, x0 - x, y0 + y)
time.sleep(0.01)
bresenham(x0 + x, y0 - y, x0 - x, y0 - y)
time.sleep(0.01)
bresenham(x0 + y, y0 + x, x0 - y, y0 + x)
time.sleep(0.01)
bresenham(x0 + y, y0 - x, x0 - y, y0 - x)
if d < 0:
d += 4 * x + 6
else:
d += 4 * (x - y) + 10
y -= 1
x += 1
# 畫多邊形
def draw_polygon(points):
for i in range(len(points)):
x0 = points[i][0]
y0 = points[i][1]
x1 = points[(i + 1) % len(points)][0]
y1 = points[(i + 1) % len(points)][1]
bresenham(x0, y0, x1, y1)
# 畫橢圓
def draw_ellipse(x0, y0, a, b):
x = 0
y = b
a2 = a * a
b2 = b * b
d = b2 - a2 * b + a2 / 4
while b2 * x <= a2 * y:
win32gui.SetPixel(dc, x0 + x, y0 + y, 0xffffff)
win32gui.SetPixel(dc, x0 - x, y0 + y, 0xffffff)
win32gui.SetPixel(dc, x0 + x, y0 - y, 0xffffff)
win32gui.SetPixel(dc, x0 - x, y0 - y, 0xffffff)
if d < 0:
d += b2 * (2 * x + 3)
else:
d += b2 * (2 * x - 2 * y + 5)
y -= 1
x += 1
d1 = b2 * (x + 0.5) * (x + 0.5) + a2 * (y - 1) * (y - 1) - a2 * b2
while y >= 0:
win32gui.SetPixel(dc, x0 + x, y0 + y, 0xffffff)
win32gui.SetPixel(dc, x0 - x, y0 + y, 0xffffff)
win32gui.SetPixel(dc, x0 + x, y0 - y, 0xffffff)
win32gui.SetPixel(dc, x0 - x, y0 - y, 0xffffff)
if d1 > 0:
d1 -= a2 * (2 * y - 1)
d1 += b2 * (2 * x + 3)
x += 1
y -= 1
# 畫矩形
def draw_rectangle(x0, y0, x1, y1):
bresenham(x0, y0, x1, y0)
bresenham(x1, y0, x1, y1)
bresenham(x1, y1, x0, y1)
bresenham(x0, y1, x0, y0)
# 掃描填充maps
def scan_fill():
seed = (271,296)
stack = []
stack.append(seed)
while len(stack) > 0:
(x,y) = stack.pop()
# 如果已經被填充過,則跳過
if(maps[x][y] == 1):
continue
# 橫向填充並記錄lx rx
i=0
time.sleep(0.01)
while(maps[x+i][y] == 0):
maps[x+i][y] = 1
win32gui.SetPixel(dc, x+i, y, 0xffffff)
i += 1
rx = x+i-1
i=1
while(maps[x-i][y] == 0):
maps[x-i][y] = 1
win32gui.SetPixel(dc, x-i, y, 0xffffff)
i+=1
lx = x-i+1
# 下一個種子
if y+1>=300:
continue
i=0
while(maps[lx+i][y+1] == 0):
if(maps[lx+i+1][y+1]==1):
stack.append((lx+i,y+1))
break
i+=1
i=0
while(maps[rx-i][y+1] == 0):
if(maps[rx-i-1][y+1]==1):
stack.append((rx-i,y+1))
break
i+=1
if y-1<=0:
continue
i=0
while(maps[lx+i][y-1] == 0):
if(maps[lx+i+1][y-1]==1):
stack.append((lx+i,y-1))
break
i+=1
i=0
while(maps[rx-i][y-1] == 0):
if(maps[rx-i-1][y-1]==1):
stack.append((rx-i,y-1))
break
i+=1
scan_fill()
while True:
# 畫線
bresenham(400, 900, 1000, 700)
# 填充圓
draw_circle_fill(900, 500, 100)
# 中心圓
draw_circle(1000, 200, 100)
# 橢圓
draw_ellipse(1500, 200, 100, 100)
# 矩形
draw_rectangle(1100, 400, 1200, 500)
# 多邊形
draw_polygon([(900, 1000), (800, 800), (1000, 900), (1100, 1000)])
#三角形
draw_polygon([(400, 200), (500, 300), (600, 200)])
裁剪
裁剪這裡簡直就是我的噩夢,因為我之前為了極客選擇了僅僅知道頂點就畫出裁剪過的多邊形,導致我沒有陣列,只能設計更極客的演算法。
最後我找到了一種裁剪凸多邊形的辦法,大致就是找到每個線段的交點,然後順時針方向對交點和在主多邊形,副多邊形的頂點排序,最後就可以實現裁剪。程式碼超級複雜,
import win32gui
import math
import pygame
dc = win32gui.GetDC(0)
# 獲取滑鼠的位置
mouse_x=win32gui.GetCursorPos()[0]
mouse_y=win32gui.GetCursorPos()[1]
temp = win32gui.GetCursorPos()
def get_mouse_pos():
global mouse_x, mouse_y
mouse_x = win32gui.GetCursorPos()[0]
mouse_y = win32gui.GetCursorPos()[1]
clock = pygame.time.Clock()
temp2 = []
def bresenham(x0, y0, x1, y1 , color):
dx = abs(x1 - x0)
dy = abs(y1 - y0)
sx = 1 if x0 < x1 else -1
sy = 1 if y0 < y1 else -1
err = dx - dy
while True:
win32gui.SetPixel(dc, x0, y0, color)
if x0 == x1 and y0 == y1:
break
e2 = 2 * err
if e2 > -dy:
err -= dy
x0 += sx
if e2 < dx:
err += dx
y0 += sy
def draw_rectangle(x0, y0, x1, y1):
bresenham(x0, y0, x1, y0,0xffffff)
bresenham(x1, y0, x1, y1,0xffffff)
bresenham(x1, y1, x0, y1,0xffffff)
bresenham(x0, y1, x0, y0,0xffffff)
def draw_polygon(points):
for i in range(len(points)):
x0 = points[i][0]
y0 = points[i][1]
x1 = points[(i + 1) % len(points)][0]
y1 = points[(i + 1) % len(points)][1]
bresenham(x0, y0, x1, y1,0x00ff00)
def draw_polygon_black(points):
for i in range(len(points)):
x0 = points[i][0]
y0 = points[i][1]
x1 = points[(i + 1) % len(points)][0]
y1 = points[(i + 1) % len(points)][1]
bresenham(x0, y0, x1, y1,0x000000)
# 線段是否相交
def IsRectCross(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y):
return min(p1x,p2x) <= max(q1x,q2x) and min(q1x,q2x) <= max(p1x,p2x) and min(p1y,p2y) <= max(q1y,q2y) and min(q1y,q2y) <= max(p1y,p2y)
def IsLineSegmentCross(pFirst1x,pFirst1y,pFirst2x,pFirst2y,pSecond1x,pSecond1y,pSecond2x,pSecond2y):
line1 = pFirst1x * (pSecond1y - pFirst2y) + pFirst2x * (pFirst1y - pSecond1y) + pSecond1x * (pFirst2y - pFirst1y)
line2 = pFirst1x * (pSecond2y - pFirst2y) + pFirst2x * (pFirst1y - pSecond2y) + pSecond2x * (pFirst2y - pFirst1y)
if (((line1 ^ line2) >= 0) and not (line1 == 0 and line2 == 0)):
return False
line1 = pSecond1x * (pFirst1y - pSecond2y) + pSecond2x * (pSecond1y - pFirst1y) + pFirst1x * (pSecond2y - pSecond1y)
line2 = pSecond1x * (pFirst2y - pSecond2y) + pSecond2x * (pSecond1y - pFirst2y) + pFirst2x * (pSecond2y - pSecond1y)
if (((line1 ^ line2) >= 0) and not (line1 == 0 and line2 == 0)):
return False
return True
def GetCrossPoint(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y):
if(IsRectCross(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y)):
if (IsLineSegmentCross(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y)):
tmpLeft = (q2x - q1x) * (p1y - p2y) - (p2x - p1x) * (q1y - q2y)
tmpRight = (p1y - q1y) * (p2x - p1x) * (q2x - q1x) + q1x * (q2y - q1y) * (p2x - p1x) - p1x * (p2y - p1y) * (q2x - q1x)
if (tmpLeft == 0):
return None
x = (int)(tmpRight/tmpLeft)
tmpLeft = (p1x - p2x) * (q2y - q1y) - (p2y - p1y) * (q1x - q2x)
tmpRight = p2y * (p1x - p2x) * (q2y - q1y) + (q2x- p2x) * (q2y - q1y) * (p1y - p2y) - q2y * (q1x - q2x) * (p2y - p1y)
if (tmpLeft == 0):
return None
y = (int)(tmpRight/tmpLeft)
return (x,y)
else:
return None
else:
return None
def draw_rectangle_black(x0, y0, x1, y1):
bresenham(x0, y0, x1, y0,0x000000)
bresenham(x1, y0, x1, y1,0x000000)
bresenham(x1, y1, x0, y1,0x000000)
bresenham(x0, y1, x0, y0,0x000000)
# 判斷點是否在多邊形內
def IsPointInPolygon(points, x, y):
nCross = 0
for i in range(len(points)):
p1x = points[i][0]
p1y = points[i][1]
p2x = points[(i + 1) % len(points)][0]
p2y = points[(i + 1) % len(points)][1]
if (y > min(p1y, p2y)):
if (y <= max(p1y, p2y)):
if (x <= max(p1x, p2x)):
if (p1y != p2y):
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if (p1x == p2x or x <= xinters):
nCross += 1
if (nCross % 2 == 0):
return False
else:
return True
def getNonRepeatList(data):
new_data = []
for i in range(len(data)):
if data[i] not in new_data:
new_data.append(data[i])
return new_data
# 判斷兩多邊形重疊部分 返回一個多邊形
def IsPolygonCross(points1, points2):
result = []
for i in range(len(points1)):
p1x = points1[i][0]
p1y = points1[i][1]
p2x = points1[(i + 1) % len(points1)][0]
p2y = points1[(i + 1) % len(points1)][1]
for j in range(len(points2)):
q1x = points2[j][0]
q1y = points2[j][1]
q2x = points2[(j + 1) % len(points2)][0]
q2y = points2[(j + 1) % len(points2)][1]
if (IsPointInPolygon(points1, q1x, q1y) and (q1x, q1y) not in result):
result.append((q1x, q1y))
if (IsPointInPolygon(points1, q2x, q2y) and (q2x, q2y) not in result):
result.append((q2x, q2y))
if (IsPointInPolygon(points2, p1x, p1y) and (p1x, p1y) not in result):
result.append((p1x, p1y))
if (IsPointInPolygon(points2, p2x, p2y) and (p2x, p2y) not in result):
result.append((p2x, p2y))
if (IsRectCross(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y)):
if GetCrossPoint(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y) != None:
(x, y) = GetCrossPoint(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y)
result.append((x, y))
if (result == []):
return result
return (sort_points_in_clockwise_order(result))
w = 60
h = 100
def draw_polygon_red(points):
for i in range(len(points)):
x0 = points[i][0]
y0 = points[i][1]
x1 = points[(i + 1) % len(points)][0]
y1 = points[(i + 1) % len(points)][1]
bresenham(x0, y0, x1, y1,0xff0000)
def sort_points_in_clockwise_order(points):
center = (0, 0)
for point in points:
center = (center[0] + point[0], center[1] + point[1])
center = (center[0] / len(points), center[1] / len(points))
points_copy = list(points)
points_copy.sort(key=lambda point: math.atan2(point[0] - center[0], point[1] - center[1]))
res = []
for i in points_copy:
res.append((i[0]+500,i[1]))
return res
polygon_Points = [(600,500), (800,500), (900, 600), (900, 400),(600,300)]
while True:
draw_rectangle_black(temp[0],temp[1],temp[0]+w,temp[1]+h)
draw_rectangle(mouse_x,mouse_y,mouse_x+w,mouse_y+h)
temp = (mouse_x,mouse_y)
get_mouse_pos()
draw_polygon(polygon_Points)
res = IsPolygonCross(polygon_Points,[(mouse_x,mouse_y),(mouse_x+w,mouse_y),(mouse_x+w,mouse_y+h),(mouse_x,mouse_y+h)])
print(res)
if temp2 != []:
draw_polygon_black(temp2)
if res != [] and res != None:
draw_polygon_red(res)
temp2 = res
clock.tick(120)# 60幀
總結
計算機圖形學並沒有我之前想的那麼好學,踩了很多坑,也補了很多知識。希望後面能再接再厲