我该如何使用Pygame围绕图像中心旋转图像?

50
我一直在尝试使用pygame.transform.rotate()将图像围绕其中心旋转,但是它不起作用。具体而言,出现问题的部分是rot_image = rot_image.subsurface(rot_rect).copy()。我收到了异常:

ValueError: subsurface rectangle outside surface area

这是用于旋转图像的代码:

def rot_center(image, angle):
    """rotate an image while keeping its center and size"""
    orig_rect = image.get_rect()
    rot_image = pygame.transform.rotate(image, angle)
    rot_rect = orig_rect.copy()
    rot_rect.center = rot_image.get_rect().center
    rot_image = rot_image.subsurface(rot_rect).copy()
    return rot_image
7个回答

141

简短回答:

当您使用 pygame.transform.rotate 旋转图像时,新旋转后的图像尺寸会增加。您必须确保旋转后的图像位置在原始图像中心的基础上进行调整。为此,获取原始图像的矩形并设置其位置。获取旋转后的图像的矩形并通过原始矩形的中心位置设置其中心位置。
从函数red_center返回一个元组,包含旋转后的图像和旋转后图像的边界矩形:

def rot_center(image, angle, x, y):
    
    rotated_image = pygame.transform.rotate(image, angle)
    new_rect = rotated_image.get_rect(center = image.get_rect(center = (x, y)).center)

    return rotated_image, new_rect

或者编写一个函数,对图像进行旋转和 .blit 操作:

def blitRotateCenter(surf, image, topleft, angle):

    rotated_image = pygame.transform.rotate(image, angle)
    new_rect = rotated_image.get_rect(center = image.get_rect(topleft = topleft).center)

    surf.blit(rotated_image, new_rect)

长话短说:

一个图像(pygame.Surface)可以通过pygame.transform.rotate进行旋转。

如果在循环中逐步执行该操作,那么图像会变形并迅速增大:

while not done:

    # [...]

    image = pygame.transform.rotate(image, 1)
    screen.blit(image, pos)
    pygame.display.flip()

这是因为旋转图像的边界矩形总是大于原始图像的边界矩形(除了某些90度倍数的旋转)。
由于多个复制,图像发生了扭曲。每次旋转都会产生小误差(不精确)。误差的总和在增加,图像会损坏。

可以通过保留原始图像并从原始图像生成一个单独的旋转操作图像来修复这个问题。

angle = 0
while not done:

    # [...]

    rotated_image = pygame.transform.rotate(image, angle)
    angle += 1

    screen.blit(rotated_image, pos)
    pygame.display.flip()

现在,由于图像的大小通过旋转而改变,其位置似乎任意更改,而原点始终是图像的边界矩形的左上角。

可以通过比较旋转前后的图像轴对齐边界框来进行补偿。
以下数学计算使用pygame.math.Vector2。请注意在屏幕坐标中,y 指向屏幕下方,但是数学 y 轴从底部指向顶部。这导致在计算期间必须“翻转” y 轴。

设置一个包含边界框 4 个角点的列表:

w, h = image.get_size()
box = [pygame.math.Vector2(p) for p in [(0, 0), (w, 0), (w, -h), (0, -h)]]

通过pygame.math.Vector2.rotate将向量旋转到角点位置:

box_rotate = [p.rotate(angle) for p in box]

获取旋转后点的最小值和最大值:

min_box = (min(box_rotate, key=lambda p: p[0])[0], min(box_rotate, key=lambda p: p[1])[1])
max_box = (max(box_rotate, key=lambda p: p[0])[0], max(box_rotate, key=lambda p: p[1])[1])

通过将旋转框的最小值添加到位置,计算图像左上角点的“补偿”原点。对于y坐标,max_box[1]是最小值,因为沿y轴“翻转”:

origin = (pos[0] + min_box[0], pos[1] - max_box[1])

rotated_image = pygame.transform.rotate(image, angle)
screen.blit(rotated_image, origin)

甚至可以在原始图像上定义一个枢轴点。计算从图像中心到枢轴的偏移向量并旋转该向量。可以使用pygame.math.Vector2表示向量,并可使用pygame.math.Vector2.rotate进行旋转。请注意,pygame.math.Vector2.rotate的旋转方向与pygame.transform.rotate相反。因此,必须反转角度:

计算从图像中心到枢轴的向量:

image_rect = image.get_rect(topleft = (pos[0] - originPos[0], pos[1]-originPos[1]))
offset_center_to_pivot = pygame.math.Vector2(pos) - image_rect.center
旋转向量。
rotated_offset = offset_center_to_pivot.rotate(-angle)

计算旋转图像的中心点:

rotated_image_center = (pos[0] - rotated_offset.x, pos[1] - rotated_offset.y)

旋转并绘制图像:

rotated_image = pygame.transform.rotate(image, angle)
rotated_image_rect = rotated_image.get_rect(center = rotated_image_center)

screen.blit(rotated_image, rotated_image_rect)
在下面的示例程序中,函数blitRotate(surf, image, pos, originPos, angle)执行所有上述步骤,并将旋转后的图像“ blit ”到表面上。
  • surf是目标Surface

  • image是需要旋转和blit的Surface

  • pos是目标Surface surf上的枢轴位置(相对于surf的左上角)

  • originPosimage Surface上枢轴的位置(相对于image的左上角)

  • angle是旋转角度(以度为单位)

这意味着,blitRotate的第二个参数(pos)是窗口中枢轴点的位置,第三个参数(originPos)是旋转表面上枢轴点的位置:


最小示例: repl.it/@Rabbid76/PyGame-RotateAroundPivot

import pygame

pygame.init()
screen = pygame.display.set_mode((300, 300))
clock = pygame.time.Clock()

def blitRotate(surf, image, pos, originPos, angle):

    # offset from pivot to center
    image_rect = image.get_rect(topleft = (pos[0] - originPos[0], pos[1]-originPos[1]))
    offset_center_to_pivot = pygame.math.Vector2(pos) - image_rect.center
    
    # roatated offset from pivot to center
    rotated_offset = offset_center_to_pivot.rotate(-angle)

    # roatetd image center
    rotated_image_center = (pos[0] - rotated_offset.x, pos[1] - rotated_offset.y)

    # get a rotated image
    rotated_image = pygame.transform.rotate(image, angle)
    rotated_image_rect = rotated_image.get_rect(center = rotated_image_center)

    # rotate and blit the image
    surf.blit(rotated_image, rotated_image_rect)
  
    # draw rectangle around the image
    pygame.draw.rect(surf, (255, 0, 0), (*rotated_image_rect.topleft, *rotated_image.get_size()),2)

def blitRotate2(surf, image, topleft, angle):

    rotated_image = pygame.transform.rotate(image, angle)
    new_rect = rotated_image.get_rect(center = image.get_rect(topleft = topleft).center)

    surf.blit(rotated_image, new_rect.topleft)
    pygame.draw.rect(surf, (255, 0, 0), new_rect, 2)

try:
    image = pygame.image.load('AirPlaneFront.png')
except:
    text = pygame.font.SysFont('Times New Roman', 50).render('image', False, (255, 255, 0))
    image = pygame.Surface((text.get_width()+1, text.get_height()+1))
    pygame.draw.rect(image, (0, 0, 255), (1, 1, *text.get_size()))
    image.blit(text, (1, 1))
w, h = image.get_size()

angle = 0
done = False
while not done:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True

    pos = (screen.get_width()/2, screen.get_height()/2)
    
    screen.fill(0)
    blitRotate(screen, image, pos, (w/2, h/2), angle)
    #blitRotate2(screen, image, pos, angle)
    angle += 1
    
    pygame.draw.line(screen, (0, 255, 0), (pos[0]-20, pos[1]), (pos[0]+20, pos[1]), 3)
    pygame.draw.line(screen, (0, 255, 0), (pos[0], pos[1]-20), (pos[0], pos[1]+20), 3)
    pygame.draw.circle(screen, (0, 255, 0), pos, 7, 0)

    pygame.display.flip()
    
pygame.quit()
exit()

另请参见旋转表面和以下问题的答案:


11
不在原回答范围内,但绝对符合我的期望。我喜欢这超越了中心并可以围绕一个提供的点旋转。在超过要求的方面给出了很棒的回应。 - Sensei

8

您正在删除旋转创建的矩形。您需要保留矩形,因为旋转后其大小会改变。

如果您想要保留对象的位置,请执行以下操作:

def rot_center(image, angle):
    """rotate a Surface, maintaining position."""
    
    loc = image.get_rect().center  #rot_image is not defined 
    rot_sprite = pygame.transform.rotate(image, angle)
    rot_sprite.get_rect().center = loc
    return rot_sprite
    
    # or return tuple: (Surface, Rect)
    # return rot_sprite, rot_sprite.get_rect()

8

顶部答案存在一些问题:在函数中需要获取前一个矩形的位置,以便我们可以将其分配给新矩形,例如:

rect = new_image.get_rect(center=rect.center) 

在另一个答案中,位置是通过从原始图像创建新的矩形来获得的,但这意味着它将被放置在默认的(0,0)坐标上。
下面的示例应该可以正常工作。新的矩形需要旧矩形的中心位置,所以我们也将其传递给函数。然后旋转图像,调用get_rect获取具有正确大小的新矩形,并将旧矩形的center属性作为center参数传递。最后,将旋转后的图像和新矩形作为元组返回并在主循环中解包。
import pygame as pg


def rotate(image, rect, angle):
    """Rotate the image while keeping its center."""
    # Rotate the original image without modifying it.
    new_image = pg.transform.rotate(image, angle)
    # Get a new rect with the center of the old rect.
    rect = new_image.get_rect(center=rect.center)
    return new_image, rect


def main():
    clock = pg.time.Clock()
    screen = pg.display.set_mode((640, 480))
    gray = pg.Color('gray15')
    blue = pg.Color('dodgerblue2')

    image = pg.Surface((320, 200), pg.SRCALPHA)
    pg.draw.polygon(image, blue, ((0, 0), (320, 100), (0, 200)))
    # Keep a reference to the original to preserve the image quality.
    orig_image = image
    rect = image.get_rect(center=(320, 240))
    angle = 0

    done = False
    while not done:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                done = True

        angle += 2
        image, rect = rotate(orig_image, rect, angle)

        screen.fill(gray)
        screen.blit(image, rect)
        pg.display.flip()
        clock.tick(30)


if __name__ == '__main__':
    pg.init()
    main()
    pg.quit()

这是一个带有旋转pygame精灵的示例。
import pygame as pg


class Entity(pg.sprite.Sprite):

    def __init__(self, pos):
        super().__init__()
        self.image = pg.Surface((122, 70), pg.SRCALPHA)
        pg.draw.polygon(self.image, pg.Color('dodgerblue1'),
                        ((1, 0), (120, 35), (1, 70)))
        # A reference to the original image to preserve the quality.
        self.orig_image = self.image
        self.rect = self.image.get_rect(center=pos)
        self.angle = 0

    def update(self):
        self.angle += 2
        self.rotate()

    def rotate(self):
        """Rotate the image of the sprite around its center."""
        # `rotozoom` usually looks nicer than `rotate`. Pygame's rotation
        # functions return new images and don't modify the originals.
        self.image = pg.transform.rotozoom(self.orig_image, self.angle, 1)
        # Create a new rect with the center of the old rect.
        self.rect = self.image.get_rect(center=self.rect.center)


def main():
    screen = pg.display.set_mode((640, 480))
    clock = pg.time.Clock()
    all_sprites = pg.sprite.Group(Entity((320, 240)))

    while True:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                return

        all_sprites.update()
        screen.fill((30, 30, 30))
        all_sprites.draw(screen)
        pg.display.flip()
        clock.tick(30)


if __name__ == '__main__':
    pg.init()
    main()
    pg.quit()

我正在使用你的方法,但它似乎也有一些问题。它将我的图像显示在屏幕左上角(它仍然可以正确地在中心旋转,太好了),但我不想要它在那里,并且它有一些奇怪的事情发生(位置相同但目标不同)。我无法在这里解释所有内容,我可能会就此提问并包含此问题和你的答案的链接。 - Ethanol
@Ethanol,如果我没有看到你的代码,就无法提供帮助。上面的示例在我的电脑上可以正常运行。 - skrx
其实,我只有一个快速的问题可能会帮助我:为什么你要在这个 rect = image.get_rect(center=(320, 240)) 中分配 (320, 240) 到中心?它是做什么的,如果你把它移除了会对代码产生什么影响? - Ethanol
我正在设置rect的初始位置。这个矩形将具有imagecenter坐标(320, 240)的大小。其他像topleft这样的坐标(这是实际的blit位置)也将相应地进行调整。当图像旋转并再次调用get_rect时,矩形的大小将不同,但通过将中心坐标设置为先前的中心坐标,我们自动更新了topleft位置,使得图像将被绘制在正确的位置。 - skrx
@Ethanol 你解决了吗?如果是的话,我们可以删除这里的评论,我会在答案中添加一些额外的信息。 - skrx
很棒的答案!像个冠军一样工作。除了旋转,我想在保持旋转的同时移动精灵...我该如何添加到你的算法中? - pitosalas

4

这里是使用pygame绘制图像所需的一切。

game_display = pygame.display.set_mode((800, 600))

x = 0
y = 0
angle = 0

img = pygame.image.load("resources/image.png")
img = pygame.transform.scale(img, (50, 50)) # image size

def draw_img(self, image, x, y, angle):
    rotated_image = pygame.transform.rotate(image, angle) 
    game_display.blit(rotated_image, rotated_image.get_rect(center=image.get_rect(topleft=(x, y)).center).topleft)

# run this method with your loop
def tick():
    draw_img(img, x, y, angle)

2

找到了问题:示例很好,但需要宽度和高度相等的尺寸。修复了图片,现在可以正常工作。


1
另一个旋转图像时的重要点是图像必须具有alpha通道。许多人使用矩形、均匀填充的表面来测试他们的代码,却忽略了alpha通道。这导致看起来像是随机拉伸的矩形。如何创建一个带有alpha通道的表面在这个问题的回答中有解释:pygame不旋转表面。在这个例子中,我使用pygame.SRCALPHA标志创建了一个带有alpha通道的图像的pygame.Surface
surface = pygame.Surface((100, 50), pygame.SRCAPLHA)

第二个重要的事情是,你绝不能反复旋转一个pygame.Surface对象,因为这会导致一个越来越大的pygame.Surface和扭曲,而你必须保存原始的表面,并始终从中创建一个旋转的pygame.Surface。请看下面的例子,我在其中使用了pygame.sprite模块。

import pygame

class Player(pygame.sprite.Sprite):
    def __init__(self, image, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.original_image = image
        self.image = self.original_image
        self.rect = self.image.get_rect(center = (x, y))
        self.angle = 0
    def update(self):
        self.image = pygame.transform.rotate(self.original_image, self.angle)
        self.rect = self.image.get_rect(center=self.rect.center)
        self.angle = (self.angle + 1) % 360

pygame.init()
window = pygame.display.set_mode((400, 400))
clock = pygame.time.Clock()

surface = pygame.Surface((100, 50), pygame.SRCALPHA)
surface.fill("black")
player = Player(surface, *window.get_rect().center)
all_sprites = pygame.sprite.Group(player)

run = True
while run:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

    all_sprites.update()

    window.fill((255, 255, 255))
    all_sprites.draw(window)
    pygame.display.flip()

pygame.quit()
exit()

1

我不得不修改 skrx 的解决方案,如下所示,这种方式适合我。

angle=0
roll = true
while roll:
    # clean surface with your background color
    gameDisplay.fill(color)
    self.image = yourImage
    rotate_image = pygame.transform.rotate(self.image, angle)
    rect = rotate_image.get_rect()
    pos = (((your_surface_width - rect.width)/2),((your_surface_height - rect.height)/2))
    gameDisplay.blit(rotate_image,pos)
    pygame.display.flip()
    angle+=2
    if angle == 360:
        roll=False 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接