Skip to content

Commit 7f5624e

Browse files
icedcoffeeeepre-commit-ci[bot]naveen521kk
authored
Implement OpenGLImageMobject (#2534)
* initial commit make OpenGLImageMobject a subclass of OpenGLTexturedSurface * added typing * unused import * explicit imports * new typing style * convert to correct type * update file structure * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Added support for Path and image_mode parameter * remove duplicate function * let moderngl accept PIL Images only * allow custom resampling * allow greyscale Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Naveen M K <[email protected]>
1 parent 54bee27 commit 7f5624e

File tree

6 files changed

+154
-35
lines changed

6 files changed

+154
-35
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from __future__ import annotations
2+
3+
__all__ = [
4+
"OpenGLImageMobject",
5+
]
6+
7+
from pathlib import Path
8+
9+
import numpy as np
10+
from PIL import Image
11+
12+
from manim.mobject.opengl.opengl_surface import OpenGLSurface, OpenGLTexturedSurface
13+
from manim.utils.images import get_full_raster_image_path
14+
15+
16+
class OpenGLImageMobject(OpenGLTexturedSurface):
17+
def __init__(
18+
self,
19+
filename_or_array: str | Path | np.ndarray,
20+
width: float = None,
21+
height: float = None,
22+
image_mode: str = "RGBA",
23+
resampling_algorithm: int = Image.BICUBIC,
24+
opacity: float = 1,
25+
gloss: float = 0,
26+
shadow: float = 0,
27+
**kwargs,
28+
):
29+
self.image = filename_or_array
30+
self.resampling_algorithm = resampling_algorithm
31+
if type(filename_or_array) == np.ndarray:
32+
self.size = self.image.shape[1::-1]
33+
elif isinstance(filename_or_array, (str, Path)):
34+
path = get_full_raster_image_path(filename_or_array)
35+
self.size = Image.open(path).size
36+
37+
if width is None and height is None:
38+
width = 4 * self.size[0] / self.size[1]
39+
height = 4
40+
if height is None:
41+
height = width * self.size[1] / self.size[0]
42+
if width is None:
43+
width = height * self.size[0] / self.size[1]
44+
45+
surface = OpenGLSurface(
46+
lambda u, v: np.array([u, v, 0]),
47+
[-width / 2, width / 2],
48+
[-height / 2, height / 2],
49+
opacity=opacity,
50+
gloss=gloss,
51+
shadow=shadow,
52+
)
53+
54+
super().__init__(
55+
surface,
56+
self.image,
57+
image_mode=image_mode,
58+
opacity=opacity,
59+
gloss=gloss,
60+
shadow=shadow,
61+
**kwargs,
62+
)
63+
64+
def get_image_from_file(
65+
self,
66+
image_file: str | Path | np.ndarray,
67+
image_mode: str,
68+
):
69+
if isinstance(image_file, (str, Path)):
70+
return super().get_image_from_file(image_file, image_mode)
71+
else:
72+
return (
73+
Image.fromarray(image_file.astype("uint8"))
74+
.convert(image_mode)
75+
.resize(
76+
np.array(image_file.shape[:2])
77+
* 200, # assumption of 200 ppmu (pixels per manim unit) would suffice
78+
resample=self.resampling_algorithm,
79+
)
80+
)

manim/mobject/opengl/opengl_surface.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
from __future__ import annotations
22

3+
from pathlib import Path
4+
from typing import Iterable
5+
36
import moderngl
47
import numpy as np
8+
from PIL import Image
59

610
from manim.constants import *
711
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
812
from manim.utils.bezier import integer_interpolate, interpolate
913
from manim.utils.color import *
1014
from manim.utils.config_ops import _Data, _Uniforms
11-
from manim.utils.images import get_full_raster_image_path
15+
from manim.utils.images import change_to_rgba_array, get_full_raster_image_path
1216
from manim.utils.iterables import listify
1317
from manim.utils.space_ops import normalize_along_axis
1418

@@ -324,22 +328,37 @@ class OpenGLTexturedSurface(OpenGLSurface):
324328
num_textures = _Uniforms()
325329

326330
def __init__(
327-
self, uv_surface, image_file, dark_image_file=None, shader_folder=None, **kwargs
331+
self,
332+
uv_surface: OpenGLSurface,
333+
image_file: str | Path,
334+
dark_image_file: str | Path = None,
335+
image_mode: str | Iterable[str] = "RGBA",
336+
shader_folder: str | Path = None,
337+
**kwargs,
328338
):
329339
self.uniforms = {}
330340

331341
if not isinstance(uv_surface, OpenGLSurface):
332342
raise Exception("uv_surface must be of type OpenGLSurface")
343+
if type(image_file) == np.ndarray:
344+
image_file = change_to_rgba_array(image_file)
345+
333346
# Set texture information
334-
if dark_image_file is None:
335-
dark_image_file = image_file
336-
self.num_textures = 1
337-
else:
338-
self.num_textures = 2
347+
if isinstance(image_mode, (str, Path)):
348+
image_mode = [image_mode] * 2
349+
image_mode_light, image_mode_dark = image_mode
339350
texture_paths = {
340-
"LightTexture": get_full_raster_image_path(image_file),
341-
"DarkTexture": get_full_raster_image_path(dark_image_file),
351+
"LightTexture": self.get_image_from_file(
352+
image_file,
353+
image_mode_light,
354+
),
355+
"DarkTexture": self.get_image_from_file(
356+
dark_image_file or image_file,
357+
image_mode_dark,
358+
),
342359
}
360+
if dark_image_file:
361+
self.num_textures = 2
343362

344363
self.uv_surface = uv_surface
345364
self.uv_func = uv_surface.uv_func
@@ -349,6 +368,14 @@ def __init__(
349368
self.gloss = self.uv_surface.gloss
350369
super().__init__(texture_paths=texture_paths, **kwargs)
351370

371+
def get_image_from_file(
372+
self,
373+
image_file: str | Path,
374+
image_mode: str,
375+
):
376+
image_file = get_full_raster_image_path(image_file)
377+
return Image.open(image_file).convert(image_mode)
378+
352379
def init_data(self):
353380
super().init_data()
354381
self.im_coords = np.zeros((0, 2))

manim/mobject/types/image_mobject.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from ...mobject.mobject import Mobject
1818
from ...utils.bezier import interpolate
1919
from ...utils.color import WHITE, color_to_int_rgb
20-
from ...utils.images import get_full_raster_image_path
20+
from ...utils.images import change_to_rgba_array, get_full_raster_image_path
2121

2222

2323
class AbstractImageMobject(Mobject):
@@ -183,26 +183,13 @@ def __init__(
183183
else:
184184
self.pixel_array = np.array(filename_or_array)
185185
self.pixel_array_dtype = kwargs.get("pixel_array_dtype", "uint8")
186-
self.change_to_rgba_array()
186+
self.pixel_array = change_to_rgba_array(
187+
self.pixel_array, self.pixel_array_dtype
188+
)
187189
if self.invert:
188190
self.pixel_array[:, :, :3] = 255 - self.pixel_array[:, :, :3]
189191
super().__init__(scale_to_resolution, **kwargs)
190192

191-
def change_to_rgba_array(self):
192-
"""Converts an RGB array into RGBA with the alpha value opacity maxed."""
193-
pa = self.pixel_array
194-
if len(pa.shape) == 2:
195-
pa = pa.reshape(list(pa.shape) + [1])
196-
if pa.shape[2] == 1:
197-
pa = pa.repeat(3, axis=2)
198-
if pa.shape[2] == 3:
199-
alphas = 255 * np.ones(
200-
list(pa.shape[:2]) + [1],
201-
dtype=self.pixel_array_dtype,
202-
)
203-
pa = np.append(pa, alphas, axis=2)
204-
self.pixel_array = pa
205-
206193
def get_pixel_array(self):
207194
"""A simple getter method."""
208195
return self.pixel_array

manim/opengl/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88

99
from manim.mobject.opengl.dot_cloud import *
10+
from manim.mobject.opengl.opengl_image_mobject import *
1011
from manim.mobject.opengl.opengl_mobject import *
1112
from manim.mobject.opengl.opengl_point_cloud_mobject import *
1213
from manim.mobject.opengl.opengl_surface import *

manim/renderer/opengl_renderer.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -381,18 +381,21 @@ def render_mobject(self, mobject):
381381
mesh.render()
382382

383383
def get_texture_id(self, path):
384-
if path not in self.path_to_texture_id:
385-
# A way to increase tid's sequentially
384+
if repr(path) not in self.path_to_texture_id:
386385
tid = len(self.path_to_texture_id)
387-
im = Image.open(path)
388386
texture = self.context.texture(
389-
size=im.size,
390-
components=len(im.getbands()),
391-
data=im.tobytes(),
387+
size=path.size,
388+
components=len(path.getbands()),
389+
data=path.tobytes(),
392390
)
391+
texture.repeat_x = False
392+
texture.repeat_y = False
393+
texture.filter = (moderngl.NEAREST, moderngl.NEAREST)
394+
texture.swizzle = "RRR1" if path.mode == "L" else "RGBA"
393395
texture.use(location=tid)
394-
self.path_to_texture_id[path] = tid
395-
return self.path_to_texture_id[path]
396+
self.path_to_texture_id[repr(path)] = tid
397+
398+
return self.path_to_texture_id[repr(path)]
396399

397400
def update_skipping_status(self):
398401
"""

manim/utils/images.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
from __future__ import annotations
44

5-
__all__ = ["get_full_raster_image_path", "drag_pixels", "invert_image"]
5+
__all__ = [
6+
"get_full_raster_image_path",
7+
"drag_pixels",
8+
"invert_image",
9+
"change_to_rgba_array",
10+
]
611

712
import numpy as np
813
from PIL import Image
@@ -32,3 +37,19 @@ def invert_image(image: np.array) -> Image:
3237
arr = np.array(image)
3338
arr = (255 * np.ones(arr.shape)).astype(arr.dtype) - arr
3439
return Image.fromarray(arr)
40+
41+
42+
def change_to_rgba_array(image, dtype="uint8"):
43+
"""Converts an RGB array into RGBA with the alpha value opacity maxed."""
44+
pa = image
45+
if len(pa.shape) == 2:
46+
pa = pa.reshape(list(pa.shape) + [1])
47+
if pa.shape[2] == 1:
48+
pa = pa.repeat(3, axis=2)
49+
if pa.shape[2] == 3:
50+
alphas = 255 * np.ones(
51+
list(pa.shape[:2]) + [1],
52+
dtype=dtype,
53+
)
54+
pa = np.append(pa, alphas, axis=2)
55+
return pa

0 commit comments

Comments
 (0)