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.

Further research

LS0tCnRpdGxlOiAiRFJBRlQ6IFB1bGxpbmcgSGVhcnRiZWF0IERhdGEgZnJvbSBpUGhvbmUgdXNpbmcgb25seSB0aGUgQ2FtZXJhIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgoKCllvdSBjYW4gZmluZCB0aGUgZ2lmIG9mIHRoZSBbdmlkZW9dKGh0dHBzOi8vZ2lwaHkuY29tL2dpZnMvckRGa3VySlB2NmNyQykKClRoaXMgbWVudGFsIGV4ZXJjaXNlIHdhcyBmb3JtZWQgb3V0IG9mIGN1cmlvdXNpdHkgaW4gdXNpbmcgZGF0YSBpbiB1bmNvbnZlbnRpb25hbCB3YXlzLiBUaGVyZSBpcyBkYXRhIGV2ZXJ5d2hlcmUgYW5kIGl0IGlzIG91ciBqb2JzIGFzIGVuZ2luZWVycyB0byBwdWxsIHRoZSByYWJiaXQgb3V0IG9mIHRoZSBiYWcuIE9mIGNvdXJzZSB0aGVyZSBuZWVkcyB0byBiZSBwcmFjdGljZSB0byBkbyBzby4gRGF0YSBjYW4gbG9vayBtZXNzeSwgYnV0IGl0IGRvZXNuJ3QgbWVhbiBpdCBpcyB1bnVzYWJsZSBpbiBhbGwgY2FzZXMuIEFzIHRoZSBvbGQgYWRhZ2Ugc2F5cywgdGhlIGRldmlsIGlzIGluIHRoZSBkZXRhaWxzLiBBbmQgdGhhdCBpcyB3aGVyZSB3ZSBwcm92aWRlIHRoZSBkIFNvIEkgd2FzIHRoaW5raW5nIHRoZSBvdGhlciBkYXksIGFib3V0IGFuIGFwcCBJIGRvd25sb2FkZWQgYSBjb3VwbGUgeWVhcnMgYWdvIGNhbGxlZCBJbnN0YW50IEhlYXJ0IFJhdGUgICtbMV0oaHR0cHM6Ly9pdHVuZXMuYXBwbGUuY29tL3VzL2FwcC9pbnN0YW50LWhlYXJ0LXJhdGUtaGVhcnQtcmF0ZS1wdWxzZS1tb25pdG9yL2lkNDA5NjI1MDY4P210PTgpLiBUaGlzICB3YXMgaW50ZXJlc3RpbmcgdG8gbWUgYXQgdGhlIHRpbWUgYW5kIHRodXMgYm91Z2h0IHRoZSBhcHAuIE5vdyBob3cgYWNjdXJhdGUgaXQgd2FzLCBJIGRvIG5vdCBrbm93IGJlY2F1c2UgSSBkaWRuJ3QgbmVjZXNzYXJpbHkgdXNlIGl0IGEgd2hvbGUgbG90IGFuZCBkaWRuJ3QgZG8gYW55IHNvcnQgb2YgdGVzdGluZyBvbiB0aGUgYXBwLiBTbyBmb2xsb3dpbmcgYXJlIHRoZSBqb3VybmV5cyBJJ3ZlIHRha2VuIHRvIHRyeSB0byB1bmRlcnN0YW5kIGl0IGJldHRlci4gCgpUaGUgZmlyc3QgdGhpbmcgd2UgaGF2ZSB0byBkbyBpcyBjdXQgdGhlIHZpZGVvIGludG8gZnJhbWVzIHNvIHRoYXQgaXQgY2FuIGJlIGV2YWx1YXRlZC4gU2luY2UgSSBkaWRuJ3QgZmluZCBhIGxpYnJhcnkgdGhhdCBtaWdodCBkbyB0aGF0IGluIFIuIEkganVzdCB1c2VkIHNvbWUgUHl0aG9uIENWMiB0byBnZW5lcmF0ZSBteSBmcmFtZXMuIAoKYGBge3B5dGhvbiwgZWNobz1UUlVFfQojIERvbmUgdXNpbmcgUHl0aG9uIAppbXBvcnQgY3YyCgp2aWRQYXRoID0gJ0lNR180NjcwLk1PVicgCmNvdW50ID0gMAoKdmlkY2FwID0gY3YyLlZpZGVvQ2FwdHVyZSh2aWRQYXRoKQpzdWNjZXNzLGltYWdlID0gdmlkY2FwLnJlYWQoKQpzdWNjZXNzID0gVHJ1ZQp3aGlsZSBzdWNjZXNzOgogIHN1Y2Nlc3MsaW1hZ2UgPSB2aWRjYXAucmVhZCgpCiAgcHJpbnQoJ1JlYWQgYSBuZXcgZnJhbWU6ICcsIHN1Y2Nlc3MpCiAgY3VycmVudENvdW50ID0gY291bnQgKyAxCiAgY3YyLmltd3JpdGUoIiVkLmpwZyIgJSBjdXJyZW50Q291bnQsIGltYWdlKSAgICAgIyBzYXZlIGZyYW1lIGFzIEpQRUcgZmlsZQogIGNvdW50ICs9IDEKYGBgCgpUaGUgYWJvdmUgY29kZSBtYW5hZ2VkIHRvIGRlbGl2ZXIgLmpwZyBmaWxlcyBpbiBzZXF1ZW5jZSB0aGF0IGNhbiBiZSBhbmFseXplZCBpbiBSLiAKCmBgYHtyIExpYnJhcmllcyBhbmQgUGFyYW1ldGVycywgaW5jbHVkZT1GQUxTRX0KbGlicmFyeShzdHJpbmdyKQpsaWJyYXJ5KGltYWdlcikKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KGRwbHlyKQpsaWJyYXJ5KHBsb3RseSkKCmdyYXBoc1lOIDwtIDAKZmlsZURpcmVjdG9yeSA8LSAifi9Eb2N1bWVudHMvci1zZXNzaW9ucy9JbWFnZVJlY29nbml0aW9uL2RhdGEvIgoKCmdldFBlYWtzIDwtIGZ1bmN0aW9uICh4LCB0aHJlc2ggPSAwKSAKewogICAgcGtzIDwtIHdoaWNoKGRpZmYoc2lnbihkaWZmKHgsIG5hLnBhZCA9IEZBTFNFKSksIG5hLnBhZCA9IEZBTFNFKSA8IDApICsgMgogICAgaWYgKCFtaXNzaW5nKHRocmVzaCkpIHsKICAgICAgICBwa3NbeFtwa3MgLSAxXSAtIHhbcGtzXSA+IHRocmVzaF0KICAgIH0KICAgIGVsc2UgcGtzCn0KYGBgCgoKCmBgYHtyIFByZXBhcmF0aW9ufQoKI1JlYWQganBlZyBmaWxlcyBpbnRvIHRoZSBzeXN0ZW0gCmZpbGVzIDwtIGxpc3QuZmlsZXMoImRhdGEiLCBwYXR0ZXJuID0gIiouanBnIikKCiNDcmVhdGUgdGhlIGRhdGEgdGFibGUgCmltYWdlRGF0YXNldCA8LSBkYXRhLmZyYW1lKE5hbWUgPSBudW1lcmljKCksIEludGVuc2l0eSA9IGRvdWJsZSgpKQoKYGBgCgoKTm93LCBoZXJlIHdlIGFyZSB0YWtpbmcgdGhlIGltYWdlcyBhbmQgZG9pbmcgc29tZSBtYW5pcHVsYXRpb24gdG8gdGhlbSBzbyB0aGF0IHdlIGNhbiB1c2UgdGhlIGltYWdlJ3MgaGlzdG9ncmFtIHRvIGV4dHJhY3QgdGhlIGRhdGEgIHRoYXQgd2UgYXJlIGxvb2tpbmcgdG8gdXNlLiAKYGBge3IgUHJvY2VzcyB0aGUgRmlsZXN9CmZvcihpIGluIGZpbGVzKQp7CiAgZmlsZV9kZiA8LSBsb2FkLmltYWdlKHBhc3RlMChmaWxlRGlyZWN0b3J5LGkpKQogIAogICNQcm9jZXNzIGltYWdlcyBieSBjb252ZXJ0aW5nIHRvIEdyYXlzY2FsZSwgUmVzaXppbmcgYW5kIHRoZW4gc3RvcmluZyB0aGUgaW50ZW5zaXR5IHBpeGVscyB0byBiZSBvYnNlcnZlZCBhcyBhIGhpc3RvZ3JhbSAKICBpbWcgPC0gZ3JheXNjYWxlKGFzLmNpbWcoZmlsZV9kZikpCiAgaW1nIDwtIHJlc2l6ZShpbWcscm91bmQod2lkdGgoaW1nKS80KSwgcm91bmQoaGVpZ2h0KGltZykvNCkpCiAgaW1nX2dzX3IgPC0gYXMuZGF0YS5mcmFtZShpbWcpCiAgaW1nX2dzX3JfZGYgPC0gZGF0YS5mcmFtZShOYW1lID0gYXMubnVtZXJpYyhzdHJfcmVwbGFjZShpLCAiLmpwZyIsICIiKSksIEludGVuc2l0eSA9IGltZ19nc19yJHZhbHVlKQogCiAgcGVyY2VudFVuZGVyIDwtIGltZ19nc19yX2RmICU+JSBmaWx0ZXIoSW50ZW5zaXR5IDw9IDAuMykgJT4lIHN1bW1hcml6ZShMb3dlclRoYW4gPSBuKCkpCiAgaW1hZ2VEYXRhc2V0IDwtIHJiaW5kKGltYWdlRGF0YXNldCwgYyhhcy5udW1lcmljKHN0cl9yZXBsYWNlKGksICIuanBnIiwgIiIpKSxwZXJjZW50VW5kZXIkTG93ZXJUaGFuKSkKICAKICAjIENoZWNrIDEgdG8gY3JlYXRlIGhpc3RvZ3JhbSBvZiBncmFwaAogIGlmKGdyYXBoc1lOID09IDEpCiAgewogICAgIGdyYXBoIDwtIGltZ3IgJT4lIGdncGxvdCggYWVzKEludGVuc2l0eSwgeG1pbiA9IDAuMjUsIHhtYXggPSAwLjM1KSkgKyBnZW9tX2hpc3RvZ3JhbShiaW5zID0gNTApCiAgICAgZ2dzYXZlKHBhc3RlMChzdHJfcmVwbGFjZShpLCAiLmpwZyIsICIiKSwnLmpwZWcnKSwgcGxvdCA9IGdyYXBoKQogIH0KIAp9CmBgYAoKCkkndmUgdGFrZW4gdGhlIGRhdGEgZnJvbSB0aGUgaW1hZ2VzIGFuZCBzaG93IHRob3NlIHBvaW50cyB0aGVtIG9uIGEgbGluZSBncmFwaC4gCgpgYGB7ciBWaXN1YWxpemUgdGhlIGRhdGF9CmNvbG5hbWVzKGltYWdlRGF0YXNldCkgPC0gYygiRnJhbWUiLCJJbnRlbnNpdHkiKQpnIDwtIGltYWdlRGF0YXNldCAlPiUgCiAgZ2dwbG90KGFlcyhGcmFtZSwgSW50ZW5zaXR5KSkgKyAKICBnZW9tX2xpbmUoKSArIAogIGxhYnModGl0bGUgPSAiSGVhcnRiZWF0IERhdGEiKQoKZwoKYGBgCgoKQmVlbiBhYmxlIHRvIHNob3cgZGlzdGluY3RpdmUgcGVha3MgZnJvbSB0aGUgZGF0YTsgbm93IG9ud2FyZCB0byBkZXRlcm1pbmluZyB0aGUgYWN0dWFsIGhlYXJ0IHJhdGUgb2YgdGhlIGNsaXAuIFByZXZpb3VzbHkgd2UgbmVlZCB0byB3cml0ZSBhIGZ1bmN0aW9uIHRoYXQgd2lsbCBwdWxsIG91dCB0aGUgcGVha3MgZnJvbSB0aGUgZGF0YSBzbyB0aGF0IHdlIGNhbiB1bmRlcnN0YW5kIHRoZSBudW1iZXIgb2YgdGh1bXBzIHBlciBob3VyLgoKYGBge3J9CnRvdGFsUG9pbnRzIDwtIGltYWdlRGF0YXNldFtnZXRQZWFrcyhpbWFnZURhdGFzZXQkSW50ZW5zaXR5LCB0aHJlc2ggPSAxKSxdCmBgYApIZXJlIHdlIHNlbGVjdCB0aGUgcGVha3MgYnkgcHVsbGluZyBvdXQgdGhlaXIgaW5kaWNlcyBzbyB0aGF0IHdlIGNhbiBjcmVhdGUgb3VyIHBlYWsgZGF0YXNldCB3aGljaCB0aGVuIGNhbiBiZSB2aXN1YWxpemVkIHRvIHZlcmlmeSB0aGF0IG91ciBwZWFrcyBhcmUgZ2l2aW5nIHVzIHRoZSBpbWFnZXMgd2UgbmVlZC4gSG93ZXZlciBoZXJlIHdlIGZpbmQgdGhhdCB0aGVyZSBhcmUgbW9yZSBtYXhpbXVtcyBpbiB0aGUgZGF0YSB0aGFuIG5lZWRlZC4KCmBgYHtyfQpnX2FubjEgPC0gZyArIAogIGdlb21fcG9pbnQoZGF0YSA9IHRvdGFsUG9pbnRzLCBhZXMoeCA9IEZyYW1lLCB5ID0gSW50ZW5zaXR5KSwgY29sb3IgPSAicmVkIikgKyAKICBsYWJzKHRpdGxlID0gIkhlYXJ0YmVhdCBEYXRhIEFubm90YXRlZCB3aXRoIFBlYWtzIikKZ19hbm4xCmBgYAoKVGhpcyBpcyBzbGlnaHRseSBtZXNzeSBhbmQgd291bGQgYmUgZGlmZmljdWx0IHRvIGV4dHJhY3QgYSBoZWFydGJlYXQgZnJvbSB0aGVzZSBmcmFtZXM7IHNvIHdlIG5lZWQgdG8gY3JlYXRlIHRocmVzaG9sZHMgdG8gZWxpbWluYXRlIHRoZXNlIHBlYWtzLiBJbiB0aGlzIGNhc2UgSSBzb3J0IG9mIHJpZ2dlZCBpdCBieSBwdWxsb3V0IG91dCB0aGUgZGF0YSBtYW51YWxseS4gVGhpcyB3b3VsZCBub3QgYmUgYW4gYWR2aXNlZCBzb3J0IG9mIGluc3RhbmNlIGFzIGl0IGlzbid0IHJlcHJvZHVjaWJsZSBvbiBhbm90aGVyIGRhdGFzZXQuIEhvd2V2ZXIgc2luY2UgdGhpcyBpcyBidXQgYSBxdWlja2VyIGV4ZXJjaXNlIHdlIHdpbGwgYWRkIGEgZmV3IGZpbHRlcnMgdG8gZ2V0IHVzIHRoZSBkYXRhLgoKYGBge3J9CnRvdGFsUG9pbnRzRmlsdGVyZWQgPC0gdG90YWxQb2ludHMgJT4lIGZpbHRlcihJbnRlbnNpdHkgPiA1MDAwLCAhKEZyYW1lICVpbiUgYygzNyw0MSwyNzEsIDI5MSwzMTEsMzMxLDMzOSwgMzYxLDM4MCw0MDApKSkKZ19hbm4gPC0gZyArIGdlb21fcG9pbnQoZGF0YSA9IHRvdGFsUG9pbnRzRmlsdGVyZWQsIGFlcyh4ID0gRnJhbWUsIHkgPSBJbnRlbnNpdHkpLCBjb2xvciA9ICJyZWQiKSArIGxhYnModGl0bGUgPSAiSGVhcnRiZWF0IERhdGEgQW5ub3RhdGVkIHdpdGggUGVha3MiKQpnX2FubgpgYGAKU28gcHJldHR5IGdvb2QsIGJ1dCB3b24ndCBnaXZlIG1lIGV4YWN0bHkgd2hhdCBJIG5lZWQgc2luY2UgaXQgaXMgZmluZGluZyB0aGUgbWF4aW11bXM7IGhvd2V2ZXIgaXQgaXMgZmluZGluZyB0b28gbWFueSBvZiB0aGVtLiAKIAoKYGBge3J9CgpkaXN0YW5jZSA8LSBhcy5tYXRyaXgoZGlzdChzb3J0KHRvdGFsUG9pbnRzRmlsdGVyZWQkRnJhbWUpKSkKZCA8LSB2ZWN0b3IoKQoKZm9yIChpIGluIDE6KG5jb2woZGlzdGFuY2UpLTEpKQp7IAogIGQgPC0gcmJpbmQoZCxjKGRpc3RhbmNlW2ksaSsxXSkpCn0KCiNMZW5ndGggb2YgdGhlIHZpZGVvIAp2aWRlb1RpbWUgPC0gMTMKCmJlYXRzRnJhbWUgPC0gYXMudmVjdG9yKHVubGlzdChkKSkKCiMgSGVhcnRiZWF0IHVzaW5nIGF2ZXJhZ2UgYnkgZnJhbWVzIChuZWVkcyBmcmFtZSApCigzMCo2MCkvbWVhbihiZWF0c0ZyYW1lKQoKIyBIZWFydGJlYXQgcmF0ZSBieSBiZWF0IGNvdW50CihsZW5ndGgoYmVhdHNGcmFtZSkgLyB2aWRlb1RpbWUpKjYwCmBgYAoKCkluIG9yZGVyIHRvIG5vdCBsb3NlIHByZWNpb3VzIGRhdGE7IGVzcGVjaWFsbHkgaWYgaXQgcnVucyBtb3JlIHNsb3dseSB0aGFuIG9uIG90aGVyIGNvbXB1dGVyczsganVzdCBzYXZlIHRoZSBkYXRhIG9mIGludGVyZXN0IHNvIHRoYXQgeW91IG1pZ2h0IGJlIGFibGUgdG8gcmVwcm9kdWNlIHlvdXIgZ3JhcGhzLiAKCmBgYHtyfQp3cml0ZS5jc3YoaW1hZ2VEYXRhc2V0LCAiaW1nNDYyNi5jc3YiKQpgYGAKCgojIyBDb25jbHVzaW9ucwpUaGlzIG1pZ2h0IGJlIGEgbGl0dGxlIHJvdWdoIGFyb3VuZCB0aGUgZWRnZXM7IGJ1dCBpdCBkb2VzIGdpdmUgdXMgdGhlIGFiaWxpdHkgIHRvIGV4cGFuZCBvdXIgbWluZHMgdG8gY29tZSB1cCB3aXRoIGNyZWF0aXZlIHdheXMgdG8gdXNlIGluZm9ybWF0aW9uIHdlIGNhbiBmaW5kIGFyb3VuZCB1cyBtaWdodCBnaXZlIHVzIG1vcmUgdGhhbiB3ZSBtaWdodCB0aGluay4gCgoqIE9uZSBoYXMgdG8gZ2V0IGEgZ29vZCBjb3ZlciBvdmVyIHRoZSBjYW1lcmEuIAoKIyMgRnVydGhlciByZXNlYXJjaCAKKiBDdXJyZW50bHkgdGhpcyBpcyBhIHJhdGhlciBicnV0ZWZvcmNlIHdheSBvZiBnZXR0aW5nIHRoZSBkYXRhIEkgbmVlZCwgaG93ZXZlciBpdCBkb2VzIHByb3ZlIHRoZSBwb2ludCBhbmQgdGh1cyBjYW4gYmUgZnVydGhlciBtb2RpZmllZCB0byBnZXQgYmV0dGVyIHJlc3VsdHMuIA==