Generating MAME Bezel Layouts with Python
I spent a few hours tweaking a nice little script to help me generate MAME bezel layouts for existing artwork more quickly using Python.
In case you don’t know what a MAME bezel is used for, take a look. Here’s our old friend Galaga, without any fancy effects or artwork:
Yeah “it’s Galaga” but it doesn’t really look like it did in the arcade. It’s sterile and boring. Here’s the same game, but with bezel artwork and some HLSL effects added:
Now that looks like the Galaga I remember!
Getting the HLSL effects in there is a whole different topic. But I have found hundreds, nay thousands, of these bezel artworks “around”. The problem is that most of them don’t come with MAME layout files, or they’re not correct.
The MAME .LAY File
The layout file is just a piece of XML that positions the emulated screen within the artwork in the correct location. It looks like this:
<mamelayout version="2">
<element name="bezel">
<image file="galaga_bezel.png"/>
</element>
<view name="Bezel Artwork">
<screen index="0">
<bounds x="970" y="736" width="2040" height="2720"/>
</screen>
<bezel element="bezel">
<bounds x="0" y="0" width="4000" height="3713"/>
</bezel>
</view>
</mamelayout>
You can see that if you have hundreds of games, figuring out these coordinates is going to be, well, not fun or fast.
How does this script work to save me time?
You will edit the script to point it at your directory full of multiple Bezel image files. When the script runs, it will pop up each image in turn, and let you draw a rectangle where the screen is located within the image. Then hit the space bar, or any other key. It will generate the Layout XML and move that, along with the bezel art, into a folder named for the game. Then all you do is drop it into the MAME/artwork folder and presto, you have Bezel. I went through 400 bezels while listening to some trippy ambient music and it was quite relaxing.
OK Chatty Cathy, Where’s the Damn Script?
You can find it in my Git repository, along with a README. If you improve this script, please consider sending me a pull request.
It’s a small enough script so I will show it to you here. It requires Python 3.6.1+ with Numpy 1.13.1 & OpenCV 3.3.1+contrib as dependencies.
# import the necessary packages
import os
import numpy as np
import cv2
import csv
import shutildirName = "./arcade-bezel-overlays/"
MAX_DISPLAY_H = 1920
MAX_DISPLAY_V = 1080
xy1 = ()
xy2 = ()
bezelImage = np.array([])
gameName = ""
aspect = 1.0
mouseIsDown = False
def drawRect(img, xy_1, xy_2):
if xy_1 != () and xy_2 != ():
cv2.rectangle(img, xy_1, xy_2, (0, 255, 0), 2)
def makeTemplate(name, img, xy_1, xy_2):
width = img.shape[1]
height = img.shape[0]
template = f'''<!-- {name}.lay -->
<mamelayout version="2">
<element name="bezel">
<image file="{name}.png" />
</element>
<view name="Bezel Artwork">
<bezel element="bezel">
<bounds left="0" top="0" right="{width}" bottom="{height}" />
</bezel>
<screen index="0">
<bounds left="{xy_1[0]}" top="{xy_1[1]}" right="{xy_2[0]}" bottom="{xy_2[1]}" />
</screen>
</view>
</mamelayout>'''
return template
def parseAllGames():
games = {}
with open('resolutions.csv') as csvfile:
reader = csv.DictReader(csvfile, delimiter=";")
for row in reader:
games[str(row["game_name"])] = row
return games
def click_and_move(event, x, y, flags, param):
global xy1, xy2, mouseIsDown, gameName, bezelImage, aspect
if event == cv2.EVENT_LBUTTONDOWN:
xy1 = (x, y)
xy2 = ()
mouseIsDown = True
elif event == cv2.EVENT_LBUTTONUP:
xy2 = (x, y)
mouseIsDown = False
elif mouseIsDown and event == cv2.EVENT_MOUSEMOVE:
xa = int(xy1[0] + aspect * (y - xy1[1]))
# Hold down SHIFT to constrain to game aspect
xy2 = (xa, y) if flags & cv2.EVENT_FLAG_SHIFTKEY else (x, y)
# draw a rectangle around the region of interest
imgCopy = bezelImage.copy()
drawRect(imgCopy, xy1, xy2)
cv2.imshow(gameName, imgCopy)
def estimateRect(game, img):
global aspect
gw = float(game["video_x"])
gh = float(game["video_y"])
aspect = gw / gh
width = img.shape[1]
height = img.shape[0]
top = 0.025 * height
bottom = 0.975 * height
vspan = bottom - top
hspan = aspect * vspan
left = 0.5 * (width - hspan)
right = width - 0.5 * (width - hspan)
return (int(left), int(top)), (int(right), int(bottom))
def mainLoop():
global xy1, xy2, bezelImage, gameName
all_games = parseAllGames()
ld = os.listdir(dirName)
for filename in ld:
# print("opening " + dirName + filename)
gameName = str(os.path.splitext(filename)[0])
if gameName in all_games:
game = all_games[gameName]
bezelImage = cv2.imread(dirName + filename)
cv2.namedWindow(gameName, cv2.WINDOW_NORMAL)
width = min(MAX_DISPLAY_H, bezelImage.shape[1])
height = min(MAX_DISPLAY_V, bezelImage.shape[0])
cv2.resizeWindow(gameName, width, height)
xy1, xy2 = estimateRect(game, bezelImage)
imgCopy = bezelImage.copy()
drawRect(imgCopy, xy1, xy2)
cv2.imshow(gameName, imgCopy)
cv2.setMouseCallback(gameName, click_and_move)
cv2.waitKey(0)
template = makeTemplate(gameName, bezelImage, xy1, xy2)
os.makedirs(f'out/{gameName}', 0o777, True)
f = open(f'out/{gameName}/default.lay', 'w')
f.write(template)
f.close()
# shutil.copyfile(dirName + filename, f'out/{gameName}/{filename}')
shutil.move(dirName + filename, f'out/{gameName}/{filename}')
cv2.destroyAllWindows()
else:
print(f'WARNING: Could not locate {gameName} in list of all known MAME entries.')
mainLoop()
In Summary
I hope you find this useful, and please let me know what you think!