You can find the gif of the video
This mental exercise was formed out of curiousity in using data in unconventional ways. There is data everywhere and it is our jobs as engineers to pull the rabbit out of the bag. Of course there needs to be practice to do so. Data can look messy, but it doesn’t mean it is unusable in all cases. As the old adage says, the devil is in the details. And that is where we provide the d So I was thinking the other day, about an app I downloaded a couple years ago called Instant Heart Rate +1. This was interesting to me at the time and thus bought the app. Now how accurate it was, I do not know because I didn’t necessarily use it a whole lot and didn’t do any sort of testing on the app. So following are the journeys I’ve taken to try to understand it better.
The first thing we have to do is cut the video into frames so that it can be evaluated. Since I didn’t find a library that might do that in R. I just used some Python CV2 to generate my frames.
# Done using Python
import cv2
vidPath = 'IMG_4670.MOV'
count = 0
vidcap = cv2.VideoCapture(vidPath)
success,image = vidcap.read()
success = True
while success:
success,image = vidcap.read()
print('Read a new frame: ', success)
currentCount = count + 1
cv2.imwrite("%d.jpg" % currentCount, image) # save frame as JPEG file
count += 1
The above code managed to deliver .jpg files in sequence that can be analyzed in R.
#Read jpeg files into the system
files <- list.files("data", pattern = "*.jpg")
#Create the data table
imageDataset <- data.frame(Name = numeric(), Intensity = double())
Now, here we are taking the images and doing some manipulation to them so that we can use the image’s histogram to extract the data that we are looking to use.
for(i in files)
{
file_df <- load.image(paste0(fileDirectory,i))
#Process images by converting to Grayscale, Resizing and then storing the intensity pixels to be observed as a histogram
img <- grayscale(as.cimg(file_df))
img <- resize(img,round(width(img)/4), round(height(img)/4))
img_gs_r <- as.data.frame(img)
img_gs_r_df <- data.frame(Name = as.numeric(str_replace(i, ".jpg", "")), Intensity = img_gs_r$value)
percentUnder <- img_gs_r_df %>% filter(Intensity <= 0.3) %>% summarize(LowerThan = n())
imageDataset <- rbind(imageDataset, c(as.numeric(str_replace(i, ".jpg", "")),percentUnder$LowerThan))
# Check 1 to create histogram of graph
if(graphsYN == 1)
{
graph <- imgr %>% ggplot( aes(Intensity, xmin = 0.25, xmax = 0.35)) + geom_histogram(bins = 50)
ggsave(paste0(str_replace(i, ".jpg", ""),'.jpeg'), plot = graph)
}
}
I’ve taken the data from the images and show those points them on a line graph.
colnames(imageDataset) <- c("Frame","Intensity")
g <- imageDataset %>%
ggplot(aes(Frame, Intensity)) +
geom_line() +
labs(title = "Heartbeat Data")
g

Been able to show distinctive peaks from the data; now onward to determining the actual heart rate of the clip. Previously we need to write a function that will pull out the peaks from the data so that we can understand the number of thumps per hour.
totalPoints <- imageDataset[getPeaks(imageDataset$Intensity, thresh = 1),]
Here we select the peaks by pulling out their indices so that we can create our peak dataset which then can be visualized to verify that our peaks are giving us the images we need. However here we find that there are more maximums in the data than needed.
g_ann1 <- g +
geom_point(data = totalPoints, aes(x = Frame, y = Intensity), color = "red") +
labs(title = "Heartbeat Data Annotated with Peaks")
g_ann1

This is slightly messy and would be difficult to extract a heartbeat from these frames; so we need to create thresholds to eliminate these peaks. In this case I sort of rigged it by pullout out the data manually. This would not be an advised sort of instance as it isn’t reproducible on another dataset. However since this is but a quicker exercise we will add a few filters to get us the data.
totalPointsFiltered <- totalPoints %>% filter(Intensity > 5000, !(Frame %in% c(37,41,271, 291,311,331,339, 361,380,400)))
g_ann <- g + geom_point(data = totalPointsFiltered, aes(x = Frame, y = Intensity), color = "red") + labs(title = "Heartbeat Data Annotated with Peaks")
g_ann

So pretty good, but won’t give me exactly what I need since it is finding the maximums; however it is finding too many of them.
distance <- as.matrix(dist(sort(totalPointsFiltered$Frame)))
d <- vector()
for (i in 1:(ncol(distance)-1))
{
d <- rbind(d,c(distance[i,i+1]))
}
#Length of the video
videoTime <- 13
beatsFrame <- as.vector(unlist(d))
# Heartbeat using average by frames (needs frame )
(30*60)/mean(beatsFrame)
[1] 86.63102
# Heartbeat rate by beat count
(length(beatsFrame) / videoTime)*60
[1] 83.07692
In order to not lose precious data; especially if it runs more slowly than on other computers; just save the data of interest so that you might be able to reproduce your graphs.
write.csv(imageDataset, "img4626.csv")
Conclusions
This might be a little rough around the edges; but it does give us the ability to expand our minds to come up with creative ways to use information we can find around us might give us more than we might think.
- One has to get a good cover over the camera.
Further research
- Currently this is a rather bruteforce way of getting the data I need, however it does prove the point and thus can be further modified to get better results.
LS0tCnRpdGxlOiAiRFJBRlQ6IFB1bGxpbmcgSGVhcnRiZWF0IERhdGEgZnJvbSBpUGhvbmUgdXNpbmcgb25seSB0aGUgQ2FtZXJhIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgoKCllvdSBjYW4gZmluZCB0aGUgZ2lmIG9mIHRoZSBbdmlkZW9dKGh0dHBzOi8vZ2lwaHkuY29tL2dpZnMvckRGa3VySlB2NmNyQykKClRoaXMgbWVudGFsIGV4ZXJjaXNlIHdhcyBmb3JtZWQgb3V0IG9mIGN1cmlvdXNpdHkgaW4gdXNpbmcgZGF0YSBpbiB1bmNvbnZlbnRpb25hbCB3YXlzLiBUaGVyZSBpcyBkYXRhIGV2ZXJ5d2hlcmUgYW5kIGl0IGlzIG91ciBqb2JzIGFzIGVuZ2luZWVycyB0byBwdWxsIHRoZSByYWJiaXQgb3V0IG9mIHRoZSBiYWcuIE9mIGNvdXJzZSB0aGVyZSBuZWVkcyB0byBiZSBwcmFjdGljZSB0byBkbyBzby4gRGF0YSBjYW4gbG9vayBtZXNzeSwgYnV0IGl0IGRvZXNuJ3QgbWVhbiBpdCBpcyB1bnVzYWJsZSBpbiBhbGwgY2FzZXMuIEFzIHRoZSBvbGQgYWRhZ2Ugc2F5cywgdGhlIGRldmlsIGlzIGluIHRoZSBkZXRhaWxzLiBBbmQgdGhhdCBpcyB3aGVyZSB3ZSBwcm92aWRlIHRoZSBkIFNvIEkgd2FzIHRoaW5raW5nIHRoZSBvdGhlciBkYXksIGFib3V0IGFuIGFwcCBJIGRvd25sb2FkZWQgYSBjb3VwbGUgeWVhcnMgYWdvIGNhbGxlZCBJbnN0YW50IEhlYXJ0IFJhdGUgICtbMV0oaHR0cHM6Ly9pdHVuZXMuYXBwbGUuY29tL3VzL2FwcC9pbnN0YW50LWhlYXJ0LXJhdGUtaGVhcnQtcmF0ZS1wdWxzZS1tb25pdG9yL2lkNDA5NjI1MDY4P210PTgpLiBUaGlzICB3YXMgaW50ZXJlc3RpbmcgdG8gbWUgYXQgdGhlIHRpbWUgYW5kIHRodXMgYm91Z2h0IHRoZSBhcHAuIE5vdyBob3cgYWNjdXJhdGUgaXQgd2FzLCBJIGRvIG5vdCBrbm93IGJlY2F1c2UgSSBkaWRuJ3QgbmVjZXNzYXJpbHkgdXNlIGl0IGEgd2hvbGUgbG90IGFuZCBkaWRuJ3QgZG8gYW55IHNvcnQgb2YgdGVzdGluZyBvbiB0aGUgYXBwLiBTbyBmb2xsb3dpbmcgYXJlIHRoZSBqb3VybmV5cyBJJ3ZlIHRha2VuIHRvIHRyeSB0byB1bmRlcnN0YW5kIGl0IGJldHRlci4gCgpUaGUgZmlyc3QgdGhpbmcgd2UgaGF2ZSB0byBkbyBpcyBjdXQgdGhlIHZpZGVvIGludG8gZnJhbWVzIHNvIHRoYXQgaXQgY2FuIGJlIGV2YWx1YXRlZC4gU2luY2UgSSBkaWRuJ3QgZmluZCBhIGxpYnJhcnkgdGhhdCBtaWdodCBkbyB0aGF0IGluIFIuIEkganVzdCB1c2VkIHNvbWUgUHl0aG9uIENWMiB0byBnZW5lcmF0ZSBteSBmcmFtZXMuIAoKYGBge3B5dGhvbiwgZWNobz1UUlVFfQojIERvbmUgdXNpbmcgUHl0aG9uIAppbXBvcnQgY3YyCgp2aWRQYXRoID0gJ0lNR180NjcwLk1PVicgCmNvdW50ID0gMAoKdmlkY2FwID0gY3YyLlZpZGVvQ2FwdHVyZSh2aWRQYXRoKQpzdWNjZXNzLGltYWdlID0gdmlkY2FwLnJlYWQoKQpzdWNjZXNzID0gVHJ1ZQp3aGlsZSBzdWNjZXNzOgogIHN1Y2Nlc3MsaW1hZ2UgPSB2aWRjYXAucmVhZCgpCiAgcHJpbnQoJ1JlYWQgYSBuZXcgZnJhbWU6ICcsIHN1Y2Nlc3MpCiAgY3VycmVudENvdW50ID0gY291bnQgKyAxCiAgY3YyLmltd3JpdGUoIiVkLmpwZyIgJSBjdXJyZW50Q291bnQsIGltYWdlKSAgICAgIyBzYXZlIGZyYW1lIGFzIEpQRUcgZmlsZQogIGNvdW50ICs9IDEKYGBgCgpUaGUgYWJvdmUgY29kZSBtYW5hZ2VkIHRvIGRlbGl2ZXIgLmpwZyBmaWxlcyBpbiBzZXF1ZW5jZSB0aGF0IGNhbiBiZSBhbmFseXplZCBpbiBSLiAKCmBgYHtyIExpYnJhcmllcyBhbmQgUGFyYW1ldGVycywgaW5jbHVkZT1GQUxTRX0KbGlicmFyeShzdHJpbmdyKQpsaWJyYXJ5KGltYWdlcikKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KGRwbHlyKQpsaWJyYXJ5KHBsb3RseSkKCmdyYXBoc1lOIDwtIDAKZmlsZURpcmVjdG9yeSA8LSAifi9Eb2N1bWVudHMvci1zZXNzaW9ucy9JbWFnZVJlY29nbml0aW9uL2RhdGEvIgoKCmdldFBlYWtzIDwtIGZ1bmN0aW9uICh4LCB0aHJlc2ggPSAwKSAKewogICAgcGtzIDwtIHdoaWNoKGRpZmYoc2lnbihkaWZmKHgsIG5hLnBhZCA9IEZBTFNFKSksIG5hLnBhZCA9IEZBTFNFKSA8IDApICsgMgogICAgaWYgKCFtaXNzaW5nKHRocmVzaCkpIHsKICAgICAgICBwa3NbeFtwa3MgLSAxXSAtIHhbcGtzXSA+IHRocmVzaF0KICAgIH0KICAgIGVsc2UgcGtzCn0KYGBgCgoKCmBgYHtyIFByZXBhcmF0aW9ufQoKI1JlYWQganBlZyBmaWxlcyBpbnRvIHRoZSBzeXN0ZW0gCmZpbGVzIDwtIGxpc3QuZmlsZXMoImRhdGEiLCBwYXR0ZXJuID0gIiouanBnIikKCiNDcmVhdGUgdGhlIGRhdGEgdGFibGUgCmltYWdlRGF0YXNldCA8LSBkYXRhLmZyYW1lKE5hbWUgPSBudW1lcmljKCksIEludGVuc2l0eSA9IGRvdWJsZSgpKQoKYGBgCgoKTm93LCBoZXJlIHdlIGFyZSB0YWtpbmcgdGhlIGltYWdlcyBhbmQgZG9pbmcgc29tZSBtYW5pcHVsYXRpb24gdG8gdGhlbSBzbyB0aGF0IHdlIGNhbiB1c2UgdGhlIGltYWdlJ3MgaGlzdG9ncmFtIHRvIGV4dHJhY3QgdGhlIGRhdGEgIHRoYXQgd2UgYXJlIGxvb2tpbmcgdG8gdXNlLiAKYGBge3IgUHJvY2VzcyB0aGUgRmlsZXN9CmZvcihpIGluIGZpbGVzKQp7CiAgZmlsZV9kZiA8LSBsb2FkLmltYWdlKHBhc3RlMChmaWxlRGlyZWN0b3J5LGkpKQogIAogICNQcm9jZXNzIGltYWdlcyBieSBjb252ZXJ0aW5nIHRvIEdyYXlzY2FsZSwgUmVzaXppbmcgYW5kIHRoZW4gc3RvcmluZyB0aGUgaW50ZW5zaXR5IHBpeGVscyB0byBiZSBvYnNlcnZlZCBhcyBhIGhpc3RvZ3JhbSAKICBpbWcgPC0gZ3JheXNjYWxlKGFzLmNpbWcoZmlsZV9kZikpCiAgaW1nIDwtIHJlc2l6ZShpbWcscm91bmQod2lkdGgoaW1nKS80KSwgcm91bmQoaGVpZ2h0KGltZykvNCkpCiAgaW1nX2dzX3IgPC0gYXMuZGF0YS5mcmFtZShpbWcpCiAgaW1nX2dzX3JfZGYgPC0gZGF0YS5mcmFtZShOYW1lID0gYXMubnVtZXJpYyhzdHJfcmVwbGFjZShpLCAiLmpwZyIsICIiKSksIEludGVuc2l0eSA9IGltZ19nc19yJHZhbHVlKQogCiAgcGVyY2VudFVuZGVyIDwtIGltZ19nc19yX2RmICU+JSBmaWx0ZXIoSW50ZW5zaXR5IDw9IDAuMykgJT4lIHN1bW1hcml6ZShMb3dlclRoYW4gPSBuKCkpCiAgaW1hZ2VEYXRhc2V0IDwtIHJiaW5kKGltYWdlRGF0YXNldCwgYyhhcy5udW1lcmljKHN0cl9yZXBsYWNlKGksICIuanBnIiwgIiIpKSxwZXJjZW50VW5kZXIkTG93ZXJUaGFuKSkKICAKICAjIENoZWNrIDEgdG8gY3JlYXRlIGhpc3RvZ3JhbSBvZiBncmFwaAogIGlmKGdyYXBoc1lOID09IDEpCiAgewogICAgIGdyYXBoIDwtIGltZ3IgJT4lIGdncGxvdCggYWVzKEludGVuc2l0eSwgeG1pbiA9IDAuMjUsIHhtYXggPSAwLjM1KSkgKyBnZW9tX2hpc3RvZ3JhbShiaW5zID0gNTApCiAgICAgZ2dzYXZlKHBhc3RlMChzdHJfcmVwbGFjZShpLCAiLmpwZyIsICIiKSwnLmpwZWcnKSwgcGxvdCA9IGdyYXBoKQogIH0KIAp9CmBgYAoKCkkndmUgdGFrZW4gdGhlIGRhdGEgZnJvbSB0aGUgaW1hZ2VzIGFuZCBzaG93IHRob3NlIHBvaW50cyB0aGVtIG9uIGEgbGluZSBncmFwaC4gCgpgYGB7ciBWaXN1YWxpemUgdGhlIGRhdGF9CmNvbG5hbWVzKGltYWdlRGF0YXNldCkgPC0gYygiRnJhbWUiLCJJbnRlbnNpdHkiKQpnIDwtIGltYWdlRGF0YXNldCAlPiUgCiAgZ2dwbG90KGFlcyhGcmFtZSwgSW50ZW5zaXR5KSkgKyAKICBnZW9tX2xpbmUoKSArIAogIGxhYnModGl0bGUgPSAiSGVhcnRiZWF0IERhdGEiKQoKZwoKYGBgCgoKQmVlbiBhYmxlIHRvIHNob3cgZGlzdGluY3RpdmUgcGVha3MgZnJvbSB0aGUgZGF0YTsgbm93IG9ud2FyZCB0byBkZXRlcm1pbmluZyB0aGUgYWN0dWFsIGhlYXJ0IHJhdGUgb2YgdGhlIGNsaXAuIFByZXZpb3VzbHkgd2UgbmVlZCB0byB3cml0ZSBhIGZ1bmN0aW9uIHRoYXQgd2lsbCBwdWxsIG91dCB0aGUgcGVha3MgZnJvbSB0aGUgZGF0YSBzbyB0aGF0IHdlIGNhbiB1bmRlcnN0YW5kIHRoZSBudW1iZXIgb2YgdGh1bXBzIHBlciBob3VyLgoKYGBge3J9CnRvdGFsUG9pbnRzIDwtIGltYWdlRGF0YXNldFtnZXRQZWFrcyhpbWFnZURhdGFzZXQkSW50ZW5zaXR5LCB0aHJlc2ggPSAxKSxdCmBgYApIZXJlIHdlIHNlbGVjdCB0aGUgcGVha3MgYnkgcHVsbGluZyBvdXQgdGhlaXIgaW5kaWNlcyBzbyB0aGF0IHdlIGNhbiBjcmVhdGUgb3VyIHBlYWsgZGF0YXNldCB3aGljaCB0aGVuIGNhbiBiZSB2aXN1YWxpemVkIHRvIHZlcmlmeSB0aGF0IG91ciBwZWFrcyBhcmUgZ2l2aW5nIHVzIHRoZSBpbWFnZXMgd2UgbmVlZC4gSG93ZXZlciBoZXJlIHdlIGZpbmQgdGhhdCB0aGVyZSBhcmUgbW9yZSBtYXhpbXVtcyBpbiB0aGUgZGF0YSB0aGFuIG5lZWRlZC4KCmBgYHtyfQpnX2FubjEgPC0gZyArIAogIGdlb21fcG9pbnQoZGF0YSA9IHRvdGFsUG9pbnRzLCBhZXMoeCA9IEZyYW1lLCB5ID0gSW50ZW5zaXR5KSwgY29sb3IgPSAicmVkIikgKyAKICBsYWJzKHRpdGxlID0gIkhlYXJ0YmVhdCBEYXRhIEFubm90YXRlZCB3aXRoIFBlYWtzIikKZ19hbm4xCmBgYAoKVGhpcyBpcyBzbGlnaHRseSBtZXNzeSBhbmQgd291bGQgYmUgZGlmZmljdWx0IHRvIGV4dHJhY3QgYSBoZWFydGJlYXQgZnJvbSB0aGVzZSBmcmFtZXM7IHNvIHdlIG5lZWQgdG8gY3JlYXRlIHRocmVzaG9sZHMgdG8gZWxpbWluYXRlIHRoZXNlIHBlYWtzLiBJbiB0aGlzIGNhc2UgSSBzb3J0IG9mIHJpZ2dlZCBpdCBieSBwdWxsb3V0IG91dCB0aGUgZGF0YSBtYW51YWxseS4gVGhpcyB3b3VsZCBub3QgYmUgYW4gYWR2aXNlZCBzb3J0IG9mIGluc3RhbmNlIGFzIGl0IGlzbid0IHJlcHJvZHVjaWJsZSBvbiBhbm90aGVyIGRhdGFzZXQuIEhvd2V2ZXIgc2luY2UgdGhpcyBpcyBidXQgYSBxdWlja2VyIGV4ZXJjaXNlIHdlIHdpbGwgYWRkIGEgZmV3IGZpbHRlcnMgdG8gZ2V0IHVzIHRoZSBkYXRhLgoKYGBge3J9CnRvdGFsUG9pbnRzRmlsdGVyZWQgPC0gdG90YWxQb2ludHMgJT4lIGZpbHRlcihJbnRlbnNpdHkgPiA1MDAwLCAhKEZyYW1lICVpbiUgYygzNyw0MSwyNzEsIDI5MSwzMTEsMzMxLDMzOSwgMzYxLDM4MCw0MDApKSkKZ19hbm4gPC0gZyArIGdlb21fcG9pbnQoZGF0YSA9IHRvdGFsUG9pbnRzRmlsdGVyZWQsIGFlcyh4ID0gRnJhbWUsIHkgPSBJbnRlbnNpdHkpLCBjb2xvciA9ICJyZWQiKSArIGxhYnModGl0bGUgPSAiSGVhcnRiZWF0IERhdGEgQW5ub3RhdGVkIHdpdGggUGVha3MiKQpnX2FubgpgYGAKU28gcHJldHR5IGdvb2QsIGJ1dCB3b24ndCBnaXZlIG1lIGV4YWN0bHkgd2hhdCBJIG5lZWQgc2luY2UgaXQgaXMgZmluZGluZyB0aGUgbWF4aW11bXM7IGhvd2V2ZXIgaXQgaXMgZmluZGluZyB0b28gbWFueSBvZiB0aGVtLiAKIAoKYGBge3J9CgpkaXN0YW5jZSA8LSBhcy5tYXRyaXgoZGlzdChzb3J0KHRvdGFsUG9pbnRzRmlsdGVyZWQkRnJhbWUpKSkKZCA8LSB2ZWN0b3IoKQoKZm9yIChpIGluIDE6KG5jb2woZGlzdGFuY2UpLTEpKQp7IAogIGQgPC0gcmJpbmQoZCxjKGRpc3RhbmNlW2ksaSsxXSkpCn0KCiNMZW5ndGggb2YgdGhlIHZpZGVvIAp2aWRlb1RpbWUgPC0gMTMKCmJlYXRzRnJhbWUgPC0gYXMudmVjdG9yKHVubGlzdChkKSkKCiMgSGVhcnRiZWF0IHVzaW5nIGF2ZXJhZ2UgYnkgZnJhbWVzIChuZWVkcyBmcmFtZSApCigzMCo2MCkvbWVhbihiZWF0c0ZyYW1lKQoKIyBIZWFydGJlYXQgcmF0ZSBieSBiZWF0IGNvdW50CihsZW5ndGgoYmVhdHNGcmFtZSkgLyB2aWRlb1RpbWUpKjYwCmBgYAoKCkluIG9yZGVyIHRvIG5vdCBsb3NlIHByZWNpb3VzIGRhdGE7IGVzcGVjaWFsbHkgaWYgaXQgcnVucyBtb3JlIHNsb3dseSB0aGFuIG9uIG90aGVyIGNvbXB1dGVyczsganVzdCBzYXZlIHRoZSBkYXRhIG9mIGludGVyZXN0IHNvIHRoYXQgeW91IG1pZ2h0IGJlIGFibGUgdG8gcmVwcm9kdWNlIHlvdXIgZ3JhcGhzLiAKCmBgYHtyfQp3cml0ZS5jc3YoaW1hZ2VEYXRhc2V0LCAiaW1nNDYyNi5jc3YiKQpgYGAKCgojIyBDb25jbHVzaW9ucwpUaGlzIG1pZ2h0IGJlIGEgbGl0dGxlIHJvdWdoIGFyb3VuZCB0aGUgZWRnZXM7IGJ1dCBpdCBkb2VzIGdpdmUgdXMgdGhlIGFiaWxpdHkgIHRvIGV4cGFuZCBvdXIgbWluZHMgdG8gY29tZSB1cCB3aXRoIGNyZWF0aXZlIHdheXMgdG8gdXNlIGluZm9ybWF0aW9uIHdlIGNhbiBmaW5kIGFyb3VuZCB1cyBtaWdodCBnaXZlIHVzIG1vcmUgdGhhbiB3ZSBtaWdodCB0aGluay4gCgoqIE9uZSBoYXMgdG8gZ2V0IGEgZ29vZCBjb3ZlciBvdmVyIHRoZSBjYW1lcmEuIAoKIyMgRnVydGhlciByZXNlYXJjaCAKKiBDdXJyZW50bHkgdGhpcyBpcyBhIHJhdGhlciBicnV0ZWZvcmNlIHdheSBvZiBnZXR0aW5nIHRoZSBkYXRhIEkgbmVlZCwgaG93ZXZlciBpdCBkb2VzIHByb3ZlIHRoZSBwb2ludCBhbmQgdGh1cyBjYW4gYmUgZnVydGhlciBtb2RpZmllZCB0byBnZXQgYmV0dGVyIHJlc3VsdHMuIA==