Spaces:
Runtime error
Runtime error
oskarastrom
commited on
Commit
•
7a4b92f
1
Parent(s):
c37bb1d
First Commit
Browse files- .gitignore +18 -0
- __init__.py +0 -0
- app.py +212 -0
- aris.py +505 -0
- aws_handler.py +27 -0
- dataloader.py +367 -0
- dump.rdb +0 -0
- file_reader.py +281 -0
- inference.py +167 -0
- main.py +77 -0
- project_path.py +11 -0
- pyDIDSON.py +495 -0
- pyDIDSON_format.py +364 -0
- requirements.txt +79 -0
- state_handler.py +375 -0
- uploader.py +41 -0
- visualizer.py +191 -0
.gitignore
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
venv/
|
2 |
+
.vscode
|
3 |
+
#*.pt
|
4 |
+
#*.jpg
|
5 |
+
static/tmp.jpg
|
6 |
+
redis-stable/*
|
7 |
+
user_data/*
|
8 |
+
*.pyc
|
9 |
+
|
10 |
+
.ipynb_checkpoints
|
11 |
+
.tmp*
|
12 |
+
*.mp4
|
13 |
+
*.jpg
|
14 |
+
*.json
|
15 |
+
*.zip
|
16 |
+
*.aris
|
17 |
+
*.log
|
18 |
+
*.DS_STORE
|
__init__.py
ADDED
File without changes
|
app.py
ADDED
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
from uploader import save_data
|
3 |
+
from main import predict_task
|
4 |
+
from state_handler import load_example_result, reset_state
|
5 |
+
from file_reader import File
|
6 |
+
import numpy as np
|
7 |
+
from aws_handler import upload_file
|
8 |
+
from aris import create_metadata_table
|
9 |
+
|
10 |
+
table_headers = ["TOTAL" , "FRAME_NUM", "DIR", "R", "THETA", "L", "TIME", "DATE", "SPECIES"]
|
11 |
+
info_headers = [
|
12 |
+
"TOTAL_TIME", "DATE", "START", "END",
|
13 |
+
"TOTAL_FISH", "UPSTREAM_FISH", "DOWNSTREAM_FISH", "NONDIRECTIONAL_FISH",
|
14 |
+
"TOTAL_FRAMES", "FRAME_RATE",
|
15 |
+
"UPSTREAM_MOTION", "INTENSITY", "THRESHOLD", "WINDOW_START", "WINDOW_END", "WATER_TEMP"
|
16 |
+
]
|
17 |
+
css = """
|
18 |
+
#result_json {
|
19 |
+
height: 500px;
|
20 |
+
overflow: scroll !important;
|
21 |
+
}
|
22 |
+
#marking_json textarea {
|
23 |
+
height: 100% !important;
|
24 |
+
}
|
25 |
+
#marking_json label {
|
26 |
+
height: calc(100% - 30px) !important;
|
27 |
+
}
|
28 |
+
"""
|
29 |
+
js_update_tabs = """
|
30 |
+
async () => {
|
31 |
+
let el_list = document.getElementById("result_handler").getElementsByClassName("svelte-1kcgrqr")
|
32 |
+
let idx = (el_list[1].value === "LOADING") ? 1 : parseInt(el_list[1].value)
|
33 |
+
console.log(idx)
|
34 |
+
style_sheet = document.getElementById("tab_style")
|
35 |
+
style_sheet.innerHTML = ""
|
36 |
+
for (let i = 1; i <= idx; i++) {
|
37 |
+
style_sheet.innerHTML += "button.svelte-kqij2n:nth-child(" + i + "):before {content: 'Result " + i + "';}"
|
38 |
+
}
|
39 |
+
}
|
40 |
+
"""
|
41 |
+
|
42 |
+
#Initialize State & Result
|
43 |
+
state = {
|
44 |
+
'files': [],
|
45 |
+
'index': 1,
|
46 |
+
'total': 1
|
47 |
+
}
|
48 |
+
result = {}
|
49 |
+
|
50 |
+
# Start function, called on file upload
|
51 |
+
def on_input(file_list):
|
52 |
+
|
53 |
+
# Reset Result
|
54 |
+
reset_state(result, state)
|
55 |
+
state['files'] = file_list
|
56 |
+
state['total'] = len(file_list)
|
57 |
+
|
58 |
+
# Update loading_space to start inference on first file
|
59 |
+
return {
|
60 |
+
inference_handler: gr.update(value = str(np.random.rand()), visible=True)
|
61 |
+
}
|
62 |
+
|
63 |
+
# Iterative function that performs inference on the next file in line
|
64 |
+
def handle_next(_, progress=gr.Progress()):
|
65 |
+
|
66 |
+
if state['index'] >= state['total']:
|
67 |
+
return {
|
68 |
+
result_handler: gr.update(),
|
69 |
+
inference_handler: gr.update()
|
70 |
+
}
|
71 |
+
|
72 |
+
# Correct progress function for batch file input
|
73 |
+
set_progress = lambda pct, msg : progress(pct, desc=msg)
|
74 |
+
if state['total'] > 1:
|
75 |
+
set_progress = lambda pct, msg : progress(pct, desc="File " + str(state['index']+1) + "/" + str(state['total']) + ": " + msg)
|
76 |
+
set_progress(0, "Starting...")
|
77 |
+
|
78 |
+
file_info = state['files'][state['index']]
|
79 |
+
file_name = file_info[0].split("/")[-1]
|
80 |
+
bytes = file_info[1]
|
81 |
+
valid, file_path, dir_name = save_data(bytes, file_name)
|
82 |
+
print(dir_name)
|
83 |
+
print(file_path)
|
84 |
+
|
85 |
+
if not valid:
|
86 |
+
return {
|
87 |
+
result_handler: gr.update(),
|
88 |
+
inference_handler: gr.update()
|
89 |
+
}
|
90 |
+
|
91 |
+
upload_file(file_path, "fishcounting", "webapp_uploads/" + file_name)
|
92 |
+
metadata, json_filepath, zip_filepath, video_filepath, marking_filepath = predict_task(file_path, gradio_progress=set_progress)
|
93 |
+
result["path_video"].append(video_filepath)
|
94 |
+
result["path_zip"].append(zip_filepath)
|
95 |
+
result["path_json"].append(json_filepath)
|
96 |
+
result["path_marking"].append(marking_filepath)
|
97 |
+
fish_table, fish_info = create_metadata_table(metadata, table_headers, info_headers)
|
98 |
+
result["fish_table"].append(fish_table)
|
99 |
+
result["fish_info"].append(fish_info)
|
100 |
+
|
101 |
+
state['index'] += 1
|
102 |
+
|
103 |
+
return {
|
104 |
+
result_handler: gr.update(value = str(state["index"])),
|
105 |
+
inference_handler: gr.update()
|
106 |
+
}
|
107 |
+
|
108 |
+
# Show result UI based on example data
|
109 |
+
def show_example_data():
|
110 |
+
load_example_result(result, table_headers, info_headers)
|
111 |
+
state["index"] = 1
|
112 |
+
return gr.update(value=str(state["index"]))
|
113 |
+
|
114 |
+
def show_data():
|
115 |
+
i = state["index"] - 1
|
116 |
+
|
117 |
+
# Only show result for up to max_tabs files
|
118 |
+
if i >= max_tabs:
|
119 |
+
return {
|
120 |
+
zip_out: gr.update(value=result["path_zip"])
|
121 |
+
}
|
122 |
+
|
123 |
+
not_done = state['index'] < state['total']
|
124 |
+
message = "Result " + str(state['index']) + "/" + str(state['total'])
|
125 |
+
return {
|
126 |
+
zip_out: gr.update(value=result["path_zip"]),
|
127 |
+
tabs[i]['tab']: gr.update(),
|
128 |
+
tabs[i]['video']: gr.update(value=result["path_video"][i], visible=True),
|
129 |
+
tabs[i]['metadata']: gr.update(value=result["fish_info"][i], visible=True),
|
130 |
+
tabs[i]['table']: gr.update(value=result["fish_table"][i], visible=True),
|
131 |
+
tab_parent: gr.update(selected=i),
|
132 |
+
inference_handler: gr.update(value = str(np.random.rand()), visible=not_done)
|
133 |
+
}
|
134 |
+
|
135 |
+
|
136 |
+
|
137 |
+
max_tabs = 10
|
138 |
+
demo = gr.Blocks()
|
139 |
+
with demo:
|
140 |
+
with gr.Blocks(css=css) as inner_body:
|
141 |
+
|
142 |
+
# Title of page
|
143 |
+
gr.HTML(
|
144 |
+
"""
|
145 |
+
<h1 align="center" style="font-size:xxx-large">Caltech Fisheye</h1>
|
146 |
+
<p align="center">Submit an .aris file to analyze result.</p>
|
147 |
+
<style id="tab_style"></style>
|
148 |
+
"""
|
149 |
+
)
|
150 |
+
|
151 |
+
#Input field for aris submission
|
152 |
+
input = File(file_types=[".aris", ".ddf"], type="binary", label="ARIS Input", file_count="multiple")
|
153 |
+
|
154 |
+
# Dummy element to call inference events, this also displays the inference progress
|
155 |
+
inference_handler = gr.Text(value=str(np.random.rand()), visible=False)
|
156 |
+
|
157 |
+
# Dummy element to call UI events
|
158 |
+
result_handler = gr.Text(value="LOADING", visible=False, elem_id="result_handler")
|
159 |
+
|
160 |
+
# List of all UI components that will recieve outputs from the result_handler
|
161 |
+
UI_components = []
|
162 |
+
|
163 |
+
# Zip file output
|
164 |
+
zip_out = gr.File(label="ZIP Output", interactive=False)
|
165 |
+
UI_components.append(zip_out)
|
166 |
+
|
167 |
+
# Create result tabs
|
168 |
+
tabs = []
|
169 |
+
with gr.Tabs() as tab_parent:
|
170 |
+
UI_components.append(tab_parent)
|
171 |
+
for i in range(max_tabs):
|
172 |
+
with gr.Tab(label="", id=i, elem_id="result_tab"+str(i)) as tab:
|
173 |
+
with gr.Row():
|
174 |
+
metadata_out = gr.JSON(label="Info", visible=False, elem_id="marking_json")
|
175 |
+
video_out = gr.Video(label='Annotated Video', interactive=False, visible=False)
|
176 |
+
table_out = gr.Matrix(label='Indentified Fish', headers=table_headers, interactive=False, visible=False)
|
177 |
+
tabs.append({
|
178 |
+
'tab': tab,
|
179 |
+
'metadata': metadata_out,
|
180 |
+
'video': video_out,
|
181 |
+
'table': table_out
|
182 |
+
})
|
183 |
+
UI_components.extend([tab, metadata_out, video_out, table_out])
|
184 |
+
|
185 |
+
# Button to show example result
|
186 |
+
gr.Button(value="Show Example Result").click(show_example_data, None, result_handler)
|
187 |
+
|
188 |
+
# Disclaimer at the bottom of page
|
189 |
+
gr.HTML(
|
190 |
+
"""
|
191 |
+
<p align="center">
|
192 |
+
<b>Note</b>: The software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement.
|
193 |
+
In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software.
|
194 |
+
</p>
|
195 |
+
"""
|
196 |
+
)
|
197 |
+
|
198 |
+
# When a file is uploaded to the input, tell the inference_handler to start inference
|
199 |
+
input.upload(fn=on_input, inputs=input, outputs=[inference_handler])
|
200 |
+
|
201 |
+
# When inference handler updates, tell result_handler to show the new result
|
202 |
+
# Also, add inference_handler as the output in order to have it display the progress
|
203 |
+
inference_handler.change(handle_next, None, [result_handler, inference_handler])
|
204 |
+
|
205 |
+
# Send UI changes based on the new results to the UI_components, and tell the inference_handler to start next inference
|
206 |
+
result_handler.change(show_data, None, UI_components + [inference_handler], _js=js_update_tabs)
|
207 |
+
|
208 |
+
demo.queue().launch()
|
209 |
+
|
210 |
+
show_data()
|
211 |
+
|
212 |
+
|
aris.py
ADDED
@@ -0,0 +1,505 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import project_path
|
2 |
+
|
3 |
+
import numpy as np
|
4 |
+
import cv2
|
5 |
+
import os
|
6 |
+
from collections import namedtuple, defaultdict
|
7 |
+
import struct
|
8 |
+
from PIL import Image
|
9 |
+
from tqdm import tqdm
|
10 |
+
import datetime
|
11 |
+
from decimal import Decimal, ROUND_HALF_UP
|
12 |
+
import json
|
13 |
+
import pytz
|
14 |
+
from copy import deepcopy
|
15 |
+
from multiprocessing import Pool
|
16 |
+
|
17 |
+
import pyARIS
|
18 |
+
from tracker import Tracker
|
19 |
+
|
20 |
+
|
21 |
+
BEAM_WIDTH_DIR = 'lib/fish_eye/beam_widths/'
|
22 |
+
|
23 |
+
ImageData = namedtuple('ImageData', [
|
24 |
+
'pixel_meter_size',
|
25 |
+
'xdim', 'ydim',
|
26 |
+
'x_meter_start', 'y_meter_start', 'x_meter_stop', 'y_meter_stop',
|
27 |
+
'sample_read_rows', 'sample_read_cols', 'image_write_rows', 'image_write_cols'
|
28 |
+
])
|
29 |
+
|
30 |
+
def FastARISRead(ARIS_data, start_frame, end_frame):
|
31 |
+
""" Just read in the ARIS frame, and not the other meta data.
|
32 |
+
"""
|
33 |
+
FrameSize = ARIS_data.SamplesPerChannel*ARIS_data.NumRawBeams
|
34 |
+
frames = np.empty([end_frame-start_frame, ARIS_data.SamplesPerChannel,
|
35 |
+
ARIS_data.NumRawBeams], dtype=np.uint8)
|
36 |
+
with open(ARIS_data.filename, 'rb') as data:
|
37 |
+
for i, j in enumerate(range(start_frame, end_frame)):
|
38 |
+
data.seek(j*(1024+(FrameSize))+2048, 0)
|
39 |
+
raw_data = struct.unpack("%dB" % FrameSize, data.read(FrameSize))
|
40 |
+
frames[i] = np.fliplr(np.reshape(
|
41 |
+
raw_data, [ARIS_data.SamplesPerChannel, ARIS_data.NumRawBeams]))
|
42 |
+
# Close the data file
|
43 |
+
data.close()
|
44 |
+
return frames
|
45 |
+
|
46 |
+
def get_info(aris_fp, beam_width_dir=BEAM_WIDTH_DIR):
|
47 |
+
"""
|
48 |
+
Return:
|
49 |
+
image_meter_width, image_meter_height, fps
|
50 |
+
"""
|
51 |
+
ARISdata, aris_frame = pyARIS.DataImport(aris_fp)
|
52 |
+
beam_width_data = pyARIS.load_beam_width_data(aris_frame, beam_width_dir=beam_width_dir)[0]
|
53 |
+
min_pixel_size = pyARIS.get_minimum_pixel_meter_size(aris_frame, beam_width_data)
|
54 |
+
sample_length = aris_frame.sampleperiod * 0.000001 * aris_frame.soundspeed / 2
|
55 |
+
pixel_meter_size = max(min_pixel_size, sample_length)
|
56 |
+
xdim, ydim, x_meter_start, y_meter_start, x_meter_stop, y_meter_stop = pyARIS.compute_image_bounds(
|
57 |
+
pixel_meter_size, aris_frame, beam_width_data,
|
58 |
+
additional_pixel_padding_x=0,
|
59 |
+
additional_pixel_padding_y=0
|
60 |
+
)
|
61 |
+
return pixel_meter_size * xdim, pixel_meter_size * ydim, aris_frame.framerate
|
62 |
+
|
63 |
+
def write_frames(aris_fp, out_dir, cb=None, max_mb=-1, beam_width_dir=BEAM_WIDTH_DIR, bg_out_dir=None, num_workers=0):
|
64 |
+
"""
|
65 |
+
Write all frames from an ARIS file to disk, using our 3-channel format:
|
66 |
+
(raw img, blurred & mean subtracted img, optical flow approximation)
|
67 |
+
|
68 |
+
Args:
|
69 |
+
aris_fp: path to aris file
|
70 |
+
out_dir: directory for frame extraction. frames will be named 0.jpg, 1.jpg, ... {n}.jpg
|
71 |
+
cb: a callback function for updating progress
|
72 |
+
max_mb: maximum amount of the file to be processed, in megabytes
|
73 |
+
beam_width_dir: location of ARIS camera information
|
74 |
+
bg_out_dir: where to write the background frame; None disables writing
|
75 |
+
|
76 |
+
Return:
|
77 |
+
(float) image_meter_width - the width of each image, in meters
|
78 |
+
(float) image_meter_height
|
79 |
+
(float) fps
|
80 |
+
"""
|
81 |
+
# Load in the ARIS file
|
82 |
+
ARISdata, aris_frame = pyARIS.DataImport(aris_fp)
|
83 |
+
|
84 |
+
if cb:
|
85 |
+
cb(2, msg="Decoding ARIS data...")
|
86 |
+
|
87 |
+
beam_width_data = pyARIS.load_beam_width_data(aris_frame, beam_width_dir=beam_width_dir)[0]
|
88 |
+
# What is the meter resolution of the smallest sample?
|
89 |
+
min_pixel_size = pyARIS.get_minimum_pixel_meter_size(aris_frame, beam_width_data)
|
90 |
+
# What is the meter resolution of the sample length?
|
91 |
+
sample_length = aris_frame.sampleperiod * 0.000001 * aris_frame.soundspeed / 2
|
92 |
+
# Choose the size of a pixel (or hard code it to some specific value)
|
93 |
+
pixel_meter_size = max(min_pixel_size, sample_length)
|
94 |
+
# Determine the image dimensions
|
95 |
+
xdim, ydim, x_meter_start, y_meter_start, x_meter_stop, y_meter_stop = pyARIS.compute_image_bounds(
|
96 |
+
pixel_meter_size, aris_frame, beam_width_data,
|
97 |
+
additional_pixel_padding_x=0,
|
98 |
+
additional_pixel_padding_y=0
|
99 |
+
)
|
100 |
+
|
101 |
+
# Compute the mapping from the samples to the image
|
102 |
+
sample_read_rows, sample_read_cols, image_write_rows, image_write_cols = pyARIS.compute_mapping_from_sample_to_image(
|
103 |
+
pixel_meter_size,
|
104 |
+
xdim, ydim, x_meter_start, y_meter_start,
|
105 |
+
aris_frame, beam_width_data
|
106 |
+
)
|
107 |
+
image_data = ImageData(
|
108 |
+
pixel_meter_size,
|
109 |
+
xdim, ydim, x_meter_start, y_meter_start, x_meter_stop, y_meter_stop,
|
110 |
+
sample_read_rows, sample_read_cols, image_write_rows, image_write_cols
|
111 |
+
)
|
112 |
+
start_frame = 0
|
113 |
+
end_frame = ARISdata.FrameCount
|
114 |
+
|
115 |
+
bytes_per_frame = 1024+ARISdata.SamplesPerChannel*ARISdata.NumRawBeams
|
116 |
+
print("ARIS bytes per frame", bytes_per_frame)
|
117 |
+
|
118 |
+
img_bytes_per_frame = image_data.ydim * image_data.xdim * 4 # for fp32 frames
|
119 |
+
print("Image bytes per frame", img_bytes_per_frame)
|
120 |
+
|
121 |
+
max_bytes = max(bytes_per_frame, img_bytes_per_frame)
|
122 |
+
|
123 |
+
if max_mb > 0:
|
124 |
+
max_frames = int(max_mb*1000000 / (max_bytes))
|
125 |
+
if end_frame > max_frames:
|
126 |
+
end_frame = max_frames
|
127 |
+
|
128 |
+
# use a max of 4gb per batch to avoid memory errors (16gb RAM on a g4dn.xlarge)
|
129 |
+
batch_size = 1000 # int(4000*1000000 / (max_bytes))
|
130 |
+
clips = [[pos, pos+batch_size+1] for pos in range(0, end_frame, batch_size)]
|
131 |
+
clips[-1][1] = ARISdata.FrameCount
|
132 |
+
print("Batch size:", batch_size)
|
133 |
+
|
134 |
+
with tqdm(total=(end_frame-start_frame-1), desc="Extracting frames", ncols=0) as pbar:
|
135 |
+
# compute info for bg subtraction using first batch
|
136 |
+
# TODO: make this a sliding window
|
137 |
+
mean_blurred_frame, mean_normalization_value = write_frame_range(ARISdata, image_data, out_dir, clips[0][0], clips[0][1], None, None, cb, pbar)
|
138 |
+
|
139 |
+
# do rest of batches in parallel
|
140 |
+
if num_workers > 0:
|
141 |
+
args = [ (ARISdata, image_data, out_dir, start, end, mean_blurred_frame, mean_normalization_value, cb) for (start, end) in clips[1:] ] # TODO: can't pass pbar to thread
|
142 |
+
with Pool(num_workers) as pool:
|
143 |
+
results = [ pool.apply_async(write_frame_range, arg) for arg in args ]
|
144 |
+
results = [ r.get() for r in results ] # need this call to block on thread execution
|
145 |
+
pbar.update(sum([ arg[4] - arg[3] for arg in args ]))
|
146 |
+
else:
|
147 |
+
for j, (start, end) in enumerate(clips[1:]):
|
148 |
+
write_frame_range(ARISdata, image_data, out_dir, start, end, mean_blurred_frame, mean_normalization_value, cb, pbar)
|
149 |
+
|
150 |
+
if bg_out_dir is not None:
|
151 |
+
bg_img = (mean_blurred_frame * 255).astype(np.uint8)
|
152 |
+
out_fp = os.path.join(bg_out_dir, 'bg_start.jpg')
|
153 |
+
Image.fromarray(bg_img).save(out_fp, quality=95)
|
154 |
+
|
155 |
+
return pixel_meter_size * xdim, pixel_meter_size * ydim, aris_frame.framerate
|
156 |
+
|
157 |
+
def write_frame_range(ARISdata, image_data, out_dir, start, end, mean_blurred_frame=None, mean_normalization_value=None, cb=None, pbar=None):
|
158 |
+
try:
|
159 |
+
frames = np.zeros([end-start, image_data.ydim, image_data.xdim], dtype=np.uint8)
|
160 |
+
frames[:, image_data.image_write_rows, image_data.image_write_cols] = FastARISRead(ARISdata, start, end)[:, image_data.sample_read_rows, image_data.sample_read_cols]
|
161 |
+
except:
|
162 |
+
print("Error extracting frames from", ARISdata.filename, "during batch", i)
|
163 |
+
return
|
164 |
+
|
165 |
+
blurred_frames = frames.astype(np.float32)
|
166 |
+
for i in range(frames.shape[0]):
|
167 |
+
blurred_frames[i] = cv2.GaussianBlur(
|
168 |
+
blurred_frames[i],
|
169 |
+
(5,5),
|
170 |
+
0
|
171 |
+
)
|
172 |
+
|
173 |
+
if mean_blurred_frame is None:
|
174 |
+
mean_blurred_frame = blurred_frames.mean(axis=0)
|
175 |
+
|
176 |
+
blurred_frames -= mean_blurred_frame
|
177 |
+
|
178 |
+
if mean_normalization_value is None:
|
179 |
+
mean_normalization_value = np.max(np.abs(blurred_frames))
|
180 |
+
|
181 |
+
blurred_frames /= mean_normalization_value
|
182 |
+
blurred_frames += 1
|
183 |
+
blurred_frames /= 2
|
184 |
+
|
185 |
+
# Because of the optical flow computation, we only go to end_frame - 1
|
186 |
+
for i, frame_offset in enumerate(range(start, end - 1)):
|
187 |
+
frame_image = np.dstack([
|
188 |
+
frames[i] / 255,
|
189 |
+
blurred_frames[i],
|
190 |
+
np.abs(blurred_frames[i+1] - blurred_frames[i])
|
191 |
+
]).astype(np.float32)
|
192 |
+
frame_image = (frame_image * 255).astype(np.uint8)
|
193 |
+
out_fp = os.path.join(out_dir, f'{start+i}.jpg') # = frame_offset.jpg?
|
194 |
+
Image.fromarray(frame_image).save(out_fp, quality=95)
|
195 |
+
|
196 |
+
if pbar:
|
197 |
+
pbar.update(1)
|
198 |
+
if cb:
|
199 |
+
pct = 2 + int( (start+i) / (end_frame - start_frame - 1) * 98)
|
200 |
+
cb(pct, msg=pbar.__str__())
|
201 |
+
|
202 |
+
return mean_blurred_frame, mean_normalization_value
|
203 |
+
|
204 |
+
def prep_for_mm(json_data):
|
205 |
+
"""Prepare json results for writing to a manual marking file."""
|
206 |
+
json_data = deepcopy(json_data)
|
207 |
+
|
208 |
+
# map fish id -> [ (bbox, frame_num), (bbox, frame_num), ... ]
|
209 |
+
tracks = defaultdict(list)
|
210 |
+
for frame in json_data['frames']:
|
211 |
+
for bbox in frame['fish']:
|
212 |
+
tracks[bbox['fish_id']].append((bbox['bbox'], frame['frame_num']))
|
213 |
+
|
214 |
+
# find frame number for manual marking
|
215 |
+
# look for first time a track crosses the center
|
216 |
+
# if it never crosses the center, use the closest box to the center
|
217 |
+
mm_frame_nums = {}
|
218 |
+
for f_id, track in tracks.items():
|
219 |
+
# keep track of frame closest to the center
|
220 |
+
closest_frame = 0
|
221 |
+
closest_dist = 1.0
|
222 |
+
for i, (box, frame) in enumerate(track):
|
223 |
+
x = (box[0] + box[2]) / 2.0
|
224 |
+
if i > 0:
|
225 |
+
last_x = (track[i-1][0][0] + track[i-1][0][2]) / 2.0
|
226 |
+
if (x < 0.5 and last_x >= 0.5) or (last_x < 0.5 and x >= 0.5):
|
227 |
+
closest_frame = frame
|
228 |
+
break
|
229 |
+
dist = abs(x - 0.5)
|
230 |
+
if dist < closest_dist:
|
231 |
+
closest_frame = frame
|
232 |
+
closest_dist = dist
|
233 |
+
mm_frame_nums[f_id] = closest_frame
|
234 |
+
|
235 |
+
# sort tracks by their frame numbers and re-key
|
236 |
+
# IDs are 1-indexed
|
237 |
+
id_frame = [ (k, v) for k,v in mm_frame_nums.items() ]
|
238 |
+
id_frame = sorted(id_frame, key=lambda x: x[1])
|
239 |
+
id_map = {}
|
240 |
+
for i, (f_id, frame) in enumerate(id_frame, start=1):
|
241 |
+
id_map[f_id] = i
|
242 |
+
|
243 |
+
# map IDs and keep frame['fish'] sorted by ID
|
244 |
+
for i, frame in enumerate(json_data['frames']):
|
245 |
+
new_frame_entries = []
|
246 |
+
for frame_entry in frame['fish']:
|
247 |
+
frame_entry['fish_id'] = id_map[frame_entry['fish_id']]
|
248 |
+
new_frame_entries.append(frame_entry)
|
249 |
+
frame['fish'] = sorted(new_frame_entries, key=lambda k: k['fish_id'])
|
250 |
+
|
251 |
+
# store manual marking frame and re-map 'fish' field
|
252 |
+
for fish in json_data['fish']:
|
253 |
+
fish['marking_frame'] = mm_frame_nums[fish['id']] # mm_frame_nums refers to old IDs
|
254 |
+
fish['id'] = id_map[fish['id']]
|
255 |
+
json_data['fish'] = sorted(json_data['fish'], key=lambda x: x['id'])
|
256 |
+
|
257 |
+
return json_data
|
258 |
+
|
259 |
+
|
260 |
+
def create_metadata_dictionary(aris_fp, json_fp, beam_width_dir=BEAM_WIDTH_DIR):
|
261 |
+
"""
|
262 |
+
Return:
|
263 |
+
dictionary, for manual marking
|
264 |
+
"""
|
265 |
+
with open(json_fp) as json_file:
|
266 |
+
json_data = json.load(json_file)
|
267 |
+
|
268 |
+
metadata = {}
|
269 |
+
metadata["FILE_NAME"] = aris_fp
|
270 |
+
ARISdata, frame = pyARIS.DataImport(aris_fp)
|
271 |
+
metadata["FRAME_RATE"] = frame.framerate
|
272 |
+
|
273 |
+
# Load in the beam width information
|
274 |
+
beam_width_data, camera_type = pyARIS.load_beam_width_data(frame, beam_width_dir=beam_width_dir)
|
275 |
+
|
276 |
+
# What is the meter resolution of the smallest sample?
|
277 |
+
min_pixel_size = pyARIS.get_minimum_pixel_meter_size(frame, beam_width_data)
|
278 |
+
|
279 |
+
# What is the meter resolution of the sample length?
|
280 |
+
sample_length = frame.sampleperiod * 0.000001 * frame.soundspeed / 2
|
281 |
+
|
282 |
+
# Choose the size of a pixel
|
283 |
+
pixel_meter_size = max(min_pixel_size, sample_length)
|
284 |
+
|
285 |
+
# Determine the image dimensions
|
286 |
+
xdim, ydim, x_meter_start, y_meter_start, x_meter_stop, y_meter_stop = pyARIS.compute_image_bounds(
|
287 |
+
pixel_meter_size, frame, beam_width_data,
|
288 |
+
additional_pixel_padding_x=0,
|
289 |
+
additional_pixel_padding_y=0
|
290 |
+
)
|
291 |
+
|
292 |
+
# Compute the mapping from the samples to the image
|
293 |
+
sample_read_rows, sample_read_cols, image_write_rows, image_write_cols = pyARIS.compute_mapping_from_sample_to_image(
|
294 |
+
pixel_meter_size,
|
295 |
+
xdim, ydim, x_meter_start, y_meter_start,
|
296 |
+
frame, beam_width_data
|
297 |
+
)
|
298 |
+
|
299 |
+
marking_mapping = dict(zip(zip(image_write_rows, image_write_cols),
|
300 |
+
zip(sample_read_rows, sample_read_cols)))
|
301 |
+
|
302 |
+
# Manual marking format rounds 0.5 to 1 instead of 0 in IEEE 754
|
303 |
+
def round(number, ndigits=0):
|
304 |
+
return float(Decimal(number).quantize(ndigits, ROUND_HALF_UP))
|
305 |
+
|
306 |
+
right, left, none = Tracker.count_dirs(json_data)
|
307 |
+
|
308 |
+
metadata["UPSTREAM_FISH"] = left # TODO
|
309 |
+
metadata["DOWNSTREAM_FISH"] = right # TODO
|
310 |
+
metadata["NONDIRECTIONAL_FISH"] = none # TODO
|
311 |
+
metadata["TOTAL_FISH"] = metadata["UPSTREAM_FISH"] + metadata["DOWNSTREAM_FISH"] + metadata["NONDIRECTIONAL_FISH"]
|
312 |
+
|
313 |
+
metadata["TOTAL_FRAMES"] = ARISdata.FrameCount
|
314 |
+
metadata["EXPECTED_FRAMES"] = -1 # What is this?
|
315 |
+
metadata["TOTAL_TIME"] = str(datetime.timedelta(seconds=round(metadata["TOTAL_FRAMES"]/metadata["FRAME_RATE"])))
|
316 |
+
metadata["EXPECTED_TIME"] = str(datetime.timedelta(seconds=round(metadata["EXPECTED_FRAMES"]/metadata["FRAME_RATE"])))
|
317 |
+
|
318 |
+
metadata["UPSTREAM_MOTION"] = 'Right To Left' or 'Left To Right' #TODO
|
319 |
+
|
320 |
+
metadata["COUNT_FILE_NAME"] = 'N/A'
|
321 |
+
metadata["EDITOR_ID"] = 'N/A'
|
322 |
+
metadata["INTENSITY"] = f'{round(frame.threshold, 1):.1f} dB' # Missing
|
323 |
+
metadata["THRESHOLD"] = f'{round(frame.threshold, 1):.1f} dB' # Missing
|
324 |
+
metadata["WINDOW_START"] = round(frame.windowstart, 2)
|
325 |
+
metadata["WINDOW_END"] = round(frame.windowstart + frame.windowlength, 2)
|
326 |
+
metadata["WATER_TEMP"] = f'{int(round(frame.watertemp))} degC'
|
327 |
+
|
328 |
+
s = f''''''
|
329 |
+
|
330 |
+
upstream_motion_map = {}
|
331 |
+
if (metadata["UPSTREAM_MOTION"] == 'Left To Right'):
|
332 |
+
upstream_motion_map = {
|
333 |
+
'right': ' Up',
|
334 |
+
'left': 'Down',
|
335 |
+
'none': ' N/A',
|
336 |
+
}
|
337 |
+
elif (metadata["UPSTREAM_MOTION"] == 'Right To Left'):
|
338 |
+
upstream_motion_map = {
|
339 |
+
'left': ' Up',
|
340 |
+
'right': 'Down',
|
341 |
+
'none': ' N/A',
|
342 |
+
}
|
343 |
+
|
344 |
+
def get_entry(fish):
|
345 |
+
if 'marking_frame' in fish:
|
346 |
+
frame_num = fish['marking_frame']
|
347 |
+
entry = None
|
348 |
+
for json_frame in json_data['frames']:
|
349 |
+
if json_frame['frame_num'] == frame_num:
|
350 |
+
for json_frame_entry in json_frame['fish']:
|
351 |
+
if json_frame_entry['fish_id'] == fish['id']:
|
352 |
+
json_frame_entry = json_frame_entry.copy()
|
353 |
+
json_frame_entry['frame_num'] = frame_num
|
354 |
+
return json_frame_entry
|
355 |
+
else:
|
356 |
+
print("Warning: JSON not correctly formatted for manual marking creation. Use aris.prep_for_mm()")
|
357 |
+
entries = []
|
358 |
+
for json_frame in json_data['frames']:
|
359 |
+
for json_frame_entry in json_frame['fish']:
|
360 |
+
if json_frame_entry['fish_id'] == fish['id']:
|
361 |
+
entries.append({'frame_num': json_frame['frame_num'], **json_frame_entry})
|
362 |
+
entry = entries[len(entries)//2]
|
363 |
+
return entry
|
364 |
+
print("Error, could not find entry for", fish)
|
365 |
+
return None # TODO better error handling
|
366 |
+
|
367 |
+
entries = []
|
368 |
+
for fish in json_data['fish']:
|
369 |
+
entry = get_entry(fish)
|
370 |
+
entry['length'] = fish['length']*100
|
371 |
+
entry['direction'] = fish['direction']
|
372 |
+
entries.append(entry)
|
373 |
+
|
374 |
+
metadata["FISH"] = []
|
375 |
+
for entry in sorted(entries, key=lambda x: x['fish_id']):
|
376 |
+
frame_num = entry['frame_num']
|
377 |
+
frame = pyARIS.FrameRead(ARISdata, frame_num)
|
378 |
+
|
379 |
+
y = (entry['bbox'][1]+entry['bbox'][3])/2
|
380 |
+
x = (entry['bbox'][0]+entry['bbox'][2])/2
|
381 |
+
h = np.max(image_write_rows)
|
382 |
+
w = np.max(image_write_cols)
|
383 |
+
# TODO actually fix this
|
384 |
+
try:
|
385 |
+
bin_num, beam_num = marking_mapping[(round(y*h), round(x*w))]
|
386 |
+
except:
|
387 |
+
bin_num = 0
|
388 |
+
beam_num = 0
|
389 |
+
|
390 |
+
fish_entry = {}
|
391 |
+
fish_entry['FILE'] = 1
|
392 |
+
fish_entry['TOTAL'] = entry['fish_id']
|
393 |
+
fish_entry['FRAME_NUM'] = entry['frame_num']
|
394 |
+
fish_entry['DIR'] = upstream_motion_map[entry['direction']]
|
395 |
+
fish_entry['R'] = bin_num * pixel_meter_size + frame.windowstart
|
396 |
+
fish_entry['THETA'] = beam_width_data['beam_center'][beam_num]
|
397 |
+
fish_entry['L'] = entry['length']
|
398 |
+
fish_entry['DR'] = -1.0 # What is this?
|
399 |
+
fish_entry['LDR'] = -1.0 # What is this?
|
400 |
+
fish_entry['ASPECT'] = -1.0 # What is this?
|
401 |
+
TIME, DATE = datetime.datetime.fromtimestamp(frame.sonartimestamp/1000000, pytz.timezone('UTC')).strftime('%H:%M:%S %Y-%m-%d').split()
|
402 |
+
fish_entry['TIME'] = TIME
|
403 |
+
fish_entry['DATE'] = DATE
|
404 |
+
fish_entry['LATITUDE'] = frame.latitude or 'N 00 d 0.00000 m'
|
405 |
+
fish_entry['LONGITUDE'] = frame.longitude or 'E 000 d 0.00000 m'
|
406 |
+
fish_entry['PAN'] = frame.sonarpan
|
407 |
+
fish_entry['TILT'] = frame.sonartilt
|
408 |
+
fish_entry['ROLL'] = frame.roll # May be wrong number but sonarroll was NaN
|
409 |
+
fish_entry['SPECIES'] = 'Unknown'
|
410 |
+
fish_entry['MOTION'] = 'Running <-->'
|
411 |
+
fish_entry['Q'] = -1 #5 # I don't know what this is or where it comes from
|
412 |
+
fish_entry['N'] = -1 #1 # I don't know what this is or where it comes from
|
413 |
+
fish_entry['COMMENT'] = ''
|
414 |
+
|
415 |
+
metadata["FISH"].append(fish_entry)
|
416 |
+
|
417 |
+
# What are these?
|
418 |
+
# Maybe the date and time range for the recording?
|
419 |
+
first_frame = pyARIS.FrameRead(ARISdata, 0)
|
420 |
+
last_frame = pyARIS.FrameRead(ARISdata, metadata["TOTAL_FRAMES"]-1)
|
421 |
+
start_time, start_date = datetime.datetime.fromtimestamp(first_frame.sonartimestamp/1000000, pytz.timezone('UTC')).strftime('%H:%M:%S %Y-%m-%d').split()
|
422 |
+
end_time, end_date = datetime.datetime.fromtimestamp(last_frame.sonartimestamp/1000000, pytz.timezone('UTC')).strftime('%H:%M:%S %Y-%m-%d').split()
|
423 |
+
metadata["DATE"] = start_date
|
424 |
+
metadata["START"] = start_time
|
425 |
+
metadata["END"] = end_time
|
426 |
+
|
427 |
+
return metadata
|
428 |
+
|
429 |
+
def create_metadata_table(metadata, table_headers, info_headers):
|
430 |
+
table = []
|
431 |
+
for fish in metadata["FISH"]:
|
432 |
+
row = []
|
433 |
+
for header in table_headers:
|
434 |
+
row.append(fish[header])
|
435 |
+
table.append(row)
|
436 |
+
|
437 |
+
if len(metadata["FISH"]) == 0:
|
438 |
+
row = []
|
439 |
+
for header in table_headers:
|
440 |
+
row.append("-")
|
441 |
+
table.append(row)
|
442 |
+
|
443 |
+
info = {}
|
444 |
+
for header in info_headers:
|
445 |
+
info[header] = metadata[header]
|
446 |
+
|
447 |
+
return table, info
|
448 |
+
|
449 |
+
def create_manual_marking(metadata, out_path=None):
|
450 |
+
"""
|
451 |
+
Return:
|
452 |
+
string, full contents of manual marking
|
453 |
+
"""
|
454 |
+
|
455 |
+
s = f'''
|
456 |
+
Total Fish = {metadata["TOTAL_FISH"]}
|
457 |
+
Upstream = {metadata["UPSTREAM_FISH"]}
|
458 |
+
Downstream = {metadata["DOWNSTREAM_FISH"]}
|
459 |
+
?? = {metadata["NONDIRECTIONAL_FISH"]}
|
460 |
+
|
461 |
+
Total Frames = {metadata["TOTAL_FRAMES"]}
|
462 |
+
Expected Frames = {metadata["EXPECTED_FRAMES"]}
|
463 |
+
Total Time = {metadata["TOTAL_TIME"]}
|
464 |
+
Expected Time = {metadata["EXPECTED_TIME"]}
|
465 |
+
|
466 |
+
Upstream Motion = {metadata["UPSTREAM_MOTION"]}
|
467 |
+
|
468 |
+
Count File Name: {metadata["COUNT_FILE_NAME"]}
|
469 |
+
Editor ID = {metadata["EDITOR_ID"]}
|
470 |
+
Intensity = {metadata["INTENSITY"]}
|
471 |
+
Threshold = {metadata["THRESHOLD"]}
|
472 |
+
Window Start = {metadata["WINDOW_START"]:.2f}
|
473 |
+
Window End = {metadata["WINDOW_END"]:.2f}
|
474 |
+
Water Temperature = {metadata["WATER_TEMP"]}
|
475 |
+
|
476 |
+
|
477 |
+
*** Manual Marking (Manual Sizing: Q = Quality, N = Repeat Count) ***
|
478 |
+
|
479 |
+
File Total Frame# Dir R (m) Theta L(cm) dR(cm) L/dR Aspect Time Date Latitude Longitude Pan Tilt Roll Species Motion Q N Comment
|
480 |
+
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
481 |
+
'''
|
482 |
+
|
483 |
+
for entry in metadata["FISH"]:
|
484 |
+
s += f'{entry["FILE"]:>4} {entry["TOTAL"]:>5} {entry["FRAME_NUM"]:>6} {entry["DIR"]:>3} {entry["R"]:>6.2f} {entry["THETA"]:>6.1f} {entry["L"]:>6.1f} {entry["DR"]:>6.1f} {entry["LDR"]:>6.2f} {entry["ASPECT"]:>6.1f} {entry["TIME"]:>8} {entry["DATE"]:>10} {entry["LATITUDE"]:>17} {entry["LONGITUDE"]:>18} {entry["PAN"]:>7.2f} {entry["TILT"]:>7.2f} {entry["ROLL"]:>7.2f} {entry["SPECIES"]:>8} {entry["MOTION"]:>37} {entry["Q"]:>5} {entry["N"]:>2} {entry["COMMENT"]}\n'
|
485 |
+
|
486 |
+
s += f'''
|
487 |
+
*** Source File Key ***
|
488 |
+
|
489 |
+
1. Source File Name: {metadata["FILE_NAME"]}
|
490 |
+
Source File Date: {metadata["DATE"]}
|
491 |
+
Source File Start: {metadata["START"]}
|
492 |
+
Source File End: {metadata["END"]}
|
493 |
+
|
494 |
+
Settings
|
495 |
+
Upstream: {metadata["UPSTREAM_MOTION"]}
|
496 |
+
Default Mark Direction: Upstream
|
497 |
+
Editor ID: {metadata["EDITOR_ID"]}
|
498 |
+
Show Marks: ??
|
499 |
+
Show marks for ?? seconds
|
500 |
+
Loop for ?? seconds
|
501 |
+
'''
|
502 |
+
if out_path:
|
503 |
+
with open(out_path, 'w') as f:
|
504 |
+
f.write(s)
|
505 |
+
return s
|
aws_handler.py
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import boto3
|
3 |
+
from botocore.exceptions import ClientError
|
4 |
+
import os
|
5 |
+
|
6 |
+
|
7 |
+
def upload_file(file_name, bucket, object_name=None):
|
8 |
+
"""Upload a file to an S3 bucket
|
9 |
+
|
10 |
+
:param file_name: File to upload
|
11 |
+
:param bucket: Bucket to upload to
|
12 |
+
:param object_name: S3 object name. If not specified then file_name is used
|
13 |
+
:return: True if file was uploaded, else False
|
14 |
+
"""
|
15 |
+
|
16 |
+
# If S3 object_name was not specified, use file_name
|
17 |
+
if object_name is None:
|
18 |
+
object_name = os.path.basename(file_name)
|
19 |
+
|
20 |
+
# Upload the file
|
21 |
+
s3_client = boto3.client('s3')
|
22 |
+
try:
|
23 |
+
response = s3_client.upload_file(file_name, bucket, object_name)
|
24 |
+
except ClientError as e:
|
25 |
+
logging.error(e)
|
26 |
+
return False
|
27 |
+
return True
|
dataloader.py
ADDED
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import project_path
|
2 |
+
|
3 |
+
import os
|
4 |
+
import cv2
|
5 |
+
import numpy as np
|
6 |
+
import json
|
7 |
+
from threading import Lock
|
8 |
+
import struct
|
9 |
+
from contextlib import contextmanager
|
10 |
+
import torch
|
11 |
+
from torch.utils.data import Dataset
|
12 |
+
|
13 |
+
# assumes yolov5 on sys.path
|
14 |
+
from lib.yolov5.utils.general import xyxy2xywh
|
15 |
+
from lib.yolov5.utils.augmentations import letterbox
|
16 |
+
from lib.yolov5.utils.dataloaders import create_dataloader as create_yolo_dataloader
|
17 |
+
|
18 |
+
from pyDIDSON import pyDIDSON
|
19 |
+
from aris import ImageData
|
20 |
+
|
21 |
+
# use this flag to test the difference between direct ARIS dataloading and
|
22 |
+
# using the jpeg compressed version. very slow. not much difference observed.
|
23 |
+
TEST_JPG_COMPRESSION = False
|
24 |
+
|
25 |
+
|
26 |
+
# # # # # #
|
27 |
+
# Factory(ish) methods for DataLoader creation. Easy entry points to this module.
|
28 |
+
# # # # # #
|
29 |
+
|
30 |
+
def create_dataloader_aris(aris_filepath, beam_width_dir, annotations_file, batch_size=32, stride=64, pad=0.5, img_size=896, rank=-1, world_size=1, workers=0,
|
31 |
+
disable_output=False, cache_bg_frames=False):
|
32 |
+
"""
|
33 |
+
Get a PyTorch Dataset and DataLoader for ARIS files with (optional) associated fisheye-formatted labels.
|
34 |
+
"""
|
35 |
+
# Make sure only the first process in DDP process the dataset first, and the following others can use the cache
|
36 |
+
# this is a no-op for a single-gpu machine
|
37 |
+
with torch_distributed_zero_first(rank):
|
38 |
+
dataset = YOLOARISBatchedDataset(aris_filepath, beam_width_dir, annotations_file, stride, pad, img_size,
|
39 |
+
disable_output=disable_output, cache_bg_frames=cache_bg_frames)
|
40 |
+
|
41 |
+
|
42 |
+
batch_size = min(batch_size, len(dataset))
|
43 |
+
nw = min([os.cpu_count() // world_size, batch_size if batch_size > 1 else 0, workers]) # number of workers
|
44 |
+
|
45 |
+
if not disable_output:
|
46 |
+
print("dataset size", len(dataset))
|
47 |
+
print("dataset shape", dataset.shape)
|
48 |
+
print("Num workers", nw)
|
49 |
+
# sampler = torch.utils.data.distributed.DistributedSampler(dataset) if rank != -1 else None # if extending to multi-GPU inference, will need this
|
50 |
+
dataloader = torch.utils.data.dataloader.DataLoader(dataset,
|
51 |
+
batch_size=None,
|
52 |
+
sampler=OnePerBatchSampler(data_source=dataset, batch_size=batch_size),
|
53 |
+
num_workers=nw,
|
54 |
+
pin_memory=True,
|
55 |
+
collate_fn=collate_fn)
|
56 |
+
return dataloader, dataset
|
57 |
+
|
58 |
+
def create_dataloader_frames(frames_path, batch_size=32, model_stride_max=32,
|
59 |
+
pad=0.5, img_size=896, rank=-1, world_size=1, workers=0, disable_output=False):
|
60 |
+
"""
|
61 |
+
Create a DataLoader for a directory of frames without labels.
|
62 |
+
|
63 |
+
Args:
|
64 |
+
model_stride_max: use model.stride.max()
|
65 |
+
"""
|
66 |
+
|
67 |
+
gs = max(int(model_stride_max), 32) # grid size (max stride)
|
68 |
+
return create_yolo_dataloader(frames_path, img_size, batch_size, gs, single_cls=False, augment=False,
|
69 |
+
hyp=None, cache=None, rect=True, rank=rank,
|
70 |
+
workers=workers, pad=pad)[0]
|
71 |
+
|
72 |
+
# # # # # #
|
73 |
+
# End factory(ish) methods
|
74 |
+
# # # # # #
|
75 |
+
|
76 |
+
|
77 |
+
class ARISBatchedDataset(Dataset):
|
78 |
+
def __init__(self, aris_filepath, beam_width_dir, annotations_file, batch_size, num_frames_bg_subtract=1000, disable_output=False,
|
79 |
+
cache_bg_frames=False):
|
80 |
+
"""
|
81 |
+
A PyTorch Dataset class for loading an ARIS file and (optional) associated fisheye-format labels.
|
82 |
+
This class handles the ARIS frame extraction and 3-channel representation generation.
|
83 |
+
|
84 |
+
It is called a "BatchedDataset" because it loads contiguous frames in self.batch_size chunks.
|
85 |
+
** The PyTorch sampler must be aware of this!! ** Use the OnePerBatchSampler in this module when using this Dataset.
|
86 |
+
|
87 |
+
Args:
|
88 |
+
cache_bg_frames: keep the frames used for bg subtraction stored in memory. careful of memory issues. only recommended
|
89 |
+
for small values of num_frames_bg_subtract
|
90 |
+
"""
|
91 |
+
# open ARIS data stream - TODO: make sure this is one per worker
|
92 |
+
self.data = open(aris_filepath, 'rb')
|
93 |
+
self.data_lock = Lock()
|
94 |
+
self.beam_width_dir = beam_width_dir
|
95 |
+
self.disable_output = disable_output
|
96 |
+
self.aris_filepath = aris_filepath
|
97 |
+
self.cache_bg_frames = cache_bg_frames
|
98 |
+
|
99 |
+
# get header info
|
100 |
+
self.didson = pyDIDSON(self.aris_filepath, beam_width_dir=beam_width_dir)
|
101 |
+
self.xdim = self.didson.info['xdim']
|
102 |
+
self.ydim = self.didson.info['ydim']
|
103 |
+
|
104 |
+
# disable automatic batching - do it ourselves, reading batch_size frames from
|
105 |
+
# the ARIS file at a time
|
106 |
+
self.batch_size = batch_size
|
107 |
+
|
108 |
+
# load fisheye annotations
|
109 |
+
if annotations_file is None:
|
110 |
+
if not self.disable_output:
|
111 |
+
print("Loading file with no labels.")
|
112 |
+
self.start_frame = self.didson.info['startframe']
|
113 |
+
self.end_frame = self.didson.info['endframe'] or self.didson.info['numframes']
|
114 |
+
self.labels = None
|
115 |
+
else:
|
116 |
+
self._load_labels(annotations_file)
|
117 |
+
|
118 |
+
# intiialize the background subtraction
|
119 |
+
self.num_frames_bg_subtract = num_frames_bg_subtract
|
120 |
+
self._init_bg_frame()
|
121 |
+
|
122 |
+
def _init_bg_frame(self):
|
123 |
+
"""
|
124 |
+
Intialize bg frame for bg subtraction.
|
125 |
+
Uses min(self.num_frames_bg_subtract, total_frames) frames to do mean subtraction.
|
126 |
+
Caches these frames in self.extracted_frames for reuse.
|
127 |
+
"""
|
128 |
+
# ensure the number of frames used is a multiple of self.batch_size so we can cache them and retrieve full batches
|
129 |
+
# add 1 extra frame to be used for optical flow calculation
|
130 |
+
num_frames_bg = min(self.end_frame - self.start_frame, self.num_frames_bg_subtract // self.batch_size * self.batch_size + 1)
|
131 |
+
|
132 |
+
if not self.disable_output:
|
133 |
+
print("Initializing mean frame for background subtraction using", num_frames_bg, "frames...")
|
134 |
+
frames_for_bg_subtract = self.didson.load_frames(start_frame=self.start_frame, end_frame=self.start_frame + num_frames_bg)
|
135 |
+
|
136 |
+
### NEW WAY ###
|
137 |
+
# save memory (and time?) by computing these in a streaming fashion vs. in a big batch
|
138 |
+
self.mean_blurred_frame = np.zeros([self.ydim, self.xdim], dtype=np.float32)
|
139 |
+
max_blurred_frame = np.zeros([self.ydim, self.xdim], dtype=np.float32)
|
140 |
+
for i in range(frames_for_bg_subtract.shape[0]):
|
141 |
+
blurred = cv2.GaussianBlur(
|
142 |
+
frames_for_bg_subtract[i],
|
143 |
+
(5,5),
|
144 |
+
0)
|
145 |
+
self.mean_blurred_frame += blurred
|
146 |
+
max_blurred_frame = np.maximum(max_blurred_frame, np.abs(blurred))
|
147 |
+
self.mean_blurred_frame /= frames_for_bg_subtract.shape[0]
|
148 |
+
max_blurred_frame -= self.mean_blurred_frame
|
149 |
+
self.mean_normalization_value = np.max(max_blurred_frame)
|
150 |
+
|
151 |
+
# cache these for later
|
152 |
+
self.extracted_frames = []
|
153 |
+
|
154 |
+
# Because of the optical flow computation, we only go to end_frame - 1
|
155 |
+
next_blur = None
|
156 |
+
for i in range(len(frames_for_bg_subtract) - 1):
|
157 |
+
if next_blur is None:
|
158 |
+
this_blur = ((cv2.GaussianBlur(frames_for_bg_subtract[i], (5,5), 0) - self.mean_blurred_frame) / self.mean_normalization_value + 1) / 2
|
159 |
+
else:
|
160 |
+
this_blur = next_blur
|
161 |
+
next_blur = ((cv2.GaussianBlur(frames_for_bg_subtract[i+1], (5,5), 0) - self.mean_blurred_frame) / self.mean_normalization_value + 1) / 2
|
162 |
+
frame_image = np.dstack([frames_for_bg_subtract[i],
|
163 |
+
this_blur * 255,
|
164 |
+
np.abs(next_blur - this_blur) * 255]).astype(np.uint8, copy=False)
|
165 |
+
|
166 |
+
if TEST_JPG_COMPRESSION:
|
167 |
+
from PIL import Image
|
168 |
+
import os
|
169 |
+
Image.fromarray(frame_image).save(f"tmp{i}.jpg", quality=95)
|
170 |
+
frame_image = cv2.imread(f"tmp{i}.jpg")[:, :, ::-1] # BGR to RGB
|
171 |
+
os.remove(f"tmp{i}.jpg")
|
172 |
+
|
173 |
+
if self.cache_bg_frames:
|
174 |
+
self.extracted_frames.append(frame_image)
|
175 |
+
|
176 |
+
if not self.disable_output:
|
177 |
+
print("Done initializing background frame.")
|
178 |
+
|
179 |
+
def _load_labels(self, fisheye_json):
|
180 |
+
"""Load labels from a fisheye-formatted json file into self.labels in normalized
|
181 |
+
xywh format.
|
182 |
+
"""
|
183 |
+
js = json.load(open(fisheye_json, 'r'))
|
184 |
+
labels = []
|
185 |
+
|
186 |
+
for frame in js['frames']:
|
187 |
+
|
188 |
+
l = []
|
189 |
+
for fish in frame['fish']:
|
190 |
+
x, y, w, h = xyxy2xywh(fish['bbox'])
|
191 |
+
cx = x + w/2.0
|
192 |
+
cy = y + h/2.0
|
193 |
+
# Each row is `class x_center y_center width height` format. (Normalized)
|
194 |
+
l.append([0, cx, cy, w, h])
|
195 |
+
|
196 |
+
l = np.array(l, dtype=np.float32)
|
197 |
+
if len(l) == 0:
|
198 |
+
l = np.zeros((0, 5), dtype=np.float32)
|
199 |
+
|
200 |
+
labels.append(l)
|
201 |
+
|
202 |
+
self.labels = labels
|
203 |
+
self.start_frame = js['start_frame']
|
204 |
+
self.end_frame = js['end_frame']
|
205 |
+
|
206 |
+
def __len__(self):
|
207 |
+
# account for optical flow - we can't do the last frame
|
208 |
+
return self.end_frame - self.start_frame - 1
|
209 |
+
|
210 |
+
def _postprocess(self, frame_images, frame_labels):
|
211 |
+
raise NotImplementedError
|
212 |
+
|
213 |
+
def __getitem__(self, idx):
|
214 |
+
"""
|
215 |
+
Return a numpy array representing this batch of frames and labels according to pyARIS frame extraction logic.
|
216 |
+
This class returns a full batch rather than just 1 example, assuming a OnePerBatchSampler is used.
|
217 |
+
"""
|
218 |
+
final_idx = min(idx+self.batch_size, len(self))
|
219 |
+
frame_labels = self.labels[idx:final_idx] if self.labels else None
|
220 |
+
|
221 |
+
# see if we have already cached this from bg subtraction
|
222 |
+
# assumes len(self.extracted_frames) is a multiple of self.batch_size
|
223 |
+
if idx+1 < len(self.extracted_frames):
|
224 |
+
return self._postprocess(self.extracted_frames[idx:final_idx], frame_labels)
|
225 |
+
else:
|
226 |
+
frames = self.didson.load_frames(start_frame=self.start_frame+idx, end_frame=self.start_frame + final_idx + 1)
|
227 |
+
blurred_frames = frames.astype(np.float32)
|
228 |
+
for i in range(frames.shape[0]):
|
229 |
+
blurred_frames[i] = cv2.GaussianBlur(
|
230 |
+
blurred_frames[i],
|
231 |
+
(5,5),
|
232 |
+
0
|
233 |
+
)
|
234 |
+
blurred_frames -= self.mean_blurred_frame
|
235 |
+
blurred_frames /= self.mean_normalization_value
|
236 |
+
blurred_frames += 1
|
237 |
+
blurred_frames /= 2
|
238 |
+
|
239 |
+
frame_images = np.stack([ frames[:-1], blurred_frames[:-1] * 255, np.abs(blurred_frames[1:] - blurred_frames[:-1]) * 255 ], axis=-1).astype(np.uint8, copy=False)
|
240 |
+
|
241 |
+
if TEST_JPG_COMPRESSION:
|
242 |
+
from PIL import Image
|
243 |
+
import os
|
244 |
+
new_frame_images = []
|
245 |
+
for image in frame_images:
|
246 |
+
Image.fromarray(image).save(f"tmp{idx}.jpg", quality=95)
|
247 |
+
image = cv2.imread(f"tmp{idx}.jpg")[:, :, ::-1] # BGR to RGB
|
248 |
+
os.remove(f"tmp{idx}.jpg")
|
249 |
+
new_frame_images.append(image)
|
250 |
+
frame_images = new_frame_images
|
251 |
+
|
252 |
+
return self._postprocess(frame_images, frame_labels)
|
253 |
+
|
254 |
+
class YOLOARISBatchedDataset(ARISBatchedDataset):
|
255 |
+
"""An ARIS Dataset that works with YOLOv5 inference."""
|
256 |
+
|
257 |
+
def __init__(self, aris_filepath, beam_width_dir, annotations_file, stride=64, pad=0.5, img_size=896, batch_size=32,
|
258 |
+
disable_output=False, cache_bg_frames=False):
|
259 |
+
super().__init__(aris_filepath, beam_width_dir, annotations_file, batch_size, disable_output=disable_output, cache_bg_frames=cache_bg_frames)
|
260 |
+
|
261 |
+
# compute shapes for letterbox
|
262 |
+
aspect_ratio = self.ydim / self.xdim
|
263 |
+
if aspect_ratio < 1:
|
264 |
+
shape = [aspect_ratio, 1]
|
265 |
+
elif aspect_ratio > 1:
|
266 |
+
shape = [1, 1 / aspect_ratio]
|
267 |
+
self.original_shape = (self.ydim, self.xdim)
|
268 |
+
self.shape = np.ceil(np.array(shape) * img_size / stride + pad).astype(int) * stride
|
269 |
+
|
270 |
+
@classmethod
|
271 |
+
def load_image(cls, img, img_size=896):
|
272 |
+
"""Loads and resizes 1 image from dataset, returns img, original hw, resized hw.
|
273 |
+
Modified from ScaledYOLOv4.datasets.load_image()
|
274 |
+
"""
|
275 |
+
h0, w0 = img.shape[:2] # orig hw
|
276 |
+
r = img_size / max(h0, w0) # resize image to img_size
|
277 |
+
if r != 1: # always resize down, only resize up if training with augmentation
|
278 |
+
interp = cv2.INTER_AREA if r < 1 else cv2.INTER_LINEAR
|
279 |
+
img = cv2.resize(img, (int(w0 * r), int(h0 * r)), interpolation=interp)
|
280 |
+
return img, (h0, w0), img.shape[:2] # img, hw_original, hw_resized
|
281 |
+
|
282 |
+
def _postprocess(self, frame_images, frame_labels):
|
283 |
+
"""
|
284 |
+
Return a batch of data in the format used by ScaledYOLOv4.
|
285 |
+
That is, a list of tuples, on tuple per image in the batch:
|
286 |
+
[
|
287 |
+
(img ->torch.Tensor,
|
288 |
+
labels ->torch.Tensor,
|
289 |
+
shapes ->tuple describing image original dimensions and scaled/padded dimensions
|
290 |
+
),
|
291 |
+
...
|
292 |
+
]
|
293 |
+
"""
|
294 |
+
outputs = []
|
295 |
+
frame_labels = frame_labels or [ None for _ in frame_images ]
|
296 |
+
for image, x in zip(frame_images, frame_labels):
|
297 |
+
img, (h0, w0), (h, w) = self.load_image(image)
|
298 |
+
|
299 |
+
# Letterbox
|
300 |
+
img, ratio, pad = letterbox(img, self.shape, auto=False, scaleup=False)
|
301 |
+
shapes = (h0, w0), ((h / h0, w / w0), pad) # for COCO mAP rescaling
|
302 |
+
|
303 |
+
img = img.transpose(2, 0, 1) # to -> C x H x W
|
304 |
+
img = np.ascontiguousarray(img)
|
305 |
+
|
306 |
+
# Load labels
|
307 |
+
# Convert from normalized xywh to pixel xyxy format in order to add padding from letterbox
|
308 |
+
labels = []
|
309 |
+
if x is not None and x.size > 0:
|
310 |
+
labels = x.copy()
|
311 |
+
labels[:, 1] = ratio[0] * w * (x[:, 1] - x[:, 3] / 2) + pad[0] # pad width
|
312 |
+
labels[:, 2] = ratio[1] * h * (x[:, 2] - x[:, 4] / 2) + pad[1] # pad height
|
313 |
+
labels[:, 3] = ratio[0] * w * (x[:, 1] + x[:, 3] / 2) + pad[0]
|
314 |
+
labels[:, 4] = ratio[1] * h * (x[:, 2] + x[:, 4] / 2) + pad[1]
|
315 |
+
|
316 |
+
# convert back to normalized xywh with padding
|
317 |
+
nL = len(labels) # number of labels
|
318 |
+
labels_out = torch.zeros((nL, 6))
|
319 |
+
if nL:
|
320 |
+
labels[:, 1:5] = xyxy2xywh(labels[:, 1:5]) # convert xyxy to xywh
|
321 |
+
labels[:, [2, 4]] /= img.shape[1] # normalized height 0-1
|
322 |
+
labels[:, [1, 3]] /= img.shape[2] # normalized width 0-1
|
323 |
+
labels_out[:, 1:] = torch.from_numpy(labels)
|
324 |
+
|
325 |
+
outputs.append( (torch.from_numpy(img), labels_out, shapes) )
|
326 |
+
|
327 |
+
return outputs
|
328 |
+
|
329 |
+
@contextmanager
|
330 |
+
def torch_distributed_zero_first(local_rank: int):
|
331 |
+
"""
|
332 |
+
Decorator to make all processes in distributed training wait for each local_master to do something.
|
333 |
+
"""
|
334 |
+
if local_rank not in [-1, 0]:
|
335 |
+
torch.distributed.barrier()
|
336 |
+
yield
|
337 |
+
if local_rank == 0:
|
338 |
+
torch.distributed.barrier()
|
339 |
+
|
340 |
+
class OnePerBatchSampler(torch.utils.data.Sampler):
|
341 |
+
"""Yields the first index of each batch, given a batch size.
|
342 |
+
In other words, returns multiples of self.batch_size up to the size of the Dataset.
|
343 |
+
This is a workaround for Pytorch's standard batch creation that allows us to manually
|
344 |
+
select contiguous segments of an ARIS clip for each batch.
|
345 |
+
"""
|
346 |
+
|
347 |
+
def __init__(self, data_source, batch_size):
|
348 |
+
self.data_source = data_source
|
349 |
+
self.batch_size = batch_size
|
350 |
+
|
351 |
+
def __iter__(self):
|
352 |
+
idxs = [i*self.batch_size for i in range(len(self))]
|
353 |
+
return iter(idxs)
|
354 |
+
|
355 |
+
def __len__(self):
|
356 |
+
return len(self.data_source) // self.batch_size
|
357 |
+
|
358 |
+
def collate_fn(batch):
|
359 |
+
"""See YOLOv5.utils.datasets.collate_fn"""
|
360 |
+
if not len(batch):
|
361 |
+
print("help!")
|
362 |
+
print(batch)
|
363 |
+
|
364 |
+
img, label, shapes = zip(*batch) # transposed
|
365 |
+
for i, l in enumerate(label):
|
366 |
+
l[:, 0] = i # add target image index for build_targets()
|
367 |
+
return torch.stack(img, 0), torch.cat(label, 0), shapes
|
dump.rdb
ADDED
Binary file (2.68 kB). View file
|
|
file_reader.py
ADDED
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""gr.File() component"""
|
2 |
+
|
3 |
+
from __future__ import annotations
|
4 |
+
|
5 |
+
import tempfile
|
6 |
+
import warnings
|
7 |
+
from pathlib import Path
|
8 |
+
from typing import Any, Callable, Literal
|
9 |
+
|
10 |
+
from gradio_client import utils as client_utils
|
11 |
+
from gradio_client.documentation import document, set_documentation_group
|
12 |
+
from gradio_client.serializing import FileSerializable
|
13 |
+
|
14 |
+
from gradio import utils
|
15 |
+
from gradio.components.base import IOComponent, _Keywords
|
16 |
+
from gradio.deprecation import warn_deprecation
|
17 |
+
from gradio.events import (
|
18 |
+
Changeable,
|
19 |
+
Clearable,
|
20 |
+
EventListenerMethod,
|
21 |
+
Selectable,
|
22 |
+
Uploadable,
|
23 |
+
)
|
24 |
+
|
25 |
+
set_documentation_group("component")
|
26 |
+
|
27 |
+
|
28 |
+
@document()
|
29 |
+
class File(
|
30 |
+
Changeable,
|
31 |
+
Selectable,
|
32 |
+
Clearable,
|
33 |
+
Uploadable,
|
34 |
+
IOComponent,
|
35 |
+
FileSerializable,
|
36 |
+
):
|
37 |
+
"""
|
38 |
+
Creates a file component that allows uploading generic file (when used as an input) and or displaying generic files (output).
|
39 |
+
Preprocessing: passes the uploaded file as a {tempfile._TemporaryFileWrapper} or {List[tempfile._TemporaryFileWrapper]} depending on `file_count` (or a {bytes}/{List{bytes}} depending on `type`)
|
40 |
+
Postprocessing: expects function to return a {str} path to a file, or {List[str]} consisting of paths to files.
|
41 |
+
Examples-format: a {str} path to a local file that populates the component.
|
42 |
+
Demos: zip_to_json, zip_files
|
43 |
+
"""
|
44 |
+
|
45 |
+
def __init__(
|
46 |
+
self,
|
47 |
+
value: str | list[str] | Callable | None = None,
|
48 |
+
*,
|
49 |
+
file_count: Literal["single", "multiple", "directory"] = "single",
|
50 |
+
file_types: list[str] | None = None,
|
51 |
+
type: Literal["file", "binary"] = "file",
|
52 |
+
label: str | None = None,
|
53 |
+
every: float | None = None,
|
54 |
+
show_label: bool | None = None,
|
55 |
+
container: bool = True,
|
56 |
+
scale: int | None = None,
|
57 |
+
min_width: int = 160,
|
58 |
+
interactive: bool | None = None,
|
59 |
+
visible: bool = True,
|
60 |
+
elem_id: str | None = None,
|
61 |
+
elem_classes: list[str] | str | None = None,
|
62 |
+
**kwargs,
|
63 |
+
):
|
64 |
+
"""
|
65 |
+
Parameters:
|
66 |
+
value: Default file to display, given as str file path. If callable, the function will be called whenever the app loads to set the initial value of the component.
|
67 |
+
file_count: if single, allows user to upload one file. If "multiple", user uploads multiple files. If "directory", user uploads all files in selected directory. Return type will be list for each file in case of "multiple" or "directory".
|
68 |
+
file_types: List of file extensions or types of files to be uploaded (e.g. ['image', '.json', '.mp4']). "file" allows any file to be uploaded, "image" allows only image files to be uploaded, "audio" allows only audio files to be uploaded, "video" allows only video files to be uploaded, "text" allows only text files to be uploaded.
|
69 |
+
type: Type of value to be returned by component. "file" returns a temporary file object with the same base name as the uploaded file, whose full path can be retrieved by file_obj.name, "binary" returns an bytes object.
|
70 |
+
label: component name in interface.
|
71 |
+
every: If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. Queue must be enabled. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute.
|
72 |
+
show_label: if True, will display label.
|
73 |
+
container: If True, will place the component in a container - providing some extra padding around the border.
|
74 |
+
scale: relative width compared to adjacent Components in a Row. For example, if Component A has scale=2, and Component B has scale=1, A will be twice as wide as B. Should be an integer.
|
75 |
+
min_width: minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.
|
76 |
+
interactive: if True, will allow users to upload a file; if False, can only be used to display files. If not provided, this is inferred based on whether the component is used as an input or output.
|
77 |
+
visible: If False, component will be hidden.
|
78 |
+
elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.
|
79 |
+
elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
|
80 |
+
"""
|
81 |
+
self.file_count = file_count
|
82 |
+
self.file_types = file_types
|
83 |
+
if file_types is not None and not isinstance(file_types, list):
|
84 |
+
raise ValueError(
|
85 |
+
f"Parameter file_types must be a list. Received {file_types.__class__.__name__}"
|
86 |
+
)
|
87 |
+
valid_types = [
|
88 |
+
"file",
|
89 |
+
"binary",
|
90 |
+
"bytes",
|
91 |
+
] # "bytes" is included for backwards compatibility
|
92 |
+
if type not in valid_types:
|
93 |
+
raise ValueError(
|
94 |
+
f"Invalid value for parameter `type`: {type}. Please choose from one of: {valid_types}"
|
95 |
+
)
|
96 |
+
if type == "bytes":
|
97 |
+
warn_deprecation(
|
98 |
+
"The `bytes` type is deprecated and may not work as expected. Please use `binary` instead."
|
99 |
+
)
|
100 |
+
if file_count == "directory" and file_types is not None:
|
101 |
+
warnings.warn(
|
102 |
+
"The `file_types` parameter is ignored when `file_count` is 'directory'."
|
103 |
+
)
|
104 |
+
self.type = type
|
105 |
+
self.select: EventListenerMethod
|
106 |
+
"""
|
107 |
+
Event listener for when the user selects file from list.
|
108 |
+
Uses event data gradio.SelectData to carry `value` referring to name of selected file, and `index` to refer to index.
|
109 |
+
See EventData documentation on how to use this event data.
|
110 |
+
"""
|
111 |
+
IOComponent.__init__(
|
112 |
+
self,
|
113 |
+
label=label,
|
114 |
+
every=every,
|
115 |
+
show_label=show_label,
|
116 |
+
container=container,
|
117 |
+
scale=scale,
|
118 |
+
min_width=min_width,
|
119 |
+
interactive=interactive,
|
120 |
+
visible=visible,
|
121 |
+
elem_id=elem_id,
|
122 |
+
elem_classes=elem_classes,
|
123 |
+
value=value,
|
124 |
+
**kwargs,
|
125 |
+
)
|
126 |
+
|
127 |
+
def get_config(self):
|
128 |
+
return {
|
129 |
+
"file_count": self.file_count,
|
130 |
+
"file_types": self.file_types,
|
131 |
+
"value": self.value,
|
132 |
+
"selectable": self.selectable,
|
133 |
+
**IOComponent.get_config(self),
|
134 |
+
}
|
135 |
+
|
136 |
+
@staticmethod
|
137 |
+
def update(
|
138 |
+
value: Any | Literal[_Keywords.NO_VALUE] | None = _Keywords.NO_VALUE,
|
139 |
+
label: str | None = None,
|
140 |
+
show_label: bool | None = None,
|
141 |
+
container: bool | None = None,
|
142 |
+
scale: int | None = None,
|
143 |
+
min_width: int | None = None,
|
144 |
+
interactive: bool | None = None,
|
145 |
+
visible: bool | None = None,
|
146 |
+
):
|
147 |
+
return {
|
148 |
+
"label": label,
|
149 |
+
"show_label": show_label,
|
150 |
+
"container": container,
|
151 |
+
"scale": scale,
|
152 |
+
"min_width": min_width,
|
153 |
+
"interactive": interactive,
|
154 |
+
"visible": visible,
|
155 |
+
"value": value,
|
156 |
+
"__type__": "update",
|
157 |
+
}
|
158 |
+
|
159 |
+
def preprocess(
|
160 |
+
self, x: list[dict[str, Any]] | None
|
161 |
+
) -> (
|
162 |
+
bytes
|
163 |
+
| tempfile._TemporaryFileWrapper
|
164 |
+
| list[bytes | tempfile._TemporaryFileWrapper]
|
165 |
+
| None
|
166 |
+
):
|
167 |
+
"""
|
168 |
+
Parameters:
|
169 |
+
x: List of JSON objects with filename as 'name' property and base64 data as 'data' property
|
170 |
+
Returns:
|
171 |
+
File objects in requested format
|
172 |
+
"""
|
173 |
+
if x is None:
|
174 |
+
return None
|
175 |
+
|
176 |
+
def process_single_file(f) -> bytes | tempfile._TemporaryFileWrapper:
|
177 |
+
file_name, data, is_file = (
|
178 |
+
f["name"],
|
179 |
+
f["data"],
|
180 |
+
f.get("is_file", False),
|
181 |
+
)
|
182 |
+
if self.type == "file":
|
183 |
+
if is_file:
|
184 |
+
path = self.make_temp_copy_if_needed(file_name)
|
185 |
+
else:
|
186 |
+
data, _ = client_utils.decode_base64_to_binary(data)
|
187 |
+
path = self.file_bytes_to_file(
|
188 |
+
data, dir=self.DEFAULT_TEMP_DIR, file_name=file_name
|
189 |
+
)
|
190 |
+
path = str(utils.abspath(path))
|
191 |
+
self.temp_files.add(path)
|
192 |
+
|
193 |
+
# Creation of tempfiles here
|
194 |
+
file = tempfile.NamedTemporaryFile(
|
195 |
+
delete=False, dir=self.DEFAULT_TEMP_DIR
|
196 |
+
)
|
197 |
+
file.name = path
|
198 |
+
file.orig_name = file_name # type: ignore
|
199 |
+
return file
|
200 |
+
elif (
|
201 |
+
self.type == "binary" or self.type == "bytes"
|
202 |
+
): # "bytes" is included for backwards compatibility
|
203 |
+
if is_file:
|
204 |
+
with open(file_name, "rb") as file_data:
|
205 |
+
return (file_name, file_data.read())
|
206 |
+
return (file_name, client_utils.decode_base64_to_binary(data)[0])
|
207 |
+
else:
|
208 |
+
raise ValueError(
|
209 |
+
"Unknown type: "
|
210 |
+
+ str(self.type)
|
211 |
+
+ ". Please choose from: 'file', 'bytes'."
|
212 |
+
)
|
213 |
+
|
214 |
+
if self.file_count == "single":
|
215 |
+
if isinstance(x, list):
|
216 |
+
return process_single_file(x[0])
|
217 |
+
else:
|
218 |
+
return process_single_file(x)
|
219 |
+
else:
|
220 |
+
if isinstance(x, list):
|
221 |
+
return [process_single_file(f) for f in x]
|
222 |
+
else:
|
223 |
+
return process_single_file(x)
|
224 |
+
|
225 |
+
def postprocess(
|
226 |
+
self, y: str | list[str] | None
|
227 |
+
) -> dict[str, Any] | list[dict[str, Any]] | None:
|
228 |
+
"""
|
229 |
+
Parameters:
|
230 |
+
y: file path
|
231 |
+
Returns:
|
232 |
+
JSON object with key 'name' for filename, 'data' for base64 url, and 'size' for filesize in bytes
|
233 |
+
"""
|
234 |
+
if y is None:
|
235 |
+
return None
|
236 |
+
if isinstance(y, list):
|
237 |
+
return [
|
238 |
+
{
|
239 |
+
"orig_name": Path(file).name,
|
240 |
+
"name": self.make_temp_copy_if_needed(file),
|
241 |
+
"size": Path(file).stat().st_size,
|
242 |
+
"data": None,
|
243 |
+
"is_file": True,
|
244 |
+
}
|
245 |
+
for file in y
|
246 |
+
]
|
247 |
+
else:
|
248 |
+
d = {
|
249 |
+
"orig_name": Path(y).name,
|
250 |
+
"name": self.make_temp_copy_if_needed(y),
|
251 |
+
"size": Path(y).stat().st_size,
|
252 |
+
"data": None,
|
253 |
+
"is_file": True,
|
254 |
+
}
|
255 |
+
return d
|
256 |
+
|
257 |
+
def as_example(self, input_data: str | list | None) -> str:
|
258 |
+
if input_data is None:
|
259 |
+
return ""
|
260 |
+
elif isinstance(input_data, list):
|
261 |
+
return ", ".join([Path(file).name for file in input_data])
|
262 |
+
else:
|
263 |
+
return Path(input_data).name
|
264 |
+
|
265 |
+
def api_info(self) -> dict[str, dict | bool]:
|
266 |
+
if self.file_count == "single":
|
267 |
+
return self._single_file_api_info()
|
268 |
+
else:
|
269 |
+
return self._multiple_file_api_info()
|
270 |
+
|
271 |
+
def serialized_info(self):
|
272 |
+
if self.file_count == "single":
|
273 |
+
return self._single_file_serialized_info()
|
274 |
+
else:
|
275 |
+
return self._multiple_file_serialized_info()
|
276 |
+
|
277 |
+
def example_inputs(self) -> dict[str, Any]:
|
278 |
+
if self.file_count == "single":
|
279 |
+
return self._single_file_example_inputs()
|
280 |
+
else:
|
281 |
+
return self._multiple_file_example_inputs()
|
inference.py
ADDED
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import project_path
|
2 |
+
|
3 |
+
import torch
|
4 |
+
from tqdm import tqdm
|
5 |
+
from functools import partial
|
6 |
+
import numpy as np
|
7 |
+
import json
|
8 |
+
from unittest.mock import patch
|
9 |
+
|
10 |
+
# assumes yolov5 on sys.path
|
11 |
+
from lib.yolov5.models.experimental import attempt_load
|
12 |
+
from lib.yolov5.utils.torch_utils import select_device
|
13 |
+
from lib.yolov5.utils.general import non_max_suppression
|
14 |
+
from lib.yolov5.utils.general import clip_boxes, scale_boxes
|
15 |
+
|
16 |
+
from lib.fish_eye.tracker import Tracker
|
17 |
+
|
18 |
+
### Configuration options
|
19 |
+
WEIGHTS = 'models/v5m_896_300best.pt'
|
20 |
+
# will need to configure these based on GPU hardware
|
21 |
+
BATCH_SIZE = 32
|
22 |
+
|
23 |
+
conf_thres = 0.3 # detection
|
24 |
+
iou_thres = 0.3 # NMS IOU
|
25 |
+
min_length = 0.3 # minimum fish length, in meters
|
26 |
+
###
|
27 |
+
|
28 |
+
def norm(bbox, w, h):
|
29 |
+
"""
|
30 |
+
Normalize a bounding box.
|
31 |
+
Args:
|
32 |
+
bbox: list of length 4. Can be [x,y,w,h] or [x0,y0,x1,y1]
|
33 |
+
w: image width
|
34 |
+
h: image height
|
35 |
+
"""
|
36 |
+
bb = bbox.copy()
|
37 |
+
bb[0] /= w
|
38 |
+
bb[1] /= h
|
39 |
+
bb[2] /= w
|
40 |
+
bb[3] /= h
|
41 |
+
return bb
|
42 |
+
|
43 |
+
def do_full_inference(dataloader, image_meter_width, image_meter_height, gp=None, weights=WEIGHTS):
|
44 |
+
|
45 |
+
model, device = setup_model(weights)
|
46 |
+
|
47 |
+
all_preds = do_detection(dataloader, model, device, gp=gp)
|
48 |
+
|
49 |
+
results = do_tracking(all_preds, image_meter_width, image_meter_height, gp=gp)
|
50 |
+
|
51 |
+
return results
|
52 |
+
|
53 |
+
|
54 |
+
def setup_model(weights_fp=WEIGHTS, imgsz=896, batch_size=32):
|
55 |
+
if torch.cuda.is_available():
|
56 |
+
device = select_device('0', batch_size=batch_size)
|
57 |
+
else:
|
58 |
+
print("CUDA not available. Using CPU inference.")
|
59 |
+
device = select_device('cpu', batch_size=batch_size)
|
60 |
+
|
61 |
+
# Setup model for inference
|
62 |
+
model = attempt_load(weights_fp, device=device)
|
63 |
+
half = device.type != 'cpu' # half precision only supported on CUDA
|
64 |
+
if half:
|
65 |
+
model.half()
|
66 |
+
model.eval();
|
67 |
+
|
68 |
+
# Create dataloader for batched inference
|
69 |
+
img = torch.zeros((1, 3, imgsz, imgsz), device=device)
|
70 |
+
_ = model(img.half() if half else img) if device.type != 'cpu' else None # run once
|
71 |
+
|
72 |
+
return model, device
|
73 |
+
|
74 |
+
def do_detection(dataloader, model, device, gp=None, batch_size=BATCH_SIZE):
|
75 |
+
"""
|
76 |
+
Args:
|
77 |
+
frames_dir: a directory containing frames to be evaluated
|
78 |
+
image_meter_width: the width of each image, in meters (used for fish length calculation)
|
79 |
+
gp: a callback function which takes as input 1 parameter, (int) percent complete
|
80 |
+
prep_for_marking: re-index fish for manual marking output
|
81 |
+
"""
|
82 |
+
|
83 |
+
if (gp): gp(0, "Detection...")
|
84 |
+
|
85 |
+
# keep predictions to feed them ordered into the Tracker
|
86 |
+
# TODO: how to deal with large files?
|
87 |
+
all_preds = {}
|
88 |
+
|
89 |
+
# Run detection
|
90 |
+
with tqdm(total=len(dataloader)*batch_size, desc="Running detection", ncols=0) as pbar:
|
91 |
+
for batch_i, (img, _, shapes) in enumerate(dataloader):
|
92 |
+
if gp: gp(batch_i / len(dataloader), pbar.__str__())
|
93 |
+
img = img.to(device, non_blocking=True)
|
94 |
+
img = img.half() if device.type != 'cpu' else img.float() # uint8 to fp16/32
|
95 |
+
img /= 255.0 # 0 - 255 to 0.0 - 1.0
|
96 |
+
nb, _, height, width = img.shape # batch size, channels, height, width
|
97 |
+
# Run model & NMS
|
98 |
+
with torch.no_grad():
|
99 |
+
inf_out, _ = model(img, augment=False)
|
100 |
+
output = non_max_suppression(inf_out, conf_thres=conf_thres, iou_thres=iou_thres)
|
101 |
+
|
102 |
+
# Format results
|
103 |
+
for si, pred in enumerate(output):
|
104 |
+
# Clip boxes to image bounds and resize to input shape
|
105 |
+
clip_boxes(pred, (height, width))
|
106 |
+
box = pred[:, :4].clone() # xyxy
|
107 |
+
confs = pred[:, 4].clone().tolist()
|
108 |
+
scale_boxes(img[si].shape[1:], box, shapes[si][0], shapes[si][1]) # to original shape
|
109 |
+
|
110 |
+
# get boxes into tracker input format - normalized xyxy with confidence score
|
111 |
+
# confidence score currently not used by tracker; set to 1.0
|
112 |
+
boxes = None
|
113 |
+
if box.shape[0]:
|
114 |
+
do_norm = partial(norm, w=shapes[si][0][1], h=shapes[si][0][0])
|
115 |
+
normed = list((map(do_norm, box[:, :4].tolist())))
|
116 |
+
boxes = np.stack([ [*bb, conf] for bb, conf in zip(normed, confs) ])
|
117 |
+
frame_num = (batch_i, si)
|
118 |
+
all_preds[frame_num] = boxes
|
119 |
+
|
120 |
+
pbar.update(1*batch_size)
|
121 |
+
|
122 |
+
return all_preds
|
123 |
+
|
124 |
+
def do_tracking(all_preds, image_meter_width, image_meter_height, gp=None):
|
125 |
+
|
126 |
+
if (gp): gp(0, "Tracking...")
|
127 |
+
|
128 |
+
# Initialize tracker
|
129 |
+
clip_info = {
|
130 |
+
'start_frame': 0,
|
131 |
+
'end_frame': len(all_preds),
|
132 |
+
'image_meter_width': image_meter_width,
|
133 |
+
'image_meter_height': image_meter_height
|
134 |
+
}
|
135 |
+
tracker = Tracker(clip_info, args={ 'max_age': 9, 'min_hits': 0, 'iou_threshold': 0.01}, min_hits=11)
|
136 |
+
|
137 |
+
# Run tracking
|
138 |
+
with tqdm(total=len(all_preds), desc="Running tracking", ncols=0) as pbar:
|
139 |
+
for i, key in enumerate(sorted(all_preds.keys())):
|
140 |
+
if gp: gp(i / len(all_preds), pbar.__str__())
|
141 |
+
boxes = all_preds[key]
|
142 |
+
if boxes is not None:
|
143 |
+
tracker.update(boxes)
|
144 |
+
else:
|
145 |
+
tracker.update()
|
146 |
+
pbar.update(1)
|
147 |
+
json_data = tracker.finalize(min_length=min_length)
|
148 |
+
|
149 |
+
return json_data
|
150 |
+
|
151 |
+
|
152 |
+
@patch('json.encoder.c_make_encoder', None)
|
153 |
+
def json_dump_round_float(some_object, out_path, num_digits=4):
|
154 |
+
"""Write a json file to disk with a specified level of precision.
|
155 |
+
See: https://gist.github.com/Sukonnik-Illia/ed9b2bec1821cad437d1b8adb17406a3
|
156 |
+
"""
|
157 |
+
# saving original method
|
158 |
+
of = json.encoder._make_iterencode
|
159 |
+
def inner(*args, **kwargs):
|
160 |
+
args = list(args)
|
161 |
+
# fifth argument is float formater which will we replace
|
162 |
+
fmt_str = '{:.' + str(num_digits) + 'f}'
|
163 |
+
args[4] = lambda o: fmt_str.format(o)
|
164 |
+
return of(*args, **kwargs)
|
165 |
+
|
166 |
+
with patch('json.encoder._make_iterencode', wraps=inner):
|
167 |
+
return json.dump(some_object, open(out_path, 'w'), indent=2)
|
main.py
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import torch
|
3 |
+
from zipfile import ZipFile
|
4 |
+
|
5 |
+
from aris import create_manual_marking, BEAM_WIDTH_DIR, create_metadata_dictionary, prep_for_mm
|
6 |
+
from dataloader import create_dataloader_aris
|
7 |
+
from inference import do_full_inference, json_dump_round_float
|
8 |
+
from visualizer import generate_video_batches
|
9 |
+
|
10 |
+
WEIGHTS = 'models/v5m_896_300best.pt'
|
11 |
+
|
12 |
+
def predict_task(filepath, weights=WEIGHTS, gradio_progress=None):
|
13 |
+
"""
|
14 |
+
Main processing task to be run in gradio
|
15 |
+
- Writes aris frames to dirname(filepath)/frames/{i}.jpg
|
16 |
+
- Writes json output to dirname(filepath)/{filename}_results.json
|
17 |
+
- Writes manual marking to dirname(filepath)/{filename}_marking.txt
|
18 |
+
- Writes video output to dirname(filepath)/{filename}_results.mp4
|
19 |
+
- Zips all results to dirname(filepath)/{filename}_results.zip
|
20 |
+
Args:
|
21 |
+
filepath (str): path to aris file
|
22 |
+
|
23 |
+
TODO: Separate into subtasks in different queues; have a GPU-only queue.
|
24 |
+
"""
|
25 |
+
if (gradio_progress): gradio_progress(0, "In task...")
|
26 |
+
print("Cuda available in task?", torch.cuda.is_available())
|
27 |
+
|
28 |
+
print(filepath)
|
29 |
+
dirname = os.path.dirname(filepath)
|
30 |
+
filename = os.path.basename(filepath).replace(".aris","").replace(".ddf","")
|
31 |
+
results_filepath = os.path.join(dirname, f"{filename}_results.json")
|
32 |
+
marking_filepath = os.path.join(dirname, f"{filename}_marking.txt")
|
33 |
+
video_filepath = os.path.join(dirname, f"{filename}_results.mp4")
|
34 |
+
zip_filepath = os.path.join(dirname, f"{filename}_results.zip")
|
35 |
+
os.makedirs(dirname, exist_ok=True)
|
36 |
+
|
37 |
+
# create dataloader
|
38 |
+
if (gradio_progress): gradio_progress(0, "Initializing Dataloader...")
|
39 |
+
dataloader, dataset = create_dataloader_aris(filepath, BEAM_WIDTH_DIR, None)
|
40 |
+
|
41 |
+
# extract aris/didson info. didson does not yet have pixel-meter info
|
42 |
+
if ".ddf" in filepath:
|
43 |
+
image_meter_width = -1
|
44 |
+
image_meter_height = -1
|
45 |
+
else:
|
46 |
+
image_meter_width = dataset.didson.info['xdim'] * dataset.didson.info['pixel_meter_width']
|
47 |
+
image_meter_height = dataset.didson.info['ydim'] * dataset.didson.info['pixel_meter_height']
|
48 |
+
frame_rate = dataset.didson.info['framerate']
|
49 |
+
|
50 |
+
# run detection + tracking
|
51 |
+
results = do_full_inference(dataloader, image_meter_width, image_meter_height, gp=gradio_progress, weights=WEIGHTS)
|
52 |
+
|
53 |
+
# re-index results if desired - this should be done before writing the file
|
54 |
+
results = prep_for_mm(results)
|
55 |
+
|
56 |
+
# write output to disk
|
57 |
+
json_dump_round_float(results, results_filepath)
|
58 |
+
|
59 |
+
metadata = None
|
60 |
+
if dataset.didson.info['version'][3] == 5: # ARIS only
|
61 |
+
metadata = create_metadata_dictionary(filepath, results_filepath)
|
62 |
+
create_manual_marking(metadata, out_path=marking_filepath)
|
63 |
+
|
64 |
+
# generate a video with tracking results
|
65 |
+
generate_video_batches(dataset.didson, results_filepath, frame_rate, video_filepath,
|
66 |
+
image_meter_width=image_meter_width, image_meter_height=image_meter_height, gp=gradio_progress)
|
67 |
+
|
68 |
+
# zip up the results
|
69 |
+
with ZipFile(zip_filepath, 'w') as z:
|
70 |
+
for file in [results_filepath, marking_filepath, video_filepath, os.path.join(dirname, 'bg_start.jpg')]:
|
71 |
+
if os.path.exists(file):
|
72 |
+
z.write(file, arcname=os.path.basename(file))
|
73 |
+
|
74 |
+
# release GPU memory
|
75 |
+
torch.cuda.empty_cache()
|
76 |
+
|
77 |
+
return metadata, results_filepath, zip_filepath, video_filepath, marking_filepath
|
project_path.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
A kind of hacky way to get all the lib paths in order.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import sys
|
6 |
+
import os
|
7 |
+
|
8 |
+
current_dir = os.path.dirname(os.path.realpath(__file__))
|
9 |
+
for d in [current_dir, os.path.join(current_dir, "lib/fish_eye/"), os.path.join(current_dir, "lib/"), os.path.join(current_dir, "lib/yolov5/")]:
|
10 |
+
if d not in sys.path:
|
11 |
+
sys.path.append(d)
|
pyDIDSON.py
ADDED
@@ -0,0 +1,495 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Utilities to read and produce to-scale images from DIDSON and ARIS sonar files.
|
3 |
+
|
4 |
+
Portions of this code were adapted from SoundMetrics MATLAB code.
|
5 |
+
"""
|
6 |
+
__version__ = 'b1.0.2'
|
7 |
+
|
8 |
+
import contextlib
|
9 |
+
import itertools
|
10 |
+
from matplotlib.cm import get_cmap
|
11 |
+
import numpy as np
|
12 |
+
import os
|
13 |
+
import pandas as pd
|
14 |
+
from PIL import Image
|
15 |
+
from shutil import make_archive, rmtree
|
16 |
+
import struct
|
17 |
+
from types import SimpleNamespace
|
18 |
+
|
19 |
+
import lib.fish_eye.pyARIS as pyARIS
|
20 |
+
from pyDIDSON_format import *
|
21 |
+
|
22 |
+
|
23 |
+
class pyDIDSON:
|
24 |
+
def __init__(self, file, beam_width_dir='beam_widths', ixsize=-1):
|
25 |
+
""" Load header info from DIDSON file and precompute some warps.
|
26 |
+
|
27 |
+
Parameters
|
28 |
+
----------
|
29 |
+
file : file-like object, string, or pathlib.Path
|
30 |
+
The DIDSON or ARIS file to read.
|
31 |
+
beam_width_dir : string or pathlib.Path, optional
|
32 |
+
Location of ARIS beam width CSV files. Only used for ARIS files.
|
33 |
+
ixsize : int, optional
|
34 |
+
x-dimension width of output warped images to produce. Width is approximate for ARIS files and definite for
|
35 |
+
DIDSON. If not specified, the default for ARIS is determined by pyARIS and the default for DIDSON is 300.
|
36 |
+
|
37 |
+
Returns
|
38 |
+
-------
|
39 |
+
info : dict
|
40 |
+
Dictionary of extracted headers and computed sonar values.
|
41 |
+
|
42 |
+
"""
|
43 |
+
|
44 |
+
if hasattr(file, 'read'):
|
45 |
+
file_ctx = contextlib.nullcontext(file)
|
46 |
+
else:
|
47 |
+
file_ctx = open(file, 'rb')
|
48 |
+
|
49 |
+
with file_ctx as fid:
|
50 |
+
assert fid.read(3) == b'DDF'
|
51 |
+
|
52 |
+
version_id = fid.read(1)[0]
|
53 |
+
print(f'Version {version_id}')
|
54 |
+
|
55 |
+
fid.seek(0)
|
56 |
+
|
57 |
+
info = {
|
58 |
+
'pydidson_version': __version__,
|
59 |
+
}
|
60 |
+
self.info = info
|
61 |
+
|
62 |
+
file_attributes, frame_attributes = {
|
63 |
+
0: NotImplementedError,
|
64 |
+
1: NotImplementedError,
|
65 |
+
2: NotImplementedError,
|
66 |
+
3: [file_attributes_3, frame_attributes_3],
|
67 |
+
4: [file_attributes_4, frame_attributes_4],
|
68 |
+
5: [file_attributes_5, frame_attributes_5],
|
69 |
+
}[version_id]
|
70 |
+
|
71 |
+
fileheaderformat = '=' + ''.join(file_attributes.values())
|
72 |
+
fileheadersize = struct.calcsize(fileheaderformat)
|
73 |
+
info.update(dict(zip(file_attributes.keys(), struct.unpack(fileheaderformat, fid.read(fileheadersize)))))
|
74 |
+
|
75 |
+
frameheaderformat = '=' + ''.join(frame_attributes.values())
|
76 |
+
frameheadersize = struct.calcsize(frameheaderformat)
|
77 |
+
info.update(dict(zip(frame_attributes.keys(), struct.unpack(frameheaderformat, fid.read(frameheadersize)))))
|
78 |
+
|
79 |
+
info.update({
|
80 |
+
'fileheaderformat': fileheaderformat,
|
81 |
+
'fileheadersize': fileheadersize,
|
82 |
+
'frameheaderformat': frameheaderformat,
|
83 |
+
'frameheadersize': frameheadersize,
|
84 |
+
})
|
85 |
+
|
86 |
+
if version_id == 0:
|
87 |
+
raise NotImplementedError
|
88 |
+
elif version_id == 1:
|
89 |
+
raise NotImplementedError
|
90 |
+
elif version_id == 2:
|
91 |
+
raise NotImplementedError
|
92 |
+
elif version_id == 3:
|
93 |
+
# Convert windowlength code to meters
|
94 |
+
info['windowlength'] = {
|
95 |
+
0b00: [0.83, 2.5, 5, 10, 20, 40], # DIDSON-S, Extended Windows
|
96 |
+
0b01: [1.125, 2.25, 4.5, 9, 18, 36], # DIDSON-S, Classic Windows
|
97 |
+
0b10: [2.5, 5, 10, 20, 40, 70], # DIDSON-LR, Extended Window
|
98 |
+
0b11: [2.25, 4.5, 9, 18, 36, 72], # DIDSON-LR, Classic Windows
|
99 |
+
}[info['configflags'] & 0b11][info['windowlength'] + 2 * (1 - info['resolution'])]
|
100 |
+
|
101 |
+
# Windowstart 1 to 31 times 0.75 (Lo) or 0.375 (Hi) or 0.419 for extended
|
102 |
+
info['windowstart'] = {
|
103 |
+
0b0: 0.419 * info['windowstart'] * (2 - info['resolution']), # meters for extended DIDSON
|
104 |
+
0b1:
|
105 |
+
0.375 * info['windowstart'] * (2 - info['resolution']), # meters for standard or long range DIDSON
|
106 |
+
}[info['configflags'] & 0b1]
|
107 |
+
|
108 |
+
info['halffov'] = 14.4
|
109 |
+
elif version_id == 4:
|
110 |
+
# Convert windowlength code to meters
|
111 |
+
info['windowlength'] = [1.25, 2.5, 5, 10, 20, 40][info['windowlength'] + 2 * (1 - info['resolution'])]
|
112 |
+
|
113 |
+
# Windowstart 1 to 31 times 0.75 (Lo) or 0.375 (Hi) or 0.419 for extended
|
114 |
+
info['windowstart'] = 0.419 * info['windowstart'] * (2 - info['resolution'])
|
115 |
+
|
116 |
+
info['halffov'] = 14.4
|
117 |
+
elif version_id == 5: #ARIS
|
118 |
+
if info['pingmode'] in [1, 2]:
|
119 |
+
BeamCount = 48
|
120 |
+
elif info['pingmode'] in [3, 4, 5]:
|
121 |
+
BeamCount = 96
|
122 |
+
elif info['pingmode'] in [6, 7, 8]:
|
123 |
+
BeamCount = 64
|
124 |
+
elif info['pingmode'] in [9, 10, 11, 12]:
|
125 |
+
BeamCount = 128
|
126 |
+
else:
|
127 |
+
raise
|
128 |
+
|
129 |
+
WinStart = info['samplestartdelay'] * 0.000001 * info['soundspeed'] / 2
|
130 |
+
|
131 |
+
info.update({
|
132 |
+
'BeamCount': BeamCount,
|
133 |
+
'WinStart': WinStart,
|
134 |
+
})
|
135 |
+
|
136 |
+
aris_frame = SimpleNamespace(**info)
|
137 |
+
|
138 |
+
beam_width_data, camera_type = pyARIS.load_beam_width_data(frame=aris_frame,
|
139 |
+
beam_width_dir=beam_width_dir)
|
140 |
+
|
141 |
+
# What is the meter resolution of the smallest sample?
|
142 |
+
min_pixel_size = pyARIS.get_minimum_pixel_meter_size(aris_frame, beam_width_data)
|
143 |
+
|
144 |
+
# What is the meter resolution of the sample length?
|
145 |
+
sample_length = aris_frame.sampleperiod * 0.000001 * aris_frame.soundspeed / 2
|
146 |
+
|
147 |
+
# Choose the size of a pixel (or hard code it to some specific value)
|
148 |
+
pixel_meter_size = max(min_pixel_size, sample_length)
|
149 |
+
|
150 |
+
# Determine the image dimensions
|
151 |
+
xdim, ydim, x_meter_start, y_meter_start, x_meter_stop, y_meter_stop = pyARIS.compute_image_bounds(
|
152 |
+
pixel_meter_size,
|
153 |
+
aris_frame,
|
154 |
+
beam_width_data,
|
155 |
+
additional_pixel_padding_x=0,
|
156 |
+
additional_pixel_padding_y=0)
|
157 |
+
|
158 |
+
if ixsize != -1:
|
159 |
+
pixel_meter_size = pixel_meter_size * xdim / ixsize
|
160 |
+
pixel_meter_size += 1e-5
|
161 |
+
xdim, ydim, x_meter_start, y_meter_start, x_meter_stop, y_meter_stop = pyARIS.compute_image_bounds(
|
162 |
+
pixel_meter_size,
|
163 |
+
aris_frame,
|
164 |
+
beam_width_data,
|
165 |
+
additional_pixel_padding_x=0,
|
166 |
+
additional_pixel_padding_y=0)
|
167 |
+
|
168 |
+
read_rows, read_cols, write_rows, write_cols = pyARIS.compute_mapping_from_sample_to_image(
|
169 |
+
pixel_meter_size, xdim, ydim, x_meter_start, y_meter_start, aris_frame, beam_width_data)
|
170 |
+
|
171 |
+
read_i = read_rows * info['numbeams'] + info['numbeams'] - read_cols - 1
|
172 |
+
|
173 |
+
pixel_meter_width = pixel_meter_size
|
174 |
+
pixel_meter_height = pixel_meter_size
|
175 |
+
|
176 |
+
info.update({
|
177 |
+
'camera_type': camera_type,
|
178 |
+
'min_pixel_size': min_pixel_size,
|
179 |
+
'sample_length': sample_length,
|
180 |
+
'x_meter_start': x_meter_start,
|
181 |
+
'y_meter_start': y_meter_start,
|
182 |
+
'x_meter_stop': x_meter_stop,
|
183 |
+
'y_meter_stop': y_meter_stop,
|
184 |
+
'beam_width_dir': os.path.abspath(beam_width_dir),
|
185 |
+
})
|
186 |
+
else:
|
187 |
+
raise
|
188 |
+
|
189 |
+
if version_id < 5:
|
190 |
+
info['xdim'] = 300 if ixsize == -1 else ixsize
|
191 |
+
ydim, xdim, write_rows, write_cols, read_i = self.__mapscan()
|
192 |
+
|
193 |
+
# widthscale meters/pixels
|
194 |
+
pixel_meter_width = 2 * (info['windowstart'] + info['windowlength']) * np.sin(np.radians(14.25)) / xdim
|
195 |
+
# heightscale meters/pixels
|
196 |
+
pixel_meter_height = ((info['windowstart'] + info['windowlength']) -
|
197 |
+
info['windowstart'] * np.cos(np.radians(14.25))) / ydim
|
198 |
+
|
199 |
+
pixel_meter_size = (pixel_meter_width + pixel_meter_height) / 2
|
200 |
+
|
201 |
+
self.write_rows = write_rows
|
202 |
+
self.write_cols = write_cols
|
203 |
+
self.read_i = read_i
|
204 |
+
|
205 |
+
info.update({
|
206 |
+
'xdim': xdim,
|
207 |
+
'ydim': ydim,
|
208 |
+
'pixel_meter_width': pixel_meter_width,
|
209 |
+
'pixel_meter_height': pixel_meter_height,
|
210 |
+
'pixel_meter_size': pixel_meter_size,
|
211 |
+
})
|
212 |
+
|
213 |
+
# Fix common but critical corruption errors
|
214 |
+
if info['startframe'] > 65535:
|
215 |
+
info['startframe'] = 0
|
216 |
+
if info['endframe'] > 65535:
|
217 |
+
info['endframe'] = 0
|
218 |
+
|
219 |
+
try:
|
220 |
+
info['filename'] = os.path.abspath(file_ctx.name)
|
221 |
+
except AttributeError:
|
222 |
+
info['filename'] = None
|
223 |
+
|
224 |
+
# Record the proportion of measurements that are present in the warp (increases as xdim increases)
|
225 |
+
info['proportion_warp'] = len(np.unique(read_i)) / (info['numbeams'] * info['samplesperchannel'])
|
226 |
+
|
227 |
+
def __lens_distortion(self, nbeams, theta):
|
228 |
+
""" Removes Lens distortion determined by empirical work at the barge.
|
229 |
+
|
230 |
+
Parameters
|
231 |
+
----------
|
232 |
+
nbeams : int
|
233 |
+
Number of sonar beams.
|
234 |
+
theta : (A,) ndarray
|
235 |
+
Angle of warp for each x index.
|
236 |
+
|
237 |
+
Returns
|
238 |
+
-------
|
239 |
+
beamnum : (A,) ndarray
|
240 |
+
Distortion-adjusted beam number for each theta.
|
241 |
+
|
242 |
+
"""
|
243 |
+
|
244 |
+
factor, a = {
|
245 |
+
48: [1, [.0015, -0.0036, 1.3351, 24.0976]],
|
246 |
+
189: [4.026, [.0015, -0.0036, 1.3351, 24.0976]],
|
247 |
+
96: [1.012, [.0030, -0.0055, 2.6829, 48.04]],
|
248 |
+
381: [4.05, [.0030, -0.0055, 2.6829, 48.04]],
|
249 |
+
}[nbeams]
|
250 |
+
|
251 |
+
return np.rint(factor * (a[0] * theta**3 + a[1] * theta**2 + a[2] * theta + a[3]) + 1).astype(np.uint32)
|
252 |
+
|
253 |
+
def __mapscan(self):
|
254 |
+
""" Calculate warp mapping from raw to scale images.
|
255 |
+
|
256 |
+
Returns
|
257 |
+
-------
|
258 |
+
ydim : int
|
259 |
+
y-dimension of warped image.
|
260 |
+
xdim : int
|
261 |
+
x-dimension of warped image.
|
262 |
+
write_rows : (A,) ndarray, np.uint16
|
263 |
+
Row indices to write to warped image.
|
264 |
+
write_cols : (A,) ndarray, np.uint16
|
265 |
+
Column indices to write to warped image.
|
266 |
+
read_i : (A,) ndarray, np.uint32
|
267 |
+
Indices to read from raw sonar measurements.
|
268 |
+
|
269 |
+
"""
|
270 |
+
|
271 |
+
xdim = self.info['xdim']
|
272 |
+
rmin = self.info['windowstart']
|
273 |
+
rmax = rmin + self.info['windowlength']
|
274 |
+
halffov = self.info['halffov']
|
275 |
+
nbeams = self.info['numbeams']
|
276 |
+
nbins = self.info['samplesperchannel']
|
277 |
+
|
278 |
+
degtorad = 3.14159 / 180 # conversion of degrees to radians
|
279 |
+
radtodeg = 180 / 3.14159 # conversion of radians to degrees
|
280 |
+
|
281 |
+
d2 = rmax * np.cos(
|
282 |
+
halffov * degtorad) # see drawing (distance from point scan touches image boundary to origin)
|
283 |
+
d3 = rmin * np.cos(halffov * degtorad) # see drawing (bottom of image frame to r,theta origin in meters)
|
284 |
+
c1 = (nbins - 1) / (rmax - rmin) # precalcualtion of constants used in do loop below
|
285 |
+
c2 = (nbeams - 1) / (2 * halffov)
|
286 |
+
|
287 |
+
gamma = xdim / (2 * rmax * np.sin(halffov * degtorad)) # Ratio of pixel number to position in meters
|
288 |
+
ydim = int(np.fix(gamma * (rmax - d3) + 0.5)) # number of pixels in image in vertical direction
|
289 |
+
svector = np.zeros(xdim * ydim, dtype=np.uint32) # make vector and fill in later
|
290 |
+
ix = np.arange(1, xdim + 1) # pixels in x dimension
|
291 |
+
x = ((ix - 1) - xdim / 2) / gamma # convert from pixels to meters
|
292 |
+
|
293 |
+
for iy in range(1, ydim + 1):
|
294 |
+
y = rmax - (iy - 1) / gamma # convert from pixels to meters
|
295 |
+
r = np.sqrt(y**2 + x**2) # convert to polar cooridinates
|
296 |
+
theta = radtodeg * np.arctan2(x, y) # theta is in degrees
|
297 |
+
binnum = np.rint((r - rmin) * c1 + 1.5).astype(np.uint32) # the rangebin number
|
298 |
+
beamnum = self.__lens_distortion(nbeams, theta) # remove lens distortion using empirical formula
|
299 |
+
|
300 |
+
# find position in sample array expressed as a vector
|
301 |
+
# make pos = 0 if outside sector, else give it the offset in the sample array
|
302 |
+
pos = (beamnum > 0) * (beamnum <= nbeams) * (binnum > 0) * (binnum <= nbins) * (
|
303 |
+
(beamnum - 1) * nbins + binnum)
|
304 |
+
svector[(ix - 1) * ydim + iy - 1] = pos # The offset in this array is the pixel offset in the image array
|
305 |
+
# The value at this offset is the offset in the sample array
|
306 |
+
|
307 |
+
svector = svector.reshape(xdim, ydim).T.flat
|
308 |
+
svectori = svector != 0
|
309 |
+
|
310 |
+
read_i = np.flipud(np.arange(nbins * nbeams, dtype=np.uint32).reshape(nbins,
|
311 |
+
nbeams).T).flat[svector[svectori] - 1]
|
312 |
+
write_rows, write_cols = np.unravel_index(np.where(svectori)[0], (ydim, xdim))
|
313 |
+
return ydim, xdim, write_rows.astype(np.uint16), write_cols.astype(np.uint16), read_i
|
314 |
+
|
315 |
+
def __FasterDIDSONRead(self, file, start_frame, end_frame):
|
316 |
+
""" Load raw frames from DIDSON.
|
317 |
+
|
318 |
+
Parameters
|
319 |
+
----------
|
320 |
+
file : file-like object, string, or pathlib.Path
|
321 |
+
The DIDSON or ARIS file to read.
|
322 |
+
info : dict
|
323 |
+
Dictionary of extracted headers and computed sonar values.
|
324 |
+
start_frame : int
|
325 |
+
Zero-indexed start of frame range (inclusive).
|
326 |
+
end_frame : int
|
327 |
+
End of frame range (exclusive).
|
328 |
+
|
329 |
+
Returns
|
330 |
+
-------
|
331 |
+
raw_frames : (end_frame - start_frame, framesize) ndarray, np.uint8
|
332 |
+
Extracted and flattened raw sonar measurements for frame range.
|
333 |
+
|
334 |
+
"""
|
335 |
+
|
336 |
+
if hasattr(file, 'read'):
|
337 |
+
file_ctx = contextlib.nullcontext(file)
|
338 |
+
else:
|
339 |
+
file_ctx = open(file, 'rb')
|
340 |
+
|
341 |
+
with file_ctx as fid:
|
342 |
+
framesize = self.info['samplesperchannel'] * self.info['numbeams']
|
343 |
+
frameheadersize = self.info['frameheadersize']
|
344 |
+
|
345 |
+
fid.seek(self.info['fileheadersize'] + start_frame * (frameheadersize + framesize) + frameheadersize, 0)
|
346 |
+
|
347 |
+
return np.array([
|
348 |
+
np.frombuffer(fid.read(framesize + frameheadersize)[:framesize], dtype=np.uint8)
|
349 |
+
for _ in range(end_frame - start_frame)
|
350 |
+
],
|
351 |
+
dtype=np.uint8)
|
352 |
+
|
353 |
+
def load_frames(self, file=None, start_frame=-1, end_frame=-1):
|
354 |
+
""" Load and warp DIDSON frames into images.
|
355 |
+
|
356 |
+
Parameters
|
357 |
+
----------
|
358 |
+
file : file-like object, string, or pathlib.Path, optional
|
359 |
+
The DIDSON or ARIS file to read. Defaults to `filename` in `info`.
|
360 |
+
start_frame : int, optional
|
361 |
+
Zero-indexed start of frame range (inclusive). Defaults to the first available.
|
362 |
+
end_frame : int, optional
|
363 |
+
End of frame range (exclusive). Defaults to the last available frame.
|
364 |
+
|
365 |
+
Returns
|
366 |
+
-------
|
367 |
+
frames : (end_frame - start_frame, ydim, xdim) ndarray, np.uint8
|
368 |
+
Warped-to-scale sonar image tensor.
|
369 |
+
|
370 |
+
"""
|
371 |
+
if file is None:
|
372 |
+
file = self.info['filename']
|
373 |
+
|
374 |
+
if hasattr(file, 'read'):
|
375 |
+
file_ctx = contextlib.nullcontext(file)
|
376 |
+
else:
|
377 |
+
file_ctx = open(file, 'rb')
|
378 |
+
|
379 |
+
with file_ctx as fid:
|
380 |
+
svector = None
|
381 |
+
if start_frame == -1:
|
382 |
+
start_frame = self.info['startframe']
|
383 |
+
if end_frame == -1:
|
384 |
+
end_frame = self.info['endframe'] or self.info['numframes']
|
385 |
+
|
386 |
+
data = self.__FasterDIDSONRead(fid, start_frame, end_frame)
|
387 |
+
frames = np.zeros((end_frame - start_frame, self.info['ydim'], self.info['xdim']), dtype=np.uint8)
|
388 |
+
frames[:, self.write_rows, self.write_cols] = data[:, self.read_i]
|
389 |
+
return frames
|
390 |
+
|
391 |
+
@staticmethod
|
392 |
+
def save_frames(path, frames, pad_zeros=False, multiprocessing=False, ydim=None, xdim=None, quality='web_high'):
|
393 |
+
""" Save frames as JPEG images.
|
394 |
+
|
395 |
+
Parameters
|
396 |
+
----------
|
397 |
+
path : string or pathlib.Path
|
398 |
+
Directory to output images to or zip file.
|
399 |
+
frames : (end_frame - start_frame, ydim, xdim) ndarray, np.uint8
|
400 |
+
Warped-to-scale sonar image tensor.
|
401 |
+
pad_zeros : bool, optional
|
402 |
+
If enabled adds appropriately padded zeros to filenames so alphabetic sort of images returns expected
|
403 |
+
ordering. Note that this option is turned off by default for compatibility with vatic.js which requires
|
404 |
+
that filenames are not padded.
|
405 |
+
multiprocessing : bool, optional
|
406 |
+
If enabled adds multi-process optimization for writing images.
|
407 |
+
ydim : int, optional
|
408 |
+
If provided resizes image to given ydim before saving.
|
409 |
+
xdim : int, optional
|
410 |
+
If provided resizes image to given xdim before saving.
|
411 |
+
quality : int or str
|
412 |
+
Either integer 1-100 or JPEG compression preset seen here:
|
413 |
+
https://github.com/python-pillow/Pillow/blob/master/src/PIL/JpegPresets.py
|
414 |
+
|
415 |
+
"""
|
416 |
+
|
417 |
+
path = str(path)
|
418 |
+
|
419 |
+
to_zip = path.endswith('.zip')
|
420 |
+
|
421 |
+
if to_zip:
|
422 |
+
path = os.path.splitext(path)[0]
|
423 |
+
|
424 |
+
if not os.path.exists(path):
|
425 |
+
os.mkdir(path)
|
426 |
+
|
427 |
+
if pad_zeros:
|
428 |
+
filename = f'{path}/{{:0{int(np.ceil(np.log10(len(frames))))}}}.jpg'
|
429 |
+
else:
|
430 |
+
filename = f'{path}/{{}}.jpg'
|
431 |
+
|
432 |
+
ydim = ydim or frames.shape[1]
|
433 |
+
xdim = xdim or frames.shape[2]
|
434 |
+
|
435 |
+
viridis = get_cmap()
|
436 |
+
|
437 |
+
def f(n):
|
438 |
+
Image.fromarray(viridis(n[1], bytes=True)[..., :3]).resize((xdim, ydim)).save(filename.format(n[0]),
|
439 |
+
quality=quality)
|
440 |
+
|
441 |
+
ns = enumerate(frames)
|
442 |
+
if multiprocessing:
|
443 |
+
__mpmap(f, ns)
|
444 |
+
else:
|
445 |
+
list(map(f, ns))
|
446 |
+
|
447 |
+
if to_zip:
|
448 |
+
make_archive(path, 'zip', path)
|
449 |
+
rmtree(path)
|
450 |
+
|
451 |
+
|
452 |
+
def __mpmap(func, iterable, processes=os.cpu_count() - 1, niceness=1, threading=False, flatten=False):
|
453 |
+
""" Helper function to add simple multiprocessing capabilities.
|
454 |
+
|
455 |
+
Parameters
|
456 |
+
----------
|
457 |
+
func : function
|
458 |
+
Function to be mapped.
|
459 |
+
iterable : iterable
|
460 |
+
Domain to be mapped over.
|
461 |
+
processes : int, optional
|
462 |
+
Number of processes to spawn. Default is one for all but one CPU core.
|
463 |
+
niceness : int, optional
|
464 |
+
Process niceness.
|
465 |
+
threading : bool, optional
|
466 |
+
If enabled replaces multiprocessing with multithreading
|
467 |
+
flatten : bool, optional
|
468 |
+
If enabled chains map output together before returning.
|
469 |
+
|
470 |
+
Returns
|
471 |
+
-------
|
472 |
+
output : list
|
473 |
+
Image of mapped func over iterable.
|
474 |
+
|
475 |
+
"""
|
476 |
+
|
477 |
+
import multiprocess as mp
|
478 |
+
import multiprocess.dummy
|
479 |
+
|
480 |
+
def initializer():
|
481 |
+
os.nice(niceness)
|
482 |
+
|
483 |
+
pool_class = mp.dummy.Pool if threading else mp.Pool
|
484 |
+
|
485 |
+
pool = pool_class(processes=processes, initializer=initializer)
|
486 |
+
|
487 |
+
out = pool.map(func, iterable)
|
488 |
+
|
489 |
+
if flatten:
|
490 |
+
out = list(itertools.chain.from_iterable(out))
|
491 |
+
|
492 |
+
pool.close()
|
493 |
+
pool.join()
|
494 |
+
|
495 |
+
return out
|
pyDIDSON_format.py
ADDED
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
DIDSON and ARIS file and frame header formats
|
3 |
+
"""
|
4 |
+
|
5 |
+
file_attributes_3 = {
|
6 |
+
'version': '4s',
|
7 |
+
'numframes': 'i',
|
8 |
+
'framerate': 'i',
|
9 |
+
'resolution': 'i', # 0=lo 1 = Hi
|
10 |
+
'numbeams': 'i', # 48 Lo 96 Hi for standard mode
|
11 |
+
'samplerate': 'f',
|
12 |
+
'samplesperchannel': 'i',
|
13 |
+
'receivergain': 'i', # 0-40 dB
|
14 |
+
'windowstart': 'i', # Windowstart 1 to 31
|
15 |
+
'windowlength': 'i', # Windowlength coded as 0 to 3
|
16 |
+
'reverse': 'i',
|
17 |
+
'serialnumber': 'i',
|
18 |
+
'date': '32s', # date file was made
|
19 |
+
'idstring': '256s', # User supplied identification notes
|
20 |
+
'id1': 'i', # four user supplied integers
|
21 |
+
'id2': 'i',
|
22 |
+
'id3': 'i',
|
23 |
+
'id4': 'i',
|
24 |
+
'startframe': 'i', # used if this is a snippet file from source file
|
25 |
+
'endframe': 'i', # Used if this is a snippet file from source file
|
26 |
+
'timelapse': 'i', # Logic 0 or 1 (1 = timelapse active);
|
27 |
+
'recordInterval': 'i', # Ask Bill
|
28 |
+
'radioseconds': 'i', # Needed for timelapse -- ask Bill
|
29 |
+
'frameinterval': 'i', # Interval between frames in time lapse
|
30 |
+
'userassigned': '136s', # User assigned space
|
31 |
+
}
|
32 |
+
|
33 |
+
file_attributes_4 = {
|
34 |
+
'version': '4s',
|
35 |
+
'numframes': 'i',
|
36 |
+
'framerate': 'i',
|
37 |
+
'resolution': 'i', # 0=lo 1 = Hi
|
38 |
+
'numbeams': 'i', # 48 Lo 96 Hi for standard mode
|
39 |
+
'samplerate': 'f',
|
40 |
+
'samplesperchannel': 'i',
|
41 |
+
'receivergain': 'i', # 0-40 dB
|
42 |
+
'windowstart': 'i', # Windowstart 1 to 31
|
43 |
+
'windowlength': 'i', # Windowlength coded as 0 to 3
|
44 |
+
'reverse': 'i',
|
45 |
+
'serialnumber': 'i',
|
46 |
+
'date': '32s', # date file was made
|
47 |
+
'idstring': '256s', # User supplied identification notes
|
48 |
+
'id1': 'i', # four user supplied integers
|
49 |
+
'id2': 'i',
|
50 |
+
'id3': 'i',
|
51 |
+
'id4': 'i',
|
52 |
+
'startframe': 'i', # used if this is a snippet file from source file
|
53 |
+
'endframe': 'i', # Used if this is a snippet file from source file
|
54 |
+
'timelapse': 'i', # Logic 0 or 1 (1 = timelapse active);
|
55 |
+
'recordInterval': 'i', # Ask Bill
|
56 |
+
'radioseconds': 'i', # Needed for timelapse -- ask Bill
|
57 |
+
'frameinterval': 'i', # Interval between frames in time lapse
|
58 |
+
'userassigned': '136s', # User assigned space
|
59 |
+
}
|
60 |
+
|
61 |
+
file_attributes_5 = {
|
62 |
+
'version': '4s',
|
63 |
+
'numframes': 'I', # Total frames in file
|
64 |
+
'framerate': 'I', # Initial recorded frame rate
|
65 |
+
'resolution': 'I', # Non-zero if HF, zero if LF
|
66 |
+
'numbeams': 'I', # ARIS 3000 = 128/64, ARIS 1800 = 96/48, ARIS 1200 = 48
|
67 |
+
'samplerate': 'f', # 1/Sample Period
|
68 |
+
'samplesperchannel': 'I', # Number of range samples in each beam
|
69 |
+
'receivergain': 'I', # Relative gain in dB: 0 - 40
|
70 |
+
'windowstart': 'f', # Image window start range in meters (code [0..31] in DIDSON)
|
71 |
+
'windowlength': 'f', # Image window length in meters (code [0..3] in DIDSON)
|
72 |
+
'reverse': 'I', # Non-zero = lens down (DIDSON) or lens up (ARIS), zero = opposite
|
73 |
+
'serialnumber': 'I', # Sonar serial number
|
74 |
+
'strdate': '32s', # Date that file was recorded
|
75 |
+
'idstring': '256s', # User input to identify file in 256 characters
|
76 |
+
'id1': 'i', # User-defined integer quantity
|
77 |
+
'id2': 'i', # User-defined integer quantity
|
78 |
+
'id3': 'i', # User-defined integer quantity
|
79 |
+
'id4': 'i', # User-defined integer quantity
|
80 |
+
'startframe': 'I', # First frame number from source file (for DIDSON snippet files)
|
81 |
+
'endframe': 'I', # Last frame number from source file (for DIDSON snippet files)
|
82 |
+
'timelapse': 'I', # Non-zero indicates time lapse recording
|
83 |
+
'recordInterval': 'I', # Number of frames/seconds between recorded frames
|
84 |
+
'radioseconds': 'I', # Frames or seconds interval
|
85 |
+
'frameinterval': 'I', # Record every Nth frame
|
86 |
+
'flags': 'I', # See DDF_04 file format document
|
87 |
+
'auxflags': 'I', # See DDF_04 file format document
|
88 |
+
'sspd': 'I', # Sound velocity in water
|
89 |
+
'flags3d': 'I', # See DDF_04 file format document
|
90 |
+
'softwareversion': 'I', # DIDSON software version that recorded the file
|
91 |
+
'watertemperature': 'I', # Water temperature code: 0 = 5-15C, 1 = 15-25C, 2 = 25-35C
|
92 |
+
'salinity': 'I', # Salinity code: 0 = fresh, 1 = brackish, 2 = salt
|
93 |
+
'pulselength': 'I', # Added for ARIS but not used
|
94 |
+
'txmode': 'I', # Added for ARIS but not used
|
95 |
+
'versionfgpa': 'I', # Reserved for future use
|
96 |
+
'versionpsuc': 'I', # Reserved for future use
|
97 |
+
'thumbnailfi': 'I', # Frame index of frame used for thumbnail image of file
|
98 |
+
'filesize': 'Q', # Total file size in bytes
|
99 |
+
'optionalheadersize': 'Q', # Reserved for future use
|
100 |
+
'optionaltailsize': 'Q', # Reserved for future use
|
101 |
+
'versionminor': 'I', # DIDSON_ADJUSTED_VERSION_MINOR
|
102 |
+
'largelens': 'I', # Non-zero if telephoto lens (large lens, hi-res lens, big lens) is present
|
103 |
+
'userassigned': '568s', # Free space for user
|
104 |
+
}
|
105 |
+
|
106 |
+
frame_attributes_3 = {
|
107 |
+
'framenumber': 'i',
|
108 |
+
'frametime': 'i',
|
109 |
+
'frametime2': 'i',
|
110 |
+
'version': '4s',
|
111 |
+
'status': 'i',
|
112 |
+
'year': 'i',
|
113 |
+
'month': 'i',
|
114 |
+
'day': 'i',
|
115 |
+
'hour': 'i',
|
116 |
+
'minute': 'i',
|
117 |
+
'second': 'i',
|
118 |
+
'hsecond': 'i',
|
119 |
+
'transmit': 'i', # bit2 = 2.0 MHz, bit1 = Enable, bit0 = HF_MODE
|
120 |
+
'windowstart': 'i', # This will be updated at the end of this routine
|
121 |
+
'windowlength': 'i', # Add 2 if low resolution (index between 1 and 6)
|
122 |
+
'threshold': 'i',
|
123 |
+
'intensity': 'i',
|
124 |
+
'receivergain': 'i',
|
125 |
+
'degc1': 'i',
|
126 |
+
'degc2': 'i',
|
127 |
+
'humidity': 'i',
|
128 |
+
'focus': 'i',
|
129 |
+
'battery': 'i',
|
130 |
+
'status1': '16s', # User defined and supplied
|
131 |
+
'status2': '8s', # User defined and supplied
|
132 |
+
'panwcom': 'f', # Return from Pan/Tilt if used when compass present
|
133 |
+
'tiltwcom': 'f', # Return from Pan/Tilt if used when compass is present
|
134 |
+
'velocity': 'f', # Platform variables supplied by user
|
135 |
+
'depth': 'f',
|
136 |
+
'altitude': 'f',
|
137 |
+
'pitch': 'f',
|
138 |
+
'pitchrate': 'f',
|
139 |
+
'roll': 'f',
|
140 |
+
'rollrate': 'f',
|
141 |
+
'heading': 'f',
|
142 |
+
'headingrate': 'f',
|
143 |
+
'sonarpan': 'f',
|
144 |
+
'sonartilt': 'f', # Read from compass if used, Read from Pan/Tilt if used and no compass
|
145 |
+
'sonarroll': 'f', # Read from compass if used, Read from Pan/Tilt if used and no compass
|
146 |
+
'latitude': 'd',
|
147 |
+
'longitude': 'd',
|
148 |
+
'sonarposition': 'f',
|
149 |
+
'configflags': 'i', # bit0: 1=classic, 0=extended windows; bit1: 0=Standard, 1=LR
|
150 |
+
'userassigned': '60s', # Free space for user
|
151 |
+
}
|
152 |
+
|
153 |
+
frame_attributes_4 = {
|
154 |
+
'framenumber': 'i',
|
155 |
+
'frametime': 'i',
|
156 |
+
'frametime2': 'i',
|
157 |
+
'version': '4s',
|
158 |
+
'status': 'i',
|
159 |
+
'year': 'i',
|
160 |
+
'month': 'i',
|
161 |
+
'day': 'i',
|
162 |
+
'hour': 'i',
|
163 |
+
'minute': 'i',
|
164 |
+
'second': 'i',
|
165 |
+
'hsecond': 'i',
|
166 |
+
'transmit': 'i', # bit2 = 2.0 MHz, bit1 = Enable, bit0 = HF_MODE
|
167 |
+
'windowstart': 'i', # This will be updated at the end of this routine
|
168 |
+
'windowlength': 'i', # Add 2 if low resolution (index between 1 and 6)
|
169 |
+
'threshold': 'i',
|
170 |
+
'intensity': 'i',
|
171 |
+
'receivergain': 'i',
|
172 |
+
'degc1': 'i',
|
173 |
+
'degc2': 'i',
|
174 |
+
'humidity': 'i',
|
175 |
+
'focus': 'i',
|
176 |
+
'battery': 'i',
|
177 |
+
'status1': '16s', # User defined and supplied
|
178 |
+
'status2': '8s', # User defined and supplied
|
179 |
+
'panwcom': 'f', # Return from Pan/Tilt if used when compass present
|
180 |
+
'tiltwcom': 'f', # Return from Pan/Tilt if used when compass is present
|
181 |
+
'velocity': 'f', # Platform variables supplied by user
|
182 |
+
'depth': 'f',
|
183 |
+
'altitude': 'f',
|
184 |
+
'pitch': 'f',
|
185 |
+
'pitchrate': 'f',
|
186 |
+
'roll': 'f',
|
187 |
+
'rollrate': 'f',
|
188 |
+
'heading': 'f',
|
189 |
+
'headingrate': 'f',
|
190 |
+
'sonarpan': 'f',
|
191 |
+
'sonartilt': 'f', # Read from compass if used, Read from Pan/Tilt if used and no compass
|
192 |
+
'sonarroll': 'f', # Read from compass if used, Read from Pan/Tilt if used and no compass
|
193 |
+
'latitude': 'd',
|
194 |
+
'longitude': 'd',
|
195 |
+
'sonarposition': 'f',
|
196 |
+
'configflags': 'i', # bit0: 1=classic, 0=extended windows; bit1: 0=Standard, 1=LR
|
197 |
+
'userassigned': '828s', # Move pointer to end of frame header of length 1024 bytes
|
198 |
+
}
|
199 |
+
|
200 |
+
frame_attributes_5 = {
|
201 |
+
'framenumber': 'I',
|
202 |
+
'frametime': 'Q', # Recording timestamp
|
203 |
+
'version': '4s',
|
204 |
+
'status': 'I',
|
205 |
+
'sonartimestamp': 'Q',
|
206 |
+
'tsday': 'I',
|
207 |
+
'tshour': 'I',
|
208 |
+
'tsminute': 'I',
|
209 |
+
'tssecond': 'I',
|
210 |
+
'tshsecond': 'I',
|
211 |
+
'transmitmode': 'I',
|
212 |
+
'windowstart': 'f',
|
213 |
+
'windowlength': 'f',
|
214 |
+
'threshold': 'I',
|
215 |
+
'intensity': 'i',
|
216 |
+
'receivergain': 'I',
|
217 |
+
'degc1': 'I',
|
218 |
+
'degc2': 'I',
|
219 |
+
'humidity': 'I',
|
220 |
+
'focus': 'I',
|
221 |
+
'battery': 'I',
|
222 |
+
'uservalue1': 'f',
|
223 |
+
'uservalue2': 'f',
|
224 |
+
'uservalue3': 'f',
|
225 |
+
'uservalue4': 'f',
|
226 |
+
'uservalue5': 'f',
|
227 |
+
'uservalue6': 'f',
|
228 |
+
'uservalue7': 'f',
|
229 |
+
'uservalue8': 'f',
|
230 |
+
'velocity': 'f',
|
231 |
+
'depth': 'f',
|
232 |
+
'altitude': 'f',
|
233 |
+
'pitch': 'f',
|
234 |
+
'pitchrate': 'f',
|
235 |
+
'roll': 'f',
|
236 |
+
'rollrate': 'f',
|
237 |
+
'heading': 'f',
|
238 |
+
'headingrate': 'f',
|
239 |
+
'compassheading': 'f',
|
240 |
+
'compasspitch': 'f',
|
241 |
+
'compassroll': 'f',
|
242 |
+
'latitude': 'd',
|
243 |
+
'longitude': 'd',
|
244 |
+
'sonarposition': 'f',
|
245 |
+
'configflags': 'I',
|
246 |
+
'beamtilt': 'f',
|
247 |
+
'targetrange': 'f',
|
248 |
+
'targetbearing': 'f',
|
249 |
+
'targetpresent': 'I',
|
250 |
+
'firmwarerevision': 'I',
|
251 |
+
'flags': 'I',
|
252 |
+
'sourceframe': 'I',
|
253 |
+
'watertemp': 'f',
|
254 |
+
'timerperiod': 'I',
|
255 |
+
'sonarx': 'f',
|
256 |
+
'sonary': 'f',
|
257 |
+
'sonarz': 'f',
|
258 |
+
'sonarpan': 'f',
|
259 |
+
'sonartilt': 'f',
|
260 |
+
'sonarroll': 'f',
|
261 |
+
'panpnnl': 'f',
|
262 |
+
'tiltpnnl': 'f',
|
263 |
+
'rollpnnl': 'f',
|
264 |
+
'vehicletime': 'd',
|
265 |
+
'timeggk': 'f',
|
266 |
+
'dateggk': 'I',
|
267 |
+
'qualityggk': 'I',
|
268 |
+
'numsatsggk': 'I',
|
269 |
+
'dopggk': 'f',
|
270 |
+
'ehtggk': 'f',
|
271 |
+
'heavetss': 'f',
|
272 |
+
'yeargps': 'I',
|
273 |
+
'monthgps': 'I',
|
274 |
+
'daygps': 'I',
|
275 |
+
'hourgps': 'I',
|
276 |
+
'minutegps': 'I',
|
277 |
+
'secondgps': 'I',
|
278 |
+
'hsecondgps': 'I',
|
279 |
+
'sonarpanoffset': 'f',
|
280 |
+
'sonartiltoffset': 'f',
|
281 |
+
'sonarrolloffset': 'f',
|
282 |
+
'sonarxoffset': 'f',
|
283 |
+
'sonaryoffset': 'f',
|
284 |
+
'sonarzoffset': 'f',
|
285 |
+
'tmatrix': '64s',
|
286 |
+
'samplerate': 'f',
|
287 |
+
'accellx': 'f',
|
288 |
+
'accelly': 'f',
|
289 |
+
'accellz': 'f',
|
290 |
+
'pingmode': 'I',
|
291 |
+
'frequencyhilow': 'I',
|
292 |
+
'pulsewidth': 'I',
|
293 |
+
'cycleperiod': 'I',
|
294 |
+
'sampleperiod': 'I',
|
295 |
+
'transmitenable': 'I',
|
296 |
+
'framerate': 'f',
|
297 |
+
'soundspeed': 'f',
|
298 |
+
'samplesperbeam': 'I',
|
299 |
+
'enable150v': 'I',
|
300 |
+
'samplestartdelay': 'I',
|
301 |
+
'largelens': 'I',
|
302 |
+
'thesystemtype': 'I',
|
303 |
+
'sonarserialnumber': 'I',
|
304 |
+
'encryptedkey': 'Q',
|
305 |
+
'ariserrorflagsuint': 'I',
|
306 |
+
'missedpackets': 'I',
|
307 |
+
'arisappversion': 'I',
|
308 |
+
'available2': 'I',
|
309 |
+
'reorderedsamples': 'I',
|
310 |
+
'salinity': 'I',
|
311 |
+
'pressure': 'f',
|
312 |
+
'batteryvoltage': 'f',
|
313 |
+
'mainvoltage': 'f',
|
314 |
+
'switchvoltage': 'f',
|
315 |
+
'focusmotormoving': 'I',
|
316 |
+
'voltagechanging': 'I',
|
317 |
+
'focustimeoutfault': 'I',
|
318 |
+
'focusovercurrentfault': 'I',
|
319 |
+
'focusnotfoundfault': 'I',
|
320 |
+
'focusstalledfault': 'I',
|
321 |
+
'fpgatimeoutfault': 'I',
|
322 |
+
'fpgabusyfault': 'I',
|
323 |
+
'fpgastuckfault': 'I',
|
324 |
+
'cputempfault': 'I',
|
325 |
+
'psutempfault': 'I',
|
326 |
+
'watertempfault': 'I',
|
327 |
+
'humidityfault': 'I',
|
328 |
+
'pressurefault': 'I',
|
329 |
+
'voltagereadfault': 'I',
|
330 |
+
'voltagewritefault': 'I',
|
331 |
+
'focuscurrentposition': 'I',
|
332 |
+
'targetpan': 'f',
|
333 |
+
'targettilt': 'f',
|
334 |
+
'targetroll': 'f',
|
335 |
+
'panmotorerrorcode': 'I',
|
336 |
+
'tiltmotorerrorcode': 'I',
|
337 |
+
'rollmotorerrorcode': 'I',
|
338 |
+
'panabsposition': 'f',
|
339 |
+
'tiltabsposition': 'f',
|
340 |
+
'rollabsposition': 'f',
|
341 |
+
'panaccelx': 'f',
|
342 |
+
'panaccely': 'f',
|
343 |
+
'panaccelz': 'f',
|
344 |
+
'tiltaccelx': 'f',
|
345 |
+
'tiltaccely': 'f',
|
346 |
+
'tiltaccelz': 'f',
|
347 |
+
'rollaccelx': 'f',
|
348 |
+
'rollaccely': 'f',
|
349 |
+
'rollaccelz': 'f',
|
350 |
+
'appliedsettings': 'I',
|
351 |
+
'constrainedsettings': 'I',
|
352 |
+
'invalidsettings': 'I',
|
353 |
+
'enableinterpacketdelay': 'I',
|
354 |
+
'interpacketdelayperiod': 'I',
|
355 |
+
'uptime': 'I',
|
356 |
+
'arisappversionmajor': 'H',
|
357 |
+
'arisappversionminor': 'H',
|
358 |
+
'gotime': 'Q',
|
359 |
+
'panvelocity': 'f',
|
360 |
+
'tiltvelocity': 'f',
|
361 |
+
'rollvelocity': 'f',
|
362 |
+
'sentinel': 'I',
|
363 |
+
'userassigned': '292s', # Free space for user
|
364 |
+
}
|
requirements.txt
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
flask
|
2 |
+
requests
|
3 |
+
black
|
4 |
+
|
5 |
+
matplotlib>=3.2.2
|
6 |
+
numpy>=1.18.5
|
7 |
+
opencv-python>=4.1.2
|
8 |
+
Pillow
|
9 |
+
PyYAML>=5.3.1
|
10 |
+
scipy>=1.4.1
|
11 |
+
torch>=1.9.0
|
12 |
+
torchvision>=0.8.1
|
13 |
+
tqdm>=4.41.0
|
14 |
+
|
15 |
+
tensorboard>=2.4.1
|
16 |
+
|
17 |
+
seaborn>=0.11.0
|
18 |
+
pandas
|
19 |
+
|
20 |
+
thop # FLOPs computation
|
21 |
+
|
22 |
+
pycocotools
|
23 |
+
filterpy
|
24 |
+
celery
|
25 |
+
redis
|
26 |
+
boto3
|
27 |
+
|
28 |
+
|
29 |
+
# YOLOv5 requirements
|
30 |
+
# Usage: pip install -r requirements.txt
|
31 |
+
|
32 |
+
# Base ------------------------------------------------------------------------
|
33 |
+
--extra-index-url https://download.pytorch.org/whl/cu113
|
34 |
+
torch
|
35 |
+
gitpython>=3.1.30
|
36 |
+
matplotlib>=3.3
|
37 |
+
numpy>=1.18.5
|
38 |
+
opencv-python>=4.1.1
|
39 |
+
Pillow>=7.1.2
|
40 |
+
psutil # system resources
|
41 |
+
PyYAML>=5.3.1
|
42 |
+
requests>=2.23.0
|
43 |
+
scipy>=1.4.1
|
44 |
+
thop>=0.1.1 # FLOPs computation
|
45 |
+
torch>=1.7.0 # see https://pytorch.org/get-started/locally (recommended)
|
46 |
+
torchvision>=0.8.1
|
47 |
+
tqdm>=4.64.0
|
48 |
+
ultralytics>=8.0.111
|
49 |
+
# protobuf<=3.20.1 # https://github.com/ultralytics/yolov5/issues/8012
|
50 |
+
|
51 |
+
# Logging ---------------------------------------------------------------------
|
52 |
+
# tensorboard>=2.4.1
|
53 |
+
# clearml>=1.2.0
|
54 |
+
# comet
|
55 |
+
|
56 |
+
# Plotting --------------------------------------------------------------------
|
57 |
+
pandas>=1.1.4
|
58 |
+
seaborn>=0.11.0
|
59 |
+
|
60 |
+
# Export ----------------------------------------------------------------------
|
61 |
+
# coremltools>=6.0 # CoreML export
|
62 |
+
# onnx>=1.10.0 # ONNX export
|
63 |
+
# onnx-simplifier>=0.4.1 # ONNX simplifier
|
64 |
+
# nvidia-pyindex # TensorRT export
|
65 |
+
# nvidia-tensorrt # TensorRT export
|
66 |
+
# scikit-learn<=1.1.2 # CoreML quantization
|
67 |
+
# tensorflow>=2.4.0 # TF exports (-cpu, -aarch64, -macos)
|
68 |
+
# tensorflowjs>=3.9.0 # TF.js export
|
69 |
+
# openvino-dev # OpenVINO export
|
70 |
+
|
71 |
+
# Deploy ----------------------------------------------------------------------
|
72 |
+
setuptools>=65.5.1 # Snyk vulnerability fix
|
73 |
+
# tritonclient[all]~=2.24.0
|
74 |
+
|
75 |
+
# Extras ----------------------------------------------------------------------
|
76 |
+
# ipython # interactive notebook
|
77 |
+
# mss # screenshots
|
78 |
+
# albumentations>=1.0.3
|
79 |
+
# pycocotools>=2.0.6 # COCO mAP
|
state_handler.py
ADDED
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aris import create_metadata_table
|
2 |
+
|
3 |
+
example_metadata = {
|
4 |
+
"FILE_NAME": "static/example_metadata/fisheye",
|
5 |
+
"FRAME_RATE": 6.548702716827393,
|
6 |
+
"UPSTREAM_FISH": 0,
|
7 |
+
"DOWNSTREAM_FISH": 0,
|
8 |
+
"NONDIRECTIONAL_FISH": 14,
|
9 |
+
"TOTAL_FISH": 14,
|
10 |
+
"TOTAL_FRAMES": 644,
|
11 |
+
"EXPECTED_FRAMES": -1,
|
12 |
+
"TOTAL_TIME": "0:01:38",
|
13 |
+
"EXPECTED_TIME": "0:00:00",
|
14 |
+
"UPSTREAM_MOTION": "Right To Left",
|
15 |
+
"COUNT_FILE_NAME": "N/A",
|
16 |
+
"EDITOR_ID": "N/A",
|
17 |
+
"INTENSITY": "0.0 dB",
|
18 |
+
"THRESHOLD": "0.0 dB",
|
19 |
+
"WINDOW_START": 1,
|
20 |
+
"WINDOW_END": 17,
|
21 |
+
"WATER_TEMP": "13 degC",
|
22 |
+
"FISH": [
|
23 |
+
{
|
24 |
+
"FILE": 1,
|
25 |
+
"TOTAL": 1,
|
26 |
+
"FRAME_NUM": 12,
|
27 |
+
"DIR": " N/A",
|
28 |
+
"R": 13.403139282569885,
|
29 |
+
"THETA": 0.1706,
|
30 |
+
"L": 63.739999999999995,
|
31 |
+
"DR": -1,
|
32 |
+
"LDR": -1,
|
33 |
+
"ASPECT": -1,
|
34 |
+
"TIME": "11:54:40",
|
35 |
+
"DATE": "2018-07-09",
|
36 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
37 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
38 |
+
"PAN": None,
|
39 |
+
"TILT": None,
|
40 |
+
"ROLL": 0,
|
41 |
+
"SPECIES": "Unknown",
|
42 |
+
"MOTION": "Running <-->",
|
43 |
+
"Q": -1,
|
44 |
+
"N": -1,
|
45 |
+
"COMMENT": ""
|
46 |
+
}, {
|
47 |
+
"FILE": 1,
|
48 |
+
"TOTAL": 2,
|
49 |
+
"FRAME_NUM": 35,
|
50 |
+
"DIR": " N/A",
|
51 |
+
"R": 13.206211097755432,
|
52 |
+
"THETA": -9.1195,
|
53 |
+
"L": 73.33,
|
54 |
+
"DR": -1,
|
55 |
+
"LDR": -1,
|
56 |
+
"ASPECT": -1,
|
57 |
+
"TIME": "11:54:44",
|
58 |
+
"DATE": "2018-07-09",
|
59 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
60 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
61 |
+
"PAN": None,
|
62 |
+
"TILT": None,
|
63 |
+
"ROLL": 0,
|
64 |
+
"SPECIES": "Unknown",
|
65 |
+
"MOTION": "Running <-->",
|
66 |
+
"Q": -1,
|
67 |
+
"N": -1,
|
68 |
+
"COMMENT": ""
|
69 |
+
}, {
|
70 |
+
"FILE": 1,
|
71 |
+
"TOTAL": 3,
|
72 |
+
"FRAME_NUM": 122,
|
73 |
+
"DIR": " N/A",
|
74 |
+
"R": 13.219339643409729,
|
75 |
+
"THETA": -9.3961,
|
76 |
+
"L": 84.77,
|
77 |
+
"DR": -1,
|
78 |
+
"LDR": -1,
|
79 |
+
"ASPECT": -1,
|
80 |
+
"TIME": "11:54:58",
|
81 |
+
"DATE": "2018-07-09",
|
82 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
83 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
84 |
+
"PAN": None,
|
85 |
+
"TILT": None,
|
86 |
+
"ROLL": 0,
|
87 |
+
"SPECIES": "Unknown",
|
88 |
+
"MOTION": "Running <-->",
|
89 |
+
"Q": -1,
|
90 |
+
"N": -1,
|
91 |
+
"COMMENT": ""
|
92 |
+
}, {
|
93 |
+
"FILE": 1,
|
94 |
+
"TOTAL": 4,
|
95 |
+
"FRAME_NUM": 123,
|
96 |
+
"DIR": "N/A",
|
97 |
+
"R": 12.996154367286682,
|
98 |
+
"THETA": 10.7991,
|
99 |
+
"L": 59.919999999999995,
|
100 |
+
"DR": -1,
|
101 |
+
"LDR": -1,
|
102 |
+
"ASPECT": -1,
|
103 |
+
"TIME": "11:54:58",
|
104 |
+
"DATE": "2018-07-09",
|
105 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
106 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
107 |
+
"PAN": None,
|
108 |
+
"TILT": None,
|
109 |
+
"ROLL": 0,
|
110 |
+
"SPECIES": "Unknown",
|
111 |
+
"MOTION": "Running <-->",
|
112 |
+
"Q": -1,
|
113 |
+
"N": -1,
|
114 |
+
"COMMENT": ""
|
115 |
+
}, {
|
116 |
+
"FILE": 1,
|
117 |
+
"TOTAL": 5,
|
118 |
+
"FRAME_NUM": 130,
|
119 |
+
"DIR": " N/A",
|
120 |
+
"R": 12.484141086769105,
|
121 |
+
"THETA": -8.2654,
|
122 |
+
"L": 70.89999999999999,
|
123 |
+
"DR": -1,
|
124 |
+
"LDR": -1,
|
125 |
+
"ASPECT": -1,
|
126 |
+
"TIME": "11:54:59",
|
127 |
+
"DATE": "2018-07-09",
|
128 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
129 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
130 |
+
"PAN": None,
|
131 |
+
"TILT": None,
|
132 |
+
"ROLL": 0,
|
133 |
+
"SPECIES": "Unknown",
|
134 |
+
"MOTION": "Running <-->",
|
135 |
+
"Q": -1,
|
136 |
+
"N": -1,
|
137 |
+
"COMMENT": ""
|
138 |
+
}, {
|
139 |
+
"FILE": 1,
|
140 |
+
"TOTAL": 6,
|
141 |
+
"FRAME_NUM": 218,
|
142 |
+
"DIR": " N/A",
|
143 |
+
"R": 13.232468189064026,
|
144 |
+
"THETA": -9.3961,
|
145 |
+
"L": 77.25999999999999,
|
146 |
+
"DR": -1,
|
147 |
+
"LDR": -1,
|
148 |
+
"ASPECT": -1,
|
149 |
+
"TIME": "11:55:12",
|
150 |
+
"DATE": "2018-07-09",
|
151 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
152 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
153 |
+
"PAN": None,
|
154 |
+
"TILT": None,
|
155 |
+
"ROLL": 0,
|
156 |
+
"SPECIES": "Unknown",
|
157 |
+
"MOTION": "Running <-->",
|
158 |
+
"Q": -1,
|
159 |
+
"N": -1,
|
160 |
+
"COMMENT": ""
|
161 |
+
}, {
|
162 |
+
"FILE": 1,
|
163 |
+
"TOTAL": 7,
|
164 |
+
"FRAME_NUM": 278,
|
165 |
+
"DIR": " N/A",
|
166 |
+
"R": 13.967666745704651,
|
167 |
+
"THETA": -12.8758,
|
168 |
+
"L": 37.51,
|
169 |
+
"DR": -1,
|
170 |
+
"LDR": -1,
|
171 |
+
"ASPECT": -1,
|
172 |
+
"TIME": "11:55:22",
|
173 |
+
"DATE": "2018-07-09",
|
174 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
175 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
176 |
+
"PAN": None,
|
177 |
+
"TILT": None,
|
178 |
+
"ROLL": 0,
|
179 |
+
"SPECIES": "Unknown",
|
180 |
+
"MOTION": "Running <-->",
|
181 |
+
"Q": -1,
|
182 |
+
"N": -1,
|
183 |
+
"COMMENT": ""
|
184 |
+
}, {
|
185 |
+
"FILE": 1,
|
186 |
+
"TOTAL": 8,
|
187 |
+
"FRAME_NUM": 302,
|
188 |
+
"DIR": " N/A",
|
189 |
+
"R": 13.25872528037262,
|
190 |
+
"THETA": -9.1195,
|
191 |
+
"L": 79.5,
|
192 |
+
"DR": -1,
|
193 |
+
"LDR": -1,
|
194 |
+
"ASPECT": -1,
|
195 |
+
"TIME": "11:55:25",
|
196 |
+
"DATE": "2018-07-09",
|
197 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
198 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
199 |
+
"PAN": None,
|
200 |
+
"TILT": None,
|
201 |
+
"ROLL": 0,
|
202 |
+
"SPECIES": "Unknown",
|
203 |
+
"MOTION": "Running <-->",
|
204 |
+
"Q": -1,
|
205 |
+
"N": -1,
|
206 |
+
"COMMENT": ""
|
207 |
+
}, {
|
208 |
+
"FILE": 1,
|
209 |
+
"TOTAL": 9,
|
210 |
+
"FRAME_NUM": 331,
|
211 |
+
"DIR": " N/A",
|
212 |
+
"R": 13.25872528037262,
|
213 |
+
"THETA": -9.1195,
|
214 |
+
"L": 80.67,
|
215 |
+
"DR": -1,
|
216 |
+
"LDR": -1,
|
217 |
+
"ASPECT": -1,
|
218 |
+
"TIME": "11:55:30",
|
219 |
+
"DATE": "2018-07-09",
|
220 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
221 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
222 |
+
"PAN": None,
|
223 |
+
"TILT": None,
|
224 |
+
"ROLL": 0,
|
225 |
+
"SPECIES": "Unknown",
|
226 |
+
"MOTION": "Running <-->",
|
227 |
+
"Q": -1,
|
228 |
+
"N": -1,
|
229 |
+
"COMMENT": ""
|
230 |
+
}, {
|
231 |
+
"FILE": 1,
|
232 |
+
"TOTAL": 10,
|
233 |
+
"FRAME_NUM": 450,
|
234 |
+
"DIR": " N/A",
|
235 |
+
"R": 13.324368008644104,
|
236 |
+
"THETA": -8.5535,
|
237 |
+
"L": 83.1,
|
238 |
+
"DR": -1,
|
239 |
+
"LDR": -1,
|
240 |
+
"ASPECT": -1,
|
241 |
+
"TIME": "11:55:48",
|
242 |
+
"DATE": "2018-07-09",
|
243 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
244 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
245 |
+
"PAN": None,
|
246 |
+
"TILT": None,
|
247 |
+
"ROLL": 0,
|
248 |
+
"SPECIES": "Unknown",
|
249 |
+
"MOTION": "Running <-->",
|
250 |
+
"Q": -1,
|
251 |
+
"N": -1,
|
252 |
+
"COMMENT": ""
|
253 |
+
}, {
|
254 |
+
"FILE": 1,
|
255 |
+
"TOTAL": 11,
|
256 |
+
"FRAME_NUM": 495,
|
257 |
+
"DIR": " N/A",
|
258 |
+
"R": 13.481910556495666,
|
259 |
+
"THETA": -9.1195,
|
260 |
+
"L": 86.39,
|
261 |
+
"DR": -1,
|
262 |
+
"LDR": -1,
|
263 |
+
"ASPECT": -1,
|
264 |
+
"TIME": "11:55:55",
|
265 |
+
"DATE": "2018-07-09",
|
266 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
267 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
268 |
+
"PAN": None,
|
269 |
+
"TILT": None,
|
270 |
+
"ROLL": 0,
|
271 |
+
"SPECIES": "Unknown",
|
272 |
+
"MOTION": "Running <-->",
|
273 |
+
"Q": -1,
|
274 |
+
"N": -1,
|
275 |
+
"COMMENT": ""
|
276 |
+
}, {
|
277 |
+
"FILE": 1,
|
278 |
+
"TOTAL": 12,
|
279 |
+
"FRAME_NUM": 526,
|
280 |
+
"DIR": " N/A",
|
281 |
+
"R": 13.04866854990387,
|
282 |
+
"THETA": 10.5397,
|
283 |
+
"L": 55.37,
|
284 |
+
"DR": -1,
|
285 |
+
"LDR": -1,
|
286 |
+
"ASPECT": -1,
|
287 |
+
"TIME": "11:56:00",
|
288 |
+
"DATE": "2018-07-09",
|
289 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
290 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
291 |
+
"PAN": None,
|
292 |
+
"TILT": None,
|
293 |
+
"ROLL": 0,
|
294 |
+
"SPECIES": "Unknown",
|
295 |
+
"MOTION": "Running <-->",
|
296 |
+
"Q": -1,
|
297 |
+
"N": -1,
|
298 |
+
"COMMENT": ""
|
299 |
+
}, {
|
300 |
+
"FILE": 1,
|
301 |
+
"TOTAL": 13,
|
302 |
+
"FRAME_NUM": 538,
|
303 |
+
"DIR": " N/A",
|
304 |
+
"R": 13.416267828224182,
|
305 |
+
"THETA": -9.668,
|
306 |
+
"L": 82.38,
|
307 |
+
"DR": -1,
|
308 |
+
"LDR": -1,
|
309 |
+
"ASPECT": -1,
|
310 |
+
"TIME": "11:56:02",
|
311 |
+
"DATE": "2018-07-09",
|
312 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
313 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
314 |
+
"PAN": None,
|
315 |
+
"TILT": None,
|
316 |
+
"ROLL": 0,
|
317 |
+
"SPECIES": "Unknown",
|
318 |
+
"MOTION": "Running <-->",
|
319 |
+
"Q": -1,
|
320 |
+
"N": -1,
|
321 |
+
"COMMENT": ""
|
322 |
+
}, {
|
323 |
+
"FILE": 1,
|
324 |
+
"TOTAL": 14,
|
325 |
+
"FRAME_NUM": 624,
|
326 |
+
"DIR": " N/A",
|
327 |
+
"R": 13.29811091733551,
|
328 |
+
"THETA": -8.8385,
|
329 |
+
"L": 77.44,
|
330 |
+
"DR": -1,
|
331 |
+
"LDR": -1,
|
332 |
+
"ASPECT": -1,
|
333 |
+
"TIME": "11:56:16",
|
334 |
+
"DATE": "2018-07-09",
|
335 |
+
"LATITUDE": "N 00 d 0.00000 m",
|
336 |
+
"LONGITUDE": "E 000 d 0.00000 m",
|
337 |
+
"PAN": None,
|
338 |
+
"TILT": None,
|
339 |
+
"ROLL": 0,
|
340 |
+
"SPECIES": "Unknown",
|
341 |
+
"MOTION": "Running <-->",
|
342 |
+
"Q": -1,
|
343 |
+
"N": -1,
|
344 |
+
"COMMENT": ""
|
345 |
+
}
|
346 |
+
],
|
347 |
+
"DATE": "2018-07-09",
|
348 |
+
"START": "11:54:39",
|
349 |
+
"END": "11:56:18"
|
350 |
+
}
|
351 |
+
|
352 |
+
def load_example_result(result, table_headers, info_headers):
|
353 |
+
fish_table, fish_info = create_metadata_table(example_metadata, table_headers, info_headers)
|
354 |
+
result['path_zip'] = ["static/example/input_file_results.zip"]
|
355 |
+
result['path_video'] = ["static/example/input_file_results.mp4"]
|
356 |
+
result['path_json'] = ["static/example/input_file_results.json"]
|
357 |
+
result['path_marking'] = ["static/example/input_file_marking.txt"]
|
358 |
+
result['fish_table'] = [fish_table]
|
359 |
+
result['fish_info'] = [fish_info]
|
360 |
+
|
361 |
+
|
362 |
+
def reset_state(result, state):
|
363 |
+
|
364 |
+
# Reset Result
|
365 |
+
result["path_video"] = []
|
366 |
+
result["path_zip"] = []
|
367 |
+
result["path_json"] = []
|
368 |
+
result["path_marking"] = []
|
369 |
+
result["fish_table"] = []
|
370 |
+
result["fish_info"] = []
|
371 |
+
|
372 |
+
# Reset State
|
373 |
+
state['files'] = []
|
374 |
+
state['index'] = 0
|
375 |
+
state['total'] = 0
|
uploader.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import project_path
|
2 |
+
import os;
|
3 |
+
from datetime import datetime;
|
4 |
+
|
5 |
+
USER_DATA_DIR = "user_data/"
|
6 |
+
|
7 |
+
|
8 |
+
def save_data(bytes, filename):
|
9 |
+
"""Take a file and saved it to a new user_data folder"""
|
10 |
+
|
11 |
+
dirname = create_data_dir()
|
12 |
+
filepath = os.path.join(dirname, filename)
|
13 |
+
|
14 |
+
assert bytes[0:3] == b'DDF'
|
15 |
+
|
16 |
+
with open(filepath, 'wb') as out:
|
17 |
+
out.write(bytes)
|
18 |
+
|
19 |
+
# check this is actually a valid ARIS file to catch any malicious fish scientists
|
20 |
+
try:
|
21 |
+
with open(filepath, 'rb') as file:
|
22 |
+
assert file.read(3) == b'DDF'
|
23 |
+
except:
|
24 |
+
print("Bad file!", filepath)
|
25 |
+
return False, None, None
|
26 |
+
|
27 |
+
return True, filepath, dirname
|
28 |
+
|
29 |
+
|
30 |
+
def allowed_file(filename):
|
31 |
+
"""Only allow an ARIS file to be uploaded."""
|
32 |
+
return '.' in filename and \
|
33 |
+
filename.rsplit('.', 1)[1].lower() in ['aris', 'ddf']
|
34 |
+
|
35 |
+
def create_data_dir():
|
36 |
+
"""Create a (probably) unique directory for a task."""
|
37 |
+
dirname = os.path.join(USER_DATA_DIR, str(int(datetime.now().timestamp())))
|
38 |
+
if os.path.exists(dirname):
|
39 |
+
print("Warning,", dirname, "already exists.")
|
40 |
+
os.makedirs(dirname, exist_ok=True)
|
41 |
+
return dirname
|
visualizer.py
ADDED
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import project_path
|
2 |
+
|
3 |
+
import json
|
4 |
+
import cv2
|
5 |
+
import numpy as np
|
6 |
+
from tqdm import tqdm
|
7 |
+
|
8 |
+
from lib.fish_eye.tracker import Tracker
|
9 |
+
|
10 |
+
|
11 |
+
VERSION = "09/21"
|
12 |
+
PRED_COLOR = (255, 0, 0) # blue
|
13 |
+
WHITE = (255, 255, 255)
|
14 |
+
BLACK = (0, 0, 0)
|
15 |
+
BORDER_PAD = 3
|
16 |
+
LINE_HEIGHT= 22
|
17 |
+
VIDEO_HEIGHT = 700
|
18 |
+
INFO_PANE_WIDTH = 100
|
19 |
+
BOX_THICKNESS = 2
|
20 |
+
FONT_SCALE = 0.65
|
21 |
+
FONT_THICKNESS = 1
|
22 |
+
|
23 |
+
def generate_video_batches(didson, preds_path, frame_rate, video_out_path, gp=None, image_meter_width=None, image_meter_height=None, batch_size=1000):
|
24 |
+
"""Write a visualized video to video_out_path, given a didson object.
|
25 |
+
"""
|
26 |
+
if (gp): gp(0, "Generating results video...")
|
27 |
+
end_frame = didson.info['endframe'] or didson.info['numframes']
|
28 |
+
out = None # need to wait til we have height and width to instantiate video file
|
29 |
+
|
30 |
+
with tqdm(total=end_frame, desc="Generating results video", ncols=0) as pbar:
|
31 |
+
for i in range(0, end_frame, batch_size):
|
32 |
+
batch_end = min(end_frame, i+batch_size)
|
33 |
+
frames = didson.load_frames(start_frame=i, end_frame=batch_end)
|
34 |
+
vid_frames, h, w = get_video_frames(frames, preds_path, frame_rate, image_meter_width, image_meter_height, start_frame=i)
|
35 |
+
|
36 |
+
if out is None:
|
37 |
+
out = cv2.VideoWriter(video_out_path, cv2.VideoWriter_fourcc(*'avc1'), frame_rate, [ int(1.5*w), h ] )
|
38 |
+
|
39 |
+
for j, frame in enumerate(vid_frames):
|
40 |
+
if gp: gp(( (i+j) / end_frame), 'Generating results video...')
|
41 |
+
out.write(frame)
|
42 |
+
pbar.update(1)
|
43 |
+
|
44 |
+
del frames
|
45 |
+
del vid_frames
|
46 |
+
|
47 |
+
out.release()
|
48 |
+
|
49 |
+
def get_video_frames(frames, preds_path, frame_rate, image_meter_width=None, image_meter_height=None, start_frame=0):
|
50 |
+
"""Get visualized video frames ready for output, given raw ARIS/DIDSON frames.
|
51 |
+
Warning: all frames in frames will be stored in memory - careful of OOM errors. Consider processing large files
|
52 |
+
in batches, such as in generate_video_batches()
|
53 |
+
|
54 |
+
Returns:
|
55 |
+
list(np.ndarray), height (int), width (int)
|
56 |
+
"""
|
57 |
+
preds = json.load(open(preds_path, 'r'))
|
58 |
+
pred_lengths = { fish['id'] : "%.2fm" % fish['length'] for fish in preds['fish'] }
|
59 |
+
clip_pr_counts = Tracker.count_dirs(preds)
|
60 |
+
color_map = { fish['id'] : fish['color'] for fish in preds['fish'] }
|
61 |
+
|
62 |
+
# filter JSON, if necessary (for shorter clips)
|
63 |
+
preds['frames'] = preds['frames'][start_frame:]
|
64 |
+
|
65 |
+
vid_frames = []
|
66 |
+
if len(frames):
|
67 |
+
# assumes all frames the same size
|
68 |
+
h, w = frames[0].shape
|
69 |
+
|
70 |
+
# enforce a standard size so that text/box thickness is consistent
|
71 |
+
scale_factor = VIDEO_HEIGHT / h
|
72 |
+
h = VIDEO_HEIGHT
|
73 |
+
w = int(scale_factor*w)
|
74 |
+
|
75 |
+
num_frames = min(len(frames), len(preds['frames']))
|
76 |
+
|
77 |
+
for i, frame_raw in enumerate(frames[:num_frames]):
|
78 |
+
frame_raw = cv2.resize(cv2.cvtColor(frame_raw, cv2.COLOR_GRAY2BGR), (w,h))
|
79 |
+
pred = preds['frames'][i]
|
80 |
+
|
81 |
+
for fish in pred['fish']:
|
82 |
+
xmin, ymin, xmax, ymax = fish['bbox']
|
83 |
+
left = int(round(xmin * w))
|
84 |
+
right = int(round(xmax * w))
|
85 |
+
top = int(round(ymin * h))
|
86 |
+
bottom = int(round(ymax * h))
|
87 |
+
fish_id = str(fish['fish_id'])
|
88 |
+
fish_len = pred_lengths[fish['fish_id']]
|
89 |
+
hexx = color_map[fish['fish_id']].lstrip('#')
|
90 |
+
color = tuple(int(hexx[i:i+2], 16) for i in (0, 2, 4))
|
91 |
+
draw_fish(frame_raw, left, right, top, bottom, color, fish_id, fish_len, anno_align="right")
|
92 |
+
|
93 |
+
# add axis to frame
|
94 |
+
frame_raw = add_axis(frame_raw, image_meter_width, image_meter_height)
|
95 |
+
|
96 |
+
# add info
|
97 |
+
frame_info_panel = np.zeros((h, int(0.5*w), 3)).astype(np.uint8)
|
98 |
+
frame = np.concatenate((frame_info_panel, frame_raw), axis=1)
|
99 |
+
cv2.putText(frame, f'VERSION: {VERSION}', (BORDER_PAD, h-BORDER_PAD-LINE_HEIGHT*4), cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, WHITE, FONT_THICKNESS, cv2.LINE_AA, False)
|
100 |
+
cv2.putText(frame, f'Right count: {clip_pr_counts[0]}', (BORDER_PAD, h-BORDER_PAD-LINE_HEIGHT*3), cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, WHITE, FONT_THICKNESS, cv2.LINE_AA, False)
|
101 |
+
cv2.putText(frame, f'Left count: {clip_pr_counts[FONT_THICKNESS]}', (BORDER_PAD, h-BORDER_PAD-LINE_HEIGHT*2), cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, WHITE, FONT_THICKNESS, cv2.LINE_AA, False)
|
102 |
+
cv2.putText(frame, f'Other fish: {clip_pr_counts[2]}', (BORDER_PAD, h-BORDER_PAD-LINE_HEIGHT*1), cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, WHITE, FONT_THICKNESS, cv2.LINE_AA, False)
|
103 |
+
# cv2.putText(frame, f'Upstream: {preds["upstream_direction"]}', (0, h-1-LINE_HEIGHT*1), cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, WHITE, FONT_THICKNESS, cv2.LINE_AA, False)
|
104 |
+
cv2.putText(frame, f'Frame: {i}', (BORDER_PAD, h-BORDER_PAD-LINE_HEIGHT*0), cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, WHITE, FONT_THICKNESS, cv2.LINE_AA, False)
|
105 |
+
|
106 |
+
vid_frames.append(frame)
|
107 |
+
|
108 |
+
return vid_frames, h, w
|
109 |
+
|
110 |
+
def draw_fish(frame, left, right, top, bottom, color, fish_id, fish_len, LINE_HEIGHT=18, anno_align="left"):
|
111 |
+
cv2.rectangle(frame, (left, top), (right, bottom), color, BOX_THICKNESS)
|
112 |
+
|
113 |
+
if anno_align == "left":
|
114 |
+
anno_align = left
|
115 |
+
else:
|
116 |
+
anno_align = right
|
117 |
+
cv2.putText(frame, fish_id, (anno_align, top), cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, color, FONT_THICKNESS, cv2.LINE_AA, False)
|
118 |
+
cv2.putText(frame, fish_len, (anno_align, bottom+int(LINE_HEIGHT/2)), cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, color, FONT_THICKNESS, cv2.LINE_AA, False)
|
119 |
+
|
120 |
+
def add_axis(img, image_meter_width=None, image_meter_height=None):
|
121 |
+
h, w, c = img.shape
|
122 |
+
|
123 |
+
# add black border around image
|
124 |
+
bordersize_t = 25
|
125 |
+
bordersize_l = 45
|
126 |
+
img = cv2.copyMakeBorder(
|
127 |
+
img,
|
128 |
+
bottom=bordersize_t,
|
129 |
+
top=0,
|
130 |
+
left=bordersize_l,
|
131 |
+
right=25, # this helps with text getting cut off
|
132 |
+
borderType=cv2.BORDER_CONSTANT,
|
133 |
+
value=BLACK
|
134 |
+
)
|
135 |
+
|
136 |
+
# add axis
|
137 |
+
axis_thickness = 1
|
138 |
+
img = cv2.line(img, (bordersize_l, h+axis_thickness//2), (w+bordersize_l, h+axis_thickness//2), WHITE, axis_thickness) # x
|
139 |
+
img = cv2.line(img, (bordersize_l-axis_thickness//2, 0), (bordersize_l-axis_thickness//2, h), WHITE, axis_thickness) # y
|
140 |
+
|
141 |
+
# dist between ticks in meters
|
142 |
+
x_inc = 100
|
143 |
+
if image_meter_width and image_meter_width > 0:
|
144 |
+
x_inc = w / image_meter_width / 2 # 0.5m ticks
|
145 |
+
if image_meter_width > 4:
|
146 |
+
x_inc *= 2 # 1m ticks
|
147 |
+
if image_meter_width > 8:
|
148 |
+
x_inc *= 2 # 2m ticks
|
149 |
+
|
150 |
+
# dist between ticks in meters
|
151 |
+
y_inc = 100
|
152 |
+
if image_meter_height and image_meter_height > 0:
|
153 |
+
y_inc = h / image_meter_height / 2 # 0.5m ticks
|
154 |
+
if image_meter_height > 4:
|
155 |
+
y_inc *= 2 # 1m ticks
|
156 |
+
if image_meter_height > 8:
|
157 |
+
y_inc *= 2 # 2m ticks
|
158 |
+
if image_meter_height > 12:
|
159 |
+
y_inc *= 3/2 # 3m ticks
|
160 |
+
|
161 |
+
# tick mark labels
|
162 |
+
def x_label(x):
|
163 |
+
if image_meter_width and image_meter_width > 0:
|
164 |
+
if x_inc < w / image_meter_width: # fractional ticks
|
165 |
+
return "%.1fm" % (x / w * image_meter_width)
|
166 |
+
return "%.0fm" % (x / w * image_meter_width)
|
167 |
+
return str(x) # pixels
|
168 |
+
def y_label(y):
|
169 |
+
if image_meter_height and image_meter_height > 0:
|
170 |
+
if y_inc < y / image_meter_height: # fractional ticks
|
171 |
+
return "%.1fm" % (y / h * image_meter_height)
|
172 |
+
return "%.0fm" % (y / h * image_meter_height)
|
173 |
+
return str(y) # pixels
|
174 |
+
|
175 |
+
# add ticks
|
176 |
+
ticksize = 5
|
177 |
+
x = 0
|
178 |
+
while x < w:
|
179 |
+
img = cv2.line(img, (int(bordersize_l+x), h+axis_thickness//2), (int(bordersize_l+x), h+axis_thickness//2+ticksize), WHITE, axis_thickness)
|
180 |
+
cv2.putText(img, x_label(x), (int(bordersize_l+x), h+axis_thickness//2+LINE_HEIGHT), cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE*3/4, WHITE, FONT_THICKNESS, cv2.LINE_AA, False)
|
181 |
+
x += x_inc
|
182 |
+
y = 0
|
183 |
+
while y < h:
|
184 |
+
img = cv2.line(img, (bordersize_l-axis_thickness//2, int(h-y)), (bordersize_l-axis_thickness//2-ticksize, int(h-y)), WHITE, axis_thickness)
|
185 |
+
ylabel = y_label(y)
|
186 |
+
txt_offset = 13*len(ylabel)
|
187 |
+
cv2.putText(img, y_label(y), (bordersize_l-axis_thickness//2-ticksize - txt_offset, int(h-y)), cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE*3/4, WHITE, FONT_THICKNESS, cv2.LINE_AA, False)
|
188 |
+
y += y_inc
|
189 |
+
|
190 |
+
# resize to original dims
|
191 |
+
return cv2.resize(img, (w,h))
|