Reputation: 113
What is the best way to use python to create a application that allows image zooming and drawing line on top of it?
I have worked on using pygame to do this, by drawing image on extremely large surface (size ~15000) and with pygame.transform.scale(), it allow me to do zooming. Also, an extra surface for line drawing is created with convert_alpha() and set to 0 alpha.
The problem of this method is that it spend much time and memory if i need to create pygame.surface with large size. Also, the transform.scale() spend much time. Many extra surface is also created for type of drawing other than line.
So, are there any better way to do same thing using pygame ? or another way to do same thing? Thank you.
Upvotes: 3
Views: 2198
Reputation: 14926
Zooming an image can be achieved by copying part of the "base-image", and then scaling only that smaller portion to the same size as the window. This way the code is only ever holding the base-image, background and the sub-image. You never scale the entire image to the zoom-level.
In the example code below I have implemented this using the notion of a "pan-box", which is a PyGame rectangle (Rect
) which defines the zoomed-portion size and position. In effect it's a rectangular "magnifying glass".
The core of the code takes a copy of the background within the co-ordinates and size of this box, and then scales them to be the same size as the window, copying to a "background" image:
window_size = ( WINDOW_WIDTH, WINDOW_HEIGHT )
zoom_image = pygame.Surface( ( pan_box.width, pan_box.height ) ) # new surface
zoom_image.blit( base_image, ( 0, 0 ), pan_box ) # copy base image
pygame.transform.scale( zoom_image, window_size, background ) # scale into the background
window.blit( background, ( 0, 0 ) )
pygame.display.flip()
And that's the crux of the operation. This works well, because it just grabs the content of the original image wherever the box is, and scales it to fit the window. Panning around or zooming is just a matter of moving the pan-box, or making it bigger/smaller.
However, to keep this core-code simple, there's a whole lot more code in the example that primarily keeps the pan-box within the image. In fact most of the example is simple checks to make sure we don't go out-of-bounds.
Image copying and scaling are relatively CPU-expensive operations. So you will see points in the code where tests are made to determine if some operations need to be performed at all. For example, we don't need to create a new background image if the pan-box has not changed. Nor do we need to make a new zoom_image
if the size of the pan-box has not changed. Little checks like this make a great improvement to the speed of the code.
Reference Code:
import pygame
import sys
# Window size
WINDOW_WIDTH = 300
WINDOW_HEIGHT = 300
WINDOW_SURFACE = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE
image_filename = None
PAN_BOX_WIDTH = 64
PAN_BOX_HEIGHT = 64
PAN_STEP = 5
def errorExit( message, code=1 ):
""" Write an error message to the console, then exit with an error code """
sys.stderr.write( message + "\n" )
sys.exit( code )
# The first argument is either the image filename
# or a "--help" request for help
# Did we get any arguments?
if ( len( sys.argv ) == 1 ):
errorExit( "Give an image Filename as an argument" )
else:
# Get image filename as first argument
for arg in sys.argv[1:]:
if ( arg in [ '-h', '--help', '-?', '/?' ] ):
errorExit( "Zooms an image, using arrow keys to pan\nGive an image Filename as an argument" )
# Use the first argument as the image source
image_filename = sys.argv[1]
sys.stdout.write( "Using [%s] as the image\n" % ( image_filename ) )
### PyGame initialisation
pygame.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), WINDOW_SURFACE )
pygame.display.set_caption("Image Pan")
### Can we load the user's image OK?
try:
base_image = pygame.image.load( image_filename ).convert()
except:
errorExit( "Failed to open [%s]" % ( image_filename ) )
### Pan-position
background = pygame.Surface( ( WINDOW_WIDTH, WINDOW_HEIGHT ) ) # zoomed section is copied here
zoom_image = None
pan_box = pygame.Rect( 0, 0, PAN_BOX_WIDTH, PAN_BOX_HEIGHT ) # curent pan "cursor position"
last_box = pygame.Rect( 0, 0, 1, 1 )
### Main Loop
clock = pygame.time.Clock()
done = False
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
# Movement keys
# Pan-box moves up/down/left/right, Zooms with + and -
keys = pygame.key.get_pressed()
if ( keys[pygame.K_UP] ):
pan_box.y -= PAN_STEP
if ( keys[pygame.K_DOWN] ):
pan_box.y += PAN_STEP
if ( keys[pygame.K_LEFT] ):
pan_box.x -= PAN_STEP
if ( keys[pygame.K_RIGHT] ):
pan_box.x += PAN_STEP
if ( keys[pygame.K_PLUS] or keys[pygame.K_EQUALS] ):
pan_box.width += PAN_STEP
pan_box.height += PAN_STEP
if ( pan_box.width > WINDOW_WIDTH ): # Ensure size is sane
pan_box.width = WINDOW_WIDTH
if ( pan_box.height > WINDOW_HEIGHT ):
pan_box.height = WINDOW_HEIGHT
if ( keys[pygame.K_MINUS] ):
pan_box.width -= PAN_STEP
pan_box.height -= PAN_STEP
if ( pan_box.width < PAN_STEP ): # Ensure size is sane
pan_box.width = PAN_STEP
if ( pan_box.height < PAN_STEP ):
pan_box.height = PAN_STEP
# Ensure the pan-box stays within image
PAN_BOX_WIDTH = min( PAN_BOX_WIDTH, base_image.get_width() )
PAN_BOX_HEIGHT = min( PAN_BOX_HEIGHT, base_image.get_height() )
if ( pan_box.x < 0 ):
pan_box.x = 0
elif ( pan_box.x + pan_box.width >= base_image.get_width() ):
pan_box.x = base_image.get_width() - pan_box.width - 1
if ( pan_box.y < 0 ):
pan_box.y = 0
elif ( pan_box.y + pan_box.height >= base_image.get_height() ):
pan_box.y = base_image.get_height() - pan_box.height - 1
# Re-do the zoom, but only if the pan box has changed since last time
if ( pan_box != last_box ):
# Create a new sub-image but only if the size changed
# otherwise we can just re-use it
if ( pan_box.width != last_box.width or pan_box.height != last_box.height ):
zoom_image = pygame.Surface( ( pan_box.width, pan_box.height ) )
zoom_image.blit( base_image, ( 0, 0 ), pan_box ) # copy base image
window_size = ( WINDOW_WIDTH, WINDOW_HEIGHT )
pygame.transform.scale( zoom_image, window_size, background ) # scale into thebackground
last_box = pan_box.copy() # copy current position
window.blit( background, ( 0, 0 ) )
pygame.display.flip()
# Clamp FPS
clock.tick_busy_loop(60)
pygame.quit()
Upvotes: 2