drag_and_drop_detect.html 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. <!-- This is a Jinja2 Template that extends layout.html
  2. Implements a drag & drop file upload interface for YOLOv5 detection.
  3. Once a user image is uploaded, this code AJAX requests the image, model name
  4. and image size to the FastAPI server's /detect endpoint. The server sends
  5. the list of bounding boxes back, and the image + bounding boxes + labels
  6. are rendered inside the canvas element.
  7. Bounding box labels' height above the bounding box are adjusted to not overlap
  8. with eachother in complicated scenes (such as a busy city street).
  9. TODO:
  10. - Improve efficiency of the algorithm for making box labels not overlap in crowded scenes
  11. -->
  12. {% extends "layout.html" %} {% block title %}
  13. <title>YOLOv5 Drag & Drop Demo</title> {% endblock %} {% block header %}
  14. <!-- Include JQuery for this page -->
  15. <script
  16. src="https://code.jquery.com/jquery-3.6.0.min.js"
  17. integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
  18. crossorigin="anonymous"
  19. ></script>
  20. {% endblock %} {% block content %}
  21. <div style="overflow-x: hidden">
  22. <div class="row">
  23. <div class="col-auto m-2">
  24. <label for="model_name" class="form-label"
  25. ><b>Select YOLOv5 Model</b></label
  26. >
  27. <select class="form-select" id="model_name" name="model_name">
  28. {% for selection in model_selection_options %}
  29. <option value="{{ selection }}">{{ selection }}</option>
  30. {% endfor %}
  31. </select>
  32. <label for="img_size" class="form-label"
  33. ><b>Model Inference Size</b></label
  34. >
  35. <input
  36. type="text"
  37. class="form-control"
  38. id="img_size"
  39. name="img_size"
  40. value="1824"
  41. />
  42. </div>
  43. <div class="col">
  44. <!-- Drag and drop images in this div -->
  45. <div
  46. class="m-2"
  47. id="drop-region"
  48. style="border: 3px dashed limegreen; height: 150px"
  49. >
  50. <div class="container-fluid">
  51. <div class="d-flex justify-content-center align-items-center h-100">
  52. <b>Drag & Drop Images Or Click To Upload</b>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. </div>
  58. <!-- Our image + bboxes will be drawn here! Downscale image to fit page width-->
  59. <canvas id="canvas" style="max-width: 99%; height: auto"></canvas>
  60. </div>
  61. <script type="text/javascript">
  62. /*This script block handles canvas updates after getting bboxes from the server. */
  63. const canvas = document.getElementById('canvas')
  64. const ctx = canvas.getContext('2d')
  65. var image = new Image(60, 45) // Using optional size for image
  66. image.onload = drawImageWithBBoxes // Draw when image has loaded
  67. var colormap = {
  68. car: [255, 216, 0], //yellow
  69. person: [255, 0, 0], //red
  70. truck: [255, 0, 255], //purple
  71. }
  72. var data //list of list of dictionaries obtained from server's /detect endpoint
  73. function drawImageWithBBoxes() {
  74. // Use the intrinsic size of image in CSS pixels for the canvas element
  75. canvas.width = this.naturalWidth
  76. canvas.height = this.naturalHeight
  77. //draw the image
  78. ctx.drawImage(this, 0, 0)
  79. //draw some bboxes!
  80. ctx.lineWidth = 2
  81. ctx.font = '24px arial'
  82. ctx.strokeStyle = 'yellow'
  83. textboxlocations = []
  84. FILL_ALPHA = 1
  85. const randomBetween = (min, max) =>
  86. min + Math.floor(Math.random() * (max - min + 1))
  87. for (const item of data) {
  88. if (item['class_name'] in colormap) {
  89. rgb = colormap[item['class_name']]
  90. } else {
  91. //random color for this class
  92. rgb = [
  93. randomBetween(0, 255),
  94. randomBetween(0, 255),
  95. randomBetween(0, 255),
  96. ]
  97. colormap[item['class_name']] = rgb
  98. }
  99. ctx.fillStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${FILL_ALPHA})`
  100. ctx.strokeStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${FILL_ALPHA})`
  101. // draw bbox
  102. ctx.strokeRect(
  103. item['bbox'][0],
  104. item['bbox'][1],
  105. item['bbox'][2] - item['bbox'][0],
  106. item['bbox'][3] - item['bbox'][1]
  107. )
  108. let label = `${item['class_name']} ${do_rounding(item['confidence'])}`
  109. let textMeasures = ctx.measureText(label)
  110. let textHeight =
  111. textMeasures.actualBoundingBoxAscent +
  112. textMeasures.actualBoundingBoxDescent
  113. let padding = 2
  114. let x = item['bbox'][0]
  115. let y = item['bbox'][1] - textHeight - 2 * padding
  116. let w = textMeasures.width + 2 * padding
  117. let h = textHeight + 2 * padding
  118. //check if new textbox would overlap with previous textboxes drawn
  119. while (
  120. textboxlocations.some(box => IOU(box, [x, y, x + w, y + h]) > 0.01)
  121. ) {
  122. //if so, move the textbox up by h + 5 pixels
  123. y -= h + 3
  124. }
  125. //don't let the textbox go beyond top of the image
  126. if (y <= 0) {
  127. y = 0
  128. }
  129. textboxlocations.push([x - 1, y - 1, x + w + 1, y + h + 1])
  130. // draw text background box
  131. ctx.fillRect(x, y, w, h)
  132. //draw text
  133. if (rgb[0] + rgb[1] + rgb[2] > (255 * 3) / 2) {
  134. ctx.fillStyle = 'black'
  135. } else {
  136. ctx.fillStyle = 'white'
  137. }
  138. ctx.fillText(label, x + padding, y + h - padding)
  139. //draw line between text and bbox top left corner
  140. ctx.beginPath()
  141. ctx.moveTo(x, y)
  142. ctx.lineTo(x, item['bbox'][1])
  143. ctx.stroke()
  144. }
  145. }
  146. function IOU(boxA, boxB, isPixel = 0) {
  147. /*This function computes the IOU of 2 boxes.
  148. This is used solely to make sure bbox labels don't overlap vertically */
  149. // determine the (x, y)-coordinates of the intersection rectangle
  150. xA = Math.max(boxA[0], boxB[0])
  151. yA = Math.max(boxA[1], boxB[1])
  152. xB = Math.min(boxA[2], boxB[2])
  153. yB = Math.min(boxA[3], boxB[3])
  154. if (xA >= xB || yA >= yB) {
  155. return 0
  156. }
  157. //compute the area of intersection rectangle
  158. interArea = (xB - xA + isPixel) * (yB - yA + isPixel)
  159. //compute the area of both rectangles
  160. boxAArea = (boxA[2] - boxA[0] + isPixel) * (boxA[3] - boxA[1] + isPixel)
  161. boxBArea = (boxB[2] - boxB[0] + isPixel) * (boxB[3] - boxB[1] + isPixel)
  162. // compute the intersection over union by taking the intersection
  163. // area and dividing it by the sum of areas - the interesection area
  164. iou = interArea / (boxAArea + boxBArea - interArea)
  165. return iou
  166. }
  167. function do_rounding(num, places = 2) {
  168. return Math.round((num + Number.EPSILON) * 10 ** places) / 10 ** places
  169. }
  170. </script>
  171. <script type="text/javascript">
  172. /*This script block handles the drag and drop + AJAX request to server */
  173. // where files are dropped + file selector is opened
  174. var dropRegion = document.getElementById('drop-region')
  175. // open file selector when clicked on the drop region
  176. var fakeInput = document.createElement('input')
  177. fakeInput.type = 'file'
  178. fakeInput.accept = 'image/*'
  179. fakeInput.multiple = false //dont allow multiple file upload
  180. dropRegion.addEventListener('click', function () {
  181. fakeInput.click()
  182. })
  183. function validateImage(image) {
  184. // check the type
  185. var validTypes = ['image/jpeg', 'image/png', 'image/gif']
  186. if (validTypes.indexOf(image.type) === -1) {
  187. alert('Invalid File Type')
  188. return false
  189. }
  190. // check the size
  191. var maxSizeInBytes = 10e6 // 10MB
  192. if (image.size > maxSizeInBytes) {
  193. alert('File too large')
  194. return false
  195. }
  196. return true
  197. }
  198. function handleFiles(files) {
  199. for (var i = 0, len = files.length; i < len; i++) {
  200. if (validateImage(files[i])) previewAnduploadImage(files[i])
  201. }
  202. }
  203. fakeInput.addEventListener('change', function () {
  204. var files = fakeInput.files
  205. handleFiles(files)
  206. })
  207. function preventDefault(e) {
  208. e.preventDefault()
  209. e.stopPropagation()
  210. }
  211. dropRegion.addEventListener('dragenter', preventDefault, false)
  212. dropRegion.addEventListener('dragleave', preventDefault, false)
  213. dropRegion.addEventListener('dragover', preventDefault, false)
  214. dropRegion.addEventListener('drop', preventDefault, false)
  215. function handleDrop(e) {
  216. var data = e.dataTransfer,
  217. files = data.files
  218. handleFiles(files)
  219. }
  220. dropRegion.addEventListener('drop', handleDrop, false)
  221. function handleDrop(e) {
  222. var data = e.dataTransfer,
  223. files = data.files
  224. handleFiles(files)
  225. }
  226. dropRegion.addEventListener('drop', handleDrop, false)
  227. function handleDrop(e) {
  228. var dt = e.dataTransfer,
  229. files = dt.files
  230. if (files.length) {
  231. handleFiles(files)
  232. } else {
  233. // check for img
  234. var html = dt.getData('text/html'),
  235. match = html && /\bsrc="?([^"\s]+)"?\s*/.exec(html),
  236. url = match && match[1]
  237. if (url) {
  238. uploadImageFromURL(url)
  239. return
  240. }
  241. }
  242. function uploadImageFromURL(url) {
  243. var img = new Image()
  244. var c = document.createElement('canvas')
  245. var ctx = c.getContext('2d')
  246. img.onload = function () {
  247. c.width = this.naturalWidth // update canvas size to match image
  248. c.height = this.naturalHeight
  249. ctx.drawImage(this, 0, 0) // draw in image
  250. c.toBlob(function (blob) {
  251. // get content as PNG blob
  252. // call our main function
  253. handleFiles([blob])
  254. }, 'image/png')
  255. }
  256. img.onerror = function () {
  257. alert('Error in uploading')
  258. }
  259. img.crossOrigin = '' // if from different origin
  260. img.src = url
  261. }
  262. }
  263. function previewAnduploadImage(img) {
  264. /* This function reads the user's image, AJAX requests it to the server,
  265. JSON parses the result and draws the image onto the canvas.
  266. The bboxes are drawn in drawImageWithBBoxes() which get run after
  267. image is drawn on the canvas. */
  268. // read the image...
  269. var reader = new FileReader()
  270. reader.onload = function (e) {
  271. image.src = e.target.result
  272. }
  273. // create FormData
  274. var formData = new FormData()
  275. formData.append('file_list', img)
  276. formData.append('model_name', $('#model_name').val())
  277. formData.append('img_size', $('#img_size').val())
  278. $.ajax({
  279. url: '/detect',
  280. data: formData,
  281. processData: false,
  282. contentType: false,
  283. type: 'POST',
  284. success: function (json_result_data) {
  285. json_result_data = json_result_data.replaceAll("'", '"')
  286. data = JSON.parse(json_result_data)[0] //read json result of YOLO
  287. //read the image, triggers image to load on canvas and bbox's to be drawn
  288. reader.readAsDataURL(img)
  289. },
  290. error: function (xhr, ajaxOptions, thrownError) {
  291. alert(
  292. 'Error code ' +
  293. xhr.status +
  294. ': ' +
  295. thrownError +
  296. '\nMessage: ' +
  297. JSON.parse(xhr.responseText)['message']
  298. )
  299. },
  300. })
  301. }
  302. </script>
  303. {% endblock %}