HA HA chan
HA HA chan

Reputation: 113

Best way to create image viewer and allowing zoom, drag and line drawing by python

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

Answers (1)

Kingsley
Kingsley

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.

zoomed panned doggo

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

Related Questions