In [1]:
import numpy as np
from numpy import random

from matplotlib import pyplot as plt
from numpy import linalg as la
from enum import Enum

In [2]:
class ProgressBar(object):
    
    def __init__(self, total, prefix='', suffix='', decimals=1, length=100, fill='█'):
        """
        Creates terminal progress bar instance.
        @params:
            total      - Required  : total iterations (Int)
            prefix     - Optional  : prefix string (Str)
            suffix     - Optional  : suffix string (Str)
            decimal    - Optional  : positive number of decimals in percent complete (Int)
            length     - Optional  : character length of bar (Int):
            fill       - Optional  : bar fill character (Str)
        """
        self.total = total
        self.prefix = prefix
        self.suffix = suffix
        self.decimals = decimals
        self.length = length
        self.fill = fill


    def print_bar(self, iteration):
        """
        Updates progress bar status.
        @params:
            iteration  - Required  : current iteration (Int)
        """
        percent = ("{0:." + str(self.decimals) + "f}").format(
                        100 * (iteration / float(self.total)))
        filled_length = int(self.length * iteration // self.total)
        bar = self.fill * filled_length + '-' * (self.length - filled_length)

        print('\r{} |{}| {}% {}'.format(self.prefix, bar, percent, self.suffix), end='\r')

        # print a new line on complete
        if iteration == self.total:
            print()


# def test_progress_bar(n_items=50, length=50, sleep_interval=.2):
    
#     from time import sleep

#     pb = ProgressBar(n_items, prefix='Progress:', suffix='Complete', length=length)
#     for i in range(0, n_items):
#         sleep(sleep_interval)
#         pb.print_bar(i)

#     pb.print_bar(n_items)

# test_progress_bar(57)

In [3]:
def normalize(x):
    x /= la.norm(x)
    return x


class Shape(object):
    
    def get_color(self, surface_point=None):
        pass

    def get_normal(self, surface_point):
        pass
    
    def intersect(self, start, direction):
        return np.inf


class Sphere(Shape):
    
    def __init__(self, center, radius, surface_color, reflection=0, 
                 diffuse_color=1., specular_color=1., transparency=0, ior=1.1):
        
        self.center = center
        self.radius = radius
        self.surface_color = surface_color
        self.reflection = reflection
        self.diffuse_color = diffuse_color
        self.specular_color = specular_color
        self.transparency = transparency
        self.ior = ior
        
            
    def get_color(self, surface_point=None):
        """
        Return a color of a distinct ``surface_point``. If the sphere has
        only one color the ``surface_point`` affects nothing and can be 
        omitted.
        @params:
            surface_point  : a distinct point on the sphere surface that 
                             color is required. 
        """
        if hasattr(self.surface_color, '__len__'):
            return self.surface_color
        else:
            return self.surface_color(surface_point)
    
    
    def get_normal(self, surface_point):
        return normalize(surface_point - self.center)
    
    
    def intersect(self, start, direction):
        
        v = self.center - start
        vd = np.dot(direction, v)
        v_r = np.dot(v, v) - self.radius ** 2
        disc = vd ** 2 - v_r

        if disc > 0:
            root = np.sqrt(disc)
            t0, t1 = vd - root, vd + root
            if t0 > 0:
                return t0
            elif t1 > 0:
                return t1

        return np.inf

    
class Plane(Shape):
    
    def __init__(self, init_point, normal, surface_color, reflection=0, diffuse_color=1., specular_color=1., transparency=0):
        self.init_point = init_point
        self.normal = normal
        self.surface_color = surface_color
        self.reflection = reflection
        self.diffuse_color = diffuse_color
        self.specular_color = specular_color
        self.transparency = transparency

    
    def get_color(self, surface_point=None):
        """
        Return a color of a distinct ``surface_point``. If the plane has
        only one color the ``surface_point`` affects nothing and can be 
        omitted.
        @params:
            surface_point  : a distinct point on the sphere surface that 
                             color is required. 
        """
        if hasattr(self.surface_color, '__len__'):
            return self.surface_color
        else:
            return self.surface_color(surface_point)
        
    
    def get_normal(self, surface_point):
        return self.normal
    
        
    def intersect(self, start, direction):
    
        denom = np.dot(direction, self.normal)
        if np.abs(denom) < 1e-6:
            return np.inf

        d = np.dot(self.init_point - start, self.normal) / denom
        if d < 0:
            return np.inf

        return d

In [4]:
class Id(object):
    """
    ID generator - a singleton class.

    Example:
        ID = Id()
        Id.get()
    """

    class __Id:

        def __init__(self):
            self.generator = self.id_gen()

        def id_gen(self):
            id_ = 0
            while True:
                yield id_
                id_ += 1

        def get(self):
            return next(self.generator)
        
        def reset(self):
            self.generator = self.id_gen()

    instance = None

    def __new__(cls):
        if not Id.instance:
            Id.instance = Id.__Id()
        return Id.instance

    def __getattr__(self, name):
        return getattr(self.instance, name)

    def __setattr__(self, name):
        return setattr(self.instance, name)


class Ray(object):
    
    class Type(Enum):
        CAMERA = 0
        REFLECTED = 1
        REFRACTED = 2
    
    def __init__(self, start, direction, att=1., cum_att=1., inside=False, type_=Type.CAMERA):
        """
        Creates a new instance of ``Ray``.
        @params:
            ior     : an index of refraction of the media
            inside  : show whether ray is inside of the shape or not .
        """
        self.id = Id().get()
        self.type = type_
        
        # geometrical properties
        self.start = start
        self.direction = direction
        self.end = None
        self.length = -1
        
        # Propagation parameters including transmission media change
        self.att = att
        self.cum_att = cum_att
        self.inside = inside
    
    def __str__(self):
        string = '<{}({}) start={} end={} dir={} len={:.4}; att={:.4}({:.4}) ior={:.4}>'
        return string.format(self.type.name, self.id, self.start, self.end, 
                             self.direction, self.length, self.att,
                             self.cum_att, self.inside)


class RayNode(object):
    
    def __init__(self, ray, parent=None, tree=None):
        
        self.ray = ray
        self.parent = parent
        self.reflected = None
        self.refracted = None
        self.tree=None
        
        self.id = ray.id
        self.depth = parent.depth + 1 if parent else 0
    
    def __str__(self):
        reflected_id = str(self.reflected.ray.id) if self.reflected else '.'
        refracted_id = str(self.refracted.ray.id) if self.refracted else '.'
        self_id = str(self.ray.id)
        return '({})<({})>({})'.format(refracted_id, self_id, reflected_id)
    
    def add_child(self, ray):
        node = RayNode(ray, parent=self, tree=tree)
        
        if ray.type is Ray.Type.REFLECTED:
            self.reflected = node
        elif ray.type is Ray.Type.REFRACTED:
            self.refracted = node

    def view(self):
        refracted_view = self.refracted.view() if self.refracted else '.'
        reflected_view = self.reflected.view() if self.reflected else '.'
        return '[{}]<({})>[{}]'.format(refracted_view, str(self.ray.id), reflected_view)


class RayTree(object):
    
    def __init__(self, camera_ray):
        self.root = RayNode(camera_ray, tree=self)
        self.rays = dict((camera_ray.id, camera_ray))
        self.levels = dict((0, camera_ray))
    
    def __str__(self):
        return camera_ray.view()

In [5]:
BG_COLOR = np.array([0.05, 0., 0.2])

class RayTracer(object):
    
    def __init__(self, scene, light_point, color_light, cam_point, ambient, specular_k):
        self.scene = scene
        self.light_point = light_point
        self.color_light = color_light
        self.cam_point = cam_point
        self.ambient = ambient
        self.specular_k = specular_k


#     def 

        
    def trace_ray(self, ray_start, ray_dir):
        """
        The method computes find nearest intersected objects, compute ray
        parameters and color.
        """
        # Find the nearest intersection point with the scene.
        t = np.inf
        for s in self.scene:
            t_i = s.intersect(ray_start, ray_dir)
            if t_i < t:
                t, shape = t_i, s

        # Return None if the ray does not intersect any object.
        if t == np.inf:
            return

        # Find the intersection point on the object and properties of the object.
        hit_point = ray_start + ray_dir * t

        normal = shape.get_normal(hit_point)
        color = shape.get_color(hit_point)

        to_light = normalize(self.light_point - hit_point)
        to_cam = normalize(self.cam_point - hit_point)

        # Shadow: find if the point is shadowed or not.
        bias = normal * .0001
        
        t_next = np.inf
        for s in self.scene:
            if s != shape:
                t_i = s.intersect(hit_point + bias, to_light)
                if t_i < t_next:
                    t_next, shape_next = t_i, s
        
        shade_attenuation = 1
        if t_next < np.inf:
            if shape_next.transparency == 0:
                return
            else:
                shade_attenuation = 1 - shape_next.transparency
                
        
#         l = [s.intersect(hit_point + bias, to_light) for s in self.scene if s != shape]
#         if l and min(l) < np.inf:
#             return

        # Start computing the color.
        ray_color = self.ambient

        # Lambert shading (diffuse).
        ray_color += shape.diffuse_color * max(np.dot(normal, to_light), 0) * color

        # Blinn-Phong shading (specular).
        ray_color += shape.specular_color * (max(np.dot(normal, normalize(to_light + to_cam)), 0) 
                                             ** self.specular_k * self.color_light)

        return shape, hit_point, normal, ray_color * shade_attenuation


    def trace_path(self, ray_start, ray_dir):
    
        color = np.zeros(3)
        
        # Define a list of rays to be traced to enable tracing both reflecting and refracting rays.
        # Thus tracing procedures perform width-first traversal.
        
        primary_ray = Ray(start=ray_start, direction=ray_dir)
        rays_to_trace = [(primary_ray, 0)]

        while rays_to_trace:

            ray, depth = rays_to_trace.pop(0)
            # If ray tracing reaches depth limit stop tracing this branch.
            if depth == DEPTH_MAX:
                continue

            traced = self.trace_ray(ray.start, ray.direction)
            if not traced:
                color += BG_COLOR
                continue

            shape, hit_point, normal, ray_color = traced
            bias = normal * .0001
            neg_dn = -np.dot(ray.direction, normal)

            # Reflection: create a new ray and append it to the rays list to trace further.
            start = hit_point + bias
            direction   = ray.direction + 2 * neg_dn * normal
            
            reflected_ray = Ray(start=start, direction=direction, att=shape.reflection,
                                cum_att = ray.cum_att * shape.reflection,
                                inside=ray.inside, type_=Ray.Type.REFLECTED)
            
            rays_to_trace.append((reflected_ray, depth + 1))

            # Refraction: create a new ray and append it to the rays list to trace further.
            if shape.transparency:

                eta = shape.ior if ray.inside else 1 / shape.ior
                k = 1 - eta ** 2 * (1 - neg_dn ** 2)

                if k >= 0: # total reflection under refraction into the environment with less ior

                    start = hit_point - bias
                    direction = ray.direction * eta + normal * (eta * neg_dn - np.sqrt(k))
                    
                    refracted_ray = Ray(start=start, direction=direction, att=shape.transparency,
                                        cum_att = ray.cum_att * shape.transparency,
                                        inside=not ray.inside, type_=Ray.Type.REFRACTED)

                    rays_to_trace.append((refracted_ray, depth + 1))

            
            fresnel_effect = .1 + (1-.1) * (1 - neg_dn) ** 3
            if ray.type == Ray.Type.REFLECTED:
                pass
            elif ray.type == Ray.Type.REFRACTED:
                fresnel_effect = 1 - fresnel_effect
            else:
                fresnel_effect = 1
                
            color += ray.cum_att * ray_color
            
            # Compute current color.
#             color += ray.cum_att * ray_color

        return color

In [6]:
class Render(object):
    
    def __init__(self, scene, light_point, color_light, 
                 specular_k, ambient, cam_point,
                 screen, width, height):
        
        self.scene = scene
        self.light_point = light_point
        self.color_light = color_light
        self.specular_k = specular_k
        self.ambient = ambient
        self.cam_point = cam_point
        self.screen = screen
        self.width = width
        self.height= height
    

    def render(self):

        image = np.zeros((self.height, self.width, 3))
        cam_view = np.array([0., 0., 0.])     # Camera pointing to.
        
        tracer = RayTracer(self.scene, self.light_point, self.color_light, 
                               self.cam_point, self.ambient, self.specular_k)
        
        # Create a progress bar.
        pb_items = self.width / 10
        pb = ProgressBar(pb_items, prefix='Progress:', suffix='Complete', length=50)

        for i, x in enumerate(np.linspace(self.screen[0], self.screen[2], self.width)):

            if i % 10 == 0:
                pb.print_bar(i / 10)

            for j, y in enumerate(np.linspace(self.screen[1], self.screen[3], self.height)):
                cam_view[:2] = (x, y)
                cam_dir = normalize(cam_view - self.cam_point)

                color = tracer.trace_path(self.cam_point, cam_dir)

                image[self.height - j - 1, i, :] = np.clip(color, 0, 1)

        pb.print_bar(pb_items)
        return image

In [7]:
class SceneBuilder(object):
    
    def add_sphere(center, radius, surface_color, reflection=0):
        
        return Sphere(center=np.array(center), radius=np.array(radius), 
                      surface_color=np.array(surface_color), reflection=.5)
    
    def add_transparent_sphere(center, radius, surface_color, reflection=0):
        
        return Sphere(center=np.array(center), radius=np.array(radius), 
                      surface_color=np.array(surface_color), reflection=.1, 
                      transparency=0.9, ior=1.1)

    
    def add_plane(init_point, normal, reflection=0):
        
        color_0 = 1. * np.ones(3)
        color_1 = 0. * np.ones(3)
        color = lambda point: (color_0 
                        if (int(point[0] * 2) % 2) == (int(point[2] * 2) % 2) 
                               else color_1)
        
        return Plane(init_point=np.array(init_point), normal=np.array(normal), 
                     surface_color=color, diffuse_color=.75, 
                     specular_color=.5, reflection=.15)
    
    def build():
        return [SceneBuilder.add_transparent_sphere([.75, .3, 1.], .8, [0., 0., 1.]),
#                 SceneBuilder.add_sphere([.75, .1, 1.], .8, [0., 0., 1.]),
                SceneBuilder.add_sphere([-.75, .3, 2.25], .8, [.8, .223, .2]),
                SceneBuilder.add_sphere([-2.75, .3, 3.5], .8, [1., .472, .184]),
                SceneBuilder.add_plane([0., -.5, 0.], [0., 1., 0.]),
               ]

In [37]:
WIDTH = 400
HEIGHT = 300
DEPTH_MAX = 14

# Screen coordinates: x0, y0, x1, y1.
ASPECT_RATIO = float(WIDTH) / HEIGHT
SCREEN = (-1., -1. / ASPECT_RATIO + .25, 1., 1. / ASPECT_RATIO + .25)

render = Render(scene=SceneBuilder.build(), 
                light_point=np.array([5., 5., -10.]),
                color_light=np.ones(3),
                specular_k=50,
                ambient=.05,
                cam_point=np.array([0., 0.35, -1.]),
                screen=SCREEN,
                width=WIDTH,
                height=HEIGHT)

image = render.render()

# print(image.shape)
# def do_antialiasing(image):
#     H2 = int(np.ceil(HEIGHT / 2))
#     W2 = int(np.ceil(WIDTH / 2))
#     img = np.zeros((H2, W2, 3))
    
#     print(image.shape)
#     print(img.shape)
    
#     for i in range(H2):
#         for j in range(W2):
# #    1         print(i * 2, j * 2)
#             img[i, j, :] = .25 * (image[i * 2,     j * 2,     :] + 
#                                   image[i * 2 + 1, j * 2,     :] +
#                                   image[i * 2,     j * 2 + 1, :] + 
#                                   image[i * 2 + 1, j * 2 + 1, :])
#     return img


plt.imsave('fig1.png', image)
# plt.imsave('fig1-anti.png', do_antialiasing(image))


Progress: |██████████████████████████████████████████████████| 100.0% Complete

In [34]:
# image = np.array(image_old, copy=True)



# def srgb_encode(color, gamma):
#     assert(color.shape == (3,))
#     for i in range(3):
#         if color[i] < 0.0031308:
#             color[i] = 12.92 * color[i]
#         else:
#             color[i] = 1.055 * color[i] ** (1 / gamma) - 0.055 # inverse gamma 2.4
#     return color

# def to_srgb(image, gamma):
#     height = image.shape[0]
#     width  = image.shape[1]
    
#     image = image.reshape(height * width, 3)
    
#     for i, color in enumerate(image):
# #         print(color)
#         image[i] = srgb_encode(color, gamma)
# #         print(image[i])
    
#     return image.reshape(height, width, 3)

# plt.imsave('fig1-gamma.png', to_srgb(image, 0.5))
# # plt.imsave('fig1.png', image)

In [9]:
def id_gen():
    id_ = 0
    while True:
        yield id_
        id_ += 1

get_id = id_gen()

print(next(id_gen()))
print(next(get_id))
print(next(get_id))
print(next(get_id))
print(next(get_id))


0
0
1
2
3

In [12]:
# class Singleton(type):
#     """
#     Metaclass for all singletons in the system (e.g. Kernel).
#     The example code is taken from
#     http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Metaprogramming.html
#     """
#     instance = None

#     def __init__(cls, name, bases, attrs):
#         super().__init__(name, bases, attrs)

#     def __call__(cls, *args, **kw):
#         if not cls.instance:
#             cls.instance = super(Singleton, cls).__call__(*args, **kw)
#         return cls.instance
    
# class Dispatcher(object, metaclass=Singleton):

In [13]:
class Id(object):

    class __Id:
        
        def __init__(self):
            self.generator = self.id_gen()
        
        def id_gen(self):
            id_ = 0
            while True:
                yield id_
                id_ += 1

        def get(self):
            return next(self.generator)
        
        def reset(self):
            self.generator = self.id_gen()

    instance = None

    def __new__(cls):
        if not Id.instance:
            Id.instance = Id.__Id()
        return Id.instance

    def __getattr__(self, name):
        return getattr(self.instance, name)

    def __setattr__(self, name):
        return setattr(self.instance, name)



    
print(Id().get())
print(Id().get())
print(Id().get())
    
ID = Id()
Id().reset()

print(ID.get())
print(ID.get())
print(ID.get())
print(ID.get())
print(ID.get())

ID = Id()

print(ID.get())
print(ID.get())
print(ID.get())
print(ID.get())
print(ID.get())


0
1
2
0
1
2
3
4
5
6
7
8
9

In [5]:
import threading
import time

exitFlag = 0

class myThread (threading.Thread):
   
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter

    def run(self):
        print ("Starting " + self.name)
        print_time(self.name, self.counter, 5)
        print ("Exiting " + self.name)

def print_time(threadName, delay, counter):
    while counter:
        if exitFlag:
            threadName.exit()
        time.sleep(delay)
        print ('{}: {}'.format(threadName, time.ctime(time.time())))
        counter -= 1

# Create new threads
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# Start new Threads
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("Exiting Main Thread")


Starting Thread-1
Starting Thread-2
Thread-1: Thu May  4 15:12:22 2017
Thread-2: Thu May  4 15:12:23 2017Thread-1: Thu May  4 15:12:23 2017

Thread-1: Thu May  4 15:12:24 2017
Thread-2: Thu May  4 15:12:25 2017
Thread-1: Thu May  4 15:12:25 2017
Thread-1: Thu May  4 15:12:26 2017
Exiting Thread-1
Thread-2: Thu May  4 15:12:27 2017
Thread-2: Thu May  4 15:12:29 2017
Thread-2: Thu May  4 15:12:31 2017
Exiting Main ThreadExiting Thread-2