This blog post is part of the F# Advent Calendar in English 2017 and, as such, I thought it would be fitting with a Christmas theme. We’ll use EA SPORTS FUT Database, Microsoft Cognitive Services and F# to find the soccer player best suited to be Santa Claus 🎅
Santa Claus
Santa Claus is generally depicted as a portly, joyous, white-bearded man—sometimes with spectacles—wearing a red coat with white fur collar and cuffs, white-fur-cuffed red trousers, and black leather belt and boots and who carries a bag full of gifts for children. - Wikipedia
Apart from the clothing, the characteristics that depict Santa Claus according to Wikipedia are portly, joyous and white-bearded. Our task will be to find the soccer player that best fulfills these characteristics. One exemption though, we’ll just look for white-bearded players as the number of players with a white beard is bound to be low (if any) and the Face API doesn’t provide color of facial hair.
Program flow
Our definition of portly will be based on calculating the body mass index, which is a value derived from the height and weight of an individual. Height and weight can be found in the EA SPORTS FUT Database. Also available is a headshot of the player, which we will perform face detection on using Microsoft Cognitive Services to determine smile (joyous) and facial hair (beard). We then sort by joy and beard to find the soccer player best suited to be Santa Claus before crowning him with a Santa Hat and saving the image.
We use the lovely Forward Pipe operator, |>
, to simplify the chain of function calls, which makes for readable code:
getPlayers // Get players from EA SPORTS FUT Database.
|> Array.filter isPortly // Filter out anyone not portly.
|> Array.map detectFace // Detect face in headshot.
|> Array.filter hasFace // Filter out players where face detection failed.
|> Array.sortWith joyAndBeard // Sort by joy and beard.
|> Array.take 1 // Take top 1, the winner.
|> Array.iteri santafy // Santafy, equip with Santa Hat and save image.
Getting soccer players
The FIFA series from EA Sports is the best-selling console title in the world and EA SPORTS provides the EA SPORTS FUT Database, which is a catalogue of players in FIFA (the game) Ultimate Team. The image below shows Cristiano Ronaldo in the FUT Database. The interesting attributes for us are height and weight (to calculate body mass index), and headshot image, which we’ll use for face detection in the next section.
We’ll use the F# Data Library for accessing the APIs. Typically I’d use the JSON Type Provider to access JSON in a statically typed way, but in this case I’m cherry-picking a few values from large JSON results and decided to just use the parser that the JSON Type Provider is built on top of.
When working with well-defined JSON documents, it is easier to use the type provider, but in a more dynamic scenario or when writing quick and simple scripts, the parser might be a simpler option. - F# Data: JSON Parser
The API uses paging, so first we determine the number of pages, totalPages
, to traverse before retrieving data for each page. The retrieval is done asynchronously and in parallel to speed up the processing. In FIFA Ultimate Team, players are represented as playing cards and can have multiple cards. We want players to occur only once, so we filter to only include so-called standard cards and rare cards. A player can have only one of these.
let getPlayers =
let getPage page =
async {
let! data = JsonValue.AsyncLoad ("https://www.easports.com/fifa/ultimate-team/api/fut/item?page=" + page.ToString())
let items =
[| for item in data?items -> item |]
|> Array.filter (fun item -> item?playerType.AsString() = "rare" || item?playerType.AsString() = "standard")
return items
}
let value = JsonValue.Load "https://www.easports.com/fifa/ultimate-team/api/fut/item"
let totalPages = value?totalPages.AsInteger()
[|1..totalPages|]
|> Array.map getPage
|> Async.Parallel
|> Async.RunSynchronously
|> Array.concat
In total, the FUT Database includes 15360 unique players spanned across 670 pages.
Finding portly players using Body Mass Index
Having retrieved all the players, we are now ready to find the portly / overweight players as defined by the body mass index. The formula is simple: BMI = kg / m2.
let isPortly player =
let height = player?height.AsFloat() / 100.0 // Convert height from cm to m.
let weight = player?weight.AsFloat()
let bmi = weight / (height * height)
bmi >= 25.0 // 25 or higher is considered overweight.
Of the 15360 players, 930 are considered overweight (~6%), i.e. body mass index => 25, but keep in mind that the classification applies to the general population in which a heavier weight is likely due to an abundance of body fat, not muscle.
Next, to detect faces of the “portly” players.
Face Detection
Face detection is the process of identifying human faces in digital images. There is a wealth of services to chose from, both cloud-based solutions like Google Vision API, Amazon Rekognition, Kairos etc. and on-premise solutions like OpenCV.
We’ll use Microsoft Cognitive Services and the Face API. Head over to the Face API products page to try it out in your browser (as shown below) or register for an API key to access it programmatically. The Free Pricing Tier allows for up to 20 calls per minute and 30,000 free calls per month. If that’s not enough, you can upgrade to the Standard Pricing Tier. See Pricing Details. I opted to upgrade as making 930 requests would otherwise have taken roughly 50 minutes. The limit for Standard is 10 requests per second, so now it only took about two minutes and cost $1.50.
When doing face detection, you get back a result containing face rectangles for where in the image the faces are, along with face attributes like smile and facial hair that we’ll be using.
The detectFace
function takes a player : JsonValue
parameter, which is the JSON returned from the EA SPORTS FUT Database. I could’ve created a type for passing around the used values, but decided to just pass around the JSON in its entirety. We download the headshot of the player from the FUT Database, perform face detection and return a tuple containing both the JSON from the FUT Database and the Face API to be used in further processing.
let getImage url =
match Http.Request(url).Body with
| Binary bytes -> bytes
| _ -> failwith "Text"
let detectFace player =
let headshotImgUrl = player?headshotImgUrl.AsString()
let bytes = getImage headshotImgUrl
let face =
Http.RequestString
( "https://westeurope.api.cognitive.microsoft.com/face/v1.0/detect",
httpMethod = "POST",
body = BinaryUpload bytes,
headers = [ "Ocp-Apim-Subscription-Key", "<insert key here>" ],
query = [ "returnFaceId", "false"; "returnFaceLandmarks", "false"; "returnFaceAttributes", "smile,facialHair" ])
|> JsonValue.Parse
player, face
When creating a resource in Azure, e.g. Face API, you’re tasked with selecting a location for the resource. You can use the Azure Speed Test to find the location best for you. Living in Norway, the location that has the lowest latency for me is West Europe, so that is why the endpoint above points to westeurope...
. The image to be detected is sent using a POST request and note that the maximum size of the image is 4MB. Every request to the Face API requires that the API key is specified, either in the query string or in the request header. Face landmarks are a series of (27) specifically detailed points on a face, but in this scenario we won’t be needing them and thus specify "returnFaceLandmarks", "false"
, which also speeds up the detection. In addition, we also specify that we only need the smile
and facialHair
attributes "returnFaceAttributes", "smile,facialHair"
.
Face API, we have a problem!
For some strange reason, face detection fails for 7 players (click each letter to see who) and the Face API just returns an empty JSON result.
Looking at the players, I can tell that none of them would’ve won anyways, so we work around this quirk and make sure that only one face is detected by filtering with the following function:
let hasFace (playerFaces: JsonValue * JsonValue) =
let _, faces = playerFaces
match faces.AsArray().Length with
| 1 -> true
| _ -> false
Sorting by joy and beard
Oh joy! We are now ready to sort 923 portly players by joy and beard. There are 3 attributes for facial hair: beard
, moustache
and sideburns
. Since a Santa Claus with a beard is an absolute must, we weigh the facial hair higher than the smile by totalling the 3 facial hair values with the smile value. If not, we might have gotten a Santa Claus with a big smile, but no beard.
let joyAndBeard (playerFaces1: JsonValue * JsonValue) (playerFaces2: JsonValue * JsonValue) =
let getJoyAndBeard (face: JsonValue) =
let smile = face?faceAttributes?smile.AsFloat()
let beard = face?faceAttributes?facialHair?beard.AsFloat()
let moustache = face?faceAttributes?facialHair?moustache.AsFloat()
let sideburns = face?faceAttributes?facialHair?sideburns.AsFloat()
smile + beard + moustache + sideburns
let _, faces1 = playerFaces1
let _, faces2 = playerFaces2
let face1 = getJoyAndBeard faces1.[0]
let face2 = getJoyAndBeard faces2.[0]
if face1 > face2 then -1
elif face1 < face2 then 1
else 0
The facial hair and smile attributes all range from 0 to 1. A lot of players received full score (1.0) for smile, but the highest value for any of the facial hair attributes was 0.8.
We now have a sorted list of portly, joyous and bearded soccer players, so let’s crown the winner with a Santa Hat.
Santafy
The Face API returns attributes, faceRectangle
, indicating where in an image faces are located. We use these attributes and some trial and error to determine where to place the santa hat and then we save the image to file, including a background as well.
let santafy index (playerFaces: JsonValue * JsonValue) =
let player, faces = playerFaces
let id = player?id.AsString()
let name = (player?firstName.AsString() + "_" + player?lastName.AsString()).Replace(' ', '_')
let headshotImgUrl = player?headshotImgUrl.AsString()
let bytes = getImage headshotImgUrl
let face = faces.AsArray().[0]
let left = face?faceRectangle?left.AsInteger()
let top = face?faceRectangle?top.AsInteger()
let width = face?faceRectangle?width.AsInteger()
let height = face?faceRectangle?height.AsInteger()
use inputStream = new MemoryStream(bytes)
use faceImage = Image.FromStream(inputStream)
let santaHatImage = Image.FromFile("santa-hat.png")
let image = Image.FromFile("background.png")
use graphics = Graphics.FromImage(image)
graphics.DrawImage(faceImage, 30, 56)
graphics.DrawImage(santaHatImage, left + 2, top - 5, width + 38, height + 50)
let filename = sprintf "%i-%s-%s.jpg" (index + 1) id name
image.Save(filename, ImageFormat.Jpeg)
I deliberately chose a Santa Hat that extends a bit beyond the face of a player. This in an attempt to cover up huge, fluffy hair. Think David Luiz, Marcelo, Wilfried Bony etc. Actually, Wilfried Bony came in 4th place and the Santa Hat doesn’t cover all of his hair, but as he didn’t win I haven’t tackled this problem. Yet.
The soccer player best suited to be Santa Claus is…
After analyzing 15360 players, finding those portly and sorting by joy and beard, we have a winner… Dylan Hayes 🏆 Right back from Bohemian FC in Ireland. Visit name links below to see FUT Database profiles.
1st - Dylan Hayes | 2nd - Eren Tozlu | 3rd - Osama Ashoor |
---|---|---|
BMI: 25.51 | BMI: 25.47 | BMI: 25.69 |
Smile: 1.0 | Smile: 0.931 | Smile: 1.0 |
Beard: 0.6 | Beard: 0.6 | Beard: 0.6 |
Moustache: 0.6 | Moustache: 0.7 | Moustache: 0.6 |
Sideburns: 0.7 | Sideburns: 0.6 | Sideburns: 0.6 |
Total: 2.9 | Total: 2.831 | Total: 2.8 |
Bonus
I used Cristiano Ronaldo as an example throughout the blog post, so I’ll show him and the other Ballon d’Or 2017 finalists as a bonus:
Cristiano Ronaldo | Lionel Messi | Neymar |
---|---|---|
BMI: 23.37 | BMI: 24.91 | BMI: 22.20 |
Smile: 1.0 | Smile: 0.0 | Smile: 0.989 |
Beard: 0.0 | Beard: 0.6 | Beard: 0.0 |
Moustache: 0.1 | Moustache: 0.7 | Moustache: 0.0 |
Sideburns: 0.1 | Sideburns: 0.3 | Sideburns: 0.0 |
Total: 1.2 | Total: 1.6 | Total: 0.989 |
Wrapping Up
Thanks for reading. I hope you enjoyed the blog post and don’t forget to check out the rest of the F# Advent Calendar in English 2017. Source code is available on GitHub: Santafy, and also includes santafied images of the 10 best players in FIFA 18.
Merry Christmas! 🎄