The study area is marked in red, and is know as “The Elbow.” It is about 145 km west-northwest of Tampa Bay, Florida. The area is a popular offshore fishing area that contains contains hard bottom ridges and is hypothesized to be a paleoshoreline shaped by wave action approximately 12,000 years ago.
The three lines shown represent day-time video transects completed using a towed video camera system that flies about 2-4 meters above the seafloor. Along these transects, fish were annotated at the frame-level, and habitat was annotated every 15 seconds.
Figure S2.1
Habitat was annotated every 15 seconds of video
hab<- read_csv("Data/derived/CBASS/hab.csv")
hab<- hab %>% select(-c(CBASSLat, CBASSLon, Frame_num, Substrate))
hab<- hab %>% filter(Transect!="T1")
hab<- hab %>% mutate(habitat=ifelse(test = Relief=="None", yes = "Sand", no = Relief))
hab<- hab %>% select(-Relief)
print(hab)
Fish were annotated on using C-Vision’s video annotation software at the frame level. Each row is an individual fish, and we can see exactly what frame that fish was seen.
fish_annotations<- read_csv("Data/derived/CBASS/fish_annotations.csv")
print(fish_annotations)
To turn these annotations into counts, each individual fish was matched to the closest habitat observation in time, and the number of annotations for each taxa were then summed.
fish_counts<- bind_rows(mutate(match_cvision_fish2hab(merged_cvision_csv = select(filter(fish_annotations, Transect=="T3"), -Transect), video_num = filter(hab, Transect=="T3")$Vid, video_sec = filter(hab, Transect=="T3")$Sec, fps = 12, vid_length = 1, include_vid_sec = TRUE, include_framenum = FALSE), Transect="T3"),
mutate(match_cvision_fish2hab(merged_cvision_csv = select(filter(fish_annotations, Transect=="T5"), -Transect), video_num = filter(hab, Transect=="T5")$Vid, video_sec = filter(hab, Transect=="T5")$Sec, fps = 12, vid_length = 1, include_vid_sec = TRUE, include_framenum = FALSE), Transect="T5"),
mutate(match_cvision_fish2hab(merged_cvision_csv = select(filter(fish_annotations, Transect=="T6"), -Transect), video_num = filter(hab, Transect=="T6")$Vid, video_sec = filter(hab, Transect=="T6")$Sec, fps = 12, vid_length = 1, include_vid_sec = TRUE, include_framenum = FALSE), Transect="T6")) %>% select(Transect, everything())
fish_counts[is.na(fish_counts)]<- 0
fish_counts<- fish_counts %>% select(Transect, Video, Seconds, everything())
names(fish_counts)[2:3]<- c("Vid", "Sec")
#Merge taxa that are difficult to differentiate consistently
fish_counts<- fish_counts %>% mutate(AMBERJACK_SPP= rowSums(across(starts_with("amberjack_", ignore.case = FALSE)))) %>% select(-starts_with("amberjack_", ignore.case = FALSE))
fish_counts<- fish_counts %>% mutate(BUTTERFLYFISH_SPP= rowSums(across(starts_with("butterflyfish_", ignore.case = FALSE)))) %>% select(-starts_with("butterflyfish_",ignore.case = FALSE))
fish_counts<- fish_counts %>% mutate(PORGY_SPP= rowSums(across(starts_with("porgy_", ignore.case = FALSE)))) %>% select(-starts_with("porgy_",ignore.case = FALSE))
fish_counts<- fish_counts %>% mutate(TRIGGERFISH_SPP= rowSums(across(starts_with("triggerfish_",ignore.case = FALSE)))) %>% select(-starts_with("triggerfish_",ignore.case = FALSE))
fish_counts<- fish_counts %>% select(-seaturtle_spp) #NOt a fish
names(fish_counts)[4:ncol(fish_counts)]<- tolower(names(fish_counts)[4:ncol(fish_counts)])
fish_counts<- fish_counts %>% select(Transect, Vid, Sec, sort_species(names(fish_counts)[4:ncol(fish_counts)]))
fish_counts<- fish_counts %>% left_join(select(hab, timestamp, Transect, Vid, Sec), by=c("Transect", "Vid", "Sec"))
fish_counts<- fish_counts %>% select(Transect, Vid, Sec, timestamp, everything())
print(fish_counts)
altitude<- bind_rows<- bind_rows(mutate(read_tsv("Data/raw/CBASS/sensors/T3_D1/altimeter_readings.tsv"), Transect="T3"),
mutate(read_tsv("Data/raw/CBASS/sensors/T5_D14/altimeter_readings.tsv"), Transect="T5"),
mutate(read_tsv("Data/raw/CBASS/sensors/T6_D13/altimeter_readings.tsv"), Transect="T6"))
altitude<- altitude %>% mutate(timestamp_exact= timestamp+dmicroseconds(u_second))
compass<- bind_rows<- bind_rows(mutate(read_tsv("Data/raw/CBASS/sensors/T3_D1/compass_readings.tsv"), Transect="T3"),
mutate(read_tsv("Data/raw/CBASS/sensors/T5_D14/compass_readings.tsv"), Transect="T5"),
mutate(read_tsv("Data/raw/CBASS/sensors/T6_D13/compass_readings.tsv"), Transect="T6"))
compass<- compass %>% mutate(timestamp_exact= timestamp+dmicroseconds(u_second))
Speed<- get_HypackSpeed(read_hypack(filename = list.files("Data/raw/Hypack/", pattern = "\\.RAW$", full.names = TRUE)))
Speed<- Speed %>% mutate(Speed_mps=Speed_kph*(1000/(60*60)))
names(Speed)[1]<- "timestamp_exact"
Equations from McCollough (1893) and Grasty (2014)
Calculate the angular field of view for the camera
H_AFOV_air = 2 * atan(sensor_width/(2 * focal_length))
Use Snell’s Law to adjust for refraction between air and seawater
H_AFOV_sea = 2 * asin(sin((H_AFOV_air/2)) * (n_air/n_sea))
*n is the index of refraction for the specified medium (n_air~1; n_sea ~1.33)
Calculate the camera angle relative to the seafloor
cam_angle_to_ground = mounted_camera_angle - pitch
Adjust the raw altitude measurements to the true altitude by accounting for the pitch of the system
adjusted_altitude = cos(pitch) * altitude
Calculate the distance in meters between the camera lens and the seafloor at the center of an image
centerline_distance = adjusted_altitude/sin(cam_angle_to_ground)
Calculate the width of the image in meters at the center of an image
Width = 2 * centerline_distance * tan(H_AFOV_water/2)
Calculate distance covered in the 15 second observation window
Distance = Speed * 15s
*The median rather than the mean of speed over the 15s observation window is used so that faulty or poor readings do not adversely affect the calculation
Calculate the area viewed by the camera system
Area = Width * Distance
hab<- hab %>% mutate(Area=NA_real_)
H_AFOV_air<- calc_AFOV(d = 13.3, f = 5, medium = "air", type = "horiz", pix=c(1920, 1200))
for (i in 1:nrow(hab)) {
curr_transect<- hab$Transect[i]
mid_time<- hab$timestamp[i]
st_time<- mid_time-dseconds(7.5)
end_time<- mid_time+dseconds(7.5)
curr_alt<- median(altitude$altitude[altitude$timestamp_exact>=st_time & altitude$timestamp_exact<=end_time])
curr_pitch<- median(compass$pitch[compass$timestamp_exact>=st_time & compass$timestamp_exact<=end_time])
curr_speed<- median(Speed$Speed_mps[Speed$timestamp_exact>=st_time & Speed$timestamp_exact<=end_time])
curr_width<- calc_width(alt = curr_alt, pitch = curr_pitch, cam_angle = 32.8, H_AFOV_air = H_AFOV_air)
curr_dist<- curr_speed * 15
hab$Area[i]<- curr_width*curr_dist
}
Consecutive observations of habitat are collapsed into a single sample for community analysis and counts are summed among the merged observations. This is done to reduce the amount of rows with all zero counts which cannot be handled using semi-metric distances such as Bray-Curtis, and allows us to keep most information from sandy habitats while having more balance in sample sizes across habitat groups.
hab<- hab %>% mutate(samp_num=NA_real_)
hab$samp_num[1]<-1
samp_num<-1
for (i in 2:nrow(hab)) {
curr_hab_obs<- hab$habitat[i]
prev_hab_obs<- hab$habitat[i-1]
curr_obs_transect<- hab$Transect[i]
prev_obs_transect<- hab$Transect[i-1]
if((curr_hab_obs!=prev_hab_obs)|(curr_obs_transect!=prev_obs_transect)|is.na(hab$Area[i])){
samp_num<- samp_num+1
}
hab$samp_num[i]<-samp_num
}
fish_hab<- fish_counts %>% left_join(hab, by=c("Vid", "Sec", "timestamp", "Transect"))
fish_hab<- fish_hab %>% select(Transect,Vid, Sec, timestamp, samp_num, habitat, Area, everything())
fish_hab_collapsed<- fish_hab %>% group_by(samp_num) %>%
summarize(habitat=unique(habitat), Vid_min = first(Vid)+first(Sec)/60, Vid_max= last(Vid)+last(Sec)/60, across(Area:smnoid_smnoid, .fns = sum), .groups="drop")
print(fish_hab_collapsed)
To account for unequal lengths of the samples, counts are divided by the area duration. Additionally to down-weight over abundant taxa, densities are square root transformed.
dens_hab_collaped<- fish_hab_collapsed %>% mutate(across(amberjack_spp:smnoid_smnoid)/Area)
sq_dens_hab_collaped<- dens_hab_collaped %>% mutate(across(amberjack_spp:smnoid_smnoid, .fns=sqrt))
sq_dens_hab_collaped<- sq_dens_hab_collaped %>% filter(!is.na(Area) & habitat!="No_Vis")
sq_dens_hab_collaped<- sq_dens_hab_collaped %>% select(-c(smnoid_smnoid, lgnoid_lgnoid))
sq_dens<- sq_dens_hab_collaped %>% select(-c(samp_num, habitat, Vid_min, Vid_max, Area))
comm_hab<- sq_dens_hab_collaped %>% select(habitat)
idx<- rowSums(sq_dens)>0 #Bray Curtis distance cannot calculate distance in rows with all zeros
sq_dens<- sq_dens[idx,]
comm_hab<- comm_hab[idx,]
comm_hab$habitat<- factor(comm_hab$habitat, levels= c("Sand", "Low_Relief", "Moderate_Relief", "High_Relief"))
set.seed(5)
permdisp_res<- permutest(betadisper(vegdist(sq_dens, methd="bray"), group= comm_hab$habitat, type = "median"), permutations = 9999)
p = 0.1862
p > .05 so we meet the assumtion of homogeneity of multivariate dispersion among groups and can therefore procedure with the PERMANOVA
set.seed(6)
PERMANOVA_res<- adonis(sq_dens~habitat, data=comm_hab, permutations=999, method = "bray")
print(PERMANOVA_res)
Call:
adonis(formula = sq_dens ~ habitat, data = comm_hab, permutations = 999, method = "bray")
Permutation: free
Number of permutations: 999
Terms added sequentially (first to last)
Df SumsOfSqs MeanSqs F.Model R2 Pr(>F)
habitat 3 4.762 1.58737 4.4454 0.09052 0.001 ***
Residuals 134 47.849 0.35708 0.90948
Total 137 52.611 1.00000
---
Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
Since p < .05 in the PERMANOVA, we conclude that there are sigificant differences in the fish communities among the different habitats
To see which habitats have fish communities that differ from another, we follow up the global test with pairwise PERMANOVA tests
set.seed(7)
PW_comp<- adonis_PW(formula = sq_dens~habitat, data=comm_hab, permutations = 9999, method = "bray")
Table of pairwise comparisons
p_holm represents a p-value adjusted forf multiple comparisons using a Holm’s sequential Bonferroni correction. The adjusted p value is less than .05 for all comparisons except for between moderate and high relief rock, indicating that moderate and high relief rock did not have significantly different fish communities from one another, but all other habitats did.
Figure S2.2
CAP_result<- CAPdiscrim(as.data.frame(sq_dens)~habitat, data=as.data.frame(comm_hab), dist="bray", m=0, add=FALSE)
Overall classification success (m=10) : 73.9130434782609 percent
Sand (n=41) correct: 63.4146341463415 percent
Low_Relief (n=80) correct: 88.75 percent
Moderate_Relief (n=10) correct: 40 percent
High_Relief (n=7) correct: 14.2857142857143 percent
CAP_Plot<- plot_CAP(CAP_result = CAP_result, Y = sq_dens, Group = comm_hab$habitat, n_spec = 15, color = c("black", "blue", "orange", "red"), legend_title = "Habitat")
plot(CAP_Plot)
fish_hab2<- fish_hab %>% filter(!is.na(Area) & habitat!="No_Vis")
fish_hab2<- fish_hab2 %>% mutate(habitat=ifelse(test = habitat %in% c("Moderate_Relief", "High_Relief"), yes = "Mod/High_Relief", no = habitat))
fish_hab2<- fish_hab2 %>% select(-smnoid_smnoid) %>% mutate(All_Fish= rowSums(across(amberjack_spp:lgnoid_lgnoid))) %>% select(-lgnoid_lgnoid) #Calculate total fish include large no id's then remove large no id's
hab_types<- unique(fish_hab2$habitat)
for (i in 1:length(hab_types)) {
curr_fish_hab<- fish_hab2 %>% filter(habitat==hab_types[i])
set.seed(i)
curr_avg_dens<- avg_density(counts =select(curr_fish_hab, amberjack_spp:All_Fish), area = curr_fish_hab$Area, iter=1000, conf = .95)
curr_avg_dens<- curr_avg_dens %>% mutate(Type= c("lower_bound", "mean", "upper_bound"))
curr_avg_dens<- curr_avg_dens %>% pivot_longer(cols = -Type, names_to="Taxa", values_to="Density") #Tidy
curr_avg_dens<- curr_avg_dens %>% pivot_wider(names_from = Type, values_from= Density)
curr_avg_dens<- curr_avg_dens %>% mutate(habitat=hab_types[i])
curr_avg_dens<- curr_avg_dens %>% select(Taxa, habitat, lower_bound, mean, upper_bound)
if(i==1){
avg_dens<- curr_avg_dens}else{
avg_dens<- avg_dens %>% bind_rows(curr_avg_dens)
}
}
avg_dens$habitat<- factor(avg_dens$habitat, levels = c("Sand", "Low_Relief", "Mod/High_Relief"))
avg_dens<- avg_dens %>% arrange(Taxa, habitat)
Fish densities were estimated by habitat type for 36 species or species groups, as well as for all fish combined. For most taxa (33 of the 36 species/species groups), average fish density was highest over rocky habitats, and only 3 species had their highest densities over sand. Of the 33 taxa that had highest densities over a rocky habitat, 17 had had their highest densities over moderate-high relief rock and 16 had their highest densities over low relief rock.
Table of mean fish densities per Taxa in numbers of fish per kilometer squared with the lower and upper bounds of the 95% bootstrap confidence intervals
Plot of selected Taxa from table
Figure S2.3
hab_map<- raster("Products/Mapping/Elbow_Supervised_HabRelief.tif")
hab_map<- reclassify(hab_map, matrix(data=c(21, 1, 11, 2, 12, 3, 13, 3), ncol = 2, byrow = TRUE)) #Merge Moderate and High Relief
hab_map<- as.factor(hab_map)
levels(hab_map)[[1]]$habitat<- c("Sand", "LR", "MR/HR")
Figure S2.4
Multiply densities of taxa for each habitat by the corresponding area of the habitat to get abundance
abd<- avg_dens %>% left_join(hab_area, by="habitat") %>%
mutate(across(c(lower_bound, mean, upper_bound))*Area_m2) %>%
dplyr::select(Taxa, habitat, lower_bound, mean, upper_bound)
abd<- bind_rows((abd %>% group_by(Taxa) %>% summarize(habitat="Total", across(where(is.numeric), .fns=sum), .groups="drop")), abd)
abd$habitat<- factor(abd$habitat, levels = c("Total", "Sand", "Low_Relief", "Mod/High_Relief"))
abd<- abd %>% arrange(Taxa, habitat)
Table of abundance estimates
There are estimated to be a total of approximately 109616 fish in total within the study area. Of these, 38893 (35%) are on sand, 25299 (23%) are on low relief rock, and 45424 (41%)` are on moderate to high relief rock.
Plot of estimated abundance for select species
Figure S2.5