views:

172

answers:

3

For an art project, one of the things I'll be doing is zooming in on an image to a particular pixel. I've been rubbing my chin and would love some advice on how to proceed.

Here are the input parameters:

Screen:
sw - screen width
sh - screen height

Image:
iw - image width
ih - image height

Pixel:
px - x position of pixel in image
py - y position of pixel in image

Zoom:
zf - zoom factor (0.0 to 1.0)

Background colour:
bc - background colour to use when screen and image aspect ratios are different

Outputs:

The zoomed image (no anti-aliasing)
The screen position/dimensions of the pixel we are zooming to.

When zf is 0 the image must fit the screen with correct aspect ratio.
When zf is 1 the selected pixel fits the screen with correct aspect ratio.

One idea I had was to use something like povray and move the camera towards a big image texture or some library (e.g. pygame) to do the zooming. Anyone think of something more clever with simple pseudo code?

To keep it more simple you can make the image and screen have the same aspect ratio. I can live with that.

I'll update with more info as its required.

A: 

Edit : For art projects you can check this framework : Processing

I make it for 1D, you start by writing the direct transform from original image to zoomed image with your constraints :

As you want a linear transformation, it is in the form :

D( x ) = a x + b

You want :

for z = 0 : D( px ) = px D( px + 1 ) = px + 1

for z = 1 : D( px ) = 0 D( px + 1 ) = sw

This gives :

for z = 0 : a = 1 , b = 0 , D( x ) = x

for z = 1 : a = sw , b = -sw . px , D( x ) = sw.x - sw.px

For all z, you use a linear combination of the two :

D( x ) = z ( sw.x - sw.px ) + ( 1 - z ) ( x ) D( x ) = ( z.sw + 1 - z ).x - z.sw.px

Now you write the inverse function to get the original coordinates from the output coordinates :

ID( xout ) = ( xout + z.sw.px ) / ( z.sw + 1 - z )

Which allows you to fill the output image from the input image. For each output pixel the value is OriginalPixel[ ID( xout ) ] ( And when ID( xout ) is not in [0..sw] you use the background value )

For 2D the idea is similar, but keeping the aspect ratio will need a little more effort.

fa.
Interesting thought about 1D. I'll try to grok your equations and see if I can get my head around them. Yes, I've played with processing, very cool, probably be the first tool to use if I can't do it by hand.
zaf
A: 

If I understand correctly what you want to do. You can open image in a graphics program (like Gimp) set zoom level at 1 and take a screenshot. Then increase zoom level and take screenshot again etc. Then use mencoder to create AVI from screenshots.

Ross
You can also probably write a script to run imagemagic with incremental scale levels and fixed croping. Save each scaled image with names like 001.jpg 002.jpg ... and use mencoder to create diferent movies.
Ross
If I don't get a grip on the equations then I will look into automating other tools but I know I'll have to try afew tools before finding the one that gives me the effect I'm looking for.
zaf
+3  A: 

If color values of original image are given as array

image[x][y]

Then color values of zoomed image are

image[x+zf*(px-x)][y+zf*(py-y)]

Regarding the windows size/image size - initial preparation of image should take care of that: zoom the image up to the point that it would not fit the window any more and fill the remaining pixels with your preferred background colour.

In python you can do something like

def naivezoom(im, px, py, zf, bg):
    out = Image.new(im.mode, im.size)        
    pix = out.load()
    iw, ih = im.size
    for x in range(iw):
        for y in range(ih):
            xorg = x + zf*(px - x)
            yorg = y + zf*(py - y)
            if xorg >= 0 and xorg < iw and yorg >= 0 and yorg < ih:
                pix[x,y] = im.getpixel( (xorg , yorg) )
            else:
                pix[x,y] = bg
    return out

after you set

im = Image.open("filename.ext")

with objects from

import Image

EDIT: Given stackoverflow logo you will get

alt text

for zf = 0.3, around point 25,6

alt text

for zf = 0.96, around the same point

Images were obtained with following code

#!/bin/env python
from Tkinter import *
import Image
import ImageTk

def naivezoom(im, p, zf, bg):
    out = Image.new(im.mode, im.size)
    pix = out.load()
    iw, ih = im.size
    for x in range(iw):
        for y in range(ih):
            xorg = x + zf*(p[0] - x)
            yorg = y + zf*(p[1] - y)
            if xorg >= 0 and xorg < iw and yorg >= 0 and yorg < ih:
                pix[x,y] = im.getpixel( (xorg , yorg) )
            else:
                pix[x,y] = bg
    return out

class NaiveTkZoom:
    def __init__(self, parent=None):
        root = Tk()
        self.im = Image.open('logo.jpg')
        self.zf = 0.0
        self.deltazf = 0.02
        self.p = ( 0.1*self.im.size[0],0.1*self.im.size[1])
        self.bg = 255
        canvas = Canvas(root, width=self.im.size[0]+20 , height=self.im.size[1]+20)
        canvas.pack()
        root.bind('<Key>', self.onKey)
        self.canvas = canvas
        self.photo = ImageTk.PhotoImage(self.im)
        self.item = self.canvas.create_image(10, 10, anchor=NW, image=self.photo)
    def onKey(self, event):
        if event.char == "+":
            if self.zf < 1:
                self.zf += self.deltazf
        elif event.char == "-":
            if self.zf > 0:
                self.zf -= self.deltazf
        self.out = naivezoom(self.im, self.p, self.zf, self.bg)
        self.photo = ImageTk.PhotoImage(self.out)
        self.canvas.delete(self.item)
        self.item = self.canvas.create_image(10, 10, anchor=NW, image=self.photo)
        print self.p, self.zf

if __name__ == "__main__":
    NaiveTkZoom()
    mainloop()

The libraries used and pixel by pixel approach are not the fastest in the world, but will give you enough material to play with.

Also the above code is not very clean.

EDIT2(and3, centered the formula): Here's another attempt, added translation, but I have a feeling this is not final either (don't have the time to check the formulas). Also the speed of the translation is constant, but that may lead to zooming to slow and showing background (if the point to which you are zooming is too close to the edge).
I've also added a point on the original image so that it is visible what happens with it without need to paint on original image.

#!/bin/env python
from Tkinter import *
import Image
import ImageTk

def markImage(im, p, bg):
    pix = im.load()
    pix[ p[0], p[1] ] = bg

def naiveZoom(im, p, zf, bg):
    out = Image.new(im.mode, im.size)
    pix = out.load()
    iw, ih = im.size
    for x in range(iw):
        for y in range(ih):
            xorg = x + zf*(p[0]+0.5-x) + zf*(1-zf)*(p[0]-iw/2)
            yorg = y + zf*(p[1]+0.5-y) + zf*(1-zf)*(p[1]-ih/2)
            if xorg >= 0 and xorg < iw and yorg >= 0 and yorg < ih:
                pix[x,y] = im.getpixel( (xorg , yorg) )
            else:
                pix[x,y] = bg
    return out

class NaiveTkZoom:
    def __init__(self, parent=None):
        root = Tk()
        self.im = Image.open('py.jpg')
        self.zf = 0.0
        self.deltazf = 0.05
        self.p = (round(0.3*self.im.size[0]), round(0.3*self.im.size[1]) )
        self.bg = 255
        markImage(self.im, self.p, self.bg)
        canvas = Canvas(root, width=self.im.size[0]+20 , height=self.im.size[1]+20)
        canvas.pack()
        root.bind('<Key>', self.onKey)
        self.canvas = canvas
        self.photo = ImageTk.PhotoImage(self.im)
        self.item = self.canvas.create_image(10, 10, anchor=NW, image=self.photo)
        self.change = False
    def onKey(self, event):
        if event.char == "+":
            if self.zf < 1:
                self.zf += self.deltazf
                self.change = True
        elif event.char == "-":
            if self.zf > 0:
                self.zf -= self.deltazf
                self.change = True
        if self.change:
            self.out = naiveZoom(self.im, self.p, self.zf, self.bg)
            self.photo = ImageTk.PhotoImage(self.out)   
            self.canvas.delete(self.item)
            self.change = False
        self.item = self.canvas.create_image(10, 10, anchor=NW, image=self.photo)
        print self.p, self.zf

if __name__ == "__main__":
    NaiveTkZoom()
    mainloop()

There is quite a lot in the above that could be improved. :)

Unreason
Nice! Thanks for the equation and sample code. One thing I noticed was that if, lets say, the selected pixel is somewhere in the top left quadrant then it hugs the top left corner when you zoom in. Instead, as you zoom in I'm looking for the selected pixel to center itself.
zaf
Updated the answer, no hugging happening over here, are you sure you are applying given formulas properly?
Unreason
Thanks for the updated code. I really liked the +/- keys touch! OK - try an image with a red (or any colour marker) pixel on an image and then zoom into it and you'll see that the top left of the selected pixel stays put. What we want is the pixel center itself on the screen. I'm using a 1000x1000 image with a red pixel at 300,300.
zaf
Updated the answer again, it was missing the translation, zooming was straight to the point out of center. Now it translates as it zooms. Let me know if this works for you, still it is not perfect; for one I am not sure that the rate of zoom follows the translation properly and secondly when the zoom gets close to 1 the pixel itself is not centered but one of its corners, so I might revise the formula once more (don't have time now)
Unreason
Yes, once the pixel is up close you notice that its top left corner is whats centered on the screen. As for the translation, its fine for now, I can avoid selecting pixels on the extremes.
zaf
If you want to zoom to the center of the pixel make formulas x + zf*(p[0]+0.5-x) + zf*(1-zf)*(p[0]-iw/2) - the 0.5 centers the pixel
Unreason
Yes! We have a winner! I added +0.5 the line after as well to fix the vertical. Please update your code for the final time! And, thanks, you shall be rewarded.
zaf