drag_and_drop_detect.html 11 KB

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