Skip to content

Fix ImageMobject 3D rotation/flipping and remove resampling algorithms lanczos (antialias), box and hamming #4266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 69 additions & 47 deletions manim/camera/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@
import cairo
import numpy as np
from PIL import Image
from scipy.spatial.distance import pdist

from .. import config, logger
from ..constants import *
from ..mobject.mobject import Mobject
from ..mobject.types.image_mobject import AbstractImageMobject
from ..mobject.types.point_cloud_mobject import PMobject
from ..mobject.types.vectorized_mobject import VMobject
from ..utils.color import ManimColor, ParsableManimColor, color_to_int_rgba
from ..utils.family import extract_mobject_family_members
from ..utils.images import get_full_raster_image_path
from ..utils.iterables import list_difference_update
from ..utils.space_ops import angle_of_vector

from manim._config import config, logger
from manim.constants import *
from manim.mobject.mobject import Mobject
from manim.mobject.types.image_mobject import AbstractImageMobject
from manim.mobject.types.point_cloud_mobject import PMobject
from manim.mobject.types.vectorized_mobject import VMobject
from manim.typing import ManimFloat
from manim.utils.color import ManimColor, ParsableManimColor, color_to_int_rgba
from manim.utils.family import extract_mobject_family_members
from manim.utils.images import get_full_raster_image_path
from manim.utils.iterables import list_difference_update

LINE_JOIN_MAP = {
LineJointType.AUTO: None, # TODO: this could be improved
Expand Down Expand Up @@ -963,61 +962,84 @@ def display_multiple_image_mobjects(

def display_image_mobject(
self, image_mobject: AbstractImageMobject, pixel_array: np.ndarray
):
"""Displays an ImageMobject by changing the pixel_array suitably.
) -> None:
"""Displays an :class:`~.ImageMobject` by changing the ``pixel_array`` suitably.

Parameters
----------
image_mobject
The imageMobject to display
The :class:`~.ImageMobject` to display.
pixel_array
The Pixel array to put the imagemobject in.
The pixel array to put the :class:`~.ImageMobject` in.
"""
corner_coords = self.points_to_pixel_coords(image_mobject, image_mobject.points)
ul_coords, ur_coords, dl_coords, _ = corner_coords
right_vect = ur_coords - ul_coords
down_vect = dl_coords - ul_coords
center_coords = ul_coords + (right_vect + down_vect) / 2

sub_image = Image.fromarray(image_mobject.get_pixel_array(), mode="RGBA")
original_coords = np.array(
[
[0, 0],
[sub_image.width, 0],
[0, sub_image.height],
[sub_image.width, sub_image.height],
]
)
target_coords = self.points_to_pixel_coords(image_mobject, image_mobject.points)

# Reshape
pixel_width = max(int(pdist([ul_coords, ur_coords]).item()), 1)
pixel_height = max(int(pdist([ul_coords, dl_coords]).item()), 1)
sub_image = sub_image.resize(
(pixel_width, pixel_height),
resample=image_mobject.resampling_algorithm,
# Temporarily translate target coords to upper left corner to calculate the
# smallest possible size for the target image.
shift_vector = np.array(
[
min(*[x for x, y in target_coords]),
min(*[y for x, y in target_coords]),
]
)
target_coords -= shift_vector
target_size = (
max(*[x for x, y in target_coords]),
max(*[y for x, y in target_coords]),
)

# Rotate
angle = angle_of_vector(right_vect)
adjusted_angle = -int(360 * angle / TAU)
if adjusted_angle != 0:
sub_image = sub_image.rotate(
adjusted_angle,
resample=image_mobject.resampling_algorithm,
expand=1,
)
# Use PIL.Image.Image.transform() to apply a perspective transform to the image.
# The transform coefficients must be calculated. The following is adapted from
# https://stackoverflow.com/questions/14177744/how-does-perspective-transformation-work-in-pil
# and
# https://web.archive.org/web/20150222120106/xenia.media.mit.edu/~cwren/interpolator/

homography_matrix = []
for (x, y), (X, Y) in zip(target_coords, original_coords):
homography_matrix.append([x, y, 1, 0, 0, 0, -X * x, -X * y])
homography_matrix.append([0, 0, 0, x, y, 1, -Y * x, -Y * y])

A = np.array(homography_matrix, dtype=ManimFloat)
b = original_coords.reshape(8).astype(ManimFloat)

try:
transform_coefficients = np.linalg.solve(A, b)
except np.linalg.LinAlgError:
# The matrix A might be singular.
# In this case, do nothing and return.
return

# TODO, there is no accounting for a shear...
sub_image = sub_image.transform(
size=target_size, # Use the smallest possible size for speed.
method=Image.Transform.PERSPECTIVE,
data=transform_coefficients,
resample=image_mobject.resampling_algorithm,
)

# Paste into an image as large as the camera's pixel array
# Paste into an image as large as the camera's pixel array.
full_image = Image.fromarray(
np.zeros((self.pixel_height, self.pixel_width)),
mode="RGBA",
)
new_ul_coords = center_coords - np.array(sub_image.size) / 2
new_ul_coords = new_ul_coords.astype(int)
full_image.paste(
sub_image,
box=(
new_ul_coords[0],
new_ul_coords[1],
new_ul_coords[0] + sub_image.size[0],
new_ul_coords[1] + sub_image.size[1],
shift_vector[0],
shift_vector[1],
shift_vector[0] + target_size[0],
shift_vector[1] + target_size[1],
),
)
# Paint on top of existing pixel array
# Paint on top of existing pixel array.
self.overlay_PIL_image(pixel_array, full_image)

def overlay_rgba_array(self, pixel_array: np.ndarray, new_array: np.ndarray):
Expand Down
4 changes: 0 additions & 4 deletions manim/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,10 @@
RESAMPLING_ALGORITHMS = {
"nearest": Resampling.NEAREST,
"none": Resampling.NEAREST,
"lanczos": Resampling.LANCZOS,
"antialias": Resampling.LANCZOS,
"bilinear": Resampling.BILINEAR,
"linear": Resampling.BILINEAR,
"bicubic": Resampling.BICUBIC,
"cubic": Resampling.BICUBIC,
"box": Resampling.BOX,
"hamming": Resampling.HAMMING,
}

# Geometry: directions
Expand Down
43 changes: 17 additions & 26 deletions manim/mobject/types/image_mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,15 @@ def set_resampling_algorithm(self, resampling_algorithm: int) -> Self:
* 'hamming'
* 'lanczos' or 'antialias'
"""
if isinstance(resampling_algorithm, int):
self.resampling_algorithm = resampling_algorithm
else:
if resampling_algorithm not in RESAMPLING_ALGORITHMS.values():
raise ValueError(
"resampling_algorithm has to be an int, one of the values defined in "
"RESAMPLING_ALGORITHMS or a Pillow resampling filter constant. "
"Available algorithms: 'bicubic', 'nearest', 'box', 'bilinear', "
"'hamming', 'lanczos'.",
"Available algorithms: 'bicubic' (or 'cubic'), 'nearest' (or 'none'), "
"'bilinear' (or 'linear').",
)

self.resampling_algorithm = resampling_algorithm
return self

def reset_points(self) -> None:
Expand Down Expand Up @@ -155,27 +155,18 @@ def construct(self):
[0, 0, 0, 255]
]))

img.height = 2
img1 = img.copy()
img2 = img.copy()
img3 = img.copy()
img4 = img.copy()
img5 = img.copy()

img1.set_resampling_algorithm(RESAMPLING_ALGORITHMS["nearest"])
img2.set_resampling_algorithm(RESAMPLING_ALGORITHMS["lanczos"])
img3.set_resampling_algorithm(RESAMPLING_ALGORITHMS["linear"])
img4.set_resampling_algorithm(RESAMPLING_ALGORITHMS["cubic"])
img5.set_resampling_algorithm(RESAMPLING_ALGORITHMS["box"])
img1.add(Text("nearest").scale(0.5).next_to(img1,UP))
img2.add(Text("lanczos").scale(0.5).next_to(img2,UP))
img3.add(Text("linear").scale(0.5).next_to(img3,UP))
img4.add(Text("cubic").scale(0.5).next_to(img4,UP))
img5.add(Text("box").scale(0.5).next_to(img5,UP))

x= Group(img1,img2,img3,img4,img5)
x.arrange()
self.add(x)
img.height = 3

group = Group()
algorithm_texts = ["nearest", "linear", "cubic"]
for algorithm_text in algorithm_texts:
algorithm = RESAMPLING_ALGORITHMS[algorithm_text]
img_copy = img.copy().set_resampling_algorithm(algorithm)
img_copy.add(Text(algorithm_text).scale(0.5).next_to(img_copy, UP))
group.add(img_copy)

group.arrange()
self.add(group)
"""

def __init__(
Expand Down
Binary file not shown.
Binary file not shown.
25 changes: 10 additions & 15 deletions tests/test_graphical_units/test_img_and_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,21 +268,16 @@ def test_ImageInterpolation(scene):
img = ImageMobject(
np.uint8([[63, 0, 0, 0], [0, 127, 0, 0], [0, 0, 191, 0], [0, 0, 0, 255]]),
)
img.height = 2
img1 = img.copy()
img2 = img.copy()
img3 = img.copy()
img4 = img.copy()
img5 = img.copy()

img1.set_resampling_algorithm(RESAMPLING_ALGORITHMS["nearest"])
img2.set_resampling_algorithm(RESAMPLING_ALGORITHMS["lanczos"])
img3.set_resampling_algorithm(RESAMPLING_ALGORITHMS["linear"])
img4.set_resampling_algorithm(RESAMPLING_ALGORITHMS["cubic"])
img5.set_resampling_algorithm(RESAMPLING_ALGORITHMS["box"])

scene.add(img1, img2, img3, img4, img5)
[s.shift(4 * LEFT + pos * 2 * RIGHT) for pos, s in enumerate(scene.mobjects)]
img.height = 3

algorithm_texts = ["nearest", "linear", "cubic"]
for i, algorithm_text in enumerate(algorithm_texts):
algorithm = RESAMPLING_ALGORITHMS[algorithm_text]
img_copy = img.copy().set_resampling_algorithm(algorithm)
position = img.height * (i - (len(algorithm_texts) - 1) / 2) * RIGHT
img_copy.move_to(position)
scene.add(img_copy)

scene.wait()


Expand Down