Need to crop thousands of images / sprites while preserving midpoint - image

I have thousands of character sprites (though, really they're just PNG files with transparency) which were saved out from Maya.
These are character animations, with almost all of them being 12 frames each. Each animation is rendered from each of the 8 cardinal directions.
In each animation, the character is already centered in the frame (this is how it's exported from Maya); however, each image has a bunch of white/transparent space around it.
I need to batch crop these sprites horizontally from the outside -> in order to preserve the character's midpoint in the frame. The reason for this is if it's not preserved, as the game swaps from one animation to the next, the character would appear to move or jump around. So, it's important the character is horizontally centered (using the midpoint of their body) in the frame across all their animations.
Additionally... ideally the character is always at or very near the bottom edge of the sprite.
For the coup de grace... the final cropped image needs to be an even number, or divisible by 2, for game engine reasons.
Is there any way to at least partially, or entirely, automate this? Or are there any programs that someone could recommend to help with the automation of this; free or paid.

I dunno yet if a Python-based answer suits your but here is what I got, with a bit of work your can copy and paste this script to your script editor in Maya or execute it in your Python interpreter.
This script requires the PIL library to be installed.
To install it on Maya, you can follow this link Mistermatti's blog. Unzip the file and paste the PIL folder in your maya install here: Maya20XX\Python\Lib\site-packages
This works on Maya2014, havn't tested on other versions tho.
from PIL import Image
import time
start_time = time.time()
#parses the image to find the first pixel on the x axis which is not blank
def parseXImage(pixelMap, resWidth, resHeight):
for x in range(0, int(resWidth/2)):
for y in range(0, int(resHeight/2)):
if (pixelMap[x,y] != (0, 0, 0, 0)) or \
(pixelMap[x,resHeight-y-1] != (0, 0, 0, 0)) or \
(pixelMap[resWidth-x-1,y] != (0, 0, 0, 0)) or \
(pixelMap[resWidth-x-1,resHeight-y-1] != (0, 0, 0, 0)):
return x
return None
#parses the image to find the first pixel on the y axis which is not blank
def parseYImage(pixelMap, resWidth, resHeight):
topFound = False
bottomFound = False
yTop = None
yBottom = None
for y in range(0, int(resHeight/2)):
for x in range(0, int(resWidth/2)):
if not topFound:
if (pixelMap[x,y] != (0, 0, 0, 0)) or \
(pixelMap[resWidth-x-1,y] != (0, 0, 0, 0)):
yTop = y
topFound = True
if not bottomFound:
if (pixelMap[x,resHeight-y-1] != (0, 0, 0, 0)) or \
(pixelMap[resWidth-x-1,resHeight-y-1] != (0, 0, 0, 0)):
yBottom = y
bottomFound = True
return yTop, yBottom
imageList = [r"Path\To\Image\Mod_Turn.001.png",
r"Path\To\Image\Mod_Turn.002.png",
r"Path\To\Image\Mod_Turn.003.png",
r"Path\To\Image\Mod_Turn.004.png",
r"Path\To\Image\Mod_Turn.005.png"]
#get images resolution
imagePil = Image.open(imageList[0])
resWidth, resHeight = imagePil.size #get the resolution of the first image in the list
imagePil.close()
valueWidthList = []
valueHeightTopList = []
valueHeightBottomList = []
for imageListIndex, imagePath in enumerate(imageList):
imagePil = Image.open(imagePath)
pixelMap = imagePil.load()
xValue = parseXImage(pixelMap, resWidth, resHeight) #now parse the image to find x
yTopValue, yBottomValue = parseYImage(pixelMap, resWidth, resHeight) #now parse the image to find y
valueWidthList.append(xValue) #Store it
valueHeightTopList.append(yTopValue) #Store it
valueHeightBottomList.append(yBottomValue) #Store it
imagePil.close()
xValueToCrop = min(valueWidthList) #Get the minimal value of X crop among all the entries
yTopValueToCrop = min(valueHeightTopList) #Get the minimal value of Y crop among all the entries
yBottomValueToCrop = min(valueHeightBottomList) #Get the minimal value of Y crop among all the entries
#This is the crop square we will use
cropSquare = (xValueToCrop, yTopValueToCrop, resWidth-xValueToCrop, resHeight-yBottomValueToCrop )
for imagePath in imageList:
imagePil = Image.open(imagePath)
#Replace Turn by something in the name of your image
imagePil.crop(cropSquare).save(imagePath.replace("Turn", "Turn_Cropped")) #Crop dans save the image
imagePil.close()
print("--- %s seconds ---" % (time.time() - start_time))
You need to change the imageList dict with your own paths to images and "Turn", "Turn_Cropped" at the very end of the script.
FYI, processing 300 images #720p took about 250 seconds. This can be improved.

Perhaps give TexturePacker a try https://www.codeandweb.com/texturepacker
I've used it before when dealing with sprites and find it quite nice. I believe it also has command-line functionality, though I haven't used it myself. And it's cross-platform.

Related

Problem with images overlapping in pygame

Im having problems with blitting images to rect objects in pygame. i have a background image blitted to my main pygame window, and also an image blitted to a rect object on the screen which moves. the problem i am having is the rect object is overlapping my background image when its moving around. i was looking to only be able to see the green helicopter shape and not the black outline around it. sorry if i havent explained this very well. will try to include all files im using.
Thanks for any help
import pygame as pg
import random as r
import time
pg.init()
MAX_X = 1190
MAX_Y = 590
MIN_X = 10
MIN_Y = 10
SIZE = 100
SPEED = 1
COLOR = (0,255,0)
move_amount = 0
wn = pg.display.set_mode((1200, 600))
BG_IMG = pg.image.load('bg.png').convert()
BG_IMG = pg.transform.scale(BG_IMG, (1200, 600))
class Wall (pg.Rect):
def __init__(self, posX, posY):
self.xcor = posX
self.ycor = posY
self.rect = None
class Heli (pg.Rect):
def __init__(self, posX, posY):
self.image = pg.image.load('art.png').convert()
self.rect = self.image.get_rect()
self.xcor = posX
self.ycor = posY
# top and bottom constant walls
TOP = pg.Rect(MIN_X, MIN_Y, MAX_X, 3)
BOTTOM = pg.Rect(MIN_X, MAX_Y, MAX_X, 3)
heli = Heli(MIN_X, MAX_Y //2)
# keep moving walls in a list
moving_walls = [Wall(MAX_X, r.randint((MIN_Y + 10), (MAX_Y - 10)))]
# main loop
while True:
# fill screen
wn.fill('black')
# editing objects to move
# blitting must happen before everything else
pg.draw.rect(wn,COLOR, heli.rect)
wn.blit(BG_IMG, (0,0))
wn.blit(heli.image, heli.rect)
heli.rect.y += move_amount
heli.rect.y += 1
# use a variable to control how much movement is happening
# movement happens continuosly
# if key down it oves if key up it doesnt
for wall in moving_walls :
wall.rect = pg.Rect(wall.xcor, wall.ycor, 3, SIZE)
pg.draw.rect(wn, COLOR, wall.rect)
wall.xcor -= SPEED
if wall.xcor < MIN_X + 10:
wall.xcor = MAX_X
wall.ycor = r.randint((MIN_Y), (MAX_Y - SIZE))
# drawing all objects back to the screen
pg.draw.rect(wn, COLOR, TOP)
pg.draw.rect(wn, COLOR, BOTTOM)
# update window
pg.display.update()
# event handling
for ev in pg.event.get():
if ev.type == pg.KEYDOWN:
if ev.key == pg.K_UP:
move_amount = -3
if ev.type == pg.KEYUP:
move_amount = 0
if ev.type == pg.QUIT:
pg.quit()
time.sleep(0.01)
You discard the transparency information of the image. You have to use convert_alpha instead of convert:
self.image = pg.image.load('art.png').convert()
self.image = pg.image.load('art.png').convert_alpha()
The pygame documentation notes that:
The returned Surface will contain the same color format, colorkey and alpha transparency as the file it came from. You will often want to call convert() with no arguments, to create a copy that will draw more quickly on the screen.
For alpha transparency, like in .png images, use the convert_alpha() method after loading so that the image has per pixel transparency.
See also How can I make an Image with a transparent Backround in Pygame?

To show/join Two Images Simultaneously in Matlab's slider?

I cannot make too big file (like tried here) so I need to show only portions at a time.
However, I cannot make the transfer from one file to another smooth in Matlab.
I am trying to expand the solution of the thread To refresh imshow in Matlab? for two images. The problem is the ending of image A and start of the image B. Now, I have an interruption in the flow, which I do not like because I cannot show two images at the same time without an interruption.
In the example here, it is not needed to filter the axes.
Code
iterationCounter=1;
hFig=figure('Menubar','none', 'NumberTitle','off', 'Color','k');
while(iterationCounter < 7)
filenamePng=fullfile(homedir, '/Images/Raw/', iterationCounter, '.png');
imgRGB=imread(filenamePng);
% https://stackoverflow.com/a/29952648/54964
%%// create sliding-window video
len = 40*2^3;
signal = imgRGB(:,1:end,:);
hImg = imshow(signal(:,1:1+len,:), ...
'InitialMagnification',100, 'Border','tight');
vid = VideoWriter('signal.avi');
vid.Quality = 100;
vid.FrameRate = 60;
open(vid);
M = size(signal,2);
for k=1:M-len
set(hImg, 'CData',signal(:,k:k+len,:))
writeVideo(vid, getframe());
end
iterationCounter=iterationCounter+1;
end
close(vid);
Output for Image A and Image B
where there is an interruption in the slider after each image.
The picture is just a screenshot of two images: beginning and ending of the slider. The blue area is just OS X GUI but still indicates that there is a gap and interruption between the start and end.
How can show/join two images simultaneously in Matlab's slider?
In order to display multiple images side by side in this scrolling window, I think the easiest approach is to actually load all of the images upfront into an array that is [nRows x (nColumns * nImages) x 3] and then just use the original code to scroll through this.
hFig=figure('Menubar','none', 'NumberTitle','off', 'Color','k');
signal = [];
% Load all the images into one "long" image
for k = 1:7
filename = fullfile(homedir, sprintf('%d.png', k));
img = imread(filename);
signal = cat(2, signal, img);
end
%%// create sliding-window video
windowWidth = 320; % Width in pixels
hImg = imshow(signal(:,1:1 + windowWidth,:), ...
'InitialMagnification',100, 'Border','tight');
vid = VideoWriter('signal.avi');
vid.Quality = 100;
vid.FrameRate = 60;
open(vid);
M = size(signal,2);
for k = 1:(M - windowWidth)
set(hImg, 'CData',signal(:,k:k + windowWidth,:))
writeVideo(vid, getframe());
end
close(vid);
The command imagesc is a low-level image command where Suever's proposal does not work.
A general approach to remove padding/margins/background is to use
<plot imshow imagesc ...>
set(gca,'position',[0 0 1 1],'units','normalized')

Displaying pixel values of an image in Octave?

If I load and display an image, for example
c = imread('cameraman.tif');
imshow(c)
then in the plot window, under the image itself, there are two numbers indicating the current cursor position, in the form
[39.25, 120.6]
I have two questions about this:
Can the display be tweaked so that the positions are integers? So one image pixel per screen pixel?
Can this information include the grayscale/rgb value of the pixel, such as
[23, 46] = 127
or
[23, 46] = (46,128,210)?
I've tried fiddling with the "axis" command, but I haven't found anything which helps.
I guess what I want is something like Matlab's "Pixel Information Tool" impixelinfo:
http://www.mathworks.com.au/help/images/ref/impixelinfo.html though I know from the octave image wiki at http://wiki.octave.org/Image_package that impixelinfo is not currently implemented in Octave. But maybe there's another way to achieve the same result?
I'm using Octave-3.8.0, the image package 2.2.0, under linux (Ubuntu 12.04).
GNU Octave 3.8 uses FLTK as standard graphics_toolkit. Unfortunately WindowButtonMotionFcn is only triggered if a button is pressed while the mouse moves (dragging) with this toolkit (Today I would consider this as a bug). But you can use WindowButtonDownFcn.
This examples updates the title with the position and the image value at that position if you click in the image:
img = imread ("cameraman.png");
imshow (img)
function btn_down (obj, evt)
cp = get (gca, 'CurrentPoint');
x = round (cp(1, 1));
y = round (cp(1, 2));
img = get (findobj (gca, "type", "image"), "cdata");
img_v = NA;
if (x > 0 && y > 0 && x <= columns (img) && y <= rows (img))
img_v = squeeze (img(y, x, :));
endif
if (numel (img_v) == 3) # rgb image
title(gca, sprintf ("(%i, %i) = %i, %i, %i", x, y, img_v(1), img_v(2), img_v(3)));
elseif (numel (img_v) == 1) # gray image
title(gca, sprintf ("(%i, %i) = %i", x, y, img_v));
endif
endfunction
set (gcf, 'WindowButtonDownFcn', #btn_down);
You can also place a text next to the cursor if you want.

Octave script loop only runs once

I have a script that reads images, overlays scatterplot points on them, and then combines all of the resulting images into a movie. I have run into some difficulty translating the script to work in Octave though. I have worked out all of the kinks, except that my loop only runs once. I.e. The video created is only one frame long. I suspect that this may have something to do with gcf? ALthough I've been looking at it so long that I think it might just be something simple i'm missing. Any thoughts would be much appreciated.
The loop is:
for i = 1:subsamp:numel(dd)
if (mod(i,100)==0)
fprintf('frame %d/%d\n', i, numel(dd));
end
% read current image and trim its width and height to a multiple of 4
img = imread([img_dir,'/',dd(i).name]);
trim_x = mod(size(img,2),4);
trim_y = mod(size(img,1),4);
img = img(1:end-trim_y,1:end-trim_x);
% display current image
set(0,'CurrentFigure',hnd);
imshow(img);
% if there is truth for this frame, show it
which_block = map(i);
if (which_block > 0)
% grab data chunk for this frame
block = truth(starts(which_block):ends(which_block),:);
pts = block(:,7:8);
ids = block(:,2);
% overlay tracks and ids
hold on;
scatter(pts(:,1),pts(:,2),5);
hold off;
end
% add this frame to movie file
getframe(img);
mov = addframe(mov,frame);
end
and the "getframe()" function at the end is one I found elsewhere online. It looks like this:
function frame = getframe (h)
print ("tmp.fig", "-djpg");
frame = im2double (imread ("tmp.fig"));
## Truncate to even size to accomodate addframe()
if (mod (size (frame, 1), 2) > 0); frame = frame(2:end, :, :); endif
if (mod (size (frame, 2), 2) > 0); frame = frame(:, 2:end, :); endif
endfunction
Again, if you notice anything that could be stopping this from iterating through every frame it's much appreciated. Thanks!!

Bouncing text animation issue in Pygame

I'm trying to code a program that can take text and animate it to bounce on a loop, like a ball bouncing to the floor. I used a similar piece of code I found a starting point as I'm still fairly new to Pygame (thank you Pete Shinners, whoever you are), but after updating the code and playing with it for a long time I still can't get it to blit to the screen correctly. The text starts above the rendered area and then gradually falls into view, but the top part of the text is cut off.
I've tried moving the blitted region around the window and resizing the rectangles and surface the program is using, but nothing seems to fix it.
import os, sys, math, pygame, pygame.font, pygame.image
from pygame.locals import *
def bounce():
# define constants
G = 0.98
FLOOR = 0
COEFFICIENT = 0.8
#define variables
ball = 500
direction = 'DOWN'
v = 0
count = 0
#create array to store data
array = [ball]
while True:
if count == 4:
return array
elif ball > FLOOR and direction == 'DOWN':
v += G
if (ball - v) >= FLOOR:
ball = ball - v
array.append(round(ball,2))
else:
ball = FLOOR
array.append(round(ball,2))
direction = 'UP'
v *= COEFFICIENT
count += 1
elif ball >= FLOOR and direction == 'UP':
v -= G
if (ball + v) >= FLOOR:
ball = ball + v
array.append(round(ball,2))
if v <= 0:
direction = 'DOWN'
else:
ball = FLOOR
array.append(ball)
direction = 'UP'
v *= COEFFICIENT
class textBouncy:
array = bounce()
def __init__(self, font, message, fontcolor, amount=10):
# Render the font message
self.base = font.render(message, 0, fontcolor)
# bounce amount (height)
self.amount = amount
#size = rect of maximum height/width of text
self.size = self.base.get_rect().inflate(0, amount).size
#normalise array to meet height restriction
self.array = [round(-x/(500/amount),2) for x in array]
def animate(self):
# create window surface s
s = pygame.Surface(self.size)
# height = max inflated height
height = self.size[1]
# define a step-sized rectangle in the location of the step
src = Rect(0, 0, self.base.get_width(), height)
# moves the message according to the array list.
dst = src.move(0, self.array[i])
if (i + 1) == len(self.array):
global i
i = 0
# blits the information onto the screen
s.blit(self.base, dst, src)
return s
entry_info = 'Bouncing ball text'
if __name__ == '__main__':
pygame.init()
#create text renderer
i = 0
array = bounce()
bigfont = pygame.font.Font(None, 60)
white = 255, 255, 255
renderer = textBouncy(bigfont, entry_info, white, 16)
text = renderer.animate()
#create a window the correct size
win = pygame.display.set_mode(text.get_size())
win.blit(text, (0, 10))
pygame.display.flip()
#run animation loop
finished = 0
while True:
pygame.time.delay(10)
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
text = renderer.animate()
i += 1
win.blit(text, (0, 10)) # blits the finished product from animate
pygame.display.flip()
(Quote) "it all comes down to math really" Kay so you need to - the y axis when you want to make it go up and + the x axis to make it go side ways you could make it go up and down will moveing it horizontally and then when it reaches a point it will stop moving horizontally and just bonce up and down +ing it more every time
That was my 100$ which took me 5 mins to write
After revisiting this I managed to work this out - I needed to add everything I blitted down to compensate for the bounce up. So in the __init__function:
self.array = [round(-x/(500/amount),2)**+self.amount** for x in array]
Works perfectly now :)

Resources