From 612cf161526fd87c788e60403a367c41fa6e9791 Mon Sep 17 00:00:00 2001 From: gattlin1 Date: Sun, 6 Oct 2019 19:12:40 -0500 Subject: [PATCH 1/3] Added logic to account for black borders around beads. --- Server/resources/js/mapInspector.js | 1 + lib/colorLabeler.py | 45 +++++++++++++++++++++++++++++ lib/counting.py | 40 ++++++++++++++----------- requirements.txt | 1 + 4 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 lib/colorLabeler.py diff --git a/Server/resources/js/mapInspector.js b/Server/resources/js/mapInspector.js index 010d57a..14e772a 100644 --- a/Server/resources/js/mapInspector.js +++ b/Server/resources/js/mapInspector.js @@ -114,6 +114,7 @@ var imageObj = undefined; //the stitched map image type = 'Crushed Bead'; rgb = 'N/A'; ctx.fillStyle = $('#crushedBeadOutline').val(); + radius = 'N/A'; } else if (toolTipBead[1] === 'waterBead') { type = 'Water Bead'; ctx.fillStyle = $('#waterBeadOutline').val(); diff --git a/lib/colorLabeler.py b/lib/colorLabeler.py new file mode 100644 index 0000000..15d0329 --- /dev/null +++ b/lib/colorLabeler.py @@ -0,0 +1,45 @@ +import cv2 +import numpy as np +from collections import OrderedDict +from scipy.spatial import distance as dist + +class ColorLabeler: + def __init__(self): + self.colorsToIgnore = ['darkgray', 'darkblue', 'black'] + + colors = OrderedDict({ + "red": (255, 0, 0), + "green": (0, 255, 0), + "blue": (0, 0, 255), + "black": (0, 0, 0), + "yellow": (255, 255, 0), + "darkblue": (0, 0, 71), + "gray": (96, 96, 96), + "darkgray": (70, 70, 70), + "white": (255, 255, 255), + }) + + self.lab = np.zeros((len(colors), 1, 3), dtype="uint8") + self.colorNames = [] + + for (i, (name, rgb)) in enumerate(colors.items()): + self.lab[i] = rgb + self.colorNames.append(name) + + self.lab = cv2.cvtColor(self.lab, cv2.COLOR_RGB2LAB) + + def label(self, img, c): + mask = np.zeros(img.shape[:2], dtype="uint8") + cv2.drawContours(mask, [c], -1, 255, -1) + mask = cv2.erode(mask, None, iterations=2) + mean = cv2.mean(img, mask=mask)[:3] + + minDist = (np.inf, None) + + for (i, row) in enumerate(self.lab): + d = dist.euclidean(row[0], mean) + + if d < minDist[0]: + minDist = (d, i) + + return self.colorNames[minDist[1]] \ No newline at end of file diff --git a/lib/counting.py b/lib/counting.py index 476c2bf..de5706c 100644 --- a/lib/counting.py +++ b/lib/counting.py @@ -35,6 +35,7 @@ from enum import Enum from os import listdir, path from . import util +from . import colorLabeler """ Description: an enum class to handle the HoughCircle configuration values that are used in cv2.HoughCircles(). @@ -123,32 +124,37 @@ def isWater(self, RGB): def getCrushedBeads(self, image, circles): temp_img = self.grayScaleMap.copy() + for i in circles[0,:]: # fills in the circle cv2.circle(temp_img, (i[0],i[1]) ,i[2], (255,255,255), -1) #fills the outer edges of the circle cv2.circle(temp_img,(i[0], i[1]), i[2], (255,255,255), 17) + blur = cv2.GaussianBlur(temp_img, (19, 19), 0) + lab = cv2.cvtColor(self.colorMap, cv2.COLOR_BGR2LAB) thresh = cv2.threshold(blur, 225, 255, cv2.THRESH_BINARY_INV)[1] img_output, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - for c in contours: - # calculate moments for each contour - M = cv2.moments(c) - - # calculate x,y coordinate of center - if M["m00"] != 0: - cX = int(M["m10"] / M["m00"]) - cY = int(M["m01"] / M["m00"]) - else: - cX, cY = 0, 0 - - x = self.getBrightestColor([cX, cY, 10]) - print(x) + cl = colorLabeler.ColorLabeler() - cv2.circle(image, (cX, cY), 5, (0, 0, 255), -1) - self.crushedBeads.append([[0, 0, 0], 'crushedBead', [cX, cY, 35]]) + for c in contours: + color = cl.label(lab, c) + + if color not in cl.colorsToIgnore: + cv2.drawContours(image, [c], -1, (0, 255, 0), 2) + + # compute the center of the contour + M = cv2.moments(c) + if M['m00'] > 0: + cX = int((M["m10"] / M["m00"])) + cY = int((M["m01"] / M["m00"])) + self.crushedBeads.append([[0, 0, 0], 'crushedBead', [cX, cY, 35]]) + else: + cX = 0 + cY = 0 + cv2.circle(image, (cX, cY), 2, (0, 255, 0), 3) """ Description: a function that takes an array representing a circle's[x-coord of center, y-coord of center, radius] @@ -212,10 +218,10 @@ def getPointsInCircle(self, radius, centerX, centerY): """ - Description: + Description: @param colorFormat: a string that is either 'rgb', 'hsv', 'cmyk', or 'grayscale' @return void, writes file directly from class attributes - """ + """ def makeBeadsCSV(self, colorFormat): newPath = self.imagePath endIndex = newPath.rfind("/") diff --git a/requirements.txt b/requirements.txt index b2e4410..e1edc18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ Flask==1.0.2 gevent==1.3.6 pillow==5.3.0 imutils==0.5.3 +scipy==1.3.1 matplotlib \ No newline at end of file From ca5369c0bf12169ab5fdd057078dd01e4cbabe76 Mon Sep 17 00:00:00 2001 From: gattlin1 Date: Mon, 7 Oct 2019 19:28:32 -0500 Subject: [PATCH 2/3] Merging dev -> gat1. --- .gitignore | 3 +- Server/resources/css/results.css | 0 Server/resources/js/index.js | 23 +++++- Server/resources/js/results.js | 17 ++++ Server/routes.py | 25 +++++- Server/templates/index.html | 9 ++- Server/templates/results.html | 14 +++- lib/counting.py | 128 ++++++++++++++++++++++++++++++- lib/file_util.py | 18 +++++ lib/stitching.py | 32 +++++--- 10 files changed, 243 insertions(+), 26 deletions(-) create mode 100644 Server/resources/css/results.css create mode 100644 lib/file_util.py diff --git a/.gitignore b/.gitignore index 3e78dce..91d74cc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules/ Server/resources/uploads/* test/resources/sample*/*Copy* uploads/ -.vscode \ No newline at end of file +.vscode +venv/ \ No newline at end of file diff --git a/Server/resources/css/results.css b/Server/resources/css/results.css new file mode 100644 index 0000000..e69de29 diff --git a/Server/resources/js/index.js b/Server/resources/js/index.js index 846652f..7e7a823 100644 --- a/Server/resources/js/index.js +++ b/Server/resources/js/index.js @@ -55,6 +55,11 @@ $(document).ready(function() { minBeadValue.innerText = minSizeSlider.value; maxBeadValue.innerText = maxSizeSlider.value; + let colorAlgorithm = document.getElementById('color-algorithm-selection') + + + + minSizeSlider.oninput = function() { minBeadValue.innerHTML = this.value; } @@ -175,10 +180,18 @@ $(document).ready(function() { }); submit.click(function() { - let data = imageUpload.val() != null && imageUpload.val() !== '' ? new FormData(imageForm[0]) : new FormData(videoForm[0]); + + if (imageUpload.val() == null) { + noImagesSelected(); + return; + } + + let data = new FormData(imageForm[0]); let crushedBeadDetection = crushedBeadCheckbox[0].checked; - let url = `/uploadImages?wantsCrushed=${crushedBeadDetection}`; + let selectedColorAlgorithm = colorAlgorithm.value; + let url = `/uploadImages?wantsCrushed=${crushedBeadDetection}&colorAlgorithm=${selectedColorAlgorithm}`; + console.log("URL: " + url); overlay.removeClass('d-none'); $.ajax({ @@ -240,4 +253,10 @@ $(document).ready(function() { overlay.addClass('d-none'); createAlert('post-alert', 'An error occured while uploading your files, please try again later.', 'postTimeout'); } + + function noImagesSelected(e) { + overlay.addCass('d-none'); + createAlert('no-images-selected', 'Please select images for upload', 'postTimeout'); + } + }); \ No newline at end of file diff --git a/Server/resources/js/results.js b/Server/resources/js/results.js index e9d1575..97dafb8 100644 --- a/Server/resources/js/results.js +++ b/Server/resources/js/results.js @@ -28,6 +28,23 @@ SOFTWARE. $(window).ready(function(){ + let csvFormatSelect = document.getElementById("csvFormat"); + let csvDownloadButton = document.getElementById("csvDownload"); + + csvDownloadButton.addEventListener('click', (e) => { + let a = document.createElement('a') + const colorOutputType = csvFormatSelect.value; + + a.href = '/getResultReport/' + resDir + a.href = a.href + '?colorOutputType=' + colorOutputType; + a.href = a.href + '&resDir=' + resDir; + a.href = a.href + '&key=' + mapLocation; + + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + }); + let beadTableDiv = document.getElementById('resultsDiv'), table = document.createElement('table'), tableHeader = document.createElement('thead'), diff --git a/Server/routes.py b/Server/routes.py index 433b21e..05e8237 100644 --- a/Server/routes.py +++ b/Server/routes.py @@ -25,7 +25,7 @@ #Authors: Jacob Wakefield, Noah Zeilmann, McKenna Gates, Liam Zay from . import app -from flask import render_template, send_from_directory, request, url_for, redirect, jsonify +from flask import render_template, send_from_directory, request, url_for, redirect, jsonify, send_file from werkzeug.utils import secure_filename from lib.counting import * from lib.stitching import * @@ -51,6 +51,8 @@ def __init__(self): detectionParams = Parameters() +countingDict = {} + """ Description: a function used to see if the uploaded file is in a valid format. @Param filename - name of the file being uploaded. @@ -92,12 +94,15 @@ def error(): def uploadImages(): images = request.files.getlist("images") wantsCrushed = request.args['wantsCrushed'] + colorAlgoritm = request.args['colorAlgorithm'] if wantsCrushed == 'true': # changing the js boolean to a python boolean detectionParams.wantsCrushedBeads = True else: detectionParams.wantsCrushedBeads = False + detectionParams.detectionAlgorithm = colorAlgoritm + newDir = setupUploadDir() for i in images: @@ -156,11 +161,23 @@ def getResults(directory): serverDirectory = 'Server/resources/uploads/' + directory count = Counting(serverDirectory) + countingDict[serverDirectory] = count - circles = count.getColorBeads(magLevel, detectionParams) + circles = countingDict[serverDirectory].getColorBeads(magLevel, detectionParams) - colorOutputType = request.args.get('colorOutputType') # TODO: use the query parameter colorOutputType, as it is not hooked to frontend - count.makeBeadsCSV('placeholder') # TODO: this will be where the colorOutputType query param goes + # countingDict[serverDirectory].makeBeadsCSV('rgb') # rgb is default return render_template('results.html', colorBeads = circles, waterBeads = count.waterBeads, crushedBeads = count.crushedBeads, mapLocation = directory, resultsDirectory = resultsDirectory) + +@app.route('/getResultReport/') +def getResultReport(directory): + colorOutputType = request.args.get('colorOutputType') # this is the type of output we want + resDir = request.args.get('resDir') # this is the directory we are accessing + key = 'Server' + request.args.get('key') # this key allows access to the counting dictionary + + countingDict[key].makeBeadsCSV(colorOutputType) # access the stored counting variable and regen csv data + + uploadDir = 'resources/uploads/' + resDir + + return send_file(uploadDir + '/results/beads.csv') \ No newline at end of file diff --git a/Server/templates/index.html b/Server/templates/index.html index 578075f..50fd048 100644 --- a/Server/templates/index.html +++ b/Server/templates/index.html @@ -106,10 +106,11 @@

M.O.S.A.I.C

-->
- + + + +
Color Detection Algorithm diff --git a/Server/templates/results.html b/Server/templates/results.html index d992b82..274b610 100644 --- a/Server/templates/results.html +++ b/Server/templates/results.html @@ -532,10 +532,19 @@

Results

+
+ +
@@ -566,6 +575,7 @@

Results

/*beads = beads.replace(/False/g, "false"); beads = beads.replace(/True/g,"true");*/ beads = JSON.parse(beads); + var resDir = '{{resultsDirectory}}' diff --git a/lib/counting.py b/lib/counting.py index de5706c..00d1f97 100644 --- a/lib/counting.py +++ b/lib/counting.py @@ -32,6 +32,7 @@ import math import itertools import csv +import sys from enum import Enum from os import listdir, path from . import util @@ -53,7 +54,6 @@ class HoughConfig(Enum): """ class Counting: - def __init__(self, imagePath): self.imagePath = imagePath self.grayScaleMap = cv2.imread(imagePath,0) # create grayscale cv2 img @@ -77,6 +77,7 @@ def getColorBeads(self, houghConfig, detectionParams): circles = cv2.HoughCircles(blur,cv2.HOUGH_GRADIENT,dp=houghConfig["dp"],minDist=houghConfig["minDist"], param1=houghConfig["param1"],param2=houghConfig["param2"],minRadius=houghConfig["minRadius"],maxRadius=houghConfig["maxRadius"]) + circles = np.uint16(np.around(circles)) for i in circles[0,:]: # i[0] is x coordinate, i[1] is y coordinate, i[2] is radius @@ -85,7 +86,17 @@ def getColorBeads(self, houghConfig, detectionParams): # draw the center of the circle cv2.circle(cimg,(i[0],i[1]),2,(0,0,255),3) - color = self.getBrightestColor(i) + + if detectionParams.detectionAlgorithm == "avg": + color = self.getAverageColor(i) + elif detectionParams.detectionAlgorithm == "mid": + color = self.getMiddleColor(i) + elif detectionParams.detectionAlgorithm == "corner": + color = self.getFourQuadrantColor(i) + elif detectionParams.detectionAlgorithm == "rad": + color = self.getRadiusAverageColor(i) + + if(color[1] == 'bead'): # if the bead is a water bead, leave it out. self.colorBeads.append(color) result.append(color) @@ -202,6 +213,117 @@ def getBrightestColor(self, circleInfo): return [[average[0],average[1],average[2]], type, [circleInfo[0],circleInfo[1],circleInfo[2]]] #[[R,G,B], isWater, [x,y,radius]] + def getAverageColor(self, circleInfo): + img = self.colorMap + imgY = img.shape[0] + imgX = img.shape[1] + x = circleInfo[0] + y = circleInfo[1] + radius = circleInfo[2] + reds, greens, blues = [], [], [] + + points = self.getPointsInCircle(radius, x, y) + coordinates = list(points) + + for xCoord, yCoord in coordinates: + if (xCoord >= imgX) or (yCoord >= imgY): + pass + else: + bgrValue = img[yCoord, xCoord] + reds.append(bgrValue[2]) + greens.append(bgrValue[1]) + blues.append(bgrValue[0]) + + average = (round(np.mean(reds), 2), round(np.mean(greens), 2), round(np.mean(blues), 2)) + isWater = self.isWater(average) + type = 'waterBead' if isWater else 'bead' + return [[average[0],average[1],average[2]], type, [circleInfo[0],circleInfo[1],circleInfo[2]]] #[[R,G,B], isWater, [x,y,radius]] + + def getMiddleColor(self, circleInfo): + img = self.colorMap + imgY = img.shape[0] + imgX = img.shape[1] + x = circleInfo[0] + y = circleInfo[1] + radius = circleInfo[2] + reds, greens, blues = [], [], [] + + points = self.getPointsInCircle(radius/4, x, y) + coordinates = list(points) + + for xCoord, yCoord in coordinates: + if (xCoord >= imgX) or (yCoord >= imgY): + pass + else: + bgrValue = img[yCoord, xCoord] + reds.append(bgrValue[2]) + greens.append(bgrValue[1]) + blues.append(bgrValue[0]) + + average = (round(np.mean(reds), 2), round(np.mean(greens), 2), round(np.mean(blues), 2)) + isWater = self.isWater(average) + type = 'waterBead' if isWater else 'bead' + return [[average[0],average[1],average[2]], type, [circleInfo[0],circleInfo[1],circleInfo[2]]] #[[R,G,B], isWater, [x,y,radius]] + + + def getRadiusAverageColor(self, circleInfo): + img = self.colorMap + imgY = img.shape[0] + imgX = img.shape[1] + x = circleInfo[0] + y = circleInfo[1] + radius = circleInfo[2] + reds, greens, blues = [], [], [] + + for xCoord in range(x - radius + 5, x + radius - 5): + if(xCoord >= imgX): + pass + else: + bgrValue = img[y, xCoord] + reds.append(bgrValue[2]) + greens.append(bgrValue[1]) + blues.append(bgrValue[0]) + + average = (round(np.mean(reds), 2), round(np.mean(greens), 2), round(np.mean(blues), 2)) + isWater = self.isWater(average) + type = 'waterBead' if isWater else 'bead' + return [[average[0],average[1],average[2]], type, [circleInfo[0],circleInfo[1],circleInfo[2]]] #[[R,G,B], isWater, [x,y,radius]] + + + def getFourQuadrantColor(self, circleInfo): + img = self.colorMap + imgY = img.shape[0] + imgX = img.shape[1] + x = circleInfo[0] + y = circleInfo[1] + radius = circleInfo[2] + reds, greens, blues = [], [], [] + + for i in range(0, 4): + currentPoints = self.getPointsInCircle(radius/10, x + math.ceil((radius/2) * math.cos(math.radians(90*i + 45))), y + math.ceil((radius/2) * math.sin(math.radians(90*i + 45)))) + currentCoordinates = list(currentPoints) + + for xCoord, yCoord in currentCoordinates: + if (xCoord >= imgX) or (yCoord >= imgY): + pass + else: + bgrValue = img[yCoord, xCoord] + reds.append(bgrValue[2]) + greens.append(bgrValue[1]) + blues.append(bgrValue[0]) + + average = (round(np.mean(reds), 2), round(np.mean(greens), 2), round(np.mean(blues), 2)) + isWater = self.isWater(average) + type = 'waterBead' if isWater else 'bead' + return [[average[0],average[1],average[2]], type, [circleInfo[0],circleInfo[1],circleInfo[2]]] #[[R,G,B], isWater, [x,y,radius]] + + + + + + + + """ Description: a function that takes a bead's radius and x and y coordinates of the center and returns coordinates of every point in the bead @@ -229,4 +351,4 @@ def makeBeadsCSV(self, colorFormat): newPath = newPath.replace("maps", "results") newPath = newPath + "/beads.csv" - util.makeBeadsCSV(newPath, 'grayscale', self.colorBeads) # TODO: pass colorFormat into this method \ No newline at end of file + util.makeBeadsCSV(newPath, colorFormat, self.colorBeads) \ No newline at end of file diff --git a/lib/file_util.py b/lib/file_util.py new file mode 100644 index 0000000..ff761c9 --- /dev/null +++ b/lib/file_util.py @@ -0,0 +1,18 @@ +import cv2 +import imutils +from imutils import paths + +# method takes a relative (or an absolute? idk i havent tested it), recursively finds all images within it, and +# converts them to cv2 images and combines them into an array. +# returns an array of images found in the specified directory +def readImagesFromDirectory(path): + imagePaths = sorted(list(imutils.paths.list_images(path))) + images = [] + for imagePath in imagePaths: + image = cv2.imread(imagePath) + images.append(image) + return images + +# method takes a path and image and writes the image to that path +def writeImage(path, image): + cv2.imwrite(path, image) \ No newline at end of file diff --git a/lib/stitching.py b/lib/stitching.py index 7fd49fe..e8dd15a 100644 --- a/lib/stitching.py +++ b/lib/stitching.py @@ -33,6 +33,9 @@ import shutil import multiprocessing as mp from collections import OrderedDict +import imutils +from imutils import paths +from . import file_util """ Description: a class to deal with stitching images together and handling overlap of the images. @@ -70,6 +73,13 @@ def stitchOrderedImages(self): print('Error during stiching') return False + # method takes an array of cv2 images and stitches the ones that match, ignoring those that do not. + # returns a status and a stitched image + def stitchImagesStitcher(self, images): + stitcher = cv2.createStitcherScans(try_use_gpu=False) + (status, stitched) = stitcher.stitch(images) + return status, stitched + """ Description: a function for creating a stitched image from unordered images. @return A stitched image. @@ -222,17 +232,19 @@ def setDirectory(self, path): # Get directory of test images self.sourceDirectory = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", path)) + self.images = file_util.readImagesFromDirectory(self.sourceDirectory) + # Read images and append to image array - current_images = {} - for file in os.listdir(self.sourceDirectory): - if(file.find('jpg') != -1 or file.find('JPG') != -1): - path = os.path.join(self.sourceDirectory, file) - img = Image.open(path) - exif = { ExifTags.TAGS[k]: v for k, v in img._getexif().items() if k in ExifTags.TAGS } - current_images[path] = exif['DateTimeOriginal'] - sorted_by_value = sorted(current_images.items(), key=lambda kv: kv[1]) - for key in sorted_by_value: - self.images.append(cv2.imread(key[0], cv2.IMREAD_COLOR)) + # current_images = {} + # for file in os.listdir(self.sourceDirectory): + # if(file.find('jpg') != -1 or file.find('JPG') != -1): + # path = os.path.join(self.sourceDirectory, file) + # img = Image.open(path) + # exif = { ExifTags.TAGS[k]: v for k, v in img._getexif().items() if k in ExifTags.TAGS } + # current_images[path] = exif['DateTimeOriginal'] + # sorted_by_value = sorted(current_images.items(), key=lambda kv: kv[1]) + # for key in sorted_by_value: + # self.images.append(cv2.imread(key[0], cv2.IMREAD_COLOR)) From 6f52f04762d6a7f0bc3da4fc171e277cb0617a2d Mon Sep 17 00:00:00 2001 From: gattlin1 Date: Mon, 7 Oct 2019 19:45:39 -0500 Subject: [PATCH 3/3] Changed naming convention of color_labeler. --- Server/resources/js/mapInspector.js | 2 +- lib/{colorLabeler.py => color_labeler.py} | 0 lib/counting.py | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename lib/{colorLabeler.py => color_labeler.py} (100%) diff --git a/Server/resources/js/mapInspector.js b/Server/resources/js/mapInspector.js index 14e772a..deeff76 100644 --- a/Server/resources/js/mapInspector.js +++ b/Server/resources/js/mapInspector.js @@ -88,7 +88,7 @@ var imageObj = undefined; //the stitched map image if(toolTipBead){ const canvas = $('#mapCanvas'); const location = `(${Math.round(toolTipBead[2][0])}, ${Math.round(toolTipBead[2][1])})`; - const radius = Math.round(toolTipBead[2][2]); + let radius = Math.round(toolTipBead[2][2]); let type = ''; let rgb = `(${Math.round(toolTipBead[0][0])}, ${Math.round(toolTipBead[0][1])}, ${Math.round(toolTipBead[0][2])})`; var rectWidth = 325, diff --git a/lib/colorLabeler.py b/lib/color_labeler.py similarity index 100% rename from lib/colorLabeler.py rename to lib/color_labeler.py diff --git a/lib/counting.py b/lib/counting.py index 00d1f97..b6fe07a 100644 --- a/lib/counting.py +++ b/lib/counting.py @@ -36,7 +36,7 @@ from enum import Enum from os import listdir, path from . import util -from . import colorLabeler +from . import color_labeler """ Description: an enum class to handle the HoughCircle configuration values that are used in cv2.HoughCircles(). @@ -148,7 +148,7 @@ def getCrushedBeads(self, image, circles): thresh = cv2.threshold(blur, 225, 255, cv2.THRESH_BINARY_INV)[1] img_output, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - cl = colorLabeler.ColorLabeler() + cl = color_labeler.ColorLabeler() for c in contours: color = cl.label(lab, c)