User:Thierry Dugnolle/Python/3D line drawer

Read User:Thierry Dugnolle/Python if you want to know how to run this program on your computer. If you want more explanation, you can make use of my talk page.


The Python files:

main.py

edit
# These files makes a drawer who draws images of 3D lines.
# To see the depth of space, what is in front of us shall hide what is behind.
# Ordinary visualizations of 3D lines don't do this.
# The trick is to surround a line with a thin halo, so that it hides the lines behind.

from Line3D_Drawer import a3DlineDrawer
from Line3D import aNewCrown
from Vector3D import aNew3Dvector

print("Line3D_Drawer")

# The drawer draws an animation of a circular sinusoid in space.
TheDrawer = a3DlineDrawer()
# The image:
TheWidth_inNumberOfPixels = 500
TheHeight_inNumberOfPixels = 250
# The line of vision:
Theta = 95.0 # Theta is the angle (in degrees) of the line of vision relative to the z axis.
             # Phi will be determined later.
# The position of the optical center is determined by the position of the object seen, the line of vision
# and the distance between the object and the optical center:
TheObjectCenter = aNew3Dvector(0.0, 0.0, -0.05) # Position of the center of the object seen.
TheDistance = 1.5 # ŧ Distance between the object center and the optical center
# The Zoom = 1 means that the image is neither reduced nor enlarged.
TheZoom = 1.0
# Psi is the angle (in degrees) of rotation of the image around the line of vision
Psi = 0.0
# The line:
TheNumberOfPoints = 1000 # The number of points which determine the line
TheHeight = 0.15 # The height of the 'crown'
TheRadius = 0.6 # Its radius
TheNumberOfPeaks = 9 # Its number of 'peaks'
TheLine = aNewCrown(TheNumberOfPoints, TheHeight, TheRadius, TheNumberOfPeaks)
TheLineRealHalfWidth = 0.005
# The animation:
TheNumberOfImages=200
for i in range(TheNumberOfImages):
    # Phi is the angle (in degrees) between the projection of line of vision on the xy plane and the x axis:
    Phi=i*360/TheNumberOfImages/TheNumberOfPeaks
    TheDrawer.takesAnewCanvas(TheWidth_inNumberOfPixels, TheHeight_inNumberOfPixels)
    TheDrawer.choosesApointOfView(TheObjectCenter, TheDistance, Phi, Theta, Psi, TheZoom)
    TheDrawer.drawsA3Dline(TheLine, TheLineRealHalfWidth)
    TheDrawer.givesHisDrawing("Crown"+str(100+i)+".png")

print("Good bye")

Line3D_Drawer.py

edit
import math
from Vector3D import a3Dvector
from Vector2D import a2Dvector
from Line2D import aNew2Dline
from LineDrawer import aLineDrawer
from DepthMatrix import aDepthMatrix, aNewDepthMatrix

# The parameters of the drawer:
# The line of vision:
# phi is the angle between the projection of line of vision on the xy plane and the x axis.
# theta is the angle of the line of vision relative to the z axis.
# psi is the angle of rotation of the image around the line of vision.
# The position of the optical center is determined by the position of the object seen, the line of vision
# and the distance between the object and the optical center:
# objectCenter is the position of the center of the object seen.
# distance is the distance between the object center and the optical center.
# zoom = 1 means that the image is neither reduced nor enlarged.
# depthMatrix is the memory of the perception of the depth of the object seen (its distance relative to the observer).
# Read the commentary in DepthMatrix.py for more explanations.
class a3DlineDrawer(aLineDrawer): # This means that a3DLineDrawer inherits all the properties and methods of aLineDrawer.
    opticalCenter = a3Dvector() # the optical center
    phi = 0.0
    theta = 0.0
    psi = 0.0
    objectCenter = a3Dvector()
    distance = 0.0
    zoom = 0.0
    depthMatrix = aDepthMatrix()

    def choosesApointOfView(self, TheObjectCenter, TheDistance, Phi, Theta, Psi, zoom):
        # Phi, Theta, and Psi are in degrees not in radians.
        self.objectCenter = TheObjectCenter
        self.distance=TheDistance
        self.phi = Phi*math.pi/180 # phi is Phi in radians
        self.theta = Theta*math.pi/180
        self.psi = Psi*math.pi/180
        self.opticalCenter.x = 0.0
        self.opticalCenter.y = -self.distance
        self.opticalCenter.z = 0.0
        self.opticalCenter = self.opticalCenter.XaxisRotated(math.pi/2 - self.theta)
        self.opticalCenter = self.opticalCenter.ZaxisRotated(self.phi - math.pi/2)
        self.opticalCenter = self.opticalCenter.plus(self.objectCenter)
        self.zoom = zoom
        self.depthMatrix = aNewDepthMatrix(self.canvas.width_inNumberOfPixels, self.canvas.height_inNumberOfPixels)

    def pointImageOf(self,aPoint):
        p = a2Dvector()
        w = a3Dvector()
        w = aPoint.minus(self.opticalCenter)
        w = w.ZaxisRotated(math.pi/2-self.phi)
        w = w.XaxisRotated(self.theta-math.pi/2)
        w = w.YaxisRotated(self.psi)
        p = w.centeredProjectedOnXZ()
        p = p.times(self.zoom)
        return p

    def lineImageOf(self,aLine):
        lineProj = aNew2Dline(aLine.numberOfPoints)
        for i in range(aLine.numberOfPoints):
            lineProj.point[i] = self.pointImageOf(aLine.point[i])
        return lineProj

    # perceivesTheDepthOfAsegment is the fundamental method of a Line3D_Drawer. Read the commentaries of
    # drawsAsegment in LineDrawer and of DepthMatrix for more explanations.
    def perceivesTheDepthOfAsegment(self,TheFirstPoint, TheSecondPoint, TheLineRealHalfWidth, TheFirstPointNumber):
        imPt1=self.pointImageOf(TheFirstPoint)
        imPt2=self.pointImageOf(TheSecondPoint)
        depth=TheFirstPoint.plus(TheSecondPoint.minus(TheFirstPoint).times(0.5)).minus(self.opticalCenter).norm()
        # The window for the drawing of the line is first defined by its left-top corner
        # and right-bottom corner, in real coordinates:
        TopLeft = a2Dvector()
        BottomRight = a2Dvector()
        if imPt1.x < imPt2.x:
            TopLeft.x = imPt1.x
            BottomRight.x = imPt2.x
        else:
            TopLeft.x = imPt2.x
            BottomRight.x = imPt1.x
        if imPt1.y < imPt2.y:
            TopLeft.y = imPt1.y
            BottomRight.y = imPt2.y
        else:
            TopLeft.y = imPt2.y
            BottomRight.y = imPt1.y
        P1 = self.canvas.convertedToPixelCoordinates(TopLeft)
        P2 = self.canvas.convertedToPixelCoordinates(BottomRight)
        TheLineHalfWidth_inPixels = math.floor(
            TheLineRealHalfWidth * self.canvas.width_inNumberOfPixels / self.canvas.realWidth)
        # The window is enlarged :
        left = P1[0] - TheLineHalfWidth_inPixels
        top = P2[1] - TheLineHalfWidth_inPixels
        right = P2[0] + TheLineHalfWidth_inPixels
        bottom = P1[1] + TheLineHalfWidth_inPixels
        for i in range(left, right + 1):
            if i >= 0 and i < self.canvas.width_inNumberOfPixels:
                for j in range(top, bottom + 1):
                    if j >= 0 and j < self.canvas.height_inNumberOfPixels:
                        # The center of a pixel in real coordinates:
                        ThePixelCenter = self.canvas.convertedToRealCoordinates(i,j)
                        # The distance between the pixel center and the first point:
                        d1 = ThePixelCenter.minus(imPt1).norm()
                        # The distance between the pixel center and the second point:
                        d2 = ThePixelCenter.minus(imPt2).norm()
                        proj = ThePixelCenter.orthogonallyProjectedOnTheLine(imPt1, imPt2)
                        if proj.x>=TopLeft.x and proj.x<=BottomRight.x and proj.y>=TopLeft.y and proj.y<=BottomRight.y and d2>=TheLineRealHalfWidth or d1<=TheLineRealHalfWidth:
                            # dist is the distance between the center of the pixel and the line:
                            dist = ThePixelCenter.minus(proj).norm()
                            if dist < TheLineRealHalfWidth:
                                if self.depthMatrix.depth[i][j] > depth:
                                    self.depthMatrix.depth[i][j] = depth
                                    self.depthMatrix.dist[i][j] = dist
                                    self.depthMatrix.pointNumber[i][j] = TheFirstPointNumber

    def perceivesTheDepthOfAline(self, TheLine, TheLineRealHalfWidth):
        for i in range(TheLine.numberOfPoints - 2):
            self.perceivesTheDepthOfAsegment(TheLine.point[i], TheLine.point[i+1], TheLineRealHalfWidth, i)

    def drawsA3Dline(self, TheLine, TheLineRealHalfWidth):
        self.perceivesTheDepthOfAline(TheLine, TheLineRealHalfWidth)
        for i in range(self.canvas.width_inNumberOfPixels):
            for j in range(self.canvas.height_inNumberOfPixels):
                if self.depthMatrix.dist[i][j] < TheLineRealHalfWidth:
                    # The power of cos in the next line (here 10) determines the width of the line relative to its halo.
                    # The higher the power (always a pair integer), the thinner the line relative to its halo.
                    grey = 255-math.floor(pow(math.cos(self.depthMatrix.dist[i][j]*math.pi/2/TheLineRealHalfWidth),10)*255)
                    self.pixel[i, j] = grey

Vector3D.py

edit
import math
from Vector2D import a2Dvector

class a3Dvector:
    x = 0.0
    y = 0.0
    z = 0.0
    def plus(self,v):
        sum = a3Dvector()
        sum.x = self.x + v.x
        sum.y = self.y + v.y
        sum.z = self.z + v.z
        return sum

    def minus(self,v):
        diff = a3Dvector()
        diff.x = self.x - v.x
        diff.y = self.y - v.y
        diff.z = self.z - v.z
        return diff

    def times(self,a):
        prod = a3Dvector()
        prod.x = a*self.x
        prod.y = a*self.y
        prod.z = a*self.z
        return prod

    def norm(self):
        return math.sqrt(self.x*self.x + self.y*self.y + self.z*self.z)

    def XaxisRotated(self,theta):
        v = a3Dvector()
        costh = math.cos(theta)
        sinth = math.sin(theta)
        v.x = self.x
        v.y = costh*self.y - sinth*self.z
        v.z = sinth*self.y + costh*self.z
        return v

    def YaxisRotated(self,theta):
        v = a3Dvector()
        costh = math.cos(theta)
        sinth = math.sin(theta)
        v.y = self.y
        v.x = costh*self.x + sinth*self.z
        v.z = -sinth*self.x + costh*self.z
        return v

    def ZaxisRotated(self,theta):
        v = a3Dvector()
        costh = math.cos(theta)
        sinth = math.sin(theta)
        v.z = self.z
        v.x = costh*self.x - sinth*self.y
        v.y = sinth*self.x + costh*self.y
        return v

    # Consider the projection of the point v = (x,y,z) on the plane XZ, y = -1 followed
    # by a half turn rotation around the y axis: aPoint.centeredProjectedOnXZ() is the
    # image of aPoint by such a projection-rotation on the plane XZ, y = -1.
    # The image is first projected upside down and then rotated, like in the eye and the brain.
    def centeredProjectedOnXZ(self):
        p = a2Dvector()
        if abs(self.y) < 0.000001:
            p.x = 1000000.0
            p.y = 1000000.0
        else:
            p.x = self.x/self.y
            p.y = self.z/self.y
        return p

def aNew3Dvector(x,y,z):
    v = a3Dvector()
    v.x = x
    v.y = y
    v.z = z
    return v

Line3D.py

edit
from Vector3D import a3Dvector
import math

class a3Dline:
    numberOfPoints = 0  # number of successive points on the line
    point = []

def aNew3Dline(numberOfPoints):
    line=a3Dline()
    line.numberOfPoints=numberOfPoints
    line.point = [a3Dvector() for i in range(numberOfPoints)]
    return line

def aNewCrown(TheNumberOfPoints, TheHeight, TheRadius, TheNumberOfPeaks):
    TheCrown = aNew3Dline(TheNumberOfPoints+1)
    for i in range(TheNumberOfPoints+1):
        TheCrown.point[i].x = TheRadius*math.cos(i*2*math.pi/TheNumberOfPoints)
        TheCrown.point[i].y = TheRadius*math.sin(i*2*math.pi/TheNumberOfPoints)
        TheCrown.point[i].z = TheHeight*math.sin(i*TheNumberOfPeaks*2*math.pi/TheNumberOfPoints)
    return TheCrown

DepthMatrix.py

edit
# For the drawer to see depth, we need to associate to each pixel the nearest segment which is
# projected on it. Hence we need a matrix which associates to each pixel the point number of
# the nearest segment of the line, the distance of its middle from the optical center, and the
# distance of its image to the center of the pixel

class aDepthMatrix:
    pointNumber = [] # The point number on the line of the nearest segment to the optical center
    depth = [] # The distance of the middle of nearest segment on the line to the optical center
    dist = [] # # The distance between the center of the pixel and the image of the image of the segment

def aNewDepthMatrix(TheWidth, TheHeight):
    TheMatrix = aDepthMatrix()
    TheMatrix.pointNumber = [ [] for i in range(TheWidth)]
    TheMatrix.depth = [ [] for i in range(TheWidth)]
    TheMatrix.dist = [ [] for i in range(TheWidth)]
    for i in range(TheWidth):
        TheMatrix.pointNumber[i] = [0 for j in range(TheHeight)]
        TheMatrix.depth[i] = [1E100 for j in range(TheHeight)]
        TheMatrix.dist[i] = [1E100 for j in range(TheHeight)]
    return TheMatrix

Others files

edit

The following files are also necessary: Vector2D.py, Line2D.py and LineDrawer.py. They are here.

Remarks

edit

I give my methods step by step. The easiest ones are in User:Thierry Dugnolle/Python/High definition paintbrush. They are followed by the methods on this page. The more elaborate ones will come later. The quality of the images generated with the present methods is not always very good. Better and more complicated methods are needed, to draw an image like the one below.

In DepthMatrix, the memory of the point number is not necessary for the present method, but it will be necessary for the more elaborate ones. For the most powerful methods, a new kind of depth matrix will be needed.

 
A Chua attractor