Compare commits

..

8 Commits

Author SHA1 Message Date
1de21e93e1 basic firebrand recognition
Co-authored-by: Alex <AlexanderMcDowell@users.noreply.github.com>
2022-11-14 22:47:43 -08:00
59e0f2d861 firebrand edge detection demo
Co-authored-by: Alex <AlexanderMcDowell@users.noreply.github.com>
2022-11-13 17:57:41 -08:00
cc2820da90 start firebrand detection 2022-11-11 19:39:19 -08:00
f8d4f85858 fully working csv download 2022-11-04 18:58:35 -07:00
76e178176d save csv + paper dims
Co-authored-by: Alex <AlexanderMcDowell@users.noreply.github.com>
2022-11-04 18:42:22 -07:00
fa1e988344 use static js 2022-11-04 17:55:25 -07:00
dcf78bb88d fix mm^2 issue 2022-11-03 17:36:22 -07:00
481de4dfb1 update content 2022-10-31 21:01:37 -07:00
21 changed files with 396 additions and 85 deletions

BIN
examples/pyrometry/asdf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -2,26 +2,77 @@
import cv2 as cv import cv2 as cv
import numpy as np import numpy as np
from skimage import measure, morphology, color, segmentation
import matplotlib.pyplot as plt
# edge-detection kernel amplification file = 'streaktest2.png'
AMPLIFIER=9
MIN_INTENSITY=100
# file = '01-0001-cropped.png'
file = 'streaktest.png'
file_name = file.split(".")[0]
file_ext = file.split(".")[1]
img = cv.imread(file) img = cv.imread(file)
img = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # blurred = cv.GaussianBlur(img, (8, 8), 0)
kernel = np.array([ retval, thresh_gray = cv.threshold(img, 120, 255, cv.THRESH_BINARY)
[-1, -1, -1],
[-1, AMPLIFIER, -1],
[-1, -1, -1],
])
img = cv.filter2D(src=img, ddepth=-1, kernel=kernel)
cv.imwrite(f'{file_name}-edge-detection.{file_ext}', img) kernel = np.ones((7, 7), np.uint8)
image = cv.morphologyEx(thresh_gray, cv.MORPH_CLOSE, kernel, iterations=1)
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
retval, gray = cv.threshold(gray, 0, 255, cv.THRESH_BINARY)
gray = cv.copyMakeBorder(
gray,
20,
20,
20,
20,
cv.BORDER_CONSTANT,
value=0
)
# cv.imshow('gray', gray)
# cv.waitKey(0)
# contours = measure.find_contours(array=gray, level=100)
_img, contours = cv.findContours(gray, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)[0]
fig, ax = plt.subplots()
ax.imshow(gray, cmap=plt.cm.gray, alpha=1)
def calculate_area(countour):
c = np.expand_dims(countour.astype(np.float32), 1)
c = cv.UMat(c)
return cv.contourArea(c)
def center_of_mass(X):
x = X[:,0]
y = X[:,1]
g = (x[:-1]*y[1:] - x[1:]*y[:-1])
A = 0.5*g.sum()
cx = ((x[:-1] + x[1:])*g).sum()
cy = ((y[:-1] + y[1:])*g).sum()
return 1./(6*A)*np.array([cx,cy])
img_new = cv.cvtColor(gray, cv.COLOR_GRAY2BGR)
for contour in contours:
area = calculate_area(contour)
# if area > 250:
# cnt = np.array(contour).reshape((-1, 1, 2)).astype(np.int32)
# cv.drawContours(img_new, [cnt], -1, (0, 200, 255), thickness=10)
cv.drawContours(img_new, [contour], -1, (0, 200, 255), thickness=3)
# ax.plot(contour[:, 1], contour[:, 0], linewidth=0.5, color='orangered')
# cv.imshow('contours', img_new)
# cv.waitKey(0)
cv.imwrite("firebrand_contours_opencv.png", img_new)
ax.axis('image')
ax.set_xticks([])
ax.set_yticks([])
plt.savefig("edge_detection_figure.png", dpi=500)

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

0
firebrand_detection.py Normal file
View File

View File

@@ -1,11 +1,10 @@
from flask import Flask, render_template, request from flask import Flask, render_template, request, send_file
import numpy as np import numpy as np
from plotly_util import generate_plotly_temperature_pdf
from ratio_pyrometry import ratio_pyrometry_pipeline from ratio_pyrometry import ratio_pyrometry_pipeline
from size_projection import get_projected_area from size_projection import get_projected_area
import base64 import base64
import cv2 as cv import cv2 as cv
import plotly.figure_factory as ff
from scipy import stats
app = Flask( app = Flask(
__name__, __name__,
@@ -21,7 +20,7 @@ def index():
def ratio_pyro(): def ratio_pyro():
f = request.files['file'] f = request.files['file']
f_bytes = np.fromstring(f.read(), np.uint8) f_bytes = np.fromstring(f.read(), np.uint8)
img_orig, img_res, key, ptemps = ratio_pyrometry_pipeline( img_orig, img_res, key, ptemps, indiv_firebrands = ratio_pyrometry_pipeline(
f_bytes, f_bytes,
ISO=float(request.form['iso']), ISO=float(request.form['iso']),
I_Darkcurrent=float(request.form['i_darkcurrent']), I_Darkcurrent=float(request.form['i_darkcurrent']),
@@ -31,39 +30,37 @@ def ratio_pyro():
MIN_TEMP=float(request.form['min_temp']), MIN_TEMP=float(request.form['min_temp']),
smoothing_radius=int(request.form['smoothing_radius']), smoothing_radius=int(request.form['smoothing_radius']),
key_entries=int(request.form['legend_entries']), key_entries=int(request.form['legend_entries']),
eqn_scaling_factor=float(request.form['equation_scaling_factor']) eqn_scaling_factor=float(request.form['equation_scaling_factor']),
firebrand_min_intensity_threshold=float(request.form['intensity_threshold']),
firebrand_min_area=float(request.form['min_area']),
) )
# get base64 encoded images # get base64 encoded images
img_orig_b64 = base64.b64encode(cv.imencode('.png', img_orig)[1]).decode(encoding='utf-8') img_orig_b64 = base64.b64encode(cv.imencode('.png', img_orig)[1]).decode(encoding='utf-8')
img_res_b64 = base64.b64encode(cv.imencode('.png', img_res)[1]).decode(encoding='utf-8') img_res_b64 = base64.b64encode(cv.imencode('.png', img_res)[1]).decode(encoding='utf-8')
# generate prob. distribution histogram & return embed ptemps_list = [ptemps]
fig = ff.create_distplot(
[ptemps], for i in range(len(indiv_firebrands)):
group_labels=[f.filename], # base64 encode image data
show_rug=False, brand_data = indiv_firebrands[i]
show_hist=False, unencoded = brand_data["img_data"]
) brand_data["img_data"] = base64.b64encode(cv.imencode('.png', unencoded)[1]).decode(encoding='utf-8')
fig.update_layout( indiv_firebrands[i] = brand_data
autosize=False,
width=800, # add ptemp data to list
height=600, ptemps_list.append(brand_data["ptemps"])
)
fig.update_xaxes( freq_plot, csvstrs = generate_plotly_temperature_pdf(ptemps_list)
title_text="Temperature (°C)",
)
fig.update_yaxes(
title_text="Probability (1/°C)",
)
freq_plot = fig.to_html()
return render_template( return render_template(
'pyrometry-results.html', 'pyrometry-results.html',
img_orig_b64=img_orig_b64, img_orig_b64=img_orig_b64,
img_res_b64=img_res_b64, img_res_b64=img_res_b64,
legend=key, legend=key,
freq_plot=freq_plot freq_plot=freq_plot,
csv_data=csvstrs[0],
individual_firebrands=indiv_firebrands,
) )
@@ -81,6 +78,8 @@ def projected_area_results():
f_bytes, f_bytes,
int(request.form['area_threshold']), int(request.form['area_threshold']),
int(request.form['min_display_threshold']), int(request.form['min_display_threshold']),
float(request.form['paper_width']),
float(request.form['paper_width'])
) )
return render_template( return render_template(
@@ -88,3 +87,7 @@ def projected_area_results():
img_b64=img, img_b64=img,
dtable=dtable dtable=dtable
) )
# @app.route("/download_pyrometry_temps")
# def download_pyrometry_temps():
# return send_file()

64
plotly_util.py Normal file
View File

@@ -0,0 +1,64 @@
from typing import List
import plotly.figure_factory as ff
import pandas as pd
def generate_plotly_temperature_pdf(ptemps_list: List[list]):
"""
Generate plotly graph HTML & raw CSV data for temperature pdf
ptemps: pixel temperature LIST in order of:
- Ptemps of firebrands "overview" image
- Ptemps list for each individual firebrand
plotname: what to call the plot
Returns result in form (plot_html, csv_data)
"""
# generate prob. distribution histogram & return embed
labels = ["Full Image"]
for i in range(len(ptemps_list[1:])):
labels.append(f"Firebrand {i+1}")
labels.reverse()
fig = ff.create_distplot(
ptemps_list,
group_labels=labels,
show_rug=False,
show_hist=False,
)
fig.update_layout(
autosize=False,
width=800,
height=600,
)
fig.update_xaxes(
title_text="Temperature (°C)",
)
fig.update_yaxes(
title_text="Probability (1/°C)",
)
freq_plot = fig.to_html()
# create csv-formatted stuff
csvstrs = []
plot_data=fig.to_dict()
for i in range(len(plot_data["data"])):
x_data = plot_data["data"][i]["x"]
y_data = plot_data["data"][i]["y"]
tdata = [["Temperature", "Frequency"]]
for i in range(len(x_data)):
r = []
r.append(x_data[i])
r.append(y_data[i])
tdata.append(r)
csvstr = pd.DataFrame(tdata).to_csv(index=False, header=False)
csvstrs.append(csvstr)
return (
freq_plot,
csvstrs
)

View File

@@ -3,6 +3,7 @@ from multiprocessing.sharedctypes import Value
import cv2 as cv import cv2 as cv
import numpy as np import numpy as np
from numba import jit from numba import jit
from skimage import measure
@jit(nopython=True) @jit(nopython=True)
def rg_ratio_normalize( def rg_ratio_normalize(
@@ -85,11 +86,104 @@ def ratio_pyrometry_pipeline(
smoothing_radius: int, smoothing_radius: int,
key_entries: int, key_entries: int,
eqn_scaling_factor: float, eqn_scaling_factor: float,
# firebrand detection
firebrand_min_intensity_threshold: float,
firebrand_min_area: float
): ):
# read image & crop # read image & crop
img_orig = cv.imdecode(file_bytes, cv.IMREAD_UNCHANGED) img_orig = cv.imdecode(file_bytes, cv.IMREAD_UNCHANGED)
# ---------------------------------------------------------
# -- Firebrand detection
# ---------------------------------------------------------
img = cv.copyMakeBorder(
img_orig,
20,
20,
20,
20,
cv.BORDER_CONSTANT,
value=0
)
retval, thresh_gray = cv.threshold(img, firebrand_min_intensity_threshold, 255, cv.THRESH_BINARY)
kernel = np.ones((7, 7), np.uint8)
image = cv.morphologyEx(thresh_gray, cv.MORPH_CLOSE, kernel, iterations=1)
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
retval, gray = cv.threshold(gray, 0, 255, cv.THRESH_BINARY)
contours = measure.find_contours(array=gray, level=100)
def calculate_area(countour):
c = np.expand_dims(countour.astype(np.float32), 1)
c = cv.UMat(c)
return cv.contourArea(c)
individual_firebrands = []
for contour in contours:
if calculate_area(contour) > firebrand_min_area:
mask = np.zeros(img.shape[0:2], dtype='uint8')
cv.fillPoly(mask, pts=np.int32([np.flip(contour, 1)]), color=(255,255,255))
retval, mask = cv.threshold(mask, 0, 255, cv.THRESH_BINARY)
#apply the mask to the img
masked = cv.bitwise_and(img, img, mask=mask)
masked_ratio_rg, ptemps_indiv = rg_ratio_normalize(
masked,
I_Darkcurrent,
f_stop,
exposure_time,
ISO,
MIN_TEMP,
MAX_TEMP,
eqn_scaling_factor,
)
# build & apply smoothing conv kernel
k = []
for i in range(smoothing_radius):
k.append([1/(smoothing_radius**2) for i in range(smoothing_radius)])
kernel = np.array(k)
masked_ratio_rg = cv.filter2D(src=masked_ratio_rg, ddepth=-1, kernel=kernel)
# write colormapped image
masked_ratio_rg_jet = cv.applyColorMap(masked_ratio_rg, cv.COLORMAP_JET)
# Generate key
step = (MAX_TEMP - MIN_TEMP) / (key_entries-1)
temps = []
key_img_arr = [[]]
for i in range(key_entries):
res_temp = MIN_TEMP + (i * step)
res_color = scale_temp(res_temp, MIN_TEMP, MAX_TEMP)
temps.append(math.floor(res_temp))
key_img_arr[0].append([res_color, res_color, res_color])
key_img = np.array(key_img_arr).astype(np.uint8)
key_img_jet = cv.applyColorMap(key_img, cv.COLORMAP_JET)
tempkey = {}
for i in range(len(temps)):
c = key_img_jet[0][i]
tempkey[temps[i]] = f"rgb({c[2]}, {c[1]}, {c[0]})"
individual_firebrands.append({
"img_data": masked_ratio_rg_jet,
"legend": tempkey,
"ptemps": ptemps_indiv
})
img, ptemps = rg_ratio_normalize( img, ptemps = rg_ratio_normalize(
img_orig, img_orig,
I_Darkcurrent, I_Darkcurrent,
@@ -112,7 +206,9 @@ def ratio_pyrometry_pipeline(
# write colormapped image # write colormapped image
img_jet = cv.applyColorMap(img, cv.COLORMAP_JET) img_jet = cv.applyColorMap(img, cv.COLORMAP_JET)
# --- Generate temperature key --- # ---------------------------------------------------------
# -- Generate temperature key
# ---------------------------------------------------------
# Generate key # Generate key
step = (MAX_TEMP - MIN_TEMP) / (key_entries-1) step = (MAX_TEMP - MIN_TEMP) / (key_entries-1)
@@ -133,4 +229,4 @@ def ratio_pyrometry_pipeline(
tempkey[temps[i]] = f"rgb({c[2]}, {c[1]}, {c[0]})" tempkey[temps[i]] = f"rgb({c[2]}, {c[1]}, {c[0]})"
# original, transformed, legend # original, transformed, legend
return img_orig, img_jet, tempkey, ptemps return img_orig, img_jet, tempkey, ptemps, individual_firebrands

View File

@@ -9,9 +9,9 @@ import matplotlib.pyplot as plt
from skimage import measure, morphology, color, segmentation from skimage import measure, morphology, color, segmentation
import io import io
def get_projected_area(image, area_threshold, display_threshold): def get_projected_area(image, area_threshold, display_threshold, paper_width, paper_height):
total_px = image.size total_px = image.size
total_mm = 60322.46 total_mm = paper_width * paper_height * 25.4
output = [] output = []
original = cv.imdecode(image, cv.IMREAD_UNCHANGED) original = cv.imdecode(image, cv.IMREAD_UNCHANGED)

View File

@@ -28,6 +28,11 @@ html {
margin-left: 3rem; margin-left: 3rem;
} }
.content {
display: flex;
margin: 0rem 1rem;
}
.form { .form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -1,11 +0,0 @@
function setOnchange() {
let imgPreview = document.getElementById('img-preview');
let imgUpload = document.getElementById('img-upload');
imgUpload.onchange = event => {
const [file] = imgUpload.files;
if (file) {
console.log(file)
imgPreview.src = URL.createObjectURL(file);
}
};
}

28
static/js/csv_download.js Normal file
View File

@@ -0,0 +1,28 @@
function saveData(csvStr, filename) {
// string rep
// var csvStr = "";
// for (let r = 0; r < csvData.length; r++) {
// let row = csvData[r]
// for (let c = 0; c < row.length; c++) {
// let item = row[c]
// csvStr += item;
// if (c < row.length - 1)
// csvStr += ",";
// }
// if (r < csvStr.length - 1)
// csvStr += "\r\n";
// }
// define data blob
var data = new Blob([csvStr]);
// create & click temp link
// slightly modded https://stackoverflow.com/a/15832662
var link = document.createElement("a");
link.download = filename;
link.href = URL.createObjectURL(data);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
delete link;
}

9
static/js/img_preview.js Normal file
View File

@@ -0,0 +1,9 @@
let imgPreview = document.getElementById('img-preview');
let imgUpload = document.getElementById('img-upload');
imgUpload.onchange = event => {
const [file] = imgUpload.files;
if (file) {
console.log(file)
imgPreview.src = URL.createObjectURL(file);
}
};

View File

@@ -2,6 +2,8 @@
<head> <head>
<title>Pyrometry Application</title> <title>Pyrometry Application</title>
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
{% block head %}
{% endblock %}
</head> </head>
<body> <body>
<div class="navbar"> <div class="navbar">
@@ -12,7 +14,9 @@
</div> </div>
</div> </div>
<br> <br>
<div class="content">
{% block content required %} {% block content required %}
{% endblock %} {% endblock %}
</div>
<br><br><br> <br><br><br>
</body> </body>

View File

@@ -1,4 +1,8 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block head %}
{% endblock %}
{% block content %} {% block content %}
<form action="/ratio_pyro" method="POST" enctype="multipart/form-data"> <form action="/ratio_pyro" method="POST" enctype="multipart/form-data">
<h2>Ratio Pyrometry Interface</h2> <h2>Ratio Pyrometry Interface</h2>
@@ -44,6 +48,16 @@
<input type="number" name="equation_scaling_factor" value="0.55" step="0.001"/> <input type="number" name="equation_scaling_factor" value="0.55" step="0.001"/>
<br> <br>
<h4>Firebrand Detection Settings</h4>
<label for="intensity_threshold">intensity threshold (0-255)</label>
<input type="number" name="intensity_threshold" value="250", step="0.1"/>
<br>
<label for="min_area">min area (removes small particles)</label>
<input type="number" name="min_area" value="115", step="0.1"/>
<br>
<h4>Output Settings</h4> <h4>Output Settings</h4>
<label for="smoothing_radius">Smoothing Radius (px)</label> <label for="smoothing_radius">Smoothing Radius (px)</label>
@@ -58,17 +72,7 @@
<input type="submit" value="Generate Heatmap"/> <input type="submit" value="Generate Heatmap"/>
</form> </form>
<!-- <script src="/img_preview.js" onload="setOnChange()"></script> --> <!-- Image Preview -->
<script> <script src="/s/js/img_preview.js"></script>
let imgPreview = document.getElementById('img-preview');
let imgUpload = document.getElementById('img-upload');
imgUpload.onchange = event => {
const [file] = imgUpload.files;
if (file) {
console.log(file)
imgPreview.src = URL.createObjectURL(file);
}
};
</script>
{% endblock %} {% endblock %}

View File

@@ -11,13 +11,13 @@
<table style="background: #f1f1f1"> <table style="background: #f1f1f1">
<tr> <tr>
<th class="legend-heading">Index</th> <th class="legend-heading">Index</th>
<th class="legend-heading">Area</th> <th class="legend-heading">Area (mm²)</th>
<th class="legend-heading">Copy</th> <th class="legend-heading">Copy</th>
</tr> </tr>
{% for item in dtable %} {% for item in dtable %}
<tr> <tr>
<td class="legend-cell">{{ item.0 }}</td> <td class="legend-cell">{{ item.0 }}</td>
<td class="legend-cell">{{ item.1 }} mm²</td> <td class="legend-cell">{{ item.1 }}</td>
<td> <td>
<button onclick="() => navigator.clipboard.writeText('{{ item.1 }}')">Copy</button> <button onclick="() => navigator.clipboard.writeText('{{ item.1 }}')">Copy</button>
</td> </td>

View File

@@ -14,26 +14,25 @@
<label for="area_threshold">Area threshold (to remove dust particles) in px</label> <label for="area_threshold">Area threshold (to remove dust particles) in px</label>
<input type="number" name="area_threshold" value="250"/> <input type="number" name="area_threshold" value="250"/>
<br> <br>
<br>
<label for="min_display_threshold">Minimum display threshold (in px)</label> <label for="min_display_threshold">Minimum display threshold (in px)</label>
<input type="number" name="min_display_threshold" value="300"/> <input type="number" name="min_display_threshold" value="300"/>
<br> <br>
<br>
<label for="min_display_threshold">Page size</label>
<input type="number" name="paper_width" value="8.5"/>
X
<input type="number" name="paper_height" value="11"/> inches
<br>
<br>
<br> <br>
<input type="submit" value="Generate Projected Sizes"/> <input type="submit" value="Generate Projected Sizes"/>
</form> </form>
<!-- <script src="/img_preview.js" onload="setOnChange()"></script> --> <!-- Image Preview -->
<script> <script src="/s/js/img_preview.js"></script>
let imgPreview = document.getElementById('img-preview');
let imgUpload = document.getElementById('img-upload');
imgUpload.onchange = event => {
const [file] = imgUpload.files;
if (file) {
console.log(file)
imgPreview.src = URL.createObjectURL(file);
}
};
</script>
{% endblock %} {% endblock %}

View File

@@ -1,6 +1,15 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block head %}
<script src="/s/js/csv_download.js"></script>
{% endblock %}
{% block content %} {% block content %}
<div style="display:flex; flex-direction: column;">
<h2>General Results</h2>
<table class="img-table"> <table class="img-table">
<tr> <tr>
<th class="img-table-heading">Input Image</th> <th class="img-table-heading">Input Image</th>
@@ -33,8 +42,58 @@
</td> </td>
</tr> </tr>
</table> </table>
<h2>Individual Firebrands</h2>
<table>
<tr>
<th>Output Heatmap</th>
<th>Legend</th>
</tr>
{% for item in individual_firebrands %}
<tr>
{# output heatmap #}
<td>
<img class="img-out" src="data:image/png;base64,{{ item['img_data'] }}" alt="result image">
</td>
</td>
{# legend #}
<td>
<h3>Firebrand {{ loop.index }}</h3>
<table class="legend" id="legend">
<tr>
<th class="legend-heading">Color</th>
<th class="legend-heading">Temperature</th>
</tr>
{% for temp, color in item["legend"].items() %}
<tr>
<td class="legend-cell"><div style="width:30px;height:20px;background-color:{{ color }};"></div></td>
<td class="legend-cell">{{ temp }}°C</td>
</tr>
{% endfor %}
</table>
</td>
</tr>
{% endfor %}
</table>
<br>
{# Temperature Frequency Plot #} {# Temperature Frequency Plot #}
<div style="display: flex; flex-direction: row; align-items: center;">
<strong>Temperature Distribution</strong> <strong>Temperature Distribution</strong>
<button
style="width: 10rem; height: 2rem; margin-left: 1rem;"
onclick="saveData(`{{csv_data}}`, 'temperature-data.csv')">Download Data as CSV</button>
</div>
{{ freq_plot | safe }} {{ freq_plot | safe }}
<!-- Firebrands: {{ individual_firebrands }} -->
</div>
{% endblock %} {% endblock %}