oskarastrom commited on
Commit
7a4b92f
1 Parent(s): c37bb1d

First Commit

Browse files
Files changed (17) hide show
  1. .gitignore +18 -0
  2. __init__.py +0 -0
  3. app.py +212 -0
  4. aris.py +505 -0
  5. aws_handler.py +27 -0
  6. dataloader.py +367 -0
  7. dump.rdb +0 -0
  8. file_reader.py +281 -0
  9. inference.py +167 -0
  10. main.py +77 -0
  11. project_path.py +11 -0
  12. pyDIDSON.py +495 -0
  13. pyDIDSON_format.py +364 -0
  14. requirements.txt +79 -0
  15. state_handler.py +375 -0
  16. uploader.py +41 -0
  17. 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))