Список работ

OpenGL средствами pyglet

Содержание

Введение

Приводятся некоторые средства библиотеки pyglet для работы с OpenGL. Программирование выполняется на Python 3.6.6. Рассматривается вывод примитивов, заливка полигона с использованием растрового образа, применение материалов, наложение текстуры и употребление шейдеров.
Предполагается, что читатель знаком с основами компьютерной графики и OpenGL. По этой причине опускается рассмотрение вопросов проецирования, аффинных преобразований координат, систем цветов и пр., так же как и процедур и функций OpenGL.
Некоторые сведения об OpenGL можно найти, например, в [1-9].

Вывод прямоугольника и треугольников

Программирование вывода прямоугольника позволит привести код, обеспечивающий визуализацию на основе pyglet OpenGL.

import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window
dx, dy = 25, 15 # Размеры прямоугольника
dx2 = dx / 2
dy2 = dy / 2
wx, wy = 1.5 * dx2, 1.5 * dy2 # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
# Создаем окно визуализации
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Прямоугольник')
gl.glClearColor(0.1, 0.1, 0.1, 1.0) # Задаем почти черный цвет фона
gl.glClear(gl.GL_COLOR_BUFFER_BIT) # Заливка окна цветом фона
@window.event
def on_draw():
    # Проецирование
    gl.glMatrixMode(gl.GL_PROJECTION) # Теперь текущей является матрица проецирования
    gl.glLoadIdentity() # Инициализация матрицы проецирования
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1) # Ортографическое проецирование
    gl.glColor3f(0, 1, 0) # Зеленый цвет
    gl.glBegin(gl.GL_QUADS) # Обход против часовой стрелки
    gl.glVertex3f(-dx2, -dy2, 0)
    gl.glVertex3f(dx2, -dy2, 0)
    gl.glVertex3f(dx2, dy2, 0)
    gl.glVertex3f(-dx2, dy2, 0)
    gl.glEnd()
app.run()

Результат показан на рис. 1.

Прямоугольник

Рис. 1. Прямоугольник

Тот же результат получим, применив graphics.draw [10] (приводится только процедура on_draw).

@window.event
def on_draw():
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    graphics.draw(4, gl.GL_QUADS,
                  ('v2f', (-dx2, -dy2, dx2, -dy2, dx2, dy2, -dx2, dy2)),
                  ('c3f', (0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0)))

Первый параметр – это число вершин, далее следуют кортежи с координатами вершин и их цветом.
Цвет можно задать перед graphics.draw:

    gl.glColor3f(0, 1, 0)
    graphics.draw(4, gl.GL_QUADS,
                 ('v2f', (-dx2, -dy2, dx2, -dy2, dx2, dy2, -dx2, dy2)))

Так же можно употребить graphics.vertex_list:

@window.event
def on_draw():
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    v_lst = graphics.vertex_list(4,
                                 ('v2f', (-dx2, -dy2, dx2, -dy2, dx2, dy2, -dx2, dy2)),
                                 ('c3f', (0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0)))
    v_lst.draw(gl.GL_QUADS)

При выводе нескольких примитивов, например треугольников, с общими вершинами более компактным будет применение graphics.draw_indexed, например:

@window.event
def on_draw():
    # Проецирование
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    graphics.draw_indexed(4, gl.GL_TRIANGLES,
                          [0, 1, 2, 1, 3, 2],
                          ('v2f', (-dx2, -dy2, 0, -dy / 4, 0, dy2, dx2, -dy2)),
                          ('c3f', (1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0)))

Всего для вывода двух смежных треугольников (рис. 2 ) достаточно задать 4 вершины.

Треугольники

Рис. 2. Треугольники

Чтобы отключить интерполяцию цветов, перед graphics.draw_indexed следует задать

gl.glShadeModel(gl.GL_FLAT) # По умолчанию употребляется GL_SMOOTH

Будет использован цвет вершины с индексом 2 – (0, 1, 0).

Вывод точек

Для точки можно задать ее размер:

gl.glPointSize(16)

и режим сглаживания:

gl.glEnable(gl.GL_POINT_SMOOTH)

Отобразим 8 точек: 4, используя glBegin / glEnd, и 4 сглаженные, применив draw, (рис. 3):

import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window
d, d2 = 12, 10
wx, wy = 1.5 * d, 1.2 * d # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Точки')
gl.glClearColor(0.1, 0.1, 0.1, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
@window.event
def on_draw():
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    gl.glPointSize(16)
    gl.glBegin(gl.GL_POINTS)
    gl.glColor3f(1, 0, 0)
    gl.glVertex3f(-d, -d, 0)
    gl.glColor3f(0, 1, 0)
    gl.glVertex3f(-d, d, 0)
    gl.glColor3f(0, 0, 1)
    gl.glVertex3f(d, d, 0)
    gl.glColor3f(1, 1, 0)
    gl.glVertex3f(d, -d, 0)
    gl.glEnd()
    gl.glEnable(gl.GL_POINT_SMOOTH)
    graphics.draw(4, gl.GL_POINTS,
                  ('v2f', (-d2, -d2, -d2, d2, d2, d2, d2, -d2)),
                  ('c3B', (0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0)))
    gl.glDisable(gl.GL_POINT_SMOOTH)
app.run()

Точки

Рис. 3. Точки

Вывод линий

Для линии можно задать ее толщину:

gl.glLineWidth(4)

и образец (маску):

gl.glEnable(gl.GL_LINE_STIPPLE)
pattern = '0b1111100110011111' # '1111100110011111'
gl.glLineStipple(2, int(pattern, 2)) # Повторяем каждый бит образца 2 раза

Линии (рис. 4) выводятся после задания GL_LINES, или GL_LINE_STRIP, или GL_LINE_LOOP:

import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window
d, d2, d3 = 12, 8, 6
wx, wy = 1.5 * d, 1.2 * d # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Линии')
gl.glClearColor(0.1, 0.1, 0.1, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
@window.event
def on_draw():
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    gl.glLineWidth(4)
    gl.glBegin(gl.GL_LINES)
    gl.glColor3f(1, 0, 0)
    gl.glVertex3f(-d, -d, 0)
    gl.glVertex3f(-d, d, 0)
    gl.glColor3f(0, 1, 0)
    gl.glVertex3f(-d, d, 0)
    gl.glVertex3f(d, d, 0)
    gl.glColor3f(0, 0, 1)
    gl.glVertex3f(d, d, 0)
    gl.glVertex3f(d, -d, 0)
    gl.glColor3f(1, 1, 0)
    gl.glVertex3f(d, -d, 0)
    gl.glVertex3f(-d, -d, 0)
    gl.glEnd()
    graphics.draw(5, gl.GL_LINE_STRIP,
                  ('v2f', (-d, -d, -d, d, d, d, d, -d, -d, -d)),
                  ('c3f', (1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0)))
    gl.glEnable(gl.GL_LINE_STIPPLE)
    pattern = '0b1111100110011111' # '1111100110011111'
    gl.glLineStipple(2, int(pattern, 2)) # Повторяем каждый бит образца 2 раза
    gl.glColor3f(1, 1, 1)
    gl.glBegin(gl.GL_LINE_STRIP)
    gl.glVertex3f(-d2, -d2, 0)
    gl.glVertex3f(-d2, d2, 0)
    gl.glVertex3f(d2, d2, 0)
    gl.glVertex3f(d2, -d2, 0)
    gl.glVertex3f(-d2, -d2, 0)
    gl.glEnd()
    graphics.draw(4, gl.GL_LINE_LOOP,
                  ('v2f', (-d3, -d3, -d3, d3, d3, d3, d3, -d3)))
    gl.glDisable(gl.GL_LINE_STIPPLE)
app.run()

Линии

Рис. 4. Линии

Лицевая и нелицевая стороны полигона

Лицевая сторона будет показана, если при выводе многоугольника его вершины обходятся против часовой стрелки, в противном случае будет наблюдаться нелицевая сторона многоугольника. Каждая из сторон может быть показана либо в виде точек в вершинах многоугольника, либо в виде линий, либо залита одним цветом, либо залита с использованием интерполяции цветов, указанных в вершинах многоугольника, либо залита с использованием образца, текстуры или материала. Эти возможности, кроме трех последних, демонстрирует нижеприводимая программа, в которой первоначально левый прямоугольник выводится лицевой стороной, а правый – нелицевой, причем с интерполяцией цветов (рис. 5), поскольку вершины имеют разные цвета, а по умолчанию имеем glShadeModel(GL_SMOOTH).

GL_FRONT_AND_BACK: GL_FILL

Рис. 5. По умолчанию для GL_FRONT_AND_BACK установлено GL_FILL

Затем после нажатия на клавишу 1 лицевая сторона выводится в виде точек, а нелицевая – в виде линий (рис. 6).

После нажатия на 1

Рис. 6. После нажатия на 1

После нажатия на клавишу 2 картина меняется: лицевая сторона выводится в виде линий, а нелицевая – в виде точек (рис. 7).

После нажатия на 2

Рис. 7. После нажатия на 2

После нажатия на 3 оба прямоугольника заливаются без интерполяции цветов (рис. 8), а после нажатия на 4 получаем рис. 5.

После нажатия на 3

Рис. 8. После нажатия на 3

import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window, key
d = 12
d2 = 6
wx, wy = 1.5 * d, 1.2 * d2 # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Способы вывода многоугольника')
gl.glClearColor(0.1, 0.1, 0.1, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glEnable(gl.GL_POINT_SMOOTH)
gl.glPointSize(16)
gl.glLineWidth(4)
@window.event
def on_draw():
    window.clear()
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    gl.glBegin(gl.GL_QUADS)
    gl.glColor3f(0, 1, 0)
    gl.glVertex3f(-d, -d2, 0)
    gl.glVertex3f(-1, -d2, 0)
    gl.glVertex3f(-1, d2, 0)
    gl.glVertex3f(-d, d2, 0)
    gl.glEnd()
    graphics.draw(4, gl.GL_QUADS,
                  ('v2f', (1, -d2, 1, d2, d, d2, d, -d2)),
                  ('c3f', (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1)))
@window.event
def on_key_press(symbol, modifiers):
    mode_f = mode_b = None
    c = chr(symbol)
    if c == '1': # c == key._1
        mode_f = gl.GL_POINT
        mode_b = gl.GL_LINE
        shade_model = gl.GL_SMOOTH
    elif symbol == key._2:
        mode_f = gl.GL_LINE
        mode_b = gl.GL_POINT
        shade_model = gl.GL_SMOOTH
    elif symbol == key._3:
        mode_f = mode_b = gl.GL_FILL
        shade_model = gl.GL_FLAT
    elif symbol == key._4:
        mode_f = mode_b = gl.GL_FILL
        shade_model = gl.GL_SMOOTH
    if mode_f is not None:
        gl.glPolygonMode(gl.GL_FRONT, mode_f)
        gl.glPolygonMode(gl.GL_BACK, mode_b)
        gl.glShadeModel(shade_model)
app.run()

Заливка полигона с использованием образца

Образец размера 32*32 бита приведен на рис. 9.

Образец

Рис. 9. Образец

Он задается массивом mask из 128 элементов. Тип массива __main__.c_ubyte_Array_128. Каждые 4 элемента массива содержат 32 бита и определяют одну строку образца. Образец используется после задания

gl.glEnable(gl.GL_POLYGON_STIPPLE)
gl.glPolygonStipple(mask)

Результат показан на рис. 10.

Заливка по образцу

Рис. 10. Заливка по образцу

import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window, key
d = 12
d2 = 6
wx, wy = 1.5 * d, 1.2 * d2 # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
# Муха
mask0 = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x01, 0xc0, 0x06, 0xc0, 0x03, 0x60,
        0x04, 0x60, 0x06, 0x20, 0x04, 0x30, 0x0c, 0x20, 0x04, 0x18, 0x18, 0x20, 0x04, 0x0c, 0x30, 0x20,
        0x04, 0x06, 0x60, 0x20, 0x44, 0x03, 0xc0, 0x22, 0x44, 0x01, 0x80, 0x22, 0x44, 0x01, 0x80, 0x22,
        0x44, 0x01, 0x80, 0x22, 0x44, 0x01, 0x80, 0x22, 0x44, 0x01, 0x80, 0x22, 0x44, 0x01, 0x80, 0x22,
        0x66, 0x01, 0x80, 0x66, 0x33, 0x01, 0x80, 0xcc, 0x19, 0x81, 0x81, 0x98, 0x0c, 0xc1, 0x83, 0x30,
        0x07, 0xe1, 0x87, 0xe0, 0x03, 0x3f, 0xfc, 0xc0, 0x03, 0x31, 0x8c, 0xc0, 0x03, 0x33, 0xcc, 0xc0,
        0x06, 0x64, 0x26, 0x60, 0x0c, 0xcc, 0x33, 0x30, 0x18, 0xcc, 0x33, 0x18, 0x10, 0xc4, 0x23, 0x08,
        0x10, 0x63, 0xc6, 0x08, 0x10, 0x30, 0x0c, 0x08, 0x10, 0x18, 0x18, 0x08, 0x10, 0x00, 0x00, 0x08]
dim = 128
mask = (gl.GLubyte * dim)()
for k in range(dim):
    mask[k] = mask0[k]
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Заливка по образцу')
gl.glClearColor(0.1, 0.1, 0.1, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
@window.event
def on_draw():
    window.clear()
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    gl.glEnable(gl.GL_POLYGON_STIPPLE)
    gl.glPolygonStipple(mask)
    gl.glBegin(gl.GL_QUADS)
    gl.glColor3f(0, 1, 0)
    gl.glVertex3f(-d, -d2, 0)
    gl.glVertex3f(-1, -d2, 0)
    gl.glVertex3f(-1, d2, 0)
    gl.glVertex3f(-d, d2, 0)
    gl.glEnd()
    graphics.draw(4, gl.GL_QUADS,
                  ('v2f', (1, -d2, 1, d2, d, d2, d, -d2)),
                  ('c3f', (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1)))
app.run()

Использование текстуры

Вывод текстуры регулируется ее координатами (glTexCoord2f) и параметрами (glTexParameterf). Цвет полигона определяется либо чисто текстурой, либо в результате смешения цвета полигона и текстуры (glTexEnvf).
Текстура обычно создается на основе растрового образа, загружаемого из файла (рис. 11), но может быть создана и программно (рис. 12).

Текстура

Рис. 11. Текстура на основе файла; tile_x = 2, tile_y = 1

tile_x, tile_y – соответственно число повторов текстуры по X и Y.

Текстура

Рис. 12. Программно сгенерированная текстура; tile_x = tile_y = 1

Оба результата получены следующим кодом:

from pyglet.gl import *
from pyglet import app
from pyglet.window import Window, key
d = 12
wx, wy = 1.5 * d, 1.1 * d # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
texFromFile = True # True False
if texFromFile:
    tile_x = 2 # Число повторов текстуры по X
    tile_y = 1 # Число повторов текстуры по Y
else:
    tile_x = tile_y = 1
def to_c_float_Array(data): # Преобразование в си-массив
    return (GLfloat * len(data))(*data)
vld = to_c_float_Array([-d, -d, 0]) # Левая нижняя вершина
vrd = to_c_float_Array([d, -d, 0]) # Правая нижняя вершина
vru = to_c_float_Array([d, d, 0]) # Правая верхняя вершина
vlu = to_c_float_Array([-d, d, 0]) # Левая верхняя вершина
#
def texInit():
    if texFromFile:
        fn = 'G:\\python\\openGL\\кот.jpg'
        img = pyglet.image.load(fn)
        iWidth = img.width
        iHeight = img.height
        img = img.get_data('RGB', iWidth * 3)
    else:
        iWidth = iHeight = 64 # Размер текстуры равен iWidth * iHeight
        n = 3 * iWidth * iHeight
        # Каждый элемент текстуры содержит три компонента (формат текстуры GL_RGB)
        # GL_UNSIGNED_BYTE - это диапазон 0-255, поэтому для img задается тип uint8, а затем GLubyte
        img = np.zeros((3, iWidth, iHeight), dtype = 'uint8')
        for i in range(iHeight): # Генерация черно-белого образа, на основе которого создается текстура
         for j in range(iWidth):
            img[:, i, j] = ((i - 1) & 16 ^ (j - 1) & 16) * 255
        img = img.reshape(n)
        img = (GLubyte * n)(*img)
    p = GL_TEXTURE_2D
    r = GL_RGB
    # Задаем параметры текстуры
    glTexParameterf(p, GL_TEXTURE_WRAP_S, GL_REPEAT) # GL_CLAMP
    glTexParameterf(p, GL_TEXTURE_WRAP_T, GL_REPEAT)
    glTexParameterf(p, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameterf(p, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    # Способ взаимодействия с текущим фрагментом изображения
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
    # Создаем 2d-текстуру на основе образа img
    glTexImage2D(p, 0, r, iWidth, iHeight, 0, r, GL_UNSIGNED_BYTE, img)
    glEnable(p)
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Текстура')
glClearColor(0.1, 0.1, 0.1, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
texInit()
@window.event
def on_draw():
    window.clear()
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-wx, wx, -wy, wy, -1, 1)
    glBegin(GL_QUADS)
    glTexCoord2f(0, 0);
    glVertex3fv(vld)
    glTexCoord2f(tile_x, 0)
    glVertex3fv(vrd)
    glTexCoord2f(tile_x, tile_y)
    glVertex3fv(vru)
    glTexCoord2f(0, tile_y)
    glVertex3fv(vlu)
    glEnd()
app.run()

Использование материала

Материалы позволяют получать, с одной стороны, изображения, похожие на реальные, так и реализовывать различные цветовые фантазии. Материал применяется в режиме расчета освещенности:

gl.glEnable(gl.GL_LIGHTING)

Помимо этого задаются цвет материала, цвет источника света, его позиция и нормали к вершинам полигонов. На рис. 13 показан прямоугольник, в трех вершинах которого нормали обращены к источнику света, в четвертой вершине угол между нормалью и направлением света близок к 180°. По этой причине цвет этой вершины почти черный.

Материал

Рис. 13. Выполнен расчет освещенности

import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window, key
import numpy as np
from sys import exit
d = 12
wx, wy = 1.5 * d, 1.1 * d # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
#
mtClr0 = [1, 1, 0, 0] # Цвет материала
light_position0 = [0, 40, 40, 0] # Позиция источника света
lghtClr0 = [0.75, 0, 0, 0] # Цвет источника света
mtClr = (gl.GLfloat * 4)()
light_position = (gl.GLfloat * 4)()
lghtClr = (gl.GLfloat * 4)()
for k in range(4): mtClr[k] = mtClr0[k]
for k in range(4): light_position[k] = light_position0[k]
for k in range(4): lghtClr[k] = lghtClr0[k]
#
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Материал')
gl.glClearColor(0.1, 0.1, 0.1, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glEnable(gl.GL_LIGHTING) # Активизируем использование материалов
@window.event
def on_draw():
    window.clear()
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    gl.glShadeModel(gl.GL_SMOOTH) # GL_FLAT - без интерполяции цветов
    gl.glMaterialfv(gl.GL_FRONT, gl.GL_SPECULAR, mtClr)
    gl.glLightfv(gl.GL_LIGHT0, gl.GL_POSITION, light_position)
    gl.glLightfv(gl.GL_LIGHT0, gl.GL_SPECULAR, lghtClr)
    gl.glEnable(gl.GL_LIGHT0) # Включаем в уравнение освещенности источник GL_LIGHT0
    gl.glColor3f(1, 0, 0)
    gl.glBegin(gl.GL_QUADS)
    gl.glNormal3f(0, 0, 1)
    gl.glVertex3f(-d, -d, 0)
    gl.glNormal3f(0, 0, 1)
    gl.glVertex3f(d, -d, 0)
    gl.glNormal3f(0, 0, 1)
    gl.glVertex3f(d, d, 0)
    gl.glNormal3f(0, 0, -1)
    gl.glVertex3f(-d, d, 0)
    gl.glEnd()
@window.event
def on_key_press(symbol, modifiers):
    if symbol == key._1:
        gl.glDisable(gl.GL_LIGHTING)
    elif symbol == key._2:
        gl.glEnable(gl.GL_LIGHTING)
app.run()

Цвет полигона будет определяться glColor3f, если отказаться от использования материала:

gl.glDisable(gl.GL_LIGHTING)

Это произойдет при нажатии на 1. После нажатия на 2 материал будет применен вновь.

Вывод параболоида

Выводится параболоид (рис. 14)

y = a*x2 + a*z2

Параболоид

Рис. 14. Параболоид

Нормали рассчитываются к граням в каждой вершине параболоида (гранью является трапеция).
Разницу между нормалью к грани и к вершине иллюстрирует рис. 14.

Нормаль

Рис. 15. Порядок вывода вершин, нормаль к грани и нормаль к вершине

Вычисление нормали:

import numpy as np
n = np.cross(a, b) # Векторное произведение
n = n / np.linalg.norm(n) # Нормализация
или
n = n / np.sqrt(np.sum(n**2)) # Нормализация

Замечание.OpenGL выполнит нормализацию самостоятельно, если задать:

glEnable(GL_NORMALIZE)

Нормали к грани задаются во всех вершинах (нормали к вершине не вычисляются, поэтому видны слои фигуры даже при использовании GL_SMOOTH).
Параболоид с нормалями показан на рис. 16.

С нормалями

Рис. 16. Параболоид с нормалями

После отказа от сглаживания видны отдельные грани:

GL_FLAT

Рис. 17. Параболоид: gl.glShadeModel(gl.GL_FLAT)

При выводе текстуры (сгенерированной или из файла) имеем рис. 18.

Текстура

Рис. 18. Параболоид с текстурой

# Вывод параболоида
import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window, key
import numpy as np, math
from sys import exit
# z = ac*x**2 + ac*y**2 (реально имеем: y = ac*x**2 + ac*z**2)
save_to_file = True # Флаг записи модели в файлы
read_from_file = True # Флаг чтения модели из файлов
if read_from_file: save_to_file = False
plot_surf = False # Флаг вывода поверхности z = ac*x**2 + ac*y**2
show_top = True # Флаг вывода крышки
show_normals = True
kn = 2 # Коэффициент увеличения длины отображаемой нормали
ac = 2.5 # Коэффициент в уравнении параболоида
n = 24 # Число вершин в сечении параболоида
ch = 7 # Число сечений параболоида плоскостями y = const
h_cover = 20 # Высота параболоида
vrts = [] # Координаты вершин параболоида
nrms = [] # Координаты нормалей к граням (рассчитываются для каждой вершины)
textr = [] # Координаты текстуры (задаются для каждой вершины)
vrts_top = [] # Координаты вершин крышки
textr_top = [] # Координаты тектстуры крышки
dh = h_cover / ch # Расстояние между сечениями
dal = 2 * math.pi / n # dal - угол между вершинами сечения
hp0 = 0.1 # Низ параболоида
# Текстура
use_txtr = True
texFromFile = True # True False
if use_txtr: show_normals = False
# Координаты текстуры меняются в диапазоне 0-1
dnx = 1 / (n - 1) # Шаг изменения координат текстуры по x
dny = 1 / ch # Шаг изменения координат текстуры по y
if read_from_file:
    print('Загрузка данных из двоичных файлов')
    def load_data(file, shape = None):
        with open(file, 'rb') as r_b:
            data = np.fromfile(r_b)
        if shape is not None:
            size = len(data)
            if len(shape) == 2:
                size = int(size / (shape[0] * shape[1]))
                data = data.reshape(size, shape[0], shape[1])
            else:
                size = int(size / shape[0])
                data = data.reshape(size, shape[0])
        return data
    vrts = load_data('vrts.bin', [4, 3])
    vrts_top = load_data('vrts_top.bin', [3])
    nrms = load_data('nrms.bin', [3])
    nrm_top = load_data('nrm_top.bin', None)
    textr = load_data('textr.bin', [2])
    textr_top = load_data('textr_top.bin', [2])
else:
    hp = hp0
    for i in range(ch): # Вершины
        al = 0 # Первая вершина лежит на оси Х
        s = ac * math.sqrt(hp)
        h0 = hp
        hp += dh
        s2 = ac * math.sqrt(hp)
        for j in range(n):
            co = math.cos(al)
            si = math.sin(al)
            al += dal
            co2 = math.cos(al)
            si2 = math.sin(al)
            # Координаты вершин очередной трапеции
            v0 = np.array([s * co, h0, -s * si])
            v1 = np.array([s * co2, h0, -s * si2])
            v2 = np.array([s2 * co2, hp, -s2 * si2])
            v3 = np.array([s2 * co, hp, -s2 * si])
            vrts.append([v0, v1, v2, v3])
            a = v1 - v0
            b = v3 - v0
            sab = np.cross(a, b) # Векторное произведение
            sab = sab / np.linalg.norm(sab) # Нормализация
            #sab = sab / np.sqrt(np.sum(sab**2))
            nrms.append(sab) # Координаты нормали
            textr.append([j * dnx, (h0 - hp0) / h_cover]) # Координаты текстуры
    n_vrts = len(vrts)
    # Крышка
    al = 0
    kt = 0.25
    for j in range(n):
        co = math.cos(al); si = math.sin(al)
        al += dal
        vrts_top.append([s2 * co, hp, -s2 * si])
        nrms.append(nrms[n_vrts - n + j])
        textr.append([j * dnx, 1])
        if texFromFile:
            j2 = 2 * j * dnx
            if j <= n / 4:
                textr_top.append([j2, 0.5 - j2])
            elif j <= n / 2:
                textr_top.append([j2, j2 - 0.5])
            elif j <= 3 * n / 4:
                textr_top.append([2 - j2, j2 - 0.5])
            else:
                textr_top.append([2 - j2, 2.5 - j2])
        else:
            textr_top.append([kt * co, kt * si])
    v0 = np.array(vrts_top[0])
    v1 = np.array(vrts_top[1])
    vn = np.array(vrts_top[n - 1])
    nrm_top = np.cross(v1 - v0, vn - v0) # Нормаль к крышке
    nrm_top = nrm_top / np.linalg.norm(sab)
if save_to_file:
    print('Запись данных в двоичные файлы')
    def write_to_bin(file, data):
        fn = open(file, 'wb')
        fn.write(np.array(data))
        fn.close()
    write_to_bin('vrts.bin', vrts)
    write_to_bin('vrts_top.bin', vrts_top)
    write_to_bin('nrms.bin', nrms)
    write_to_bin('nrm_top.bin', nrm_top)
    write_to_bin('textr.bin', textr)
    write_to_bin('textr_top.bin', textr_top)
if plot_surf:
    from mpl_toolkits.mplot3d import Axes3D # Для projection = '3d'
    from matplotlib import cm
    import matplotlib.pyplot as plt
    xy, z = [], []
    x_min = x_max = vrts[0][0][0]
    y_min = y_max = vrts[0][0][2]
    for quad in vrts:
        for v in quad:
            p = [v[0], v[2]]
            if not p in xy:
                xy.append(p)
                z.append(v[1])
                if p[0] < x_min: x_min = p[0]
                if p[0] > x_max: x_max = p[0]
                if p[1] < y_min: y_min = p[1]
                if p[1] > y_max: y_max = p[1]
    step = 0.5
    X = np.arange(x_min, x_max, step) # Формирование сетки
    Y = np.arange(y_min, y_max, step)
    X, Y = np.meshgrid(X, Y)
    Z = ac*X**2 + ac*Y**2 # Формируем массив Z формы (len(X), len(Y))
    fig = plt.figure()
    ax = fig.gca(projection = '3d')
    surf = ax.plot_surface(X, Y, Z, cmap = cm.plasma) # plasma Spectral
    ax.set_xlabel('X') # Метки осей координат
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    for label in ax.xaxis.get_ticklabels(): # Настройка оси X
        label.set_color('black')
        label.set_rotation(-45)
        label.set_fontsize(9)
    for label in ax.yaxis.get_ticklabels(): # Настройка оси Y
        label.set_fontsize(9)
    for label in ax.zaxis.get_ticklabels(): # Настройка оси Z
        label.set_fontsize(9)
    ax.view_init(elev = 30, azim = 45) # Проецирование
    fig.colorbar(surf, shrink = 0.5, aspect = 5) # Шкала цветов
    plt.show() # Отображение результата
    exit()
#
def c_float_Array(data): # Преобразование в си-массив
    return (gl.GLfloat * len(data))(*data)
lghtClr0 = [0.75, 0, 0, 0]
mtClr = c_float_Array([1, 1, 0, 0])
light_position = c_float_Array([-80, 20, 90, 0])
lghtClr = c_float_Array([0.75, 0, 0, 0])
#
def texInit():
    if texFromFile:
        fn = 'G:\\python\\openGL\\кот.jpg'
        img = pyglet.image.load(fn)
        iWidth = img.width
        iHeight = img.height
        img = img.get_data('RGB', iWidth * 3)
    else:
        iWidth = iHeight = 64
        n = 3 * iWidth * iHeight
        img = np.zeros((3, iWidth, iHeight), dtype = 'uint8')
        for i in range(iHeight): # Генерация черно-белого образа, на основе которого создается текстура
         for j in range(iWidth):
            img[:, i, j] = ((i - 1) & 16 ^ (j - 1) & 16) * 255
        img = img.reshape(n)
        img = (gl.GLubyte * n)(*img)
    p = gl.GL_TEXTURE_2D
    r = gl.GL_RGB
    gl.glTexParameterf(p, gl.GL_TEXTURE_WRAP_S, gl.GL_REPEAT)
    gl.glTexParameterf(p, gl.GL_TEXTURE_WRAP_T, gl.GL_REPEAT)
    gl.glTexParameterf(p, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
    gl.glTexParameterf(p, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
    gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_DECAL)
    gl.glTexImage2D(p, 0, r, iWidth, iHeight, 0, r, gl.GL_UNSIGNED_BYTE, img)
    if use_txtr:
        gl.glEnable(p)
w = h = h_cover
width, height = 20 * w, 10 * w
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Параболоид')
gl.glClearColor(1, 1, 1, 1) # Белый цвет фона
gl.glClear(gl.GL_COLOR_BUFFER_BIT|gl.GL_DEPTH_BUFFER_BIT)
gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
gl.glLineWidth(2)
gl.glPointSize(4)
gl.glEnable(gl.GL_POINT_SMOOTH)
gl.glShadeModel(gl.GL_SMOOTH) # GL_SMOOTH, GL_FLAT - без интерполяции цветов
gl.glCullFace(gl.GL_BACK) # Запрещен вывод граней, показанных нелицевой стороной
gl.glEnable(gl.GL_CULL_FACE) # Активизируем режим GL_CULL_FACE
##gl.glEnable(gl.GL_NORMALIZE)
if show_normals:
    gl.glEnable(gl.GL_DEPTH_TEST) # Активизируем тест глубины
texInit()
@window.event
def on_draw():
    window.clear()
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-w, w, -0.5 * h, h, -w, w) # Ортографическое проецирование
    gl.glRotatef(30, 1, 0, 0) # Поворот относительно оси X
    if use_txtr and texFromFile:
        gl.glRotatef(90, 0, 1, 0) # Поворот относительно оси Y
    gl.glTranslatef(0, -7, 0) # Перенос объекта вниз (вдоль оси Y)
    gl.glMaterialfv(gl.GL_FRONT, gl.GL_SPECULAR, mtClr)
    gl.glLightfv(gl.GL_LIGHT0, gl.GL_POSITION, light_position)
    gl.glLightfv(gl.GL_LIGHT0, gl.GL_SPECULAR, lghtClr)
    gl.glEnable(gl.GL_LIGHTING) # Активизируем заданные параметры освещенности
    gl.glEnable(gl.GL_LIGHT0)
    gl.glBegin(gl.GL_QUADS)
    k = -1
    for i in range(ch):
        for j in range(n):
            k += 1
            v = vrts[k]
            v0 = v[0]; v1 = v[1]; v2 = v[2]; v3 = v[3]
            sn = nrms[k] # Нормаль к вершине (она же нормаль к грани)
            gl.glNormal3f(sn[0], sn[1], sn[2])
            tc = textr[k] # Координаты текстуры
            gl.glTexCoord2f(tc[0], tc[1])
            gl.glVertex3f(v0[0], v0[1], v0[2])
            sn = nrms[k + 1]
            gl.glNormal3f(sn[0], sn[1], sn[2])
            gl.glTexCoord2f(tc[0] + dnx, tc[1])
            gl.glVertex3f(v1[0], v1[1], v1[2])
            sn = nrms[k + n]
            gl.glNormal3f(sn[0], sn[1], sn[2])
            gl.glTexCoord2f(tc[0] + dnx, tc[1] + dny)
            gl.glVertex3f(v2[0], v2[1], v2[2])
            sn = nrms[k + n - 1]
            gl.glNormal3f(sn[0], sn[1], sn[2])
            gl.glTexCoord2f(tc[0], tc[1] + dny)
            gl.glVertex3f(v3[0], v3[1], v3[2])
    gl.glEnd() # Заканчиваем вывод боковых граней
    if show_top:
        gl.glBegin(gl.GL_POLYGON) # Вывод крышки
        gl.glNormal3f(nrm_top[0], nrm_top[1], nrm_top[2]) # Нормаль к крышке
        for j in range(n):
            v = vrts_top[j]
            tc = textr_top[j]
            gl.glTexCoord2f(tc[0], tc[1])
            gl.glVertex3f(v[0], v[1], v[2])
        gl.glEnd() # Заканчиваем вывод крышки
    if show_normals:
        # Вывод нормалей
        gl.glDisable(gl.GL_LIGHTING) # Отключаем расчет освещенности
        gl.glLineWidth(2) # Задание толщины линии
        gl.glColor3f(0, 0, 0) # Задание текущего цвета
        gl.glBegin(gl.GL_LINES)
        k = -1
        for i in range(ch):
            for j in range(n):
                k += 1
                v = vrts[k][0]
                gl.glVertex3f(v[0], v[1], v[2])
                sn = nrms[k]
                sx = kn * sn[0]
                sy = kn * sn[1]
                sz = kn * sn[2]
                gl.glVertex3f(v[0] + sx, v[1] + sy, v[2] + sz)
                # Нормали в верхнем сечении совпадают с нормалями в предыдущем сечении
                if i == ch - 1:
                    v = vrts[k][3]
                    gl.glVertex3f(v[0], v[1], v[2])
                    gl.glVertex3f(v[0] + sx, v[1] + sy, v[2] + sz)
        gl.glEnd()
        if show_top:
            gl.glColor3f(1, 1, 1) # Текущий цвет
            gl.glBegin(gl.GL_LINES) # Нормаль к крышке
            gl.glVertex3f(0, h_cover, 0)
            gl.glVertex3f(kn * nrm_top[0], h_cover + kn * nrm_top[1], kn * nrm_top[2])
            gl.glEnd()
@window.event
def on_key_press(symbol, modifiers):
    if symbol == key._1:
        gl.glDisable(gl.GL_TEXTURE_2D)
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
        gl.glShadeModel(gl.GL_SMOOTH)
        gl.glEnable(gl.GL_CULL_FACE)
    elif symbol == key._2:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
        gl.glShadeModel(gl.GL_FLAT)
        gl.glEnable(gl.GL_CULL_FACE)
    elif symbol == key._3:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_LINE) # Вывод ребер
    elif symbol == key._4:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_POINT)
    elif symbol == key._5:
        gl.glDisable(gl.GL_CULL_FACE)
app.run()

Сведения о созданной модели будут записаны в бинарные файлы, если

save_to_file = True

и

read_from_file = False

Если read_from_file = True, то модель будет прочитана из ранее созданных бинарных файлов.
Если plot_surf = True, то по уравнению параболоида будет выведена его поверхность (рис. 19).

Параболоид

Рис. 19. Параболоид

Использование нескольких текстур

Рассматривается на примере вывода игральной кости (рис. 20).

Кость

Рис. 20. Используется 6 текстур

Текстуры создаются на основе 6 файлов, содержащих приведенные на рис. 21 изображения.

Стороны кости

Рис. 21. Стороны игральной кости

Помимо куба с текстурами, приводимая ниже программа показывает после нажатия на клавиши 2-6 разные способы вывода куба (рис. 22).

Куб

Рис. 22. После нажатия на 2, 3, ..., 6

После нажатия на 1 возвращаемся к игральной кости (рис. 20). После нажатия на 4 растут размеры точек в вершинах куба до тех пор, пока не достигнут максимально возможного значения:

elif symbol == key._4:
    ps = (gl.GLfloat * 1)()
    ps_range = (gl.GLfloat * 2)()
    gl.glGetFloatv(gl.GL_POINT_SIZE, ps) # Получаем текущий размер точки
    # Допустимый диапазон изменения размеров точки
    gl.glGetFloatv(gl.GL_POINT_SIZE_RANGE, ps_range)
    ps = ps[0]
    ps = (ps + 2) if ps < ps_range[1] - 2 else 4
    gl.glPointSize(ps)
    gl.glPolygonMode(gl.GL_FRONT, gl.GL_POINT)

Программа вывода куба с текстурой и без нее:

# Игральная кость
import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window, key
h = 20 # Половины длины ребра куба
w = 2 * h # Для задания области вывода
width = height = 300 # Размер окна вывода
textureIDs = (gl.GLuint * 6)() # Массив идентификаторов (номеров) текстур
p = gl.GL_TEXTURE_2D
tc = 1 # Число повторов текстуры
rot_x = 15 # Углы поворота вокруг осей X, Y и Z
rot_y = 25 # (15, 25, 15) или (-25, 215, -15)
rot_z = 15
verts = ((h, -h, -h), # <class 'tuple'> Координаты вершин куба
         (h, h, -h),
         (-h, h, -h),
         (-h, -h, -h),
         (h, -h, h),
         (h, h, h),
         (-h, -h, h),
         (-h, h, h))
faces = ((0, 1, 2, 3), # Индексы вершин граней куба
         (3, 2, 7, 6),
         (6, 7, 5, 4),
         (4, 5, 1, 0),
         (1, 5, 7, 2),
         (4, 0, 3, 6))
clrs = ((1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0),
        (0, 1, 1), (1, 1, 1), (1, 0, 0), (0, 1, 0),
        (0, 0, 1), (1, 1, 0), (0, 1, 1), (1, 1, 1))
t_coords = ((0, 0), (0, tc), (tc, tc), (tc, 0)) # Координаты текстуры
# Индексы ребер куба (используется при выводе линий вдоль ребер куба)
edges = ((0, 1), (0, 3), (0, 4), (2, 1), (2, 3), (2, 7),
         (6, 3), (6, 4), (6, 7), (5, 1), (5, 4), (5, 7))
def texInit(): # Формирование текстур
    gl.glGenTextures(6, textureIDs)
    r = gl.GL_RGB
    p3 = gl.GL_REPEAT # GL_REPEAT GL_CLAMP_TO_EDGE
    p4 = gl.GL_LINEAR
    for k in range(6):
        fn = 'dice' + str(k) + '.jpg'
        img = pyglet.image.load(fn)
        iWidth = img.width
        iHeight = img.height
        img = img.get_data('RGB', iWidth * 3)
        gl.glBindTexture(p, textureIDs[k])
        gl.glTexParameterf(p, gl.GL_TEXTURE_WRAP_S, p3)
        gl.glTexParameterf(p, gl.GL_TEXTURE_WRAP_T, p3)
        gl.glTexParameterf(p, gl.GL_TEXTURE_MAG_FILTER, p4)
        gl.glTexParameterf(p, gl.GL_TEXTURE_MIN_FILTER, p4)
        gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_DECAL)
        gl.glTexImage2D(p, 0, r, iWidth, iHeight, 0, r, gl.GL_UNSIGNED_BYTE, img)
    gl.glEnable(p)
def cube_draw():
    k = -1
    for face in faces:
        k += 1
        m = -1
        v4, c4, t4 = (), (), ()
        gl.glBindTexture(p, textureIDs[k])
        for v in face:
            m += 1
            c4 += clrs[k + m]
            t4 += t_coords[m]
            v4 += verts[v]
        graphics.draw(4, gl.GL_QUADS, ('v3f', v4), ('c3f', c4), ('t2f', t4))
##    gl.glColor3f(1, 0, 0)
##    for edge in edges:
##        v2 = ()
##        for v in edge:
##            v2 += verts[v]
##        graphics.draw(2, gl.GL_LINES, ('v3f', v2))
#
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Куб')
gl.glClearColor(0, 0, 0, 1) # Черный цвет фона
gl.glClear(gl.GL_COLOR_BUFFER_BIT|gl.GL_DEPTH_BUFFER_BIT)
gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
gl.glPolygonMode(gl.GL_BACK, gl.GL_LINE)
gl.glLineWidth(3)
gl.glPointSize(4)
gl.glEnable(gl.GL_POINT_SMOOTH)
gl.glShadeModel(gl.GL_SMOOTH) # GL_SMOOTH, GL_FLAT
gl.glCullFace(gl.GL_BACK) # GL_FRONT GL_BACK
gl.glEnable(gl.GL_CULL_FACE)
gl.glEnable(gl.GL_DEPTH_TEST)
gl.glDepthFunc(gl.GL_LESS) # GL_LESS GL_GREATER
#
texInit() # Создаем текстуры
#
@window.event
def on_draw():
    window.clear()
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-w, w, -w, w, -w, w)
    gl.glRotatef(rot_x, 1, 0, 0) # Поворот относительно оси X
    gl.glRotatef(rot_y, 0, 1, 0) # Поворот относительно оси Y
    gl.glRotatef(rot_z, 0, 0, 1) # Поворот относительно оси Z
    cube_draw()
@window.event
def on_key_press(symbol, modifiers):
    if symbol == key._1:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
        gl.glShadeModel(gl.GL_SMOOTH)
        gl.glEnable(gl.GL_CULL_FACE)
        gl.glEnable(p)
    elif symbol == key._2:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
        gl.glShadeModel(gl.GL_FLAT)
        gl.glEnable(gl.GL_CULL_FACE)
        gl.glDisable(p)
    elif symbol == key._3:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_LINE) # Вывод ребер
    elif symbol == key._4:
        ps = (gl.GLfloat * 1)()
        ps_range = (gl.GLfloat * 2)()
        gl.glGetFloatv(gl.GL_POINT_SIZE, ps) # Получаем текущий размер точки
        # Допустимый диапазон изменения размеров точки
        gl.glGetFloatv(gl.GL_POINT_SIZE_RANGE, ps_range)
        ps = ps[0]
        ps = (ps + 2) if ps < ps_range[1] - 2 else 4
        gl.glPointSize(ps)
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_POINT)
    elif symbol == key._5:
        gl.glDisable(gl.GL_CULL_FACE)
    elif symbol == key._6:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
        gl.glShadeModel(gl.GL_SMOOTH)
        gl.glEnable(gl.GL_CULL_FACE)
        gl.glDisable(p)
app.run()

Вращающийся куб

Выводится вращающаяся игральная кость.

Шаг угла поворота 1°

Шаг угла поворота вокруг каждой оси равен либо da, либо -da.
Смена знака шага da выполняется после заданного числа поворотов (в программе – это 180).
Процедура cube_draw, выводящая куб, вызывается после каждого срабатывания таймера:

pyglet.clock.tick()

Вызов обеспечивает метод schedule:

pyglet.clock.schedule(cube_draw)

# Вращающаяся кость
import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window
n_rot, da = 0, 1
h = 20 # Половины длины ребра куба
w = 2 * h # Для задания области вывода
width = height = 300 # Размер окна вывода
textureIDs = (gl.GLuint * 6)() # Массив идентификаторов (номеров) текстур
p = gl.GL_TEXTURE_2D
tc = 1 # Число повторов текстуры
verts = ((h, -h, -h), # Координаты вершин куба
         (h, h, -h),
         (-h, h, -h),
         (-h, -h, -h),
         (h, -h, h),
         (h, h, h),
         (-h, -h, h),
         (-h, h, h))
faces = ((0, 1, 2, 3), # Индексы вершин граней куба
         (3, 2, 7, 6),
         (6, 7, 5, 4),
         (4, 5, 1, 0),
         (1, 5, 7, 2),
         (4, 0, 3, 6))
t_coords = ((0, 0), (0, tc), (tc, tc), (tc, 0)) # Координаты текстуры
def texInit(): # Формирование текстур
    gl.glGenTextures(6, textureIDs)
    r = gl.GL_RGB
    p3 = gl.GL_REPEAT
    p4 = gl.GL_LINEAR
    for k in range(6):
        fn = 'dice' + str(k) + '.jpg'
        img = pyglet.image.load(fn)
        iWidth = img.width
        iHeight = img.height
        img = img.get_data('RGB', iWidth * 3)
        gl.glBindTexture(p, textureIDs[k])
        gl.glTexParameterf(p, gl.GL_TEXTURE_WRAP_S, p3)
        gl.glTexParameterf(p, gl.GL_TEXTURE_WRAP_T, p3)
        gl.glTexParameterf(p, gl.GL_TEXTURE_MAG_FILTER, p4)
        gl.glTexParameterf(p, gl.GL_TEXTURE_MIN_FILTER, p4)
        gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_DECAL)
        gl.glTexImage2D(p, 0, r, iWidth, iHeight, 0, r, gl.GL_UNSIGNED_BYTE, img)
    gl.glEnable(p)
def cube_draw(dt):
    k = -1
    for face in faces:
        k += 1
        m = -1
        v4, t4 = (), ()
        gl.glBindTexture(p, textureIDs[k])
        for v in face:
            m += 1
            t4 += t_coords[m]
            v4 += verts[v]
        graphics.draw(4, gl.GL_QUADS, ('v3f', v4), ('t2f', t4))
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Куб')
gl.glClearColor(0, 0, 0, 1) # Черный цвет фона
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glCullFace(gl.GL_BACK) # GL_FRONT GL_BACK
gl.glEnable(gl.GL_CULL_FACE)
#
texInit() # Создаем текстуры
#
@window.event
def on_draw():
    global n_rot, da
    window.clear()
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-w, w, -w, w, -w, w)
    gl.glMatrixMode(gl.GL_MODELVIEW)
    if n_rot > 180:
        n_rot = 0
        da = -da
    n_rot += 1    
    gl.glRotatef(da, 1, 0, 0)
    gl.glRotatef(da, 0, 1, 0)
    gl.glRotatef(da, 0, 0, 1)
    pyglet.clock.tick()
#
pyglet.clock.schedule(cube_draw)
app.run()

Тест глубины

На рис. 23 показаны два прямоугольника с одинаковыми размерами, но разными z-координатами вершин: z-координата вершин красного прямоугольника (КП) равна 1, синего прямоугольника (СП) – 0.

Тест глубины

Рис. 23. Тест глубины отключен

СП перекрывает КП, хотя z-координата вершин КП больше, чем у СП (1 против 0). То есть должно быть ровно наоборот: КП перекрывает СП.
Однако этого не происходит, поскольку в приводимой ниже программе, выводящей СП и КП, первоначально отключен тест глубины:

glDisable(GL_DEPTH_TEST),

а СП выводится вслед за КП.
При нажатии на 1 включается тест глубины:

glEnable(GL_DEPTH_TEST),

и вывод осуществляется с учетом z-координат вершин СП и КП (рис. 24).

Тест глубины

Рис. 24. Выполняется тест глубины

При нажатии на 2 тест глубины отключается и получаем рис. 23.

# Тест глубины
import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window, key
d = 20 # Половина длины большой стороны прямоугольника
d2 = d / 2
z2 = 1 # z-координата вершин красного прямоугольника
wx, wy = 2 * d, 2 * d2 # Для задания области вывода
width, height = 300, 150 # Размеры окна вывода
def draw():
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -2, 2)
    gl.glMatrixMode(gl.GL_MODELVIEW)
    gl.glLoadIdentity()
    gl.glTranslatef(-d/2, -d2/2, 0)
    gl.glColor3f(1, 0, 0) # Вывод красного прямоугольника (КП)
    graphics.draw(4, gl.GL_QUADS, ('v3f', (-d, -d2, z2, d, -d2, z2, d, d2, z2, -d, d2, z2)))
    gl.glTranslatef(d, d2, 0)
    gl.glColor3f(0, 0, 1) # Вывод синего прямоугольника (СП)
    graphics.draw(4, gl.GL_QUADS, ('v2f', (-d, -d2, d, -d2, d, d2, -d, d2)))
#
window = Window(width = width, height = height, caption = 'Тест глубины')
gl.glClearColor(0.75, 0.75, 0.75, 1) # Серый цвет фона
gl.glClear(gl.GL_COLOR_BUFFER_BIT|gl.GL_DEPTH_BUFFER_BIT)
gl.glDisable(gl.GL_DEPTH_TEST)
gl.glDepthFunc(gl.GL_LESS) # GL_LESS GL_GREATER
#
@window.event
def on_draw():
    window.clear()
    draw()
@window.event
def on_key_press(symbol, modifiers):
    if symbol == key._1:
        gl.glEnable(gl.GL_DEPTH_TEST)
    elif symbol == key._2:
        gl.glDisable(gl.GL_DEPTH_TEST)
app.run()

Два материала и источника света

На рис. 25 показан результат применения двух материалов и двух источников света.
Первые материал и источник света применяются к левому прямоугольнику, вторые – к правому.

Два материала

Рис. 25. Два материала и источника света. Нормали рассчитаны к граням

При левая грань выведена с применением первой пары материала и источника света:

gl.glMaterialfv(gl.GL_FRONT, gl.GL_SPECULAR, mtClr)
gl.glEnable(gl.GL_LIGHT0)
gl.glDisable(gl.GL_LIGHT1)

правая – с применением второй пары материала и источника света:

gl.glMaterialfv(gl.GL_FRONT, gl.GL_SPECULAR, mtClr2)
gl.glDisable(gl.GL_LIGHT0)
gl.glEnable(gl.GL_LIGHT1)

Цвета первых материала и света соответственно

[0.7, 0.9, 0, 0] и [1, 0.5, 0.5, 0],

а вторых

[0, 0.7, 0.9, 0] и [0.5, 0.5, 1.0, 0].

Координаты первого и второго источников света соответственно

[-100, 200, 400, 0] и [100, 200, 400, 0].

При одновременной работе обоих источников света получаем рис. 26.

Два света

Рис. 26. Два материала и источника света. Включены оба источника

Имитация прозрачности

Прозрачность моделируется, если активизирован режим смешения цветов

glEnable(GL_BLEND)

и функция смешения цветов задана следующим образом:

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

Степень прозрачности регулируется последним компонентом RGBA, например, при задании цвета материала:

mt_clr0 = [1, 0, 0, 0.45] # Цвет материала

Прозрачность растет по мере уменьшения A-компонента цвета.
Тест глубины при имитации прозрачности следует отключить.
Пример:

# Смешение цветов (прозрачность) и тест глубины
from pyglet.gl import *
from pyglet import app, graphics
from pyglet.window import Window, key
d = 20 # Половина длины большой стороны прямоугольника
d2 = d / 2
z0 = 3
z1 = 1
z2 = 2
wx, wy = 2 * d, 2 * d2 # Для задания области вывода
width, height = 300, 150 # Размеры окна вывода
mt_clr0 = (GLfloat * 4)(*[1, 0, 0, 0.75])
mt_clr1 = (GLfloat * 4)(*[0, 1, 0, 0.5])
mt_clr2 = (GLfloat * 4)(*[0, 0, 1, 0.5])
light_position = (GLfloat * 4)(*[0, 40, 40, 0])
lght_clr = (GLfloat * 4)(*[1, 1, 1, 1])
def material(mt_clr):
    glMaterialfv(GL_FRONT, GL_SPECULAR, mt_clr)
    glMaterialfv(GL_FRONT, GL_DIFFUSE, mt_clr)
    glMaterialfv(GL_FRONT, GL_AMBIENT, mt_clr)
def rect(mt_clr, verts):
    material(mt_clr)
    graphics.draw(4, GL_QUADS, ('v3f', verts),
                  ('n3f', (0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1)))
def draw():
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-wx, wx, -wy, wy, -5, 5)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    glLightfv(GL_LIGHT0, GL_POSITION, light_position)
    rect(mt_clr0, (-d, -d2, z0, d, -d2, z0, d, d2, z0, -d, d2, z0))
    glTranslatef(3*d/4, 3*d2/4, 0)
    rect(mt_clr1, (-d, -d2, z1, d, -d2, z1, d, d2, z1, -d, d2, z1))
    glTranslatef(-3*d/2, -3*d2/2, 0)
    rect(mt_clr2, (-d, -d2, z2, d, -d2, z2, d, d2, z2, -d, d2, z2))
#
window = Window(width = width, height = height, resizable = True, caption = 'Смешение цветов')
glClearColor(1, 1, 1, 1)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
glDisable(GL_DEPTH_TEST)
glDepthFunc(GL_LESS) # GL_LESS GL_GREATER
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
glLightfv(GL_LIGHT0, GL_SPECULAR, lght_clr)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable(GL_BLEND)
#
@window.event
def on_draw():
    window.clear()
    draw()
@window.event
def on_key_press(symbol, modifiers):
    if symbol == key._1:
        if modifiers == key.MOD_CTRL:
            glDisable(GL_BLEND)
        else:
            glEnable(GL_BLEND)
    elif symbol == key._2:
        if modifiers == key.MOD_CTRL:
            glDisable(GL_DEPTH_TEST)
        else:
            glEnable(GL_DEPTH_TEST)
app.run()

Результат:

РисунокGL_BLENDGL_DEPTH_TEST
GL_BLENDДаНет
GL_BLENDДаДа
GL_BLENDНетНет
GL_BLENDНетДа
GL_BLENDglDepthFunc(GL_LESS)

Примеры вывода растровых данных

В первом примере выводится приведенная на рис. 27 картинка.

Растровая картинка

Рис. 27. Растровая картинка

Это выполняет следующий код:

import numpy as np
from pyglet.gl import *
from pyglet.window import Window
from pyglet import app
w = 200
vp = np.full((w, w, 3), 255, dtype = 'uint8')
for i in range(w):
    i0, i2 = i - 2, i + 2
    vp[i0:i2, i0:i2] = [0, 255, 0]
    i00, i20 = w - i0 - 1, w - i2 - 1
    vp[i0:i2, i20:i00] = [0, 255, 0]
vp[-5:, :] = vp[:5, :] = [0, 0, 255]
vp[:, -5:] = vp[:, :5] = [0, 0, 255]
k = w // 4; k2 = 3 * k
vp[k:k2, k:k2] = [255, 0, 0]
vp = vp.flatten()
vp = (GLubyte * (w * w * 3))(*vp)
window = Window(visible = True, width = w, height = w, caption = 'vp')
@window.event
def on_draw():
    window.clear()
    glDrawPixels(w, w, GL_RGB, GL_UNSIGNED_BYTE, vp)
app.run()

Во втором примере данные типа uint8 накапливаются в двумерном массиве vp. Они отображают распределение температуры на пластине, получаемое в результате решения краевой задачи. Массив vp содержит оттенки серого цвета: чем больше значение, тем выше температура. Перед отображением они преобразуются в RGB таким образом, что по мере снижения температуры цвет переходит от красного в синий. Это обеспечивает следующий код:

# (w, w) – форма массива vp vp2 = np.zeros((w, w, 3), dtype = 'uint8')
for i in range(w):
    for j in range(w):
        clr = vp[i, j]
        if clr > 240:
            clr2 = (0xFF, 0, 0) # Red
        elif clr > 225:
            clr2 = (0xA5, 0xA2, 0x2A) # Brown
        elif clr > 210:
            clr2 = (0xFF, 0x63, 0x47) # Tomato
        elif clr > 195:
            clr2 = (0xFA, 0x80, 0x72) # Salmon
        elif clr > 180:
            clr2 = (0x7B, 0x68, 0xEE) # MediumSlateBlue
        elif clr > 165:
            clr2 = (0xFF, 0xFF, 0) # Yellow
        elif clr > 145:
            clr2 = (0xFF, 0xEF, 0xD5) # PapayaWhip
        elif clr > 125:
            clr2 = (0xEE, 0x82, 0xEE) # Violet
        elif clr > 105:
            clr2 = (0xDB, 0x70, 0x93) # PaleVioletRed
        elif clr > 90:
            clr2 = (0x9A, 0xCD, 0x32) # YellowGreen
        elif clr > 75:
            clr2 = (0, 255, 0) # Green
        elif clr > 60:
            clr2 = (0, 0xFF, 0x7F) # SpringGreen
        elif clr > 50:
            clr2 = (0, 0xFA, 0x9A) # MediumSpringGreen
        elif clr > 40:
            clr2 = (0x3C, 0xB3, 0x71) # MediumSeaGreen
        elif clr > 30:
            clr2 = (0x46, 0x82, 0xB4) # SteelBlue
        elif clr > 20:
            clr2 = (0x41, 0x69, 0xE1) # RoyalBlue
        elif clr > 10:
            clr2 = (0, 0, 255) # Blue
        else:
            clr2 = (0, 0, 0xCD) # MediumBlue
        vp2[j, i, :] = clr2

Предварительно массив vp масштабируется (каждое его значение повторяется z_val раз):

from scipy.ndimage import zoom
if z_val > 1: vp = zoom(vp, z_val)

Отображение массива средствами OpenGL:

vp = (GLubyte * len(vp))(*vp)
window = Window(visible = True, width = w, height = w, caption = ('nH = ' + str(nH))) # nH – число нагревателей
@window.event
def on_draw():
    window.clear()
    glDrawPixels(w, w, GL_RGB, GL_UNSIGNED_BYTE, vp)
app.run()

Результат (рис. 28):

Нагретая пластина

Рис. 28. Пластина с одним, двумя и тремя нагревателями

Процесс вычислений иллюстрирует следующее видео:


Предварительно для его получения формируются файлы с картами температур пластины после 10, 20, ..., 100 шагов вычислений. Далее они воспроизводятся в результате выполнения следующего кода:

from pyglet import clock
def callback(dt):
    global vp, n_f
    n_f += 1
    if n_f > 10: n_f = 1
    fn = fn0 + str(n_f) + '.bin'
    vp = load_data(fn)
    vp = (GLubyte * len(vp))(*vp)
clock.schedule_interval(callback, 1.5)
n_f = 1
fn = fn0 + str(n_f) + '.bin'
vp = load_data(fn)
vp = (GLubyte * len(vp))(*vp)
window = Window(visible = True, width = w, height = w, caption = ('nH = ' + str(nH)))
@window.event
def on_draw():
    window.clear()
    glDrawPixels(w, w, GL_RGB, GL_UNSIGNED_BYTE, vp)
app.run()

Через заданный интервал процедура callback загружает очередной файл с данными, который затем отображается в окне вывода OpenGL, заменяя прежнее изображение на новое.
Полный код решения краевой задачи, сохранения, загрузки и отображения данных:

import numpy as np, time
# 1 - решение и запись карты цветов в файл или несколько файлов
# 2 - отображение файлов
step = 2
nH = 2 # Число нагревателей (не более трех)
gl = True # Использование OpenGL
many_files = not True # Режим анимации
if not gl: many_files = False
n = 100 # Размер сетки
n1 = n + 1
n2 = n + 2
if gl:
    from pyglet.gl import *
    from pyglet import app, graphics
    from pyglet.window import Window, key
    from scipy.ndimage import zoom
    z_val = 2
else:
    from matplotlib import pyplot as plt
    z_val = 1
w = n2 * z_val
g = 4 # Температура на границе
eps = 0.5 # Точность: eps = pow(10, -2)
tH, tH2, tH3 = 60, 140, 100 # Температуры нагревателей 1, 2 и 3
# Координаты нагревателей 1, 2 и 3
# Должны находиться в узлах сетки
xH, yH = n // 4, n // 4
xH2, yH2 = 3 * n // 4, 3 * n // 4
xH3, yH3 = xH2, yH
u = np.zeros((n2, n2), dtype = 'float32')
u_prev = np.zeros((n2, n2), dtype = 'float32')
vp = np.zeros((n2, n2), dtype = 'uint8')
max_steps = 100
fn0 = 'vp' + str(nH) + ('gl' if gl else '')
fn = fn0 + '.bin'
def save_data(fn, data):
    fp = open(fn, 'wb')
    fp.write(data.flatten())
    fp.close()
def load_data(fn):
    with open(fn, 'rb') as f:
        data = np.fromfile(f, dtype = np.uint8)
    return data
def f_r(x, y): # Правая часть
    prs = 0.1
    if abs(x - xH) < prs and abs(y - yH) < prs: return tH
    if abs(x - xH2) < prs and abs(y - yH2) < prs and nH > 1: return tH2
    if abs(x - xH3) < prs and abs(y - yH3) < prs and nH > 2: return tH3
    return 0
def maxSumAMinusB(a, b):
    max_val = -np.inf
    for i in range(1, n1):
        max_val = max(max_val, np.sum(abs(a[i, 1:n1] - b[i, 1:n1])))
    return max_val
def sumAMultB(a, b):
    return np.sum(a[1:n1, 1:n1] * b[1:n1, 1:n1])
def grid():
    u[:, 0] = g; u[:, n1] = g
    u[0, :] = g; u[n1, :] = g
def solve(vp):
    r = np.zeros((n1, n1), dtype = 'float32')
    r_prev = np.zeros((n1, n1), dtype = 'float32')
    ap = np.zeros((n1, n1), dtype = 'float32')
    p = np.zeros((n2, n2), dtype = 'float32')
    for i in range(1, n1):
        for j in range(1, n1):
            # Начальные невязки
            up_ij2 = 2 * u[i, j]
            r[i, j] = f_r(i, j) + (u[i + 1, j] - up_ij2 + u[i - 1, j]) + (u[i, j + 1] - up_ij2 + u[i, j - 1])
            p[i, j] = r[i, j]
    n_steps = m_val = n_f = 0
    while(1):
        for i in range(1, n1):
            for j in range(1, n1):
                u_prev[i, j] = u[i, j]
                up_ij2 = 2 * p[i, j]
                ap[i, j] = -(p[i + 1, j] - up_ij2 + p[i - 1, j]) - (p[i, j + 1] - up_ij2 + p[i, j - 1])
        alpha = sumAMultB(r, r) / sumAMultB(ap, p)
        u[1:n1, 1:n1] = u[1:n1, 1:n1] + alpha * p[1:n1, 1:n1] # Приближения
        r_prev[1:, 1:] = r[1:, 1:]
        r[1:, 1:] = r[1:, 1:] - alpha * ap[1:, 1:] # Невязки
        betta = sumAMultB(r, r) / sumAMultB(r_prev, r_prev)
        p[1:n1, 1:n1] = r[1:, 1:] + betta * p[1:n1, 1:n1]
        # Сравнение текущего и предшествующего приближений на предмет достижения заданной точности
        m_val = maxSumAMinusB(u, u_prev)
        n_steps += 1
        if many_files and n_steps % 10 == 0:
            vp2 = make_map(vp)
            n_f += 1
            fn = fn0 + str(n_f) + '.bin'
            save_data(fn, vp2)
        if m_val < eps or n_steps >= max_steps: break
    print('Число шагов. Предельное:', max_steps, 'Сделано:', n_steps, 'Достигнутая точность:', m_val)
def make_map(vp):
    uMax = np.max(u)
    vp[...] = 255 * u / uMax
    if gl:
        if z_val > 1: vp = zoom(vp, z_val)
        vp2 = np.zeros((w, w, 3), dtype = 'uint8')
        for i in range(w):
            for j in range(w):
                clr = vp[i, j]
                if clr > 240:
                    clr2 = (0xFF, 0, 0) # Red
                elif clr > 225:
                    clr2 = (0xA5, 0xA2, 0x2A) # Brown
                elif clr > 210:
                    clr2 = (0xFF, 0x63, 0x47) # Tomato
                elif clr > 195:
                    clr2 = (0xFA, 0x80, 0x72) # Salmon
                elif clr > 180:
                    clr2 = (0x7B, 0x68, 0xEE) # MediumSlateBlue
                elif clr > 165:
                    clr2 = (0xFF, 0xFF, 0) # Yellow
                elif clr > 145:
                    clr2 = (0xFF, 0xEF, 0xD5) # PapayaWhip
                elif clr > 125:
                    clr2 = (0xEE, 0x82, 0xEE) # Violet
                elif clr > 105:
                    clr2 = (0xDB, 0x70, 0x93) # PaleVioletRed
                elif clr > 90:
                    clr2 = (0x9A, 0xCD, 0x32) # YellowGreen
                elif clr > 75:
                    clr2 = (0, 255, 0) # Green
                elif clr > 60:
                    clr2 = (0, 0xFF, 0x7F) # SpringGreen
                elif clr > 50:
                    clr2 = (0, 0xFA, 0x9A) # MediumSpringGreen
                elif clr > 40:
                    clr2 = (0x3C, 0xB3, 0x71) # MediumSeaGreen
                elif clr > 30:
                    clr2 = (0x46, 0x82, 0xB4) # SteelBlue
                elif clr > 20:
                    clr2 = (0x41, 0x69, 0xE1) # RoyalBlue
                elif clr > 10:
                    clr2 = (0, 0, 255) # Blue
                else:
                    clr2 = (0, 0, 0xCD) # MediumBlue
                vp2[j, i, :] = clr2
        return vp2
    else:
        return np.rot90(vp)
def plot():
    global vp
    if gl:
        if step == 1: vp = vp.flatten()
        vp = (GLubyte * len(vp))(*vp)
        window = Window(visible = True, width = w, height = w, caption = ('nH = ' + str(nH)))
        @window.event
        def on_draw():
            window.clear()
            glDrawPixels(w, w, GL_RGB, GL_UNSIGNED_BYTE, vp)
        app.run()
    else:
        plt.figure(figsize = (3, 3))
        plt.title('Нагревателей: ' + str(nH))
        plt.imshow(vp, cmap = 'jet', interpolation = 'nearest') # origin = 'lower'
        plt.axis('off')
        plt.show()
if step == 1:
    grid() # Инициализация сетки (области интегрирования)
    s_t = time.time()
    solve(vp) # Решаем краевую задачу
    s_t2 = time.time()
    print('Потрачено на решение:', round(s_t2 - s_t, 1))
    vp = make_map(vp)
    save_data(fn, vp)
    if gl:
        print('Потрачено на подготовку данных:', round(time.time() - s_t2, 1))
    plot() # Вывод карты температур
elif step == 2:
    if many_files:
        from pyglet import clock
        def callback(dt):
            global vp, n_f
            n_f += 1
            if n_f > 10: n_f = 1
            fn = fn0 + str(n_f) + '.bin'
            vp = load_data(fn)
            vp = (GLubyte * len(vp))(*vp)
        clock.schedule_interval(callback, 1.5)
        n_f = 1
        fn = fn0 + str(n_f) + '.bin'
        vp = load_data(fn)
        plot() # Вывод карты температур
    else:
        vp = load_data(fn)
        if not gl:
            vp = vp.reshape(n2, n2)
        plot() # Вывод карты температур

Полученная анимация может быть захвачена и сохранена в виде gif-файла, например, GifCam. Затем gif-файл можно конвертировать, например, в mp4-файл, применив, скажем, Movavi Video Suite.

Контрольные вопросы и задачи

ЛР 2. Решаются задачи 2 и 18

  1. Примитивы OpenGL.
  2. Способы вывода точки.
  3. Различие между GL_LINES, GL_LINE_LOOP и GL_LINE_STRIP.
  4. Как при выводе линии задать и использовать шаблон.
  5. Лицевая и нелицевая стороны полигона.
  6. Способы вывода полигона.
  7. Как задать вывод лицевой стороны в виде точек, нелицевой в виде линий.
  8. Как задать заливку лицевой стороны, а нелицевую вывести в виде точек.
  9. Как задать вывод с интерполяцией цветов и без нее.
  10. Параметры метода draw.
  11. Назначение glBegin / glEnd.

ЛР 3. Решается задача 19

  1. Виды проецирования.
  2. Как задать в OpenGL изометрическую проекцию 3d-объекта (разновидность аксонометрической проекции).
  3. Реализация аффинных преобразований координат в OpenGL.
  4. Расчет цвета пикселя отрезка и полигона при линейной интерполяции цветов, заданных в вершинах примитива.
  5. Как рассчитать нормали к граням и вершинам:

    Нормали

  6. Задание материалов и источников света.
  7. Компоненты света, задаваемые GL_AMBIENT, GL_DIFFUSE и GL_SPECULAR.
  8. Вывести в задаче 19 полигоны, используя draw.
  9. Задачи, решаемые с применением компьютерной графики.

ЛР 4. Решается задача 22

Наложение текстуры на грани призмы.

Текстура

  1. Преобразование координат.
  2. Схема наложения текстуры.
  3. Определение принадлежности точки многоугольнику.
  4. Алгоритм растровой развертки полигона.
  5. Использование нескольких текстур.
  6. Расчет координат текстуры при выводе n-угольника.
  7. Приводимая ниже программа выводит черно-белое изображение с искаженной текстурой (левая трапеция).
    Внести изменения в программу, чтобы получилось черно-желтое изображение с выправленной текстурой (правая трапеция).

Правка текстуры

from pyglet.gl import *
import pyglet
from pyglet import app, graphics
from pyglet.window import Window, key
import numpy as np
d, d1, d2 = 5, 10, 15
wx, wy = 1.5 * d2, 1.5 * d2
width, height = int(30 * wx), int(30 * wy)
window = Window(visible = True, width = width, height = height, resizable = True)
glClearColor(0.1, 0.1, 0.1, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
def texInit():
    iWidth = iHeight = 64
    n = 3 * iWidth * iHeight
    img = np.zeros((3, iWidth, iHeight), dtype = 'uint8')
    for i in range(iHeight):
     for j in range(iWidth):
        img[:, i, j] = ((i - 1) & 16 ^ (j - 1) & 16) * 255
    img = img.reshape(n)
    img = (GLubyte * n)(*img)
    p, r = GL_TEXTURE_2D, GL_RGB
    glTexParameterf(p, GL_TEXTURE_WRAP_S, GL_REPEAT)
    glTexParameterf(p, GL_TEXTURE_WRAP_T, GL_REPEAT)
    glTexParameterf(p, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameterf(p, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
    glTexImage2D(p, 0, r, iWidth, iHeight, 0, r, GL_UNSIGNED_BYTE, img)
    glEnable(p)
texInit()
zv = -d2/2
v0, v1, v2, v3 = (-d2,d2,zv), (-d1,d1,0), (d1,d1,0), (d2,d2,zv)
@window.event
def on_draw():
    window.clear()
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-wx, wx, -wy, wy, -20, 20)
    glRotatef(90, 1, 0, 0)
    graphics.draw(4, GL_QUADS, ('v3f', (v0 + v1 + v2 + v3)), ('t2f', (0,1, 0,0, 1,0, 1,1)))
app.run()

ЛР 5 и 6. Решаются задачи 200 и 203

  1. Форма массива:
  2. Отказ от вывода граней, показанных нелицевой (лицевой) стороной (GL_CULL_FACE):
  3. Тест глубины (GL_DEPTH_TEST):
  4. Задание изометрической проекции.
  5. С какой целью расчет модели выносится за пределы @window.event-процедур.
  6. Использованные в программе объекты, хранящие сведения о модели фигуры.
  7. Вывести оси координат, инвариантные к аффинным преобразованиям, употребляя glPushMatrix() и gl.glPopMatrix().
  8. Перенести расчет цвета вершин за пределы on_draw.
  9. Перейти от GL_QUADS к GL_QUAD_STRIP (GL_TRIANGLES к GL_TRIANGLE_STRIP или GL_TRIANGLE_FAN).
  10. Используя средства matplotlib, вывести полусферу (если поверхность не имеет уравнения).
  11. Отобразить нормали.

ЛР 7. Решается задача 8

Состоит из двух частей. В первой ведется работа с многослойным перцептроном, во второй – со сверточными слоями.

Требования к отчету:

  1. Решаемая задача.
  2. Описание набора данных MNIST.
  3. Примеры изображений MNIST с указанием метки над изображениями.
  4. Описание слоев исходной нейронной сети (результат model.summary()).
  5. Таблица с результатами всех использованных вариантов НС. Имеет следующие столбцы: Замер времени:
    import time
    start = time.time()
    <Вычисления>
    print('Время вычислений:', time.time() - start)
  6. Графики обучения лучшего и худшего по точности на оценочном множестве варианта нейронной сети.

Контрольные вопросы и задачи:

  1. Модель нейрона. Назначение функции активации и смещения.
  2. Модель многослойного перцептрона.
  3. Схема обучение классификатора.
  4. Данные и метки в задачи классификации рукописных цифр. Число классов.
    Обучающее и оценочное множества.
    Что содержат x_train, y_train, x_test, y_test и каков тип этих объектов.
    http://yann.lecun.com/exdb/mnist/
    http://100byte.ru/python/MNIST_NN/mnist_nn.html#p1
    http://100byte.ru/python/imgClasses/imgClasses.html#p1
  5. Формы входных данных при наличии слоя Flatten или Reshape и без них.
  6. Вывести обобщенный портрет рукописной цифры MNIST обучающего (оценочного) множеств.
    Обобщенный портрет формируется следующим образом:
    берутся и суммируются массивы с описаниями заданной цифры на заданном множестве (обучающее или оценочное).
    (Данные предварительно приводятся к диапазону 0-1, тип float32).
    Результат делится на число примеров цифры в заданном множестве.
    Полученные данные выводятся в виде изображения.
  7. Категориальное (one-hot, двоичное) представление метки. Привести пример.
  8. Назначение слоев Flatten и Reshape.
  9. Что такое функция активации и ее назначение. Какие функции активации использованы. Их графики. http://100byte.ru/python/factors/factors.html#p1_3
  10. Параметры units, activation, use_bias, kernel_initializer, bias_initializer слоя Dense (http://100byte.ru/python/factors/factors.html#p1).
  11. Какие параметры в модели нейронной сети меняются при ее обучении.
  12. Назначение функции потерь. Какие функции потерь использованы. Формулы вычисления потерь. К чему может привести замена одной функции потерь на другую (http://100byte.ru/python/loss/loss.html).
  13. Параметры x, y, batch_size, epochs, verbose, shuffle метода fit. Что возвращает метод fit (http://100byte.ru/python/factors/factors.html#p3).
  14. Критерий качества обучения нейронной сети. Различие между acc и val_acc.
  15. Как проявляется переобучение и способы его преодоления (http://100byte.ru/python/MNIST_NN/mnist_nn.html#p10).
  16. Вывести 50 примеров заданной цифры из заданного множества (обучающего или оценочного) MNIST (5 строк по 10 примеров в каждой и заголовок над каждым изображением).
  17. Почему кроме оценочного применяется и тестовое множество.
  18. Как формируется выход сверточных слоев и слоев подвыборки.
  19. Как работает слой прореживания.
  20. Для чего может быть полезна генерация данных, выполняемая ImageDataGenerator.
  21. Назначение ранней остановки и порядок ее задания.
  22. Профессии, в которых ИИ может вытеснить человека.
  23. Сферы деятельности человека, в которых ИИ его превосходит.
  24. Виды деятельности, в которых человек не может быть в ближайшей перспективе заменен ИИ.

ЛР 8. Решаются задачи 224 и 225

Состоит из двух частей. В первой создается и анализируется набор данных с зашумленных изображениями кривых и геометрических фигур, во второй – обучается НС, классифицирующая изображения созданного в первой части набора данных.

Требования к отчету

Отчет по задаче 224 включает:

  1. Номер варианта и задание.
  2. Диапазоны изменения варьируемых параметров.
  3. Описание приемов, использованных при внесении шума в генерируемые изображения.
  4. Примеры сгенерированных изображений (по 10 в каждом классе).
  5. Алгоритм проверки класса на неповторяемость.
  6. Результаты проверки классов на неповторяемость (в виде таблицы).
  7. Обоснование значения ИСР_мин (ИСР – индекс структурного различия), применяемого при поиске похожих изображений.
  8. Программа генерации, визуализации и проверки классов на неповторяемость.

Отчет по задаче 225 включает:

  1. Условие решаемой задачи.
  2. Примеры загруженных изображений (по одному из каждого класса).
  3. Описание (символьное) модели НС, обеспечивающей точность классификации 100 % на оценочном множестве, графики обучения этой НС.
  4. Описание (символьное) модели НС с минимальной архитектурой, обеспечивающей ту же точность классификации, что и прежняя НС (см. п. выше), и графики обучения второй сети.
  5. Программы загрузки набора данных, визуализации изображений, создания и обучения НС.

Контрольные вопросы и задачи

  1. Приемы, использованные при генерации зашумленных изображений.
  2. Требования к набору данных.
  3. Как обосновать значение ИСР_мин (ИСР – индекс структурного различия), применяемого при поиске похожих изображений.
  4. Как выявить похожие изображения.
  5. Вычислить среднее значение ИСР изображений заданного класса в заданном множестве MNIST (обучающем или проверочном).
  6. В заданном множестве (обучающее или проверочное) вывести по 10 случайных изображений каждого класса.
  7. Обучить НС, забирая из обучающего множества 10% данных в оценочное (параметр validation_split):
    model.fit(x_train, y_train, batch_size = batch_size, epochs = epochs, validation_split = 0.1)
    Точность модели оценить затем на проверочном множестве (применить evaluate).
  8. Вывести рисунки неверно классифицированных изображений (использовать predict).
  9. Объединить наборы данных двух полностью отличающихся вариантов и обучить НС для классификации изображений полученного набора данных.
  10. Алгоритм отсечения отрезков Коэна-Сазерленда.
  11. Алгоритм разбиения средней точкой в задаче отсечения отрезков.
  12. Алгоритм отсечения отрезков Кируса – Бека.
  13. Алгоритм центрирования изображения.
  14. Вычислить число ошибок классификации в каждом классе и вывести список классов, упорядоченный по числу ошибок классификации.
    Элемент списка: (класс / число ошибок).

Вывод параметрических кривых

Выводится в зависимости от значения fun_no:

Кривые

import matplotlib.pyplot as plt, math, numpy as np
from sys import exit
fun_no = 9 # 1, 2, 3, ..., 12
if fun_no < 1 or fun_no > 12: fun_no = 3
if fun_no == 1: # Циклоида
    nm = 'Циклоида'
    r = 15
    t = np.linspace(0, 2 * np.pi, 50)
    x = r * t - r * np.sin(t)
    y = r - r * np.cos(t)
elif fun_no == 2: # Улитка Паскаля
    nm = 'Улитка Паскаля'
    r = 5
    h = 17
    t = np.linspace(0, 2 * np.pi, 500)
    x = 2 * r * np.cos(t) - h * np.cos(2 * t)
    y = 2 * r * np.sin(t) - h * np.sin(2 * t)
elif fun_no == 3: # Астроида
    nm = 'Астроида'
    a = 15
    t = np.linspace(0, 2 * np.pi, 50)
    x = a * np.cos(t)**3
    y = a * np.sin(t)**3
elif fun_no == 4: # Конхоида (1/2)
    nm = 'Конхоида (1/2)'
    a = 15
    b = 8
    t = np.linspace(-0.95 * np.pi / 2, 0 * np.pi / 2, 50)
    #t2 = np.linspace(1.05 * np.pi / 2, 0.95 * 3 * np.pi / 2, 50)
    #t = np.concatenate((t, t2))
    x = a + b * np.cos(t)
    y = a * np.tan(t) + b * np.sin(t)
elif fun_no == 5: # Эвольвента (1/2)
    nm = 'Эвольвента (1/2)'
    r = 1
    t = np.linspace(0, 30, 500)
    x = r * (np.cos(t) + t * np.sin(t))
    y = r * (np.sin(t) - t * np.cos(t))
elif fun_no == 6: # Гиперболическая спираль
    nm = 'Гиперболическая спираль'
    r = 1
    t = np.linspace(-30, -1, 100)
    #t2 = np.linspace(1, 30, 100)
    #t = np.concatenate((t, t2))
    x = r * np.cos(t) / t
    y = r * np.sin(t) / t;
elif fun_no == 7: # Локон Аньези
    nm = 'Локон Аньези'
    a = 1
    t = np.linspace(0.1, np.pi - 0.1, 100)
    x = 2 * a / np.tan(t); y = 2 * a * np.sin(t)**2
elif fun_no == 8: # Декартов лист
    nm = 'Декартов лист'
    a = 10
    t = np.linspace(-5, 5, 300)
    x = a * (t**2 - 1) / (3 * t**2 + 1)
    y = a * t * (t**2 - 1) / (3 * t**2 + 1)
elif fun_no == 9: # Циссоида
    nm = 'Циссоида'
    a = 10
    t = np.linspace(-5, 5, 100)
    x = 2 * a * t**2 / (t**2 + 1)
    y = 2 * a * t**3 / (t**2 + 1)
elif fun_no == 10: # Строфоида
    nm = 'Строфоида'
    a = 25
    t = np.linspace(-2, 2, 100)
    x = a * (t**2 - 1) / (t**2 + 1)
    y = a * t * (t**2 - 1) / (t**2 + 1)
    x_min = int(min(x))
    x_max = int(max(x))
    y_min = int(min(y))
    y_max = int(max(y))
    # Нужно уместить в 64*64
    dx = int((64 - (x_max - x_min)) / 2) # Половина свободного пространства по x
    dy = int((64 - (y_max - y_min)) / 2) # Половина свободного пространства по y
    shift_x = abs(x_min) + dx # Сдвиг по x
    shift_y = abs(y_min) + dy # Сдвиг по y
    w = h = 64 # Ширина и высота рисунка
    arrPic = np.zeros((w, h), dtype = np.uint8)
    clr_mim, clr_max = 75, 255 # Диапазон оттенков серого цвета
    for x, y in zip(x, y):
        ix = int(x) + shift_x
        iy = int(y) + shift_y
        clr = np.random.randint(clr_mim, clr_max)
        arrPic[iy, ix] = clr
    plt.figure(nm)
    plt.imshow(arrPic, cmap = 'gray')
    plt.axis('off')
    plt.show()
    exit()
elif fun_no == 11: # Эпициклоида
    nm = 'Эпициклоида'
    r = 5
    t = np.linspace(0, 2 * np.pi, 100)
    x = 3*r*np.cos(t) - r*np.cos(3*t)
    y = 3*r*np.sin(t) - r*np.sin(3*t)
elif fun_no == 12: # Лемниската Бернулли
    nm = 'Лемниската Бернулли'
    a = 5
    t = np.linspace(-70, 70, 5000)
    x = a * np.sqrt(2) * (t + t**3) / (1 + t**4)
    y = a * np.sqrt(2) * (t - t**3) / (1 + t**4)
plt.figure(nm)
plt.xlabel("x")
plt.ylabel("y")
plt.plot(x, y)
plt.show()

Пример формирования незашумленного подмножества прямоугольников без одной стороны

Прямоугольники

Данные сохраняются в двоичные файлы.

import numpy as np
import math, time
import matplotlib.pyplot as plt
from PIL import Image # Для поворота изображения
#
np.random.seed(348)
full = not True # Полный прямоугольник, если True
cls = 0 if full else 1
show = True
show_test = False
fn_train = 'dataTrain.bin'
fn_train_labels = 'labelsTrain.bin'
fn_test = 'dataTest.bin'
fn_test_labels = 'labelsTest.bin'
n_train = 600 # Число рисунков для обучения
n_test = 100 # Число тестовых рисунков
clr_mim, clr_max = 75, 255 # Диапазон оттенков серого цвета
w, h = 64, 64 # Ширина и высота рисунка
w2 = w / 2
border = 4 # Граница
#
def line(arrPic, s, e, v, hor = True):
    for i in range(s, e):
        rnd = int(np.random.uniform(-2, 2))
        if hor:
            arrPic[v, i] = np.random.randint(100, 255)
            arrPic[v + rnd, i] = np.random.randint(100, 255)
        else:
            arrPic[i, v] = np.random.randint(100, 255)
            arrPic[i, v + rnd] = np.random.randint(100, 255)
def rect(arrPic):
    xL = np.random.randint(6, 24)
    xR = np.random.randint(40, 58)
    yB = np.random.randint(6, 24)
    yT = np.random.randint(40, 58)
    if yT - yB == xR - xL:
        yB -= 1
        yT -= 1
        xL -= 1
        xR += 1
    if full:
        idx = range(4)
    else:
        mis = np.random.randint(4) # Номер отсутствующей стороны
        idx = [i for i in range(4) if i != mis]
    for i in idx:
        if i == 0: line(arrPic, xL, xR, yB)
        if i == 1: line(arrPic, xL, xR, yT)
        if i == 2: line(arrPic, yB, yT, xL, False)
        if i == 3: line(arrPic, yB, yT, xR, False)
#
def prepareData(n, fn, fn2):
    file = open(fn, 'wb')
    file2 = open(fn2, 'wb')
    xs0 = border - w2 + 1
    xe = w2 - border - 1
    dx = 0.1
    for i in range(n):
        if full: # Прямоугольник
            xs = xs0
            label = cls
        else: # Прямоугольник без одной стороны
            xs = xs0
            label = cls
        arrPic = np.zeros((w, h), dtype = np.uint8)
        rect(arrPic)
        ang = np.random.randint(-90, 90)
        arrPic = rot_img(ang, arrPic)
        file.write(arrPic)
        file2.write(np.uint8(label))
    file.close()
    file2.close()
#
def load_data(fn, fn2):
    with open(fn, 'rb') as read_binary:
        data = np.fromfile(read_binary, dtype = np.uint8)
    with open(fn2, 'rb') as read_binary:
        labels = np.fromfile(read_binary, dtype = np.uint8)
    return data, labels
#
def rot_img(ang, img_array):
    # Приводим данные к типу uint8
    img_array = np.array(img_array, dtype = 'uint8')
    # Формируем изображение по массиву img_array
    img = Image.fromarray(img_array, 'L')
    # Поворот изображения на угол ang против часовой стрелки
    img = img.rotate(ang)
    # Переводим изображение в массив
    ix = img.size[0]
    iy = img.size[1]
    img_array_rot = np.array(img.getdata(), dtype = 'uint8').reshape(iy, ix)
    return img_array_rot
if not show:
    t0 = time.time()
    print('Поехали')
    prepareData(n_train, fn_train, fn_train_labels)
    prepareData(n_test, fn_test, fn_test_labels)
    print('Потрачено времени:', round(time.time() - t0, 3))
else:
    def plotData(data, ttl):
        plt.figure(ttl)
        k = 0
        for i in range(30):
            j = np.random.randint(data.shape[0])
            k += 1
            plt.subplot(3, 10, k)
            plt.imshow(data[i], cmap = 'gray')
            plt.title(cls, fontsize = 11)
            plt.axis('off')
        plt.subplots_adjust(hspace = -0.1) # wspace
        plt.show()
    if show_test:
        test_data, _ = load_data(fn_test, fn_test_labels)
        data_show = test_data.reshape(n_test, w, h)
    else:
        train_data, _ = load_data(fn_train, fn_train_labels)
        data_show = train_data.reshape(n_train, w, h)
    ttl = 'Прямоугольник'
    plotData(data_show, ttl)

Для поворота взамен PIL можно употребить следующий код (используются данные MNIST):

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from PIL import Image # Для поворота изображения
from skimage.measure import compare_ssim
def PIL_rot(img, ang):
    # Формируем изображение по массиву img_array
    img = Image.fromarray(img, 'L')
    # Поворот изображения на угол ang против часовой стрелки
    img = img.rotate(ang)
    # Переводим изображение в массив
    img = np.array(img)
    return img
def show_x(img0, img1, img):
    def sp(n, img, ttl):
        plt.subplot(1, 3, n)
        plt.title(ttl)
        plt.axis('off')
        plt.imshow(img, cmap = 'gray')
    sp(1, img0, 'Исходное')
    sp(2, img1, 'Программа')
    sp(3, img, 'PIL')
    plt.show()
def loadBinData(img_rows, img_cols):
    print('Загрузка данных из двоичных файлов...')
    with open('imagesTrain.bin', 'rb') as f:
        x_trn = np.fromfile(f, dtype = np.uint8)
    with open('imagesTest.bin', 'rb') as f:
        x_tst = np.fromfile(f, dtype = np.uint8)
    x = np.concatenate([x_trn, x_tst])
    x = x.reshape(x.shape[0] // (img_rows * img_cols), img_rows, img_cols)
    return x
def rot_img(img, ang):
    def fill_in(vr, sy, sx, cs, sn, img, img_rot):
        cy, cx = sy // 2, sx // 2
        for j in range(sy):
            for i in range(sx):
                clr = img[i, j]
                if clr == 0: continue
                y0, x0 = i - cy, j - cx
                y = x0 * sn + y0 * cs
                x = x0 * cs - y0 * sn
                ry = round(y)
                rx = round(x)
                if 0 <= cy + ry < sy and 0 <= cx + rx < sx:
                    if vr == 1:
                        new_i, new_j = cy + int(ry), cx + int(rx)
                        img_rot[new_i, new_j] = clr
                    else:
                        new_i, new_j = cy + int(y), cx + int(x)
                        if img_rot[new_i, new_j] == 0: img_rot[new_i, new_j] = clr
    ang = -ang # Поворот против часовой стрелки
    ang_radian = ang / 180 * np.pi
    cs, sn = np.cos(ang_radian), np.sin(ang_radian)
    img_rot = np.zeros(img.shape, dtype = 'uint8')
    sy, sx = img.shape[0], img.shape[1]
    fill_in(1, sy, sx, cs, sn, img, img_rot)
    fill_in(2, sy, sx, cs, sn, img, img_rot)
    return img_rot
img_rows = img_cols = 28
x = loadBinData(img_rows, img_cols)
ang = 30
ind = 2088 # np.random.randint(0, len(x) - 1)
img = x[ind]
img_rot = rot_img(img, ang) # Поворот изображения
pil_img = PIL_rot(img, ang)
print('Сходство программа vs PIL:', compare_ssim(img_rot / 255, pil_img / 255))
show_x(img, img_rot, pil_img)
# Поворот на -ang для сравнения с исходным изображением
img_rot = rot_img(img_rot, -ang)
pil_img = PIL_rot(pil_img, -ang)
print('Сходство исходное vs программа:', compare_ssim(img / 255, img_rot / 255))
print('Сходство исходное vs PIL:', compare_ssim(img / 255, pil_img / 255))
show_x(img, img_rot, pil_img)
# После поворота на 30
# Сходство программа vs PIL: 0.8753
# После поворота на -30
# Сходство исходное vs программа: 0.906
# Сходство исходное vs PIL: 0.9625

Результаты после поворота на 30° и обратного поворота на -30°.

30

Пример формирования зашумленного подмножества половины параболы

1/2 параболы

Данные сохраняются в двоичные файлы.

import numpy as np
import math, time
import matplotlib.pyplot as plt
#
np.random.seed(348)
show = not True
show_test = False
fn_train = 'dataTrain.bin'
fn_train_labels = 'labelsTrain.bin'
fn_test = 'dataTest.bin'
fn_test_labels = 'labelsTest.bin'
n_train = 600 # Число рисунков для обучения
n_test = 100 # Число тестовых рисунков
clr_mim, clr_max = 75, 255 # Диапазон оттенков серого цвета
w, h = 64, 64 # Ширина и высота рисунка
w2 = w / 2
border = 1 # Граница
#
def parab(x, coef):
    x_noise = np.random.uniform(-0.5, 0.5)
    y_noise = np.random.uniform(-4, 4)
    y = coef * (x + x_noise)**2 + border + 2 + y_noise
    return y
#
def one_class(n, fn, fn2):
    file = open(fn, 'wb')
    file2 = open(fn2, 'wb')
    xe = w2 - border - 1
    dx = 0.1
    xs = 0
    label = 0
    for i in range(n): # n - число примеров
        sgn = np.random.randint(2) # x или -x
        coef_noise = np.random.uniform(0.3, 3.0)
        coef = 0.07 * coef_noise
        arrPic = np.zeros((w, h), dtype = np.uint8)
        x = xs - dx
        while x < xe:
            x += dx
            y = parab(x, coef)
            if sgn == 1:
                ix = min(w - 1, int(w2 + x))
            else:
                ix = max(0, int(w2 - x))
            iy = h - int(y)
            iy = max(0, iy)
            iy = min(h - 1, iy) # Уходим из физической системы координат
            clr = np.random.randint(clr_mim, clr_max)
            arrPic[iy, ix] = clr
        file.write(arrPic)
        file2.write(np.uint8(label))
    file.close()
    file2.close()
#
def load_data(fn, fn2):
    with open(fn, 'rb') as read_binary:
        data = np.fromfile(read_binary, dtype = np.uint8)
    with open(fn2, 'rb') as read_binary:
        labels = np.fromfile(read_binary, dtype = np.uint8)
    return data, labels
#
if not show:
    t0 = time.time()
    print('Поехали')
    one_class(n_train, fn_train, fn_train_labels)
    one_class(n_test, fn_test, fn_test_labels)
    print('Потрачено времени:', round(time.time() - t0, 3))
else:
    def plotData(data, ttl, cls):
        plt.figure(ttl)
        k = 0
        for i in range(30):
            j = np.random.randint(data.shape[0])
            k += 1
            plt.subplot(3, 10, k)
            plt.imshow(data[i], cmap = 'gray')
            plt.title(cls, fontsize = 11)
            plt.axis('off')
        plt.subplots_adjust(hspace = -0.1) # wspace
        plt.show()
    if show_test:
        test_data, _ = load_data(fn_test, fn_test_labels)
        data_show = test_data.reshape(n_test, w, h)
    else:
        train_data, _ = load_data(fn_train, fn_train_labels)
        data_show = train_data.reshape(n_train, w, h)
    ttl = '1/2 параболы'
    cls = 0
    plotData(data_show, ttl, cls)

ЛР 9. Решается задача 12

Содержание отчета:
  1. Задание.
  2. Статистические характеристики корпуса:
    - число биграмм; - число слов, для которых формируется правый контекст (ПК);
    - максимальная и средняя длина ПК.
    - максимальная и средняя частота появления слова в ПК.
  3. Примеры биграмм (не менее 10, случайный выбор).
  4. Примеры ПК (не менее 5, случайный выбор).
  5. Алгоритм выбора следующего слова.
  6. Примеры сгенерированных текстов (не менее 5, длина текста не менее 20 слов).
  7. Код программы.
Контрольные вопросы и задачи:
  1. Использованные в программе ЛР 9 типы данных.
  2. Заменить в составных частях речи пробелы на символ подчеркивания: после_того_как, кроме_того, для_того_чтобы...
  3. Создать json-файл с примерами записей оценок на http://100byte.ru/stdntsfrm.html (вкладка Отчеты)
    (файл создается без строки с названиями столбцов):
    ФИО281518192225200203224
    1Алешина СофьяД98И996Д8685.085.0
    2Аршинова ДианаД97И87689978.390.0
    3Волков Евгений----------0.00.0
    4Гаврилова ДарьяДИ8ИИ88ДД790.092.5
    ...
    Прочитать, используя pandas.read_json, созданный файл и затем:
    - вывести фамилию, имя студента и значения в столбцах %И и %Д;
    - вывести фамилию, имя студента и значения в столбцах %И и %Д, если %И > 80.
    Замечание. При выводе, обращаясь к pandas.core.frame.DataFrame-объекту, использовать список ключей.
  4. Как и с какой цель выполняется предварительная обработка текста.
  5. Как улучшить качество генерируемого текста (сделать его более правдоподобным).
  6. Вывести первые N (например, N = 5) наиболее часто встречаемых в заданном корпусе биграмм с указанием числа их употреблений.
  7. То же, что и в п. 7, сделать для триграмм.
    import nltk
    from nltk.util import ngrams
    lst_t = list(ngrams(s, 3)) # Триграммы строки s
  8. Алгоритм отсечения отрезков Коэна-Сазерленда.
  9. Алгоритм разбиения средней точкой в задаче отсечения отрезков.
  10. Алгоритм отсечения отрезков Кируса-Бека.
  11. Обоснование алгоритма Брезенхема (случай первого квадранта, y = ax + b, 0 ≤ a ≤ 1).

ЛР 10. Решается задача 91

Контрольные вопросы:
  1. Метод z-буфера.
  2. Иерархический z-буфер.
  3. Z-пирамида и ее применение.
  4. Z-буфер в режиме смешения цветов.
  5. Имитация прозрачности.
  6. Тест глубины в OpenGL.

ЛР 11. Решается задача 93

Вариант 1.

  1. Записать алгоритм формирования текста по начальному фрагменту, опираясь на прогноз predict_output_word и случайным образом выбирая очередное слово из текущего прогноза с учетом указанных в прогнозе вероятностей.
    Начальный фрагмент случайным образом выбирается из исходного корпуса.
    Выбранный фрагмент недопустим, если в нем имеется слово, отсутствующее в словаре word2vec-модели.
    Так же выбираемое слово не должно совпадать с ему предшествующими словами.
    Рекомендация. Использовать левый контекст.
  2. Реализовать алгоритм.
  3. Привести 5 примеров сформированных текстов с числом слов от 10 до 15.

Вариант 2.

  1. Записать алгоритм сравнения двух word2vec-моделей по критерию "Точность прогнозирования". (Для прогнозирования используется метод predict_output_word).
  2. Реализовать алгоритм.
  3. Сравнить две word2vec-модели, различающие размерами признакового пространства (параметр size).

Значение size берется из следующего списка.

  1. 25 и 50.
  2. 25 и 75.
  3. 25 и 100.
  4. 25 и 150.
  5. 50 и 75.
  6. 50 и 100.
  7. 50 и 150.
  8. 75 и 100.
  9. 75 и 150.

Подготовка к ЛР 11.

Программа 1.

  1. Создать word2vec-модель по имеющемуся json-файлу.
    import pandas as pd, re
    import multiprocessing, time
    from gensim.models import Word2Vec
    wv_model = Word2Vec(data, ...)
    1. data - это список предложений корпуса.
      Каждое предложение – это список слов, например:
      [..., ['хлое', 'был', 'тогда', 'я', 'мил'],
      ['а', 'теперь', 'мне', 'жизнь', 'могила', 'белый', 'свет', 'душе', 'постыл'],
      ['грустен', 'лес', 'поток', 'уныл', 'хлоя', 'другу', 'изменила'], ...]
      В качестве разделителей текста на предложения использовать 5 следующих знаков препинания: ".;:!?".
    2. Каждое предложение обрабатывается следующим образом:
      - перевод в нижний регистр;
      - остаются только русские буквы.
    3. Параметры модели:
      size = 50
      window = 2
      min_cnt = 2
      sg = 0 # Используем CBOW
      workers = multiprocessing.cpu_count()
      n_iter - 100 или более.
    4. Замерить время создания модели.
    5. Определить размер словаря корпуса.
  2. Сохранить word2vec-модель в файл:
    wv_model.save('w2v.model')

Программа 2.

  1. Загрузить модель из файла:
    import numpy as np
    from gensim.models import Word2Vec
    wv_model = Word2Vec.load('w2v.model')
  2. Вывести статистические характеристики модели:
    - число предложений;
    - число слов;
    - размер словаря (сравнить с размером словаря корпуса, см. п. 1.5).
  3. Проверить на пяти или более примерах, насколько хорошо модель выполняет прогноз центрального слова контекста.

Пример:

# wv – индексированные векторы слов
wv = wv_model.wv
# Словарь модели
vocab = wv.vocab # class 'dict'
sen = 'чтоб не упасть дорогой склизкой'
print(sen)
sen = sen.split()
for w in sen:
    if vocab.get(w) is None:
        print('Слова', w, 'нет в словаре')
pred_words = wv_model.predict_output_word([sen[0], sen[1], sen[3], sen[4]], topn = 5)
print(pred_words)

Результат:

чтоб не упасть дорогой склизкой
Слова склизкой нет в словаре
[('упасть', 0.0121), ('такого', 0.0096), ('хочу', 0.0087), ('тайны', 0.0078), ('того', 0.0059)]

Самостоятельные работы

СР 1. Материалы

Метрики:

from scipy.spatial import distance
from skimage.measure import compare_ssim
dist = distance.euclidean(im1, im2) # Евклидово расстояние
dist_cs = distance.cosine(im1, im2) # Косинусное расстояние
sim = compare_ssim(im1, im2) # Индекс структурного сходства изображений
где im1, im2 – векторы, содержащие данные об изображениях.
В случае compare_ssim расстояние оценивается как 1 – sim.

Случайное целое число от 0 до N-1:

k = np.random.randint(N)

Сортировка списка по ключу:

x = [['c', 8], ['b', 5], ['a', 3]]
x.sort(key = lambda r:(r[1]))
# [['a', 3], ['b', 5], ['c', 8]]

Результат:

Результат представляется в виде отчета, содержащего:
- задание;
- алгоритм;
- входные данные;
- выходные данные;
- программу.

Набор данных: MNIST.

СР 1. Задания

1. Написать процедуру, определяющую вертикальный сдвиг изображения цифры.
Вертикальный сдвиг равен нулю, если изображение центрировано по оси Y.
Используя эту процедуру, найти и вывести изображения с максимальным и минимальным вертикальным сдвигом в обучающем и проверочном множествах.

2. Задать цифру.
Найти поочередно в обучающем и проверочном множествах два наиболее похожих изображения, используя поочередно следующие метрики:
- евклидово расстояние;
– косинусное расстояние;
- индекс структурного сходства.
Вывести расстояние, индексы и рисунки цифр.
Замерить время поиска изображений.

3. Задать вид множества (обучающее или проверочное).
Выбрать в заданном множестве случайным образом изображение и найти наиболее похожее на него изображение, используя поочередно следующие метрики:
- евклидово расстояние;
– косинусное расстояние;
- индекс структурного сходства.
Вывести для каждого множества расстояние, индексы и рисунки выбранного и найденного изображений.

4. Задать цифру и вид множества (обучающее или проверочное).
Найти n (n >= 2) изображений, наиболее близких к обобщенному образу цифры, используя поочередно следующие метрики:
- евклидово расстояние;
– косинусное расстояние;
- индекс структурного сходства.
Вывести найденное расстояние, индексы и рисунки найденных изображений.

5. Задать цифру и вид множества (обучающее или проверочное).
Найти центральное изображение и n (n >= 2) изображений, наиболее удаленных от центрального изображения.
Центральное изображение - это изображение, наиболее близкое к обобщенному изображению.
Вывести найденные расстояния, индексы и рисунки найденных изображений.
Метрика - любая.

6. Задать цифру и вид множества (обучающее или проверочное).
Найти граничные изображения и затем изображение с наименьшим суммарным расстоянием до граничных изображений.
Граничные изображения - два наиболее удаленных изображения.
Вывести индексы и рисунки найденных изображений.
Метрика - любая.

7. Задать вид множества (обучающее или проверочное).
Среди подмножеств изображений указать подмножество с наименьшим минимальным расстоянием между его изображениями.
Подмножество изображений - это совокупность изображений с одинаковой меткой.
Вывести список с элементами [метка, минимальное расстояние], упорядоченный по последнему показателю
и n (n >= 10) примеров рисунков случайно выбранных изображений из найденного подмножества.

8. Выполнить, используя ImageDataGenerator, генерацию данных на основе обучающего и проверочного множеств.
Сформировать множество пар изображений-аналогов. (Берутся изображения одинаковых цифр).
Изображения А и Б являются аналогами, если индекс структурного сходства этих изображений более sim_a (sim_a > 0.7).
Изображение А берется из обучающего множества, а Б - из тестового.
Вывести 20 пар изображений сформированного множества (или все, если его размер менее 20).
Вывести размер найденного множества, а также список с элементами [цифра, число аналогов],
отсортированный по числу аналогов.
Метрика - индекс структурного сходства.

9. Задать вид множества (обучающее или проверочное).
Внести шум в n (n >= 10) случайно выбранных изображений,
изменив случайным образом значения не менее 15% нулевых пикселей в каждом из них.
Изменяемый пиксель так же выбирается случайно.
Вывести метки, индексы и рисунки изображений до и после изменения.

10. Задать вид множества (обучающее или проверочное).
Найти последовательно в обучающем и проверочном множествах два самых похожих изображения цифр 1 и 7,
используя поочередно следующие метрики:
- евклидово расстояние;
– косинусное расстояние;
- индекс структурного сходства.
Вывести расстояние, индексы и рисунки найденных изображений.
Для сокращения объема вычислений использовать подвыборки из сформированных множеств.

11. Выполнить, используя ImageDataGenerator, генерацию данных на основе обучающего и проверочного множеств.
Найти два самых похожих изображения каждой из цифр (0, 1, ..., 9), выбирая первое из множества, полученного на основе обучающего,
а второе - на основе проверочного.
Вывести индексы и рисунки найденных изображений.
Метрика любая.
Для сокращения объема вычислений использовать подвыборки из сформированных множеств.

12. Задать вид множества (обучающее или проверочное).
Упорядочить множество по числу пустых пикселей в изображении (в убывающем порядке).
Вывести рисунки цифр упорядоченного множества с шагом 100
и отдельно рисунки первого, последнего и разности последнего и первого изображений упорядоченного множества.

13. Найти и вывести в обучающем и проверочном множествах изображения
с максимальным и минимальным горизонтальным сдвигом.
Горизонтальный сдвиг равен нулю, если изображение центрировано по оси X.

14. Задать вид множества (обучающее или проверочное).
А. Выполнить, используя ImageDataGenerator, генерацию данных на основе выбранного множества.
Б. Вычислить расстояние между обобщенными портретами исходных и сгенерированных данных.
В. Запомнить обобщенные портреты исходных и сгенерированных данных и полученное расстояние.
Повторить пункты А-В n раз (n >= 2).
Вывести полученные расстояния, рисунки обобщенных портретов и их разности.

15. Выполнить, используя ImageDataGenerator, генерацию данных на основе обучающего и проверочного множеств.
Найти для каждой из цифр в каждом из сгенерированном множестве максимально разделенные изображения.
Вывести найденные расстояние и изображения (рисунки).
Метрика любая.
Для сокращения объема вычислений использовать подвыборки из сформированных множеств.

16. Выполнить, используя ImageDataGenerator, генерацию данных на основе обучающего и проверочного множеств.
Определить, оперируя подвыборками из полученных множеств, DAi - средние расстояния между изображениями одинаковых цифр
(первое изображение берется из множества, полученного на основе обучающего, а второе - на основе проверочного).
Вывести список вида [i, DAi], упорядоченный по DAi (i = 0, 1, ..., 9).
Взять первую цифру этого списка и вывести два изображения, расстояние между которыми наиболее близко к DAi
(одно изображение берется из множества, полученного на основе обучающего, а второе - на основе проверочного).
Вывести расстояние между выведенными изображениями.
Метрика любая.

17. Сформировать список минимальных расстояний между изображениями обучающего и проверочного множеств одинаковых цифр.
Для каждой цифры сформировать множество пар изображений, расстояние между которыми больше соответствующего минимального расстояния,
не более чем на P%. (Значение P случайным образом берется из отрезка [5, 10]).
Вывести список вида [цифра, число пар изображений, отвечающих критерию P], упорядоченный по второму показателю.
Вывести 20 пар изображений сформированного множества (или все, если его размер менее 20).
В противном случае вывести 20 случайно выбранных пар изображений сформированного множества.
Метрика любая.

18. Задать цифру.
Найти в обучающем и проверочном множествах изображения с максимальным и минимальным числом загруженных пикселей (то есть пикселей с ненулевым значением цвета).
Вывести найденные максимальные и минимальные значения, индексы и найденные изображения (рисунки).

19. Создать процедура поворота изображения. Используя эту процедуру, выполнить поворот случайно выбранной цифры на заданный угол.
Сравнить, используя индекс структурного сходства, результат с изображением, полученным в результате поворота той же цифры с помощью метода rotate библиотеки PIL.

import numpy as np
import matplotlib.pyplot as plt
from PIL import Image # Для поворота изображения
def img_rot(img, ang):
    # Формируем изображение по массиву img (тип данных – 'uint8')
    img = Image.fromarray(img, 'L')
    # Поворот изображения на угол ang против часовой стрелки
    img = img.rotate(ang)
    # Переводим изображение в массив
    ix = img.size[0]
    iy = img.size[1]
    img = np.array(img.getdata()).reshape(iy, ix)
    return img
... Получаем изображение img - массив формы (28, 28)
ang = 30
img = img_rot(img, ang) # Поворот изображения

СР 2. Задания

Вариант 1. Написать процедуру вывода отрезка прямой с интерполяцией цветов. При выводе отрезка использовать алгоритм Брезенхема.
Вывести, используя созданную процедуру, четырехугольник и его диагонали.
Координаты вершин фигуры случайным образом берутся из следующих интервалов:
x0, y0: [2, 11], [2, 11]
x1, y1: [39, 48], [2, 11]
x2, y2: [39, 48], [39, 48]
x3, y3: [2, 11], [39, 48]
Цвет в вершинах:
clr0: [255, 0, 0]
clr1: [0, 255, 0]
clr2: [0, 0, 255]
clr3: [255, 255, 0]
Заданный цвет выводится в углах области вывода.
Пример.

Пример

Вариант 2. Написать процедуру отсечения на основе алгоритма разбиения средней точкой.
Используя созданную процедуру, выполнить отсечение в области
(XL, YB) = (9, 11), (XR, YT) = (39, 29)
следующих отрезков:
Отрезок 1:
(x1, y1) = (2, 4), (x11, y11) = (48, 32)
Отрезок 2:
(x2, y2) = (48, 8), (x22, y22) = (20, 22)
Отрезок 3:
(x3, y3) = (13, 25), (x33, y33) = (35, 27)
Отрезок 4:
(x4, y4) = (2, 25), (x44, y44) = (13, 36)
Отрезок 5:
(x5, y5) = (13, 7), (x55, y55) = (37, 1)
При выводе отрезков использовать алгоритм Брезенхема.
Вывести начальную и конечную сцены:
Пример.

Пример

Вариант 3. Написать процедуру вывода отрезка прямой. При выводе отрезка использовать алгоритм Брезенхема.
Соединить, используя созданную процедуру, 6 случайно заданных точек.
Вдобавок вывести точки, используя следующие цвета:
[255, 0, 0]
[0, 255, 0]
[0, 0, 255]
[255, 255, 0]
[255, 0, 255]
[0, 255, 255]
Координаты точек выбираются из следующих интервалов:
x0, y0: [2, 60], [2, 10]
x1, y1: [2, 60], [12, 20]
x2, y2: [2, 60], [22, 30]
x3, y3: [2, 60], [32, 40]
x4, y4: [2, 60], [42, 50]
x5, y5: [2, 60], [52, 60]
Пример.

Пример

Вариант 4. Написать процедуру вывода отрезка прямой. При выводе отрезка использовать алгоритм Брезенхема.
Соединить, используя созданную процедуру, 6 случайно заданных точек, а затем конечную точку с начальной.
Вдобавок вывести точки, используя следующие цвета:
[255, 0, 0] – начальная точка;
[0, 255, 0];
[0, 0, 255];
[255, 255, 0];
[255, 0, 255];
[0, 255, 255] – конечная точка.
Координаты точек случайным образом выбираются из следующих интервалов:
x0, y0: [2, 10], [2, 60];
x1, y1: [12, 20], [2, 60];
x2, y2: [22, 30], [2, 60];
x3, y3: [32, 40], [2, 60];
x4, y4: [42, 50], [2, 60];
x5, y5: [52, 60], [2, 60].
При выводе отрезков использовать следующие цвета:
clr0 = [0, 0, 255] – первый отрезок, соединяющий (x0, y0) и (x1, y1);
clr1 = [255, 255, 0];
clr2 = [255, 0, 255];
clr3 = [0, 255, 255];
clr4 = [255, 0, 0];
clr5 = [0, 255, 0] – последний отрезок, соединяющий (x5, y5) и (x0, y0).
Пример.

Пример

Литература

1. OpenGL: преобразование координат. [Электронный ресурс] URL: http://100byte.ru/100btwrks/prjctn/prjctn.html. (Дата обращения: 01.09.2020).
2. Вывод граней OpenGL. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/gl/gl.html. (Дата обращения: 01.09.2020).
3. C#, OpenGL: вывод стандартных примитивов. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/stdprmtvs/stdprmtvs.html. (Дата обращения: 01.09.2020).
4. Построение, текстурирование и тонирование сферы. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/sphTK/sphTK.html. (Дата обращения: 01.09.2020).
5. Шейдер с иллюзией выступов и впадин. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/mapTK/mapTK.html. (Дата обращения: 01.09.2020).
6. Шейдер с картой-кубом в задаче воспроизведения глобуса.Шейдер с картой-кубом в задаче воспроизведения глобуса. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/earthCubeMapTK/earthCubeMapTK.html. (Дата обращения: 01.09.2020).
7. Шейдерная реализация освещения по Блинну и Фонгу. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/blinnPhongPart/blinnPhong.html. (Дата обращения: 01.09.2020).
8. Вывод текста.. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/txt.html. (Дата обращения: 01.09.2020).
9. Вывод частиц. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/pf.html. (Дата обращения: 01.09.2020).
10. Руководство по программированию. [Электронный ресурс] URL: https://pyglet.readthedocs.io/en/latest/programming_guide/graphics.html. (Дата обращения: 01.09.2020).

Список работ

Рейтинг@Mail.ru